vanilla 19.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:49:46 +02:00
parent 991d2234ca
commit d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions

View file

@ -0,0 +1,218 @@
import { describe, expect, test } from "@odoo/hoot";
import { FAKE_FIELDS } from "./calendar_test_helpers";
import { parseXML } from "@web/core/utils/xml";
import { CalendarArchParser } from "@web/views/calendar/calendar_arch_parser";
describe.current.tags("headless");
const parser = new CalendarArchParser();
const DEFAULT_ARCH_RESULTS = {
aggregate: null,
canCreate: true,
canDelete: true,
canEdit: true,
eventLimit: 5,
fieldMapping: {
date_start: "start_date",
},
fieldNames: ["start_date"],
filtersInfo: {},
formViewId: false,
hasEditDialog: false,
quickCreate: true,
quickCreateViewId: null,
isDateHidden: false,
isTimeHidden: false,
monthOverflow: true,
multiCreateView: null,
popoverFieldNodes: {},
scale: "week",
scales: ["day", "week", "month", "year"],
showUnusualDays: false,
showDatePicker: true,
};
function parseArch(arch) {
return parser.parse(parseXML(arch), { fake: { fields: FAKE_FIELDS } }, "fake");
}
function parseWith(attrs) {
const str = Object.entries(attrs)
.map(([k, v]) => `${k}="${v}"`)
.join(" ");
return parseArch(`<calendar date_start="start_date" ${str}/>`);
}
test(`throw if date_start is not set`, () => {
expect(() => parseArch(`<calendar/>`)).toThrow(
`Calendar view must define "date_start" attribute.`
);
});
test(`defaults`, () => {
expect(parseArch(`<calendar date_start="start_date"/>`)).toEqual(DEFAULT_ARCH_RESULTS);
});
test("canCreate", () => {
expect(parseWith({ create: "" }).canCreate).toBe(true);
expect(parseWith({ create: "true" }).canCreate).toBe(true);
expect(parseWith({ create: "True" }).canCreate).toBe(true);
expect(parseWith({ create: "1" }).canCreate).toBe(true);
expect(parseWith({ create: "false" }).canCreate).toBe(false);
expect(parseWith({ create: "False" }).canCreate).toBe(false);
expect(parseWith({ create: "0" }).canCreate).toBe(false);
});
test("canDelete", () => {
expect(parseWith({ delete: "" }).canDelete).toBe(true);
expect(parseWith({ delete: "true" }).canDelete).toBe(true);
expect(parseWith({ delete: "True" }).canDelete).toBe(true);
expect(parseWith({ delete: "1" }).canDelete).toBe(true);
expect(parseWith({ delete: "false" }).canDelete).toBe(false);
expect(parseWith({ delete: "False" }).canDelete).toBe(false);
expect(parseWith({ delete: "0" }).canDelete).toBe(false);
});
test("canEdit", () => {
expect(parseWith({ edit: "" }).canEdit).toBe(true);
expect(parseWith({ edit: "true" }).canEdit).toBe(true);
expect(parseWith({ edit: "True" }).canEdit).toBe(true);
expect(parseWith({ edit: "1" }).canEdit).toBe(true);
expect(parseWith({ edit: "false" }).canEdit).toBe(false);
expect(parseWith({ edit: "False" }).canEdit).toBe(false);
expect(parseWith({ edit: "0" }).canEdit).toBe(false);
});
test("eventLimit", () => {
expect(parseWith({ event_limit: "2" }).eventLimit).toBe(2);
expect(parseWith({ event_limit: "5" }).eventLimit).toBe(5);
expect(() => parseWith({ event_limit: "five" })).toThrow();
expect(() => parseWith({ event_limit: "" })).toThrow();
});
test("hasEditDialog", () => {
expect(parseWith({ event_open_popup: "" }).hasEditDialog).toBe(false);
expect(parseWith({ event_open_popup: "true" }).hasEditDialog).toBe(true);
expect(parseWith({ event_open_popup: "True" }).hasEditDialog).toBe(true);
expect(parseWith({ event_open_popup: "1" }).hasEditDialog).toBe(true);
expect(parseWith({ event_open_popup: "false" }).hasEditDialog).toBe(false);
expect(parseWith({ event_open_popup: "False" }).hasEditDialog).toBe(false);
expect(parseWith({ event_open_popup: "0" }).hasEditDialog).toBe(false);
});
test("quickCreate", () => {
expect(parseWith({ quick_create: "" }).quickCreate).toBe(true);
expect(parseWith({ quick_create: "true" }).quickCreate).toBe(true);
expect(parseWith({ quick_create: "True" }).quickCreate).toBe(true);
expect(parseWith({ quick_create: "1" }).quickCreate).toBe(true);
expect(parseWith({ quick_create: "false" }).quickCreate).toBe(false);
expect(parseWith({ quick_create: "False" }).quickCreate).toBe(false);
expect(parseWith({ quick_create: "0" }).quickCreate).toBe(false);
expect(parseWith({ quick_create: "12" }).quickCreate).toBe(true);
});
test("quickCreateViewId", () => {
expect(parseWith({ quick_create: "0", quick_create_view_id: "12" })).toEqual({
...DEFAULT_ARCH_RESULTS,
quickCreate: false,
quickCreateViewId: null,
});
expect(parseWith({ quick_create: "1", quick_create_view_id: "12" })).toEqual({
...DEFAULT_ARCH_RESULTS,
quickCreate: true,
quickCreateViewId: 12,
});
expect(parseWith({ quick_create: "1" })).toEqual({
...DEFAULT_ARCH_RESULTS,
quickCreate: true,
quickCreateViewId: null,
});
});
test("isDateHidden", () => {
expect(parseWith({ hide_date: "" }).isDateHidden).toBe(false);
expect(parseWith({ hide_date: "true" }).isDateHidden).toBe(true);
expect(parseWith({ hide_date: "True" }).isDateHidden).toBe(true);
expect(parseWith({ hide_date: "1" }).isDateHidden).toBe(true);
expect(parseWith({ hide_date: "false" }).isDateHidden).toBe(false);
expect(parseWith({ hide_date: "False" }).isDateHidden).toBe(false);
expect(parseWith({ hide_date: "0" }).isDateHidden).toBe(false);
});
test("isTimeHidden", () => {
expect(parseWith({ hide_time: "" }).isTimeHidden).toBe(false);
expect(parseWith({ hide_time: "true" }).isTimeHidden).toBe(true);
expect(parseWith({ hide_time: "True" }).isTimeHidden).toBe(true);
expect(parseWith({ hide_time: "1" }).isTimeHidden).toBe(true);
expect(parseWith({ hide_time: "false" }).isTimeHidden).toBe(false);
expect(parseWith({ hide_time: "False" }).isTimeHidden).toBe(false);
expect(parseWith({ hide_time: "0" }).isTimeHidden).toBe(false);
});
test("scale", () => {
expect(parseWith({ mode: "day" }).scale).toBe("day");
expect(parseWith({ mode: "week" }).scale).toBe("week");
expect(parseWith({ mode: "month" }).scale).toBe("month");
expect(parseWith({ mode: "year" }).scale).toBe("year");
expect(() => parseWith({ mode: "" })).toThrow(`Calendar view cannot display mode: `);
expect(() => parseWith({ mode: "other" })).toThrow(`Calendar view cannot display mode: other`);
});
test("scales", () => {
expect(parseWith({ scales: "" }).scales).toEqual(["day", "week", "month", "year"]);
expect(parseWith({ scales: "day" }).scales).toEqual(["day"]);
expect(parseWith({ scales: "day,week" }).scales).toEqual(["day", "week"]);
expect(parseWith({ scales: "day,week,month" }).scales).toEqual(["day", "week", "month"]);
expect(parseWith({ scales: "day,week,month,year" }).scales).toEqual([
"day",
"week",
"month",
"year",
]);
expect(parseWith({ scales: "week" }).scales).toEqual(["week"]);
expect(parseWith({ scales: "week,month" }).scales).toEqual(["week", "month"]);
expect(parseWith({ scales: "week,month,year" }).scales).toEqual(["week", "month", "year"]);
expect(parseWith({ scales: "month" }).scales).toEqual(["month"]);
expect(parseWith({ scales: "month,year" }).scales).toEqual(["month", "year"]);
expect(parseWith({ scales: "year" }).scales).toEqual(["year"]);
expect(parseWith({ scales: "year,day,month,week" }).scales).toEqual([
"year",
"day",
"month",
"week",
]);
expect(() =>
parseArch(`<calendar date_start="start_date" scales="month" mode="day"/>`)
).toThrow();
});
test("showUnusualDays", () => {
expect(parseWith({ show_unusual_days: "" }).showUnusualDays).toBe(false);
expect(parseWith({ show_unusual_days: "true" }).showUnusualDays).toBe(true);
expect(parseWith({ show_unusual_days: "True" }).showUnusualDays).toBe(true);
expect(parseWith({ show_unusual_days: "1" }).showUnusualDays).toBe(true);
expect(parseWith({ show_unusual_days: "false" }).showUnusualDays).toBe(false);
expect(parseWith({ show_unusual_days: "False" }).showUnusualDays).toBe(false);
expect(parseWith({ show_unusual_days: "0" }).showUnusualDays).toBe(false);
});

View file

@ -1,170 +0,0 @@
/** @odoo-module **/
import { CalendarArchParser } from "@web/views/calendar/calendar_arch_parser";
import { FAKE_FIELDS } from "./helpers";
function parseArch(arch, options = {}) {
const parser = new CalendarArchParser();
return parser.parse(arch, { fake: "fields" in options ? options.fields : FAKE_FIELDS }, "fake");
}
function check(assert, paramName, paramValue, expectedName, expectedValue) {
const arch = `<calendar date_start="start_date" ${paramName}="${paramValue}" />`;
const data = parseArch(arch);
assert.strictEqual(data[expectedName], expectedValue);
}
QUnit.module("CalendarView - ArchParser");
QUnit.test("throw if date_start is not set", (assert) => {
assert.throws(() => {
parseArch(`<calendar />`);
});
});
QUnit.test("defaults", (assert) => {
assert.deepEqual(parseArch(`<calendar date_start="start_date" />`), {
canCreate: true,
canDelete: true,
eventLimit: 5,
fieldMapping: {
date_start: "start_date",
},
fieldNames: ["start_date"],
filtersInfo: {},
formViewId: false,
hasEditDialog: false,
hasQuickCreate: true,
isDateHidden: false,
isTimeHidden: false,
popoverFields: {},
scale: "week",
scales: ["day", "week", "month", "year"],
showUnusualDays: false,
});
});
QUnit.test("canCreate", (assert) => {
check(assert, "create", "", "canCreate", true);
check(assert, "create", "true", "canCreate", true);
check(assert, "create", "True", "canCreate", true);
check(assert, "create", "1", "canCreate", true);
check(assert, "create", "false", "canCreate", false);
check(assert, "create", "False", "canCreate", false);
check(assert, "create", "0", "canCreate", false);
});
QUnit.test("canDelete", (assert) => {
check(assert, "delete", "", "canDelete", true);
check(assert, "delete", "true", "canDelete", true);
check(assert, "delete", "True", "canDelete", true);
check(assert, "delete", "1", "canDelete", true);
check(assert, "delete", "false", "canDelete", false);
check(assert, "delete", "False", "canDelete", false);
check(assert, "delete", "0", "canDelete", false);
});
QUnit.test("eventLimit", (assert) => {
check(assert, "event_limit", "2", "eventLimit", 2);
check(assert, "event_limit", "5", "eventLimit", 5);
assert.throws(() => {
parseArch(`<calendar date_start="start_date" event_limit="five" />`);
});
assert.throws(() => {
parseArch(`<calendar date_start="start_date" event_limit="" />`);
});
});
QUnit.test("hasEditDialog", (assert) => {
check(assert, "event_open_popup", "", "hasEditDialog", false);
check(assert, "event_open_popup", "true", "hasEditDialog", true);
check(assert, "event_open_popup", "True", "hasEditDialog", true);
check(assert, "event_open_popup", "1", "hasEditDialog", true);
check(assert, "event_open_popup", "false", "hasEditDialog", false);
check(assert, "event_open_popup", "False", "hasEditDialog", false);
check(assert, "event_open_popup", "0", "hasEditDialog", false);
});
QUnit.test("hasQuickCreate", (assert) => {
check(assert, "quick_add", "", "hasQuickCreate", true);
check(assert, "quick_add", "true", "hasQuickCreate", true);
check(assert, "quick_add", "True", "hasQuickCreate", true);
check(assert, "quick_add", "1", "hasQuickCreate", true);
check(assert, "quick_add", "false", "hasQuickCreate", false);
check(assert, "quick_add", "False", "hasQuickCreate", false);
check(assert, "quick_add", "0", "hasQuickCreate", false);
check(assert, "quick_add", "390", "hasQuickCreate", true);
});
QUnit.test("isDateHidden", (assert) => {
check(assert, "hide_date", "", "isDateHidden", false);
check(assert, "hide_date", "true", "isDateHidden", true);
check(assert, "hide_date", "True", "isDateHidden", true);
check(assert, "hide_date", "1", "isDateHidden", true);
check(assert, "hide_date", "false", "isDateHidden", false);
check(assert, "hide_date", "False", "isDateHidden", false);
check(assert, "hide_date", "0", "isDateHidden", false);
});
QUnit.test("isTimeHidden", (assert) => {
check(assert, "hide_time", "", "isTimeHidden", false);
check(assert, "hide_time", "true", "isTimeHidden", true);
check(assert, "hide_time", "True", "isTimeHidden", true);
check(assert, "hide_time", "1", "isTimeHidden", true);
check(assert, "hide_time", "false", "isTimeHidden", false);
check(assert, "hide_time", "False", "isTimeHidden", false);
check(assert, "hide_time", "0", "isTimeHidden", false);
});
QUnit.test("scale", (assert) => {
check(assert, "mode", "day", "scale", "day");
check(assert, "mode", "week", "scale", "week");
check(assert, "mode", "month", "scale", "month");
check(assert, "mode", "year", "scale", "year");
assert.throws(() => {
parseArch(`<calendar date_start="start_date" mode="other" />`);
});
assert.throws(() => {
parseArch(`<calendar date_start="start_date" mode="" />`);
});
});
QUnit.test("scales", (assert) => {
function check(scales, expectedScales) {
const arch = `<calendar date_start="start_date" scales="${scales}" />`;
const data = parseArch(arch);
assert.deepEqual(data.scales, expectedScales);
}
check("", []);
check("day", ["day"]);
check("day,week", ["day", "week"]);
check("day,week,month", ["day", "week", "month"]);
check("day,week,month,year", ["day", "week", "month", "year"]);
check("week", ["week"]);
check("week,month", ["week", "month"]);
check("week,month,year", ["week", "month", "year"]);
check("month", ["month"]);
check("month,year", ["month", "year"]);
check("year", ["year"]);
check("year,day,month,week", ["year", "day", "month", "week"]);
assert.throws(() => {
parseArch(`<calendar date_start="start_date" scales="month" mode="day" />`);
});
});
QUnit.test("showUnusualDays", (assert) => {
check(assert, "show_unusual_days", "", "showUnusualDays", false);
check(assert, "show_unusual_days", "true", "showUnusualDays", true);
check(assert, "show_unusual_days", "True", "showUnusualDays", true);
check(assert, "show_unusual_days", "1", "showUnusualDays", true);
check(assert, "show_unusual_days", "false", "showUnusualDays", false);
check(assert, "show_unusual_days", "False", "showUnusualDays", false);
check(assert, "show_unusual_days", "0", "showUnusualDays", false);
});

View file

@ -0,0 +1,163 @@
import { describe, expect, test } from "@odoo/hoot";
import { click } from "@odoo/hoot-dom";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { DEFAULT_DATE, FAKE_MODEL } from "./calendar_test_helpers";
import { CalendarCommonPopover } from "@web/views/calendar/calendar_common/calendar_common_popover";
describe.current.tags("desktop");
const FAKE_RECORD = {
id: 5,
title: "Meeting",
isAllDay: false,
start: DEFAULT_DATE,
end: DEFAULT_DATE.plus({ hours: 3, minutes: 15 }),
colorIndex: 0,
isTimeHidden: false,
rawRecord: {
name: "Meeting",
},
};
const FAKE_PROPS = {
model: FAKE_MODEL,
record: FAKE_RECORD,
createRecord() {},
deleteRecord() {},
editRecord() {},
close() {},
};
async function start(props = {}) {
await mountWithCleanup(CalendarCommonPopover, {
props: { ...FAKE_PROPS, ...props },
});
}
test(`mount a CalendarCommonPopover`, async () => {
await start();
expect(`.popover-header`).toHaveCount(1);
expect(`.popover-header`).toHaveText("Meeting");
expect(`.list-group`).toHaveCount(2);
expect(`.list-group.o_cw_popover_fields_secondary`).toHaveCount(1);
expect(`.card-footer .o_cw_popover_edit`).toHaveCount(1);
expect(`.card-footer .o_cw_popover_delete`).toHaveCount(1);
});
test(`date duration: is all day and is same day`, async () => {
await start({
record: { ...FAKE_RECORD, isAllDay: true, isTimeHidden: true },
});
expect(`.list-group:eq(0)`).toHaveText("July 16, 2021");
});
test(`date duration: is all day and two days duration`, async () => {
await start({
record: {
...FAKE_RECORD,
end: DEFAULT_DATE.plus({ days: 1 }),
isAllDay: true,
isTimeHidden: true,
},
});
expect(`.list-group:eq(0)`).toHaveText("July 16-17, 2021 2 days");
});
test(`time duration: 1 hour diff`, async () => {
await start({
record: { ...FAKE_RECORD, end: DEFAULT_DATE.plus({ hours: 1 }) },
model: { ...FAKE_MODEL, isDateHidden: true },
});
expect(`.list-group:eq(0)`).toHaveText("08:00 - 09:00 (1 hour)");
});
test(`time duration: 2 hours diff`, async () => {
await start({
record: { ...FAKE_RECORD, end: DEFAULT_DATE.plus({ hours: 2 }) },
model: { ...FAKE_MODEL, isDateHidden: true },
});
expect(`.list-group:eq(0)`).toHaveText("08:00 - 10:00 (2 hours)");
});
test(`time duration: 1 minute diff`, async () => {
await start({
record: { ...FAKE_RECORD, end: DEFAULT_DATE.plus({ minutes: 1 }) },
model: { ...FAKE_MODEL, isDateHidden: true },
});
expect(`.list-group:eq(0)`).toHaveText("08:00 - 08:01 (1 minute)");
});
test(`time duration: 2 minutes diff`, async () => {
await start({
record: { ...FAKE_RECORD, end: DEFAULT_DATE.plus({ minutes: 2 }) },
model: { ...FAKE_MODEL, isDateHidden: true },
});
expect(`.list-group:eq(0)`).toHaveText("08:00 - 08:02 (2 minutes)");
});
test(`time duration: 3 hours and 15 minutes diff`, async () => {
await start({
model: { ...FAKE_MODEL, isDateHidden: true },
});
expect(`.list-group:eq(0)`).toHaveText("08:00 - 11:15 (3 hours, 15 minutes)");
});
test(`isDateHidden is true`, async () => {
await start({
model: { ...FAKE_MODEL, isDateHidden: true },
});
expect(`.list-group:eq(0)`).toHaveText("08:00 - 11:15 (3 hours, 15 minutes)");
});
test(`isDateHidden is false`, async () => {
await start({
model: { ...FAKE_MODEL, isDateHidden: false },
});
expect(`.list-group:eq(0)`).toHaveText("July 16, 2021\n08:00 - 11:15 (3 hours, 15 minutes)");
});
test(`isTimeHidden is true`, async () => {
await start({
record: { ...FAKE_RECORD, isTimeHidden: true },
});
expect(`.list-group:eq(0)`).toHaveText("July 16, 2021");
});
test(`isTimeHidden is false`, async () => {
await start({
record: { ...FAKE_RECORD, isTimeHidden: false },
});
expect(`.list-group:eq(0)`).toHaveText("July 16, 2021\n08:00 - 11:15 (3 hours, 15 minutes)");
});
test(`canDelete is true`, async () => {
await start({
model: { ...FAKE_MODEL, canDelete: true },
});
expect(`.o_cw_popover_delete`).toHaveCount(1);
});
test(`canDelete is false`, async () => {
await start({
model: { ...FAKE_MODEL, canDelete: false },
});
expect(`.o_cw_popover_delete`).toHaveCount(0);
});
test(`click on delete button`, async () => {
await start({
model: { ...FAKE_MODEL, canDelete: true },
deleteRecord: () => expect.step("delete"),
});
await click(`.o_cw_popover_delete`);
expect.verifySteps(["delete"]);
});
test(`click on edit button`, async () => {
await start({
editRecord: () => expect.step("edit"),
});
await click(`.o_cw_popover_edit`);
expect.verifySteps(["edit"]);
});

View file

@ -1,222 +0,0 @@
/** @odoo-module **/
import { CalendarCommonPopover } from "@web/views/calendar/calendar_common/calendar_common_popover";
import { click, getFixture } from "../../helpers/utils";
import { makeEnv, makeFakeDate, makeFakeModel, mountComponent } from "./helpers";
let target;
function makeFakeRecord(data = {}) {
return {
id: 5,
title: "Meeting",
isAllDay: false,
start: makeFakeDate(),
end: makeFakeDate().plus({ hours: 3, minutes: 15 }),
colorIndex: 0,
isTimeHidden: false,
rawRecord: {
name: "Meeting",
},
...data,
};
}
async function start(params = {}) {
const { services, props, model: modelParams } = params;
const env = await makeEnv(services);
const model = makeFakeModel(modelParams);
return await mountComponent(CalendarCommonPopover, env, {
model,
record: makeFakeRecord(),
createRecord() {},
deleteRecord() {},
editRecord() {},
close() {},
...props,
});
}
/** @todo Add tests for fields **/
QUnit.module("CalendarView - CommonPopover", ({ beforeEach }) => {
beforeEach(() => {
target = getFixture();
});
QUnit.test("mount a CalendarCommonPopover", async (assert) => {
await start({});
assert.containsOnce(target, ".popover-header");
assert.strictEqual(target.querySelector(".popover-header").textContent, "Meeting");
assert.containsN(target, ".list-group", 2);
assert.containsOnce(target, ".list-group.o_cw_popover_fields_secondary");
assert.containsOnce(target, ".card-footer .o_cw_popover_edit");
assert.containsOnce(target, ".card-footer .o_cw_popover_delete");
});
QUnit.test("date duration: is all day and is same day", async (assert) => {
await start({
props: {
record: makeFakeRecord({ isAllDay: true, isTimeHidden: true }),
},
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "July 16, 2021 (All day)");
});
QUnit.test("date duration: is all day and two days duration", async (assert) => {
await start({
props: {
record: makeFakeRecord({
end: makeFakeDate().plus({ days: 1 }),
isAllDay: true,
isTimeHidden: true,
}),
},
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "July 16-17, 2021 (2 days)");
});
QUnit.test("time duration: 1 hour diff", async (assert) => {
await start({
props: {
record: makeFakeRecord({ end: makeFakeDate().plus({ hours: 1 }) }),
},
model: { isDateHidden: true },
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "08:00 - 09:00 (1 hour)");
});
QUnit.test("time duration: 2 hours diff", async (assert) => {
await start({
props: {
record: makeFakeRecord({ end: makeFakeDate().plus({ hours: 2 }) }),
},
model: { isDateHidden: true },
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "08:00 - 10:00 (2 hours)");
});
QUnit.test("time duration: 1 minute diff", async (assert) => {
await start({
props: {
record: makeFakeRecord({ end: makeFakeDate().plus({ minutes: 1 }) }),
},
model: { isDateHidden: true },
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "08:00 - 08:01 (1 minute)");
});
QUnit.test("time duration: 2 minutes diff", async (assert) => {
await start({
props: {
record: makeFakeRecord({ end: makeFakeDate().plus({ minutes: 2 }) }),
},
model: { isDateHidden: true },
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "08:00 - 08:02 (2 minutes)");
});
QUnit.test("time duration: 3 hours and 15 minutes diff", async (assert) => {
await start({
model: { isDateHidden: true },
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "08:00 - 11:15 (3 hours, 15 minutes)");
});
QUnit.test("isDateHidden is true", async (assert) => {
await start({
model: { isDateHidden: true },
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "08:00 - 11:15 (3 hours, 15 minutes)");
});
QUnit.test("isDateHidden is false", async (assert) => {
await start({
model: { isDateHidden: false },
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(
dateTimeLabels,
"July 16, 2021 08:00 - 11:15 (3 hours, 15 minutes)"
);
});
QUnit.test("isTimeHidden is true", async (assert) => {
await start({
props: {
record: makeFakeRecord({ isTimeHidden: true }),
},
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "July 16, 2021");
});
QUnit.test("isTimeHidden is false", async (assert) => {
await start({
props: {
record: makeFakeRecord({ isTimeHidden: false }),
},
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(
dateTimeLabels,
"July 16, 2021 08:00 - 11:15 (3 hours, 15 minutes)"
);
});
QUnit.test("canDelete is true", async (assert) => {
await start({
model: { canDelete: true },
});
assert.containsOnce(target, ".o_cw_popover_delete");
});
QUnit.test("canDelete is false", async (assert) => {
await start({
model: { canDelete: false },
});
assert.containsNone(target, ".o_cw_popover_delete");
});
QUnit.test("click on delete button", async (assert) => {
assert.expect(2);
await start({
model: { canDelete: true },
props: {
deleteRecord: () => assert.step("delete"),
},
});
await click(target, ".o_cw_popover_delete");
assert.verifySteps(["delete"]);
});
QUnit.test("click on edit button", async (assert) => {
assert.expect(2);
await start({
props: {
editRecord: () => assert.step("edit"),
},
});
await click(target, ".o_cw_popover_edit");
assert.verifySteps(["edit"]);
});
});

View file

@ -0,0 +1,170 @@
import { beforeEach, expect, test } from "@odoo/hoot";
import { queryAllTexts, queryFirst, queryRect } from "@odoo/hoot-dom";
import { runAllTimers } from "@odoo/hoot-mock";
import { mockService, mountWithCleanup, preloadBundle } from "@web/../tests/web_test_helpers";
import {
DEFAULT_DATE,
FAKE_MODEL,
clickAllDaySlot,
clickEvent,
selectTimeRange,
} from "./calendar_test_helpers";
import { CalendarCommonRenderer } from "@web/views/calendar/calendar_common/calendar_common_renderer";
import { CallbackRecorder } from "@web/search/action_hook";
const FAKE_PROPS = {
model: FAKE_MODEL,
createRecord() {},
deleteRecord() {},
editRecord() {},
callbackRecorder: new CallbackRecorder(),
onSquareSelection() {},
cleanSquareSelection() {},
};
async function start(props = {}, target) {
await mountWithCleanup(CalendarCommonRenderer, {
props: { ...FAKE_PROPS, ...props },
target,
});
}
preloadBundle("web.fullcalendar_lib");
beforeEach(() => {
luxon.Settings.defaultZone = "UTC+1";
});
test(`mount a CalendarCommonRenderer`, async () => {
await start();
expect(`.o_calendar_widget.fc`).toHaveCount(1);
});
test(`Day: mount a CalendarCommonRenderer`, async () => {
await start({ model: { ...FAKE_MODEL, scale: "day" } });
expect(`.o_calendar_widget.fc .fc-timeGridDay-view`).toHaveCount(1);
});
test(`Week: mount a CalendarCommonRenderer`, async () => {
await start({ model: { ...FAKE_MODEL, scale: "week" } });
expect(`.o_calendar_widget.fc .fc-timeGridWeek-view`).toHaveCount(1);
});
test(`Month: mount a CalendarCommonRenderer`, async () => {
await start({ model: { ...FAKE_MODEL, scale: "month" } });
expect(`.o_calendar_widget.fc .fc-dayGridMonth-view`).toHaveCount(1);
});
test(`Day: check week number`, async () => {
await start({ model: { ...FAKE_MODEL, scale: "day" } });
expect(`[aria-label^="Week "]`).toHaveCount(1);
expect(`[aria-label^="Week "]`).toHaveText(/(Week )?28/);
});
test(`Day: check date`, async () => {
await start({ model: { ...FAKE_MODEL, scale: "day" } });
expect(`.fc-col-header-cell.fc-day`).toHaveCount(1);
expect(`.fc-col-header-cell.fc-day:eq(0) .o_cw_day_name`).toHaveText("Friday");
expect(`.fc-col-header-cell.fc-day:eq(0) .o_cw_day_number`).toHaveText("16");
});
test(`Day: click all day slot`, async () => {
await start({
model: { ...FAKE_MODEL, scale: "day" },
createRecord(record) {
expect.step("create");
expect(record.isAllDay).toBe(true);
expect(record.start.valueOf()).toBe(DEFAULT_DATE.startOf("day").valueOf());
},
});
await clickAllDaySlot("2021-07-16");
expect.verifySteps(["create"]);
});
test.tags("desktop");
test(`Day: select range`, async () => {
await start({
model: { ...FAKE_MODEL, scale: "day" },
createRecord(record) {
expect.step("create");
expect(record.isAllDay).toBe(false);
expect(record.start.valueOf()).toBe(luxon.DateTime.local(2021, 7, 16, 8, 0).valueOf());
expect(record.end.valueOf()).toBe(luxon.DateTime.local(2021, 7, 16, 10, 0).valueOf());
},
});
await selectTimeRange("2021-07-16 08:00:00", "2021-07-16 10:00:00");
expect.verifySteps(["create"]);
});
test(`Day: check event`, async () => {
await start({ model: { ...FAKE_MODEL, scale: "day" } });
expect(`.o_event`).toHaveCount(1);
expect(`.o_event`).toHaveAttribute("data-event-id", "1");
});
test.tags("desktop");
test(`Day: click on event`, async () => {
mockService("popover", () => ({
add(target, component, { record }) {
expect.step("popover");
expect(record.id).toBe(1);
return () => {};
},
}));
await start({ model: { ...FAKE_MODEL, scale: "day" } });
await clickEvent(1);
await runAllTimers();
expect.verifySteps(["popover"]);
});
test(`Week: check week number`, async () => {
await start({ model: { ...FAKE_MODEL, scale: "week" } });
expect(`.fc-scrollgrid-section-header .fc-timegrid-axis-cushion`).toHaveCount(1);
expect(`.fc-scrollgrid-section-header .fc-timegrid-axis-cushion`).toHaveText(/(Week )?28/);
});
test(`Week: check dates`, async () => {
await start({ model: { ...FAKE_MODEL, scale: "week" } });
expect(`.fc-col-header-cell.fc-day`).toHaveCount(7);
expect(queryAllTexts(`.fc-col-header-cell .o_cw_day_name`)).toEqual([
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
]);
expect(queryAllTexts`.fc-col-header-cell .o_cw_day_number`).toEqual([
"11",
"12",
"13",
"14",
"15",
"16",
"17",
]);
});
test(`Day: automatically scroll to 6am`, async () => {
await mountWithCleanup(`<div class="scrollable" style="height: 500px;"/>`);
await start({ model: { ...FAKE_MODEL, scale: "day" } }, queryFirst(`.scrollable`));
const containerDimensions = queryRect(`.fc-scrollgrid-section-liquid .fc-scroller`);
const dayStartDimensions = queryRect(`.fc-timegrid-slot[data-time="06:00:00"]:eq(0)`);
expect(Math.abs(dayStartDimensions.y - containerDimensions.y)).toBeLessThan(2);
});
test(`Week: automatically scroll to 6am`, async () => {
await mountWithCleanup(`<div class="scrollable" style="height: 500px;"/>`);
await start({ model: { ...FAKE_MODEL, scale: "week" } }, queryFirst(`.scrollable`));
const containerDimensions = queryRect(`.fc-scrollgrid-section-liquid .fc-scroller`);
const dayStartDimensions = queryRect(`.fc-timegrid-slot[data-time="06:00:00"]:eq(0)`);
expect(Math.abs(dayStartDimensions.y - containerDimensions.y)).toBeLessThan(2);
});
test("Month: remove row when no day of current month", async () => {
await start({ model: { ...FAKE_MODEL, scale: "month" } });
expect(".fc-day-other, .fc-day-disabled").toHaveCount(4);
});

View file

@ -1,162 +0,0 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { CalendarCommonRenderer } from "@web/views/calendar/calendar_common/calendar_common_renderer";
import { getFixture, patchWithCleanup } from "../../helpers/utils";
import {
makeEnv,
makeFakeModel,
mountComponent,
clickAllDaySlot,
selectTimeRange,
clickEvent,
makeFakeDate,
} from "./helpers";
let target;
function makeFakePopoverService(add) {
return { start: () => ({ add }) };
}
async function start(params = {}) {
const { services, props, model: modelParams } = params;
const env = await makeEnv(services);
const model = makeFakeModel(modelParams);
return await mountComponent(CalendarCommonRenderer, env, {
model,
createRecord() {},
deleteRecord() {},
editRecord() {},
displayName: "Plop",
...props,
});
}
QUnit.module("CalendarView - CommonRenderer", ({ beforeEach }) => {
beforeEach(() => {
target = getFixture();
});
QUnit.test("mount a CalendarCommonRenderer", async (assert) => {
await start({});
assert.containsOnce(target, ".o_calendar_widget.fc");
});
QUnit.test("Day: mount a CalendarCommonRenderer", async (assert) => {
await start({ model: { scale: "day" } });
assert.containsOnce(target, ".o_calendar_widget.fc .fc-timeGridDay-view");
});
QUnit.test("Week: mount a CalendarCommonRenderer", async (assert) => {
await start({ model: { scale: "week" } });
assert.containsOnce(target, ".o_calendar_widget.fc .fc-timeGridWeek-view");
});
QUnit.test("Month: mount a CalendarCommonRenderer", async (assert) => {
await start({ model: { scale: "month" } });
assert.containsOnce(target, ".o_calendar_widget.fc .fc-dayGridMonth-view");
});
QUnit.test("Day: check week number", async (assert) => {
await start({ model: { scale: "day" } });
assert.containsOnce(target, ".fc-week-number");
assert.strictEqual(target.querySelector(".fc-week-number").textContent, "Week 28");
});
QUnit.test("Day: check date", async (assert) => {
await start({ model: { scale: "day" } });
assert.containsOnce(target, ".fc-day-header");
assert.strictEqual(target.querySelector(".fc-day-header").textContent, "July 16, 2021");
});
QUnit.test("Day: click all day slot", async (assert) => {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout() {},
});
await start({
model: { scale: "day" },
props: {
createRecord(record) {
const date = makeFakeDate().startOf("day");
assert.ok(record.isAllDay);
assert.strictEqual(record.start.valueOf(), date.valueOf());
assert.step("create");
},
},
});
await clickAllDaySlot(target, "2021-07-16");
assert.verifySteps(["create"]);
});
QUnit.test("Day: select range", async (assert) => {
await start({
model: { scale: "day" },
props: {
createRecord(record) {
assert.notOk(record.isAllDay);
assert.strictEqual(
record.start.valueOf(),
luxon.DateTime.local(2021, 7, 16, 8, 0).valueOf()
);
assert.strictEqual(
record.end.valueOf(),
luxon.DateTime.local(2021, 7, 16, 10, 0).valueOf()
);
assert.step("create");
},
},
});
await selectTimeRange(target, "2021-07-16 08:00:00", "2021-07-16 10:00:00");
assert.verifySteps(["create"]);
});
QUnit.test("Day: check event", async (assert) => {
await start({ model: { scale: "day" } });
assert.containsOnce(target, ".o_event");
assert.hasAttrValue(target.querySelector(".o_event"), "data-event-id", "1");
});
QUnit.test("Day: click on event", async (assert) => {
assert.expect(1);
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout() {},
});
await start({
model: { scale: "day" },
services: {
popover: makeFakePopoverService((target, _, props) => {
assert.strictEqual(props.record.id, 1);
return () => {};
}),
},
});
await clickEvent(target, 1);
});
QUnit.test("Week: check week number", async (assert) => {
await start({ model: { scale: "week" } });
assert.containsOnce(target, ".fc-week-number");
assert.strictEqual(target.querySelector(".fc-week-number").textContent, "Week 28");
});
QUnit.test("Week: check dates", async (assert) => {
await start({ model: { scale: "week" } });
assert.containsN(target, ".fc-day-header", 7);
const dates = ["Sun 11", "Mon 12", "Tue 13", "Wed 14", "Thu 15", "Fri 16", "Sat 17"];
const els = target.querySelectorAll(".fc-day-header");
for (let i = 0; i < els.length; i++) {
assert.strictEqual(els[i].textContent, dates[i]);
}
});
});

View file

@ -0,0 +1,182 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { queryAllTexts } from "@odoo/hoot-dom";
import { mockDate } from "@odoo/hoot-mock";
import {
contains,
defineModels,
defineParams,
fields,
findComponent,
models,
mountView,
preloadBundle,
} from "@web/../tests/web_test_helpers";
import { CalendarController } from "@web/views/calendar/calendar_controller";
describe.current.tags("desktop");
class Event extends models.Model {
name = fields.Char();
start = fields.Date();
has_access() {
return true;
}
}
defineModels([Event]);
preloadBundle("web.fullcalendar_lib");
beforeEach(() => {
mockDate("2021-08-14T08:00:00");
});
test(`Mount a CalendarDatePicker`, async () => {
await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="day"/>`,
});
expect(`.o_datetime_picker`).toHaveCount(1);
expect(`.o_datetime_picker .o_selected`).toHaveCount(1);
expect(`.o_datetime_picker .o_selected`).toHaveText("14");
expect(`.o_datetime_picker_header .o_datetime_button`).toHaveText("August 2021");
expect(queryAllTexts`.o_datetime_picker .o_day_of_week_cell`).toEqual([
"S",
"M",
"T",
"W",
"T",
"F",
"S",
]);
});
test(`Scale: init with day`, async () => {
await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="day"/>`,
});
expect(`.o_datetime_picker .o_selected`).toHaveCount(1);
expect(`.o_datetime_picker .o_selected`).toHaveText("14");
});
test(`Scale: init with week`, async () => {
await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="week"/>`,
});
expect(`.o_datetime_picker .o_selected`).toHaveCount(1);
expect(`.o_datetime_picker .o_selected`).toHaveText("14");
});
test(`Scale: init with month`, async () => {
await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="month"/>`,
});
expect(`.o_datetime_picker .o_selected`).toHaveCount(1);
expect(`.o_datetime_picker .o_selected`).toHaveText("14");
});
test(`Scale: init with year`, async () => {
await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="year"/>`,
});
expect(`.o_datetime_picker .o_selected`).toHaveCount(1);
expect(`.o_datetime_picker .o_selected`).toHaveText("14");
});
test(`First day: 0 = Sunday`, async () => {
// the week start depends on the locale
defineParams({
lang_parameters: { week_start: 0 },
});
await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="day"/>`,
});
expect(queryAllTexts`.o_datetime_picker .o_day_of_week_cell`).toEqual([
"S",
"M",
"T",
"W",
"T",
"F",
"S",
]);
});
test(`First day: 1 = Monday`, async () => {
// the week start depends on the locale
defineParams({
lang_parameters: { week_start: 1 },
});
await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="day"/>`,
});
expect(queryAllTexts`.o_datetime_picker .o_day_of_week_cell`).toEqual([
"M",
"T",
"W",
"T",
"F",
"S",
"S",
]);
});
test(`Click on active day should change scale : day -> month`, async () => {
const view = await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="day"/>`,
});
const calendar = findComponent(view, (component) => component instanceof CalendarController);
await contains(`.o_datetime_picker .o_selected`).click();
expect(calendar.model.scale).toBe("month");
expect(calendar.model.date.valueOf()).toBe(luxon.DateTime.local(2021, 8, 14).valueOf());
});
test(`Click on active day should change scale : month -> week`, async () => {
const view = await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="month"/>`,
});
const calendar = findComponent(view, (component) => component instanceof CalendarController);
await contains(`.o_datetime_picker .o_selected`).click();
expect(calendar.model.scale).toBe("week");
expect(calendar.model.date.valueOf()).toBe(luxon.DateTime.local(2021, 8, 14).valueOf());
});
test(`Click on active day should change scale : week -> day`, async () => {
const view = await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="week"/>`,
});
const calendar = findComponent(view, (component) => component instanceof CalendarController);
await contains(`.o_datetime_picker .o_selected`).click();
expect(calendar.model.scale).toBe("day");
expect(calendar.model.date.valueOf()).toBe(luxon.DateTime.local(2021, 8, 14).valueOf());
});
test(`Scale: today is correctly highlighted`, async () => {
mockDate("2021-07-04T08:00:00");
await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start" mode="month"/>`,
});
expect(`.o_datetime_picker .o_today`).toHaveClass("o_selected");
expect(`.o_datetime_picker .o_today`).toHaveText("4");
});

View file

@ -1,124 +0,0 @@
/** @odoo-module **/
import { CalendarDatePicker } from "@web/views/calendar/date_picker/calendar_date_picker";
import { click, getFixture, patchDate } from "../../helpers/utils";
import { makeEnv, makeFakeModel, mountComponent } from "./helpers";
let target;
async function start(params = {}) {
const { services, props, model: modelParams } = params;
const env = await makeEnv(services);
const model = makeFakeModel(modelParams);
return await mountComponent(CalendarDatePicker, env, {
model,
...props,
});
}
QUnit.module("CalendarView - DatePicker", ({ beforeEach }) => {
beforeEach(() => {
target = getFixture();
patchDate(2021, 7, 14, 8, 0, 0);
});
QUnit.test("Mount a CalendarDatePicker", async (assert) => {
await start({ model: { scale: "day" } });
assert.containsOnce(target, ".o_calendar_mini.hasDatepicker");
assert.strictEqual(target.querySelector(".o_selected_range").textContent, "16");
assert.containsOnce(target, `[data-month="6"][data-year="2021"] .o_selected_range`);
assert.strictEqual(target.querySelector(".ui-datepicker-month").textContent, "Jul");
assert.strictEqual(target.querySelector(".ui-datepicker-year").textContent, "2021");
assert.strictEqual(target.querySelector("thead").textContent, "SMTWTFS");
});
QUnit.test("Scale: init with day", async (assert) => {
await start({ model: { scale: "day" } });
assert.containsOnce(target, ".o_selected_range");
assert.containsOnce(target, "a.o_selected_range");
assert.strictEqual(target.querySelector(".o_selected_range").textContent, "16");
});
QUnit.test("Scale: init with week", async (assert) => {
await start({ model: { scale: "week" } });
assert.containsOnce(target, ".o_selected_range");
assert.containsOnce(target, "tr.o_selected_range");
assert.hasClass(target.querySelector("tr.o_selected_range"), "o_color");
assert.strictEqual(target.querySelector(".o_selected_range").textContent, "11121314151617");
});
QUnit.test("Scale: init with month", async (assert) => {
await start({ model: { scale: "month" } });
assert.containsN(target, "td.o_selected_range", 35);
});
QUnit.test("Scale: init with year", async (assert) => {
await start({ model: { scale: "year" } });
assert.containsN(target, "td.o_selected_range", 35);
});
QUnit.test("First day: 0 = Sunday", async (assert) => {
await start({ model: { scale: "day", firstDayOfWeek: 0 } });
assert.strictEqual(target.querySelector("thead").textContent, "SMTWTFS");
});
QUnit.test("First day: 1 = Monday", async (assert) => {
await start({ model: { scale: "day", firstDayOfWeek: 1 } });
assert.strictEqual(target.querySelector("thead").textContent, "MTWTFSS");
});
QUnit.test("Click on active day should change scale : day -> month", async (assert) => {
assert.expect(2);
await start({
model: {
scale: "day",
load(params) {
assert.strictEqual(params.scale, "month");
assert.ok(params.date.equals(luxon.DateTime.local(2021, 7, 16)));
},
},
});
await click(target, ".ui-state-active");
});
QUnit.test("Click on active day should change scale : month -> week", async (assert) => {
assert.expect(2);
await start({
model: {
scale: "month",
load(params) {
assert.strictEqual(params.scale, "week");
assert.ok(params.date.equals(luxon.DateTime.local(2021, 7, 16)));
},
},
});
await click(target, ".ui-state-active");
});
QUnit.test("Click on active day should change scale : week -> day", async (assert) => {
assert.expect(2);
await start({
model: {
scale: "week",
load(params) {
assert.strictEqual(params.scale, "day");
assert.ok(params.date.equals(luxon.DateTime.local(2021, 7, 16)));
},
},
});
await click(target, ".ui-state-active");
});
QUnit.test("Scale: today is correctly highlighted", async (assert) => {
patchDate(2021, 6, 4, 8, 0, 0);
await start({ model: { scale: "month" } });
assert.containsOnce(target, ".ui-datepicker-today");
assert.strictEqual(target.querySelector(".ui-datepicker-today").textContent, "4");
});
});

View file

@ -1,213 +0,0 @@
/** @odoo-module **/
import { CalendarFilterPanel } from "@web/views/calendar/filter_panel/calendar_filter_panel";
import { click, getFixture, triggerEvent } from "../../helpers/utils";
import { makeEnv, makeFakeModel, mountComponent } from "./helpers";
let target;
async function start(params = {}) {
const { services, props, model: modelParams } = params;
const env = await makeEnv(services);
const model = makeFakeModel(modelParams);
return await mountComponent(CalendarFilterPanel, env, {
model,
...props,
});
}
QUnit.module("CalendarView - FilterPanel", ({ beforeEach }) => {
beforeEach(() => {
target = getFixture();
});
QUnit.test("render filter panel", async (assert) => {
await start({});
assert.containsN(target, ".o_calendar_filter", 2);
const sections = target.querySelectorAll(".o_calendar_filter");
let header = sections[0].querySelector(".o_cw_filter_label");
assert.strictEqual(header.textContent, "Attendees");
assert.containsN(sections[0], ".o_calendar_filter_item", 4);
header = sections[1].querySelector(".o_cw_filter_label");
assert.strictEqual(header.textContent, "Users");
assert.containsN(sections[1], ".o_calendar_filter_item", 2);
});
QUnit.test("filters are correctly sorted", async (assert) => {
await start({});
assert.containsN(target, ".o_calendar_filter", 2);
const sections = target.querySelectorAll(".o_calendar_filter");
let header = sections[0].querySelector(".o_cw_filter_label");
assert.strictEqual(header.textContent, "Attendees");
assert.containsN(sections[0], ".o_calendar_filter_item", 4);
assert.strictEqual(
sections[0].textContent.trim(),
"AttendeesMitchell AdminMarc DemoBrandon FreemanEverybody's calendar"
);
header = sections[1].querySelector(".o_cw_filter_label");
assert.strictEqual(header.textContent, "Users");
assert.containsN(sections[1], ".o_calendar_filter_item", 2);
assert.strictEqual(sections[1].textContent.trim(), "UsersMarc DemoBrandon Freeman");
});
QUnit.test("section can collapse", async (assert) => {
await start({});
const section = target.querySelectorAll(".o_calendar_filter")[0];
assert.containsOnce(section, ".o_cw_filter_collapse_icon");
assert.containsN(section, ".o_calendar_filter_item", 4);
await click(section, ".o_cw_filter_label");
assert.containsNone(section, ".o_calendar_filter_item");
await click(section, ".o_cw_filter_label");
assert.containsN(section, ".o_calendar_filter_item", 4);
});
QUnit.test("section cannot collapse", async (assert) => {
await start({});
const section = target.querySelectorAll(".o_calendar_filter")[1];
assert.containsNone(section, ".o_cw_filter_label > i");
assert.doesNotHaveClass(section, "o_calendar_filter-collapsed");
assert.containsN(section, ".o_calendar_filter_item", 2);
await click(section, ".o_cw_filter_label");
assert.doesNotHaveClass(section, "o_calendar_filter-collapsed");
assert.containsN(section, ".o_calendar_filter_item", 2);
});
QUnit.test("filters can have avatar", async (assert) => {
await start({});
const section = target.querySelectorAll(".o_calendar_filter")[0];
const filters = section.querySelectorAll(".o_calendar_filter_item");
assert.containsN(section, ".o_cw_filter_avatar", 4);
assert.containsN(section, "img.o_cw_filter_avatar", 3);
assert.containsOnce(section, "i.o_cw_filter_avatar");
assert.hasAttrValue(
filters[0].querySelector(".o_cw_filter_avatar"),
"data-src",
"/web/image/res.partner/3/avatar_128"
);
assert.hasAttrValue(
filters[1].querySelector(".o_cw_filter_avatar"),
"data-src",
"/web/image/res.partner/6/avatar_128"
);
assert.hasAttrValue(
filters[2].querySelector(".o_cw_filter_avatar"),
"data-src",
"/web/image/res.partner/4/avatar_128"
);
});
QUnit.test("filters cannot have avatar", async (assert) => {
await start({});
const section = target.querySelectorAll(".o_calendar_filter")[1];
assert.containsN(section, ".o_calendar_filter_item", 2);
assert.containsNone(section, ".o_cw_filter_avatar");
});
QUnit.test("filter can have remove button", async (assert) => {
await start({});
const section = target.querySelectorAll(".o_calendar_filter")[0];
const filters = section.querySelectorAll(".o_calendar_filter_item");
assert.containsN(section, ".o_calendar_filter_item", 4);
assert.containsN(section, ".o_calendar_filter_item .o_remove", 2);
assert.containsNone(filters[0], ".o_remove");
assert.containsOnce(filters[1], ".o_remove");
assert.containsOnce(filters[2], ".o_remove");
assert.containsNone(filters[3], ".o_remove");
});
QUnit.test("click on remove button", async (assert) => {
assert.expect(3);
await start({
model: {
unlinkFilter(fieldName, recordId) {
assert.step(`${fieldName} ${recordId}`);
},
},
});
const section = target.querySelectorAll(".o_calendar_filter")[0];
const filters = section.querySelectorAll(".o_calendar_filter_item");
await click(filters[1], ".o_calendar_filter_item .o_remove");
await click(filters[2], ".o_calendar_filter_item .o_remove");
assert.verifySteps(["partner_ids 2", "partner_ids 1"]);
});
QUnit.test("click on filter", async (assert) => {
assert.expect(6);
await start({
model: {
updateFilters(fieldName, args) {
assert.step(`${fieldName} ${Object.keys(args)[0]} ${Object.values(args)[0]}`);
},
},
});
const section = target.querySelectorAll(".o_calendar_filter")[0];
const filters = section.querySelectorAll(".o_calendar_filter_item");
await click(filters[0], "input");
await click(filters[1], "input");
await click(filters[2], "input");
await click(filters[3], "input");
await click(filters[3], "input");
assert.verifySteps([
"partner_ids 3 false",
"partner_ids 6 true",
"partner_ids 4 false",
"partner_ids all true",
"partner_ids all false",
]);
});
QUnit.test("hover filter opens tooltip", async (assert) => {
await start({
services: {
popover: {
start: () => ({
add: (target, _, props) => {
assert.step(props.filter.label);
assert.step("" + props.filter.hasAvatar);
assert.step("" + props.filter.value);
return () => {
assert.step("popOver Closed");
};
},
}),
},
},
});
const section = target.querySelectorAll(".o_calendar_filter")[0];
const filters = section.querySelectorAll(".o_calendar_filter_item");
await triggerEvent(filters[0], null, "mouseenter");
assert.verifySteps(["Mitchell Admin", "true", "3"]);
await triggerEvent(filters[0], null, "mouseleave");
assert.verifySteps(["popOver Closed"]);
await triggerEvent(filters[3], null, "mouseenter");
assert.verifySteps([]);
await triggerEvent(filters[3], null, "mouseleave");
assert.verifySteps([]);
});
});

View file

@ -0,0 +1,144 @@
import { expect, test } from "@odoo/hoot";
import { click, queryAllTexts } from "@odoo/hoot-dom";
import { contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
import { FAKE_FILTER_SECTIONS, FAKE_MODEL } from "./calendar_test_helpers";
import { CalendarFilterSection } from "@web/views/calendar/calendar_filter_section/calendar_filter_section";
import { runAllTimers } from "@odoo/hoot-mock";
test(`render filter panel`, async () => {
await mountWithCleanup(CalendarFilterSection, {
props: {
model: FAKE_MODEL,
section: FAKE_FILTER_SECTIONS[0],
},
});
expect(`.o_calendar_filter`).toHaveCount(1);
expect(`.o_calendar_filter .o_cw_filter_label`).toHaveText("Attendees");
expect(`.o_calendar_filter .o_calendar_filter_item`).toHaveCount(3);
});
test(`filters are correctly sorted`, async () => {
await mountWithCleanup(CalendarFilterSection, {
props: {
model: FAKE_MODEL,
section: FAKE_FILTER_SECTIONS[0],
},
});
expect(queryAllTexts`.o_calendar_filter .o_calendar_filter_item`).toEqual([
"Mitchell Admin",
"Brandon Freeman",
"Marc Demo",
]);
});
test(`section can collapse`, async () => {
await mountWithCleanup(CalendarFilterSection, {
props: {
model: FAKE_MODEL,
section: FAKE_FILTER_SECTIONS[0],
},
});
expect(`.o_calendar_filter .o_cw_filter_collapse_icon`).toHaveCount(1);
expect(`.o_calendar_filter .o_calendar_filter_item`).toHaveCount(3);
await contains(`.o_calendar_filter .o_cw_filter_label`).click();
await runAllTimers();
expect(`.o_calendar_filter .o_calendar_filter_item`).toHaveCount(0);
await contains(`.o_calendar_filter .o_cw_filter_label`).click();
await runAllTimers();
expect(`.o_calendar_filter .o_calendar_filter_item`).toHaveCount(3);
});
test(`filters can have avatar`, async () => {
await mountWithCleanup(CalendarFilterSection, {
props: {
model: FAKE_MODEL,
section: FAKE_FILTER_SECTIONS[0],
},
});
expect(`.o_calendar_filter .o_cw_filter_avatar`).toHaveCount(3);
expect(`.o_calendar_filter img.o_cw_filter_avatar`).toHaveCount(3);
expect(`.o_calendar_filter .o_calendar_filter_item:eq(0) .o_cw_filter_avatar`).toHaveAttribute(
"data-src",
"/web/image/res.partner/3/avatar_128"
);
expect(`.o_calendar_filter .o_calendar_filter_item:eq(1) .o_cw_filter_avatar`).toHaveAttribute(
"data-src",
"/web/image/res.partner/4/avatar_128"
);
expect(`.o_calendar_filter .o_calendar_filter_item:eq(2) .o_cw_filter_avatar`).toHaveAttribute(
"data-src",
"/web/image/res.partner/6/avatar_128"
);
});
test(`filters with no avatar`, async () => {
await mountWithCleanup(CalendarFilterSection, {
props: {
model: FAKE_MODEL,
section: FAKE_FILTER_SECTIONS[1],
},
});
expect(`.o_calendar_filter .o_calendar_filter_item`).toHaveCount(2);
expect(`.o_calendar_filter .o_cw_filter_avatar`).toHaveCount(0);
});
test(`filter can have remove button`, async () => {
await mountWithCleanup(CalendarFilterSection, {
props: {
model: FAKE_MODEL,
section: FAKE_FILTER_SECTIONS[0],
},
});
expect(`.o_calendar_filter .o_calendar_filter_item`).toHaveCount(3);
expect(`.o_calendar_filter .o_calendar_filter_item .o_remove`).toHaveCount(2);
expect(`.o_calendar_filter .o_calendar_filter_item:eq(0) .o_remove`).toHaveCount(0);
expect(`.o_calendar_filter .o_calendar_filter_item:eq(1) .o_remove`).toHaveCount(1);
expect(`.o_calendar_filter .o_calendar_filter_item:eq(2) .o_remove`).toHaveCount(1);
expect(`.o_calendar_filter .o_calendar_filter_item:eq(3) .o_remove`).toHaveCount(0);
});
test(`click on remove button`, async () => {
await mountWithCleanup(CalendarFilterSection, {
props: {
model: {
...FAKE_MODEL,
unlinkFilter(fieldName, recordId) {
expect.step(`${fieldName} ${recordId}`);
},
},
section: FAKE_FILTER_SECTIONS[0],
},
});
await click(`.o_calendar_filter .o_calendar_filter_item:eq(1) .o_remove`);
await click(`.o_calendar_filter .o_calendar_filter_item:eq(2) .o_remove`);
expect.verifySteps(["partner_ids 1", "partner_ids 2"]);
});
test(`click on filter`, async () => {
await mountWithCleanup(CalendarFilterSection, {
props: {
model: {
...FAKE_MODEL,
updateFilters(fieldName, filters, active) {
expect.step(`${fieldName} ${filters.map((f) => f.value)} ${active}`);
},
},
section: FAKE_FILTER_SECTIONS[0],
},
});
await click(`.o_calendar_filter .o_calendar_filter_item:eq(0) input`);
await click(`.o_calendar_filter .o_calendar_filter_item:eq(1) input`);
await click(`.o_calendar_filter .o_calendar_filter_item:eq(2) input`);
await click(`.o_calendar_filter .o_calendar_filter_items_checkall input`);
await click(`.o_calendar_filter .o_calendar_filter_items_checkall input`);
expect.verifySteps([
"partner_ids 3 false",
"partner_ids 4 false",
"partner_ids 6 true",
"partner_ids 3,4,6 true",
"partner_ids 3,4,6 false",
]);
});

View file

@ -0,0 +1,117 @@
import { expect, test } from "@odoo/hoot";
import { waitFor } from "@odoo/hoot-dom";
import {
contains,
getService,
mountWithCleanup,
preloadBundle,
} from "@web/../tests/web_test_helpers";
import { FAKE_MODEL } from "./calendar_test_helpers";
import { MainComponentsContainer } from "@web/core/main_components_container";
import { CalendarQuickCreate } from "@web/views/calendar/quick_create/calendar_quick_create";
const FAKE_PROPS = {
model: FAKE_MODEL,
record: {},
editRecord() {},
};
/**
* @param {{
* props?: object;
* dialogOptions?: import("@web/core/dialog/dialog_service").DialogServiceInterfaceAddOptions;
* }} [params]
*/
async function start(params = {}) {
await mountWithCleanup(MainComponentsContainer);
getService("dialog").add(
CalendarQuickCreate,
{ ...FAKE_PROPS, ...params.props },
params.dialogOptions
);
await waitFor(`.o_dialog`);
}
preloadBundle("web.fullcalendar_lib");
test.tags("desktop");
test(`mount a CalendarQuickCreate`, async () => {
await start();
expect(`.o-calendar-quick-create`).toHaveCount(1);
expect(`.o_dialog .modal-sm`).toHaveCount(1);
expect(`.modal-title`).toHaveText("New Event");
expect(`input[name="title"]`).toBeFocused();
expect(`.o-calendar-quick-create--create-btn`).toHaveCount(1);
expect(`.o-calendar-quick-create--edit-btn`).toHaveCount(1);
expect(`.o-calendar-quick-create--cancel-btn`).toHaveCount(1);
});
test(`click on create button`, async () => {
await start({
props: {
model: { ...FAKE_MODEL, createRecord: () => expect.step("create") },
},
dialogOptions: { onClose: () => expect.step("close") },
});
await contains(`.o-calendar-quick-create--create-btn`).click();
expect.verifySteps([]);
expect(`input[name=title]`).toHaveClass("o_field_invalid");
});
test(`click on create button (with name)`, async () => {
await start({
props: {
model: {
...FAKE_MODEL,
createRecord(record) {
expect.step("create");
expect(record.title).toBe("TEST");
},
},
},
dialogOptions: { onClose: () => expect.step("close") },
});
await contains(`.o-calendar-quick-create--input`).edit("TEST", { confirm: "blur" });
await contains(`.o-calendar-quick-create--create-btn`).click();
expect.verifySteps(["create", "close"]);
});
test(`click on edit button`, async () => {
await start({
props: { editRecord: () => expect.step("edit") },
dialogOptions: { onClose: () => expect.step("close") },
});
await contains(`.o-calendar-quick-create--edit-btn`).click();
expect.verifySteps(["edit", "close"]);
});
test(`click on edit button (with name)`, async () => {
await start({
props: {
editRecord(record) {
expect.step("edit");
expect(record.title).toBe("TEST");
},
},
dialogOptions: { onClose: () => expect.step("close") },
});
await contains(`.o-calendar-quick-create--input`).edit("TEST", { confirm: "blur" });
await contains(`.o-calendar-quick-create--edit-btn`).click();
expect.verifySteps(["edit", "close"]);
});
test(`click on cancel button`, async () => {
await start({
dialogOptions: { onClose: () => expect.step("close") },
});
await contains(`.o-calendar-quick-create--cancel-btn`).click();
expect.verifySteps(["close"]);
});
test(`check default title`, async () => {
await start({
props: { title: "Example Title" },
});
expect(`.o-calendar-quick-create--input`).toHaveValue("Example Title");
});

View file

@ -1,131 +0,0 @@
/** @odoo-module **/
import { CalendarQuickCreate } from "@web/views/calendar/quick_create/calendar_quick_create";
import { click, getFixture } from "../../helpers/utils";
import { makeEnv, makeFakeModel, mountComponent } from "./helpers";
let target;
async function start(params = {}) {
const { services, props, model: modelParams } = params;
const env = await makeEnv(services);
env.dialogData = {
isActive: true,
close() {},
};
const model = makeFakeModel(modelParams);
return await mountComponent(CalendarQuickCreate, env, {
model,
record: {},
close() {},
editRecord() {},
...props,
});
}
QUnit.module("CalendarView - QuickCreate", ({ beforeEach }) => {
beforeEach(() => {
target = getFixture();
});
QUnit.test("mount a CalendarQuickCreate", async (assert) => {
await start({});
assert.containsOnce(target, ".o-calendar-quick-create");
assert.containsOnce(target, ".o_dialog .modal-sm");
assert.strictEqual(target.querySelector(".modal-title").textContent, "New Event");
assert.strictEqual(target.querySelector(`input[name="title"]`), document.activeElement);
assert.containsOnce(target, ".o-calendar-quick-create--create-btn");
assert.containsOnce(target, ".o-calendar-quick-create--edit-btn");
assert.containsOnce(target, ".o-calendar-quick-create--cancel-btn");
});
QUnit.test("click on create button", async (assert) => {
assert.expect(2);
await start({
props: {
close: () => assert.step("close"),
},
model: {
createRecord: () => assert.step("create"),
},
});
await click(target, ".o-calendar-quick-create--create-btn");
assert.verifySteps([]);
assert.hasClass(target.querySelector("input[name=title]"), "o_field_invalid");
});
QUnit.test("click on create button (with name)", async (assert) => {
assert.expect(4);
await start({
props: {
close: () => assert.step("close"),
},
model: {
createRecord(record) {
assert.step("create");
assert.strictEqual(record.title, "TEST");
},
},
});
const input = target.querySelector(".o-calendar-quick-create--input");
input.value = "TEST";
await click(target, ".o-calendar-quick-create--create-btn");
assert.verifySteps(["create", "close"]);
});
QUnit.test("click on edit button", async (assert) => {
assert.expect(3);
await start({
props: {
close: () => assert.step("close"),
editRecord: () => assert.step("edit"),
},
});
await click(target, ".o-calendar-quick-create--edit-btn");
assert.verifySteps(["edit", "close"]);
});
QUnit.test("click on edit button (with name)", async (assert) => {
assert.expect(4);
await start({
props: {
close: () => assert.step("close"),
editRecord(record) {
assert.step("edit");
assert.strictEqual(record.title, "TEST");
},
},
});
const input = target.querySelector(".o-calendar-quick-create--input");
input.value = "TEST";
await click(target, ".o-calendar-quick-create--edit-btn");
assert.verifySteps(["edit", "close"]);
});
QUnit.test("click on cancel button", async (assert) => {
assert.expect(2);
await start({
props: {
close: () => assert.step("close"),
},
});
await click(target, ".o-calendar-quick-create--cancel-btn");
assert.verifySteps(["close"]);
});
QUnit.test("check default title", async (assert) => {
assert.expect(1);
await start({
props: {
title: "Example Title",
},
});
const input = target.querySelector(".o-calendar-quick-create--input");
assert.strictEqual(input.value, "Example Title");
});
});

View file

@ -0,0 +1,742 @@
import { click, drag, edit, hover, queryFirst, queryRect } from "@odoo/hoot-dom";
import { advanceFrame, advanceTime, animationFrame } from "@odoo/hoot-mock";
import { EventBus } from "@odoo/owl";
import { contains, getMockEnv, swipeLeft, swipeRight } from "@web/../tests/web_test_helpers";
import { createElement } from "@web/core/utils/xml";
import { CalendarModel } from "@web/views/calendar/calendar_model";
import { Field } from "@web/views/fields/field";
export const DEFAULT_DATE = luxon.DateTime.local(2021, 7, 16, 8, 0, 0, 0);
export const FAKE_RECORDS = {
1: {
id: 1,
title: "1 day, all day in July",
start: DEFAULT_DATE,
isAllDay: true,
end: DEFAULT_DATE,
},
2: {
id: 2,
title: "3 days, all day in July",
start: DEFAULT_DATE.plus({ days: 2 }),
isAllDay: true,
end: DEFAULT_DATE.plus({ days: 4 }),
},
3: {
id: 3,
title: "1 day, all day in June",
start: DEFAULT_DATE.plus({ months: -1 }),
isAllDay: true,
end: DEFAULT_DATE.plus({ months: -1 }),
},
4: {
id: 4,
title: "3 days, all day in June",
start: DEFAULT_DATE.plus({ months: -1, days: 2 }),
isAllDay: true,
end: DEFAULT_DATE.plus({ months: -1, days: 4 }),
},
5: {
id: 5,
title: "Over June and July",
start: DEFAULT_DATE.startOf("month").plus({ days: -2 }),
isAllDay: true,
end: DEFAULT_DATE.startOf("month").plus({ days: 2 }),
},
};
export const FAKE_FILTER_SECTIONS = [
{
label: "Attendees",
fieldName: "partner_ids",
avatar: {
model: "res.partner",
field: "avatar_128",
},
hasAvatar: true,
write: {
model: "filter_partner",
field: "partner_id",
},
canAddFilter: true,
filters: [
{
type: "user",
label: "Mitchell Admin",
active: true,
value: 3,
colorIndex: 3,
recordId: null,
canRemove: false,
hasAvatar: true,
},
{
type: "record",
label: "Brandon Freeman",
active: true,
value: 4,
colorIndex: 4,
recordId: 1,
canRemove: true,
hasAvatar: true,
},
{
type: "record",
label: "Marc Demo",
active: false,
value: 6,
colorIndex: 6,
recordId: 2,
canRemove: true,
hasAvatar: true,
},
],
},
{
label: "Users",
fieldName: "user_id",
avatar: {
model: null,
field: null,
},
hasAvatar: false,
write: {
model: null,
field: null,
},
canAddFilter: false,
filters: [
{
type: "record",
label: "Brandon Freeman",
active: false,
value: 1,
colorIndex: false,
recordId: null,
canRemove: true,
hasAvatar: true,
},
{
type: "record",
label: "Marc Demo",
active: false,
value: 2,
colorIndex: false,
recordId: null,
canRemove: true,
hasAvatar: true,
},
],
},
];
export const FAKE_FIELDS = {
id: { string: "Id", type: "integer" },
user_id: { string: "User", type: "many2one", relation: "user", default: -1 },
partner_id: {
string: "Partner",
type: "many2one",
relation: "partner",
related: "user_id.partner_id",
default: 1,
},
name: { string: "Name", type: "char" },
start_date: { string: "Start Date", type: "date" },
stop_date: { string: "Stop Date", type: "date" },
start: { string: "Start Datetime", type: "datetime" },
stop: { string: "Stop Datetime", type: "datetime" },
delay: { string: "Delay", type: "float" },
allday: { string: "Is All Day", type: "boolean" },
partner_ids: {
string: "Attendees",
type: "one2many",
relation: "partner",
default: [[6, 0, [1]]],
},
type: { string: "Type", type: "integer" },
event_type_id: { string: "Event Type", type: "many2one", relation: "event_type" },
color: { string: "Color", type: "integer", related: "event_type_id.color" },
};
export const FAKE_MODEL = {
bus: new EventBus(),
canCreate: true,
canDelete: true,
canEdit: true,
date: DEFAULT_DATE,
fieldMapping: {
date_start: "start_date",
date_stop: "stop_date",
date_delay: "delay",
all_day: "allday",
color: "color",
},
fieldNames: ["start_date", "stop_date", "color", "delay", "allday", "user_id"],
fields: FAKE_FIELDS,
filterSections: FAKE_FILTER_SECTIONS,
firstDayOfWeek: 0,
isDateHidden: false,
isTimeHidden: false,
hasAllDaySlot: true,
hasEditDialog: false,
quickCreate: false,
popoverFieldNodes: {
name: Field.parseFieldNode(
createElement("field", { name: "name" }),
{ event: { fields: FAKE_FIELDS } },
"event",
"calendar"
),
},
activeFields: {
name: {
context: "{}",
invisible: false,
readonly: false,
required: false,
onChange: false,
},
},
rangeEnd: DEFAULT_DATE.endOf("month"),
rangeStart: DEFAULT_DATE.startOf("month"),
records: FAKE_RECORDS,
resModel: "event",
scale: "month",
scales: ["day", "week", "month", "year"],
unusualDays: [],
load() {},
createFilter() {},
createRecord() {},
unlinkFilter() {},
unlinkRecord() {},
updateFilter() {},
updateRecord() {},
};
// DOM Utils
//------------------------------------------------------------------------------
/**
* @param {HTMLElement} element
*/
function instantScrollTo(element) {
element.scrollIntoView({ behavior: "instant", block: "center" });
}
/**
* @param {string} date
* @returns {HTMLElement}
*/
export function findAllDaySlot(date) {
return queryFirst(`.fc-daygrid-body .fc-day[data-date="${date}"]`);
}
/**
* @param {string} date
* @returns {HTMLElement}
*/
export function findDateCell(date) {
return queryFirst(`.fc-day[data-date="${date}"]`);
}
/**
* @param {number} eventId
* @returns {HTMLElement}
*/
export function findEvent(eventId) {
return queryFirst(`.o_event[data-event-id="${eventId}"]`);
}
/**
* @param {string} date
* @returns {HTMLElement}
*/
export function findDateColumn(date) {
return queryFirst(`.fc-col-header-cell.fc-day[data-date="${date}"]`);
}
/**
* @param {string} time
* @returns {HTMLElement}
*/
export function findTimeRow(time) {
return queryFirst(`.fc-timegrid-slot[data-time="${time}"]:eq(1)`);
}
/**
* @param {string} sectionName
* @returns {HTMLElement}
*/
export function findFilterPanelSection(sectionName) {
return queryFirst(`.o_calendar_filter[data-name="${sectionName}"]`);
}
/**
* @param {string} sectionName
* @param {string} filterValue
* @returns {HTMLElement}
*/
export function findFilterPanelFilter(sectionName, filterValue) {
const root = findFilterPanelSection(sectionName);
return queryFirst(`.o_calendar_filter_item[data-value="${filterValue}"]`, { root });
}
/**
* @param {string} sectionName
* @returns {HTMLElement}
*/
export function findFilterPanelSectionFilter(sectionName) {
const root = findFilterPanelSection(sectionName);
return queryFirst(`.o_calendar_filter_items_checkall`, { root });
}
/**
* @param {string} date
* @returns {Promise<void>}
*/
export async function pickDate(date) {
const day = date.split("-")[2];
const iDay = parseInt(day, 10) - 1;
await click(`.o_datetime_picker .o_date_item_cell:not(.o_out_of_range):eq(${iDay})`);
await animationFrame();
}
/**
* @param {string} date
* @returns {Promise<void>}
*/
export async function clickAllDaySlot(date) {
const slot = findAllDaySlot(date);
instantScrollTo(slot);
await click(slot);
await animationFrame();
}
/**
* @param {string} date
* @returns {Promise<void>}
*/
export async function clickDate(date) {
const cell = findDateCell(date);
instantScrollTo(cell);
await click(cell);
await advanceTime(500);
}
/**
* @param {number} eventId
* @returns {Promise<void>}
*/
export async function clickEvent(eventId) {
const eventEl = findEvent(eventId);
instantScrollTo(eventEl);
await click(eventEl);
await advanceTime(500); // wait for the popover to open (debounced)
}
export function expandCalendarView() {
// Expends Calendar view and FC too
let tmpElement = queryFirst(".fc");
do {
tmpElement = tmpElement.parentElement;
tmpElement.classList.add("h-100");
} while (!tmpElement.classList.contains("o_view_controller"));
}
/**
* @param {string} startDateTime
* @param {string} endDateTime
* @returns {Promise<void>}
*/
export async function selectTimeRange(startDateTime, endDateTime) {
const [startDate, startTime] = startDateTime.split(" ");
const [endDate, endTime] = endDateTime.split(" ");
// Try to display both rows on the screen before drag'n'drop.
const startHour = Number(startTime.slice(0, 2));
const endHour = Number(endTime.slice(0, 2));
const midHour = Math.floor((startHour + endHour) / 2);
const midTime = `${String(midHour).padStart(2, "0")}:00:00`;
instantScrollTo(
queryFirst(`.fc-timegrid-slot[data-time="${midTime}"]:eq(1)`, { visible: false })
);
const startColumnRect = queryRect(`.fc-col-header-cell.fc-day[data-date="${startDate}"]`);
const startRow = queryFirst(`.fc-timegrid-slot[data-time="${startTime}"]:eq(1)`);
const endColumnRect = queryRect(`.fc-col-header-cell.fc-day[data-date="${endDate}"]`);
const endRow = queryFirst(`.fc-timegrid-slot[data-time="${endTime}"]:eq(1)`);
const optionStart = {
relative: true,
position: { y: 1, x: startColumnRect.left },
};
await hover(startRow, optionStart);
await animationFrame();
const { drop } = await drag(startRow, optionStart);
await animationFrame();
await drop(endRow, {
position: { y: -1, x: endColumnRect.left },
relative: true,
});
await animationFrame();
}
/**
* @param {string} startDate
* @param {string} endDate
* @returns {Promise<void>}
*/
export async function selectDateRange(startDate, endDate) {
const startCell = findDateCell(startDate);
const endCell = findDateCell(endDate);
instantScrollTo(startCell);
await hover(startCell);
await animationFrame();
const { moveTo, drop } = await drag(startCell);
await animationFrame();
await moveTo(endCell);
await animationFrame();
await drop();
await animationFrame();
}
/**
* @param {string} startDate
* @param {string} endDate
* @returns {Promise<void>}
*/
export async function selectAllDayRange(startDate, endDate) {
const start = findAllDaySlot(startDate);
const end = findAllDaySlot(endDate);
instantScrollTo(start);
await hover(start);
await animationFrame();
const { drop } = await drag(start);
await animationFrame();
await drop(end);
await animationFrame();
}
export async function closeCwPopOver() {
if (getMockEnv().isSmall) {
await contains(`.oi-arrow-left`).click();
} else {
await contains(`.o_cw_popover_close`).click();
}
}
/**
* @param {number} eventId
* @param {string} date
* @param {{ disableDrop: boolean }} [options]
* @returns {Promise<void>}
*/
export async function moveEventToDate(eventId, date, options) {
const eventEl = findEvent(eventId);
const cell = findDateCell(date);
instantScrollTo(eventEl);
await hover(eventEl);
await animationFrame();
const { drop, moveTo } = await drag(eventEl);
await animationFrame();
await moveTo(cell);
await animationFrame();
if (!options?.disableDrop) {
await drop();
}
await animationFrame();
await animationFrame();
}
/**
* @param {number} eventId
* @param {string} dateTime
* @returns {Promise<void>}
*/
export async function moveEventToTime(eventId, dateTime) {
const eventEl = findEvent(eventId);
const [date, time] = dateTime.split(" ");
instantScrollTo(eventEl);
const row = findTimeRow(time);
const rowRect = queryRect(row);
const column = findDateColumn(date);
const columnRect = queryRect(column);
const { drop, moveTo } = await drag(eventEl, {
position: { y: 1 },
relative: true,
});
if (getMockEnv().isSmall) {
await advanceTime(500);
}
await animationFrame();
await moveTo(row, {
position: {
y: rowRect.y + 1,
x: columnRect.x + columnRect.width / 2,
},
});
await animationFrame();
await drop();
await advanceFrame(5);
}
export async function selectHourOnPicker(selectedValue) {
await click(".o_time_picker_input:eq(0)");
await animationFrame();
await edit(selectedValue, { confirm: "enter" });
await animationFrame();
}
/**
* @param {number} eventId
* @param {string} date
* @returns {Promise<void>}
*/
export async function moveEventToAllDaySlot(eventId, date) {
const eventEl = findEvent(eventId);
const slot = findAllDaySlot(date);
instantScrollTo(eventEl);
const columnRect = queryRect(eventEl);
const slotRect = queryRect(slot);
const { drop, moveTo } = await drag(eventEl, {
position: { y: 1 },
relative: true,
});
if (getMockEnv().isSmall) {
await advanceTime(500);
}
await animationFrame();
await moveTo(slot, {
position: {
x: columnRect.x + columnRect.width / 2,
y: slotRect.y,
},
});
await animationFrame();
await drop();
await advanceFrame(5);
}
/**
* @param {number} eventId
* @param {string} dateTime
* @returns {Promise<void>}
*/
export async function resizeEventToTime(eventId, dateTime) {
const eventEl = findEvent(eventId);
instantScrollTo(eventEl);
await hover(`.fc-event-main:first`, { root: eventEl });
await animationFrame();
const resizer = queryFirst(`.fc-event-resizer-end`, { root: eventEl });
Object.assign(resizer.style, {
display: "block",
height: "1px",
bottom: "0",
});
const [date, time] = dateTime.split(" ");
const row = findTimeRow(time);
const column = findDateColumn(date);
const columnRect = queryRect(column);
await (
await drag(resizer)
).drop(row, {
position: { x: columnRect.x, y: -1 },
relative: true,
});
await advanceTime(500);
}
/**
* @param {number} eventId
* @param {string} date
* @returns {Promise<void>}
*/
export async function resizeEventToDate(eventId, date) {
const eventEl = findEvent(eventId);
const slot = findAllDaySlot(date);
instantScrollTo(eventEl);
await hover(".fc-event-main", { root: eventEl });
await animationFrame();
// Show the resizer
const resizer = queryFirst(".fc-event-resizer-end", { root: eventEl });
Object.assign(resizer.style, { display: "block", height: "1px", bottom: "0" });
instantScrollTo(slot);
const rowRect = queryRect(resizer);
// Find the date cell and calculate the positions for dragging
const dateCell = findDateCell(date);
const columnRect = queryRect(dateCell);
// Perform the drag-and-drop operation
await hover(resizer, {
position: { x: 0 },
relative: true,
});
await animationFrame();
const { drop } = await drag(resizer);
await animationFrame();
await drop(dateCell, {
position: { y: rowRect.y - columnRect.y },
relative: true,
});
await advanceTime(500);
}
/**
* @param {"day" | "week" | "month" | "year"} scale
* @returns {Promise<void>}
*/
export async function changeScale(scale) {
await contains(`.o_view_scale_selector .scale_button_selection`).click();
await contains(`.o-dropdown--menu .o_scale_button_${scale}`).click();
}
export async function displayCalendarPanel() {
if (getMockEnv().isSmall) {
await contains(".o_calendar_container .o_other_calendar_panel").click();
}
}
export async function hideCalendarPanel() {
if (getMockEnv().isSmall) {
await contains(".o_calendar_container .o_other_calendar_panel").click();
}
}
/**
* @param {"prev" | "next"} direction
* @returns {Promise<void>}
*/
export async function navigate(direction) {
if (getMockEnv().isSmall) {
if (direction === "next") {
await swipeLeft(".o_calendar_widget");
} else {
await swipeRight(".o_calendar_widget");
}
await advanceFrame(16);
} else {
await contains(`.o_calendar_navigation_buttons .o_calendar_button_${direction}`).click();
}
}
/**
* @param {string} sectionName
* @param {string} filterValue
* @returns {Promise<void>}
*/
export async function toggleFilter(sectionName, filterValue) {
const otherCalendarPanel = queryFirst(".o_other_calendar_panel");
if (otherCalendarPanel) {
click(otherCalendarPanel);
await animationFrame();
}
const root = findFilterPanelFilter(sectionName, filterValue);
const input = queryFirst(`input`, { root });
instantScrollTo(input);
await click(input);
await animationFrame();
if (otherCalendarPanel) {
await click(otherCalendarPanel);
await animationFrame();
}
await advanceTime(CalendarModel.DEBOUNCED_LOAD_DELAY);
await animationFrame();
}
/**
* @param {string} sectionName
* @returns {Promise<void>}
*/
export async function toggleSectionFilter(sectionName) {
const otherCalendarPanel = queryFirst(".o_other_calendar_panel");
if (otherCalendarPanel) {
await click(otherCalendarPanel);
await animationFrame();
}
const root = findFilterPanelSectionFilter(sectionName);
const input = queryFirst(`input`, { root });
instantScrollTo(input);
await click(input);
await animationFrame();
if (otherCalendarPanel) {
await click(otherCalendarPanel);
await animationFrame();
}
await advanceTime(CalendarModel.DEBOUNCED_LOAD_DELAY);
await animationFrame();
}
/**
* @param {string} sectionName
* @param {string} filterValue
* @returns {Promise<void>}
*/
export async function removeFilter(sectionName, filterValue) {
const root = findFilterPanelFilter(sectionName, filterValue);
const button = queryFirst(`.o_remove`, { root });
instantScrollTo(button);
await click(button);
await advanceTime(CalendarModel.DEBOUNCED_LOAD_DELAY);
await animationFrame();
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,115 @@
import { describe, expect, test } from "@odoo/hoot";
import { queryAllTexts } from "@odoo/hoot-dom";
import { contains, mountWithCleanup, preloadBundle } from "@web/../tests/web_test_helpers";
import { DEFAULT_DATE, FAKE_MODEL } from "./calendar_test_helpers";
import { CalendarYearPopover } from "@web/views/calendar/calendar_year/calendar_year_popover";
describe.current.tags("desktop");
const FAKE_RECORDS = [
{
id: 1,
start: DEFAULT_DATE,
end: DEFAULT_DATE,
isAllDay: true,
title: "R1",
},
{
id: 2,
start: DEFAULT_DATE.set({ hours: 14 }),
end: DEFAULT_DATE.set({ hours: 16 }),
isAllDay: false,
title: "R2",
},
{
id: 3,
start: DEFAULT_DATE.minus({ days: 1 }),
end: DEFAULT_DATE.plus({ days: 1 }),
isAllDay: true,
title: "R3",
},
{
id: 4,
start: DEFAULT_DATE.minus({ days: 3 }),
end: DEFAULT_DATE.plus({ days: 1 }),
isAllDay: true,
title: "R4",
},
{
id: 5,
start: DEFAULT_DATE.minus({ days: 1 }),
end: DEFAULT_DATE.plus({ days: 3 }),
isAllDay: true,
title: "R5",
},
];
const FAKE_PROPS = {
model: FAKE_MODEL,
date: DEFAULT_DATE,
records: FAKE_RECORDS,
createRecord() {},
deleteRecord() {},
editRecord() {},
close() {},
};
async function start(props = {}) {
await mountWithCleanup(CalendarYearPopover, {
props: { ...FAKE_PROPS, ...props },
});
}
preloadBundle("web.fullcalendar_lib");
test(`canCreate is true`, async () => {
await start({
model: { ...FAKE_MODEL, canCreate: true },
});
expect(`.o_cw_popover_create`).toHaveCount(1);
});
test(`canCreate is false`, async () => {
await start({
model: { ...FAKE_MODEL, canCreate: false },
});
expect(`.o_cw_popover_create`).toHaveCount(0);
});
test(`click on create button`, async () => {
await start({
createRecord: () => expect.step("create"),
model: { ...FAKE_MODEL, canCreate: true },
});
expect(`.o_cw_popover_create`).toHaveCount(1);
await contains(`.o_cw_popover_create`).click();
expect.verifySteps(["create"]);
});
test(`group records`, async () => {
await start();
expect(`.o_cw_body > div`).toHaveCount(4);
expect(`.o_cw_body > a`).toHaveCount(1);
expect(queryAllTexts`.o_cw_body > div`).toEqual([
"July 16, 2021\nR1\n14:00\nR2",
"July 13-17, 2021\nR4",
"July 15-17, 2021\nR3",
"July 15-19, 2021\nR5",
]);
expect(`.o_cw_body`).toHaveText(
"July 16, 2021\nR1\n14:00\nR2\nJuly 13-17, 2021\nR4\nJuly 15-17, 2021\nR3\nJuly 15-19, 2021\nR5\n Create"
);
});
test(`click on record`, async () => {
await start({
records: [FAKE_RECORDS[3]],
editRecord: () => expect.step("edit"),
});
expect(`.o_cw_body a.o_cw_popover_link`).toHaveCount(1);
await contains(`.o_cw_body a.o_cw_popover_link`).click();
expect.verifySteps(["edit"]);
});

View file

@ -1,120 +0,0 @@
/** @odoo-module **/
import { CalendarYearPopover } from "@web/views/calendar/calendar_year/calendar_year_popover";
import { click, getFixture } from "../../helpers/utils";
import { mountComponent, makeEnv, makeFakeModel, makeFakeRecords, makeFakeDate } from "./helpers";
let target, fakePopoverRecords;
async function start(params = {}) {
const { services, props, model: modelParams } = params;
const env = await makeEnv(services);
const model = makeFakeModel(modelParams);
return await mountComponent(CalendarYearPopover, env, {
model,
date: makeFakeDate(),
records: fakePopoverRecords,
createRecord() {},
deleteRecord() {},
editRecord() {},
close() {},
...props,
});
}
QUnit.module("CalendarView - YearPopover", ({ beforeEach }) => {
beforeEach(() => {
target = getFixture();
fakePopoverRecords = [
{
id: 1,
start: makeFakeDate(),
end: makeFakeDate(),
isAllDay: true,
title: "R1",
},
{
id: 2,
start: makeFakeDate().set({ hours: 14 }),
end: makeFakeDate().set({ hours: 16 }),
isAllDay: false,
title: "R2",
},
{
id: 3,
start: makeFakeDate().minus({ days: 1 }),
end: makeFakeDate().plus({ days: 1 }),
isAllDay: true,
title: "R3",
},
{
id: 4,
start: makeFakeDate().minus({ days: 3 }),
end: makeFakeDate().plus({ days: 1 }),
isAllDay: true,
title: "R4",
},
{
id: 5,
start: makeFakeDate().minus({ days: 1 }),
end: makeFakeDate().plus({ days: 3 }),
isAllDay: true,
title: "R5",
},
];
});
QUnit.test("canCreate is true", async (assert) => {
await start({ model: { canCreate: true } });
assert.containsOnce(target, ".o_cw_popover_create");
});
QUnit.test("canCreate is false", async (assert) => {
await start({ model: { canCreate: false } });
assert.containsNone(target, ".o_cw_popover_create");
});
QUnit.test("click on create button", async (assert) => {
assert.expect(3);
await start({
props: {
createRecord: () => assert.step("create"),
},
model: { canCreate: true },
});
assert.containsOnce(target, ".o_cw_popover_create");
await click(target, ".o_cw_popover_create");
assert.verifySteps(["create"]);
});
QUnit.test("group records", async (assert) => {
await start({});
assert.containsN(target, ".o_cw_body > div", 5);
assert.containsN(target, ".o_cw_body > a", 5);
const sectionTitles = target.querySelectorAll(".o_cw_body > div");
assert.strictEqual(sectionTitles[0].textContent.trim(), "July 16, 2021");
assert.strictEqual(sectionTitles[1].textContent.trim(), "July 13-17, 2021");
assert.strictEqual(sectionTitles[2].textContent.trim(), "July 15-17, 2021");
assert.strictEqual(sectionTitles[3].textContent.trim(), "July 15-19, 2021");
assert.strictEqual(
target.querySelector(".o_cw_body").textContent.trim(),
"July 16, 2021R114:00 R2July 13-17, 2021R4July 15-17, 2021R3July 15-19, 2021R5 Create"
);
});
QUnit.test("click on record", async (assert) => {
assert.expect(3);
await start({
props: {
records: [makeFakeRecords()[3]],
editRecord: () => assert.step("edit"),
},
});
assert.containsOnce(target, ".o_cw_body > a");
await click(target, ".o_cw_body > a");
assert.verifySteps(["edit"]);
});
});

View file

@ -0,0 +1,158 @@
import { expect, test } from "@odoo/hoot";
import { queryAllTexts, resize } from "@odoo/hoot-dom";
import { mockTimeZone, runAllTimers } from "@odoo/hoot-mock";
import {
mockService,
mountWithCleanup,
preloadBundle,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import { FAKE_MODEL, clickDate, selectDateRange } from "./calendar_test_helpers";
import { CalendarYearRenderer } from "@web/views/calendar/calendar_year/calendar_year_renderer";
const FAKE_PROPS = {
model: FAKE_MODEL,
createRecord() {},
deleteRecord() {},
editRecord() {},
};
async function start(props = {}) {
await mountWithCleanup(CalendarYearRenderer, {
props: { ...FAKE_PROPS, ...props },
});
}
preloadBundle("web.fullcalendar_lib");
test(`mount a CalendarYearRenderer`, async () => {
await start();
expect(`.fc-month-container`).toHaveCount(12);
// check "title format"
expect(`.fc-toolbar-chunk:nth-child(2) .fc-toolbar-title`).toHaveCount(12);
expect(queryAllTexts`.fc-toolbar-chunk:nth-child(2) .fc-toolbar-title`).toEqual([
"January 2021",
"February 2021",
"March 2021",
"April 2021",
"May 2021",
"June 2021",
"July 2021",
"August 2021",
"September 2021",
"October 2021",
"November 2021",
"December 2021",
]);
// check day header format
expect(`.fc-month:eq(0) .fc-col-header-cell`).toHaveCount(7);
expect(queryAllTexts`.fc-month:eq(0) .fc-col-header-cell`).toEqual([
"S",
"M",
"T",
"W",
"T",
"F",
"S",
]);
// check showNonCurrentDates
expect(`:not(.fc-day-disabled) > * > * > .fc-daygrid-day-number`).toHaveCount(365);
});
test.tags("desktop");
test(`display events`, async () => {
mockService("popover", () => ({
add(target, component, props) {
expect.step(`${props.date.toISODate()} ${props.records[0].title}`);
return () => {};
},
}));
await start({
createRecord(record) {
expect.step(`${record.start.toISODate()} allDay:${record.isAllDay} no event`);
},
});
await clickDate("2021-07-15");
expect.verifySteps(["2021-07-15 allDay:true no event"]);
await clickDate("2021-07-16");
expect.verifySteps(["2021-07-16 1 day, all day in July"]);
await clickDate("2021-07-17");
expect.verifySteps(["2021-07-17 allDay:true no event"]);
await clickDate("2021-07-18");
expect.verifySteps(["2021-07-18 3 days, all day in July"]);
await clickDate("2021-07-19");
expect.verifySteps(["2021-07-19 3 days, all day in July"]);
await clickDate("2021-07-20");
expect.verifySteps(["2021-07-20 3 days, all day in July"]);
await clickDate("2021-07-21");
expect.verifySteps(["2021-07-21 allDay:true no event"]);
await clickDate("2021-06-28");
expect.verifySteps(["2021-06-28 allDay:true no event"]);
await clickDate("2021-06-29");
expect.verifySteps(["2021-06-29 Over June and July"]);
await clickDate("2021-06-30");
expect.verifySteps(["2021-06-30 Over June and July"]);
await clickDate("2021-07-01");
expect.verifySteps(["2021-07-01 Over June and July"]);
await clickDate("2021-07-02");
expect.verifySteps(["2021-07-02 Over June and July"]);
await clickDate("2021-07-03");
expect.verifySteps(["2021-07-03 Over June and July"]);
await clickDate("2021-07-04");
expect.verifySteps(["2021-07-04 allDay:true no event"]);
});
test.tags("desktop");
test(`select a range of date`, async () => {
await start({
createRecord({ isAllDay, start, end }) {
expect.step("create");
expect(isAllDay).toBe(true);
expect(start.toSQL()).toBe("2021-07-02 00:00:00.000 +01:00");
expect(end.toSQL()).toBe("2021-07-05 00:00:00.000 +01:00");
},
});
await selectDateRange("2021-07-02", "2021-07-05");
expect.verifySteps(["create"]);
});
test(`display correct column header for days, independent of the timezone`, async () => {
// Regression test: when the system tz is somewhere in a negative GMT (in our example Alaska)
// the day headers of a months were incorrectly set. (S S M T W T F) instead of (S M T W T F S)
// if the first day of the week is Sunday.
mockTimeZone(-9);
await start();
expect(queryAllTexts`.fc-month:eq(0) .fc-col-header-cell`).toEqual([
"S",
"M",
"T",
"W",
"T",
"F",
"S",
]);
});
test("remove row when no day of current month", async () => {
await start();
expect(".fc-day-other, .fc-day-disabled").toHaveCount(76);
});
test("resize callback is being called", async () => {
patchWithCleanup(CalendarYearRenderer.prototype, {
onWindowResize() {
expect.step("onWindowResize");
},
});
await start();
expect.verifySteps([]);
await resize({ height: 500 });
await runAllTimers();
expect.verifySteps(new Array(12).fill("onWindowResize")); // one for each FullCalendar instance
});

View file

@ -1,154 +0,0 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { CalendarYearRenderer } from "@web/views/calendar/calendar_year/calendar_year_renderer";
import { getFixture, patchTimeZone, patchWithCleanup } from "../../helpers/utils";
import { clickDate, mountComponent, selectDateRange, makeEnv, makeFakeModel } from "./helpers";
let target;
async function start(params = {}) {
const { services, props, model: modelParams } = params;
const env = await makeEnv(services);
const model = makeFakeModel(modelParams);
return await mountComponent(CalendarYearRenderer, env, {
model,
createRecord() {},
deleteRecord() {},
editRecord() {},
...props,
});
}
QUnit.module("CalendarView - YearRenderer", ({ beforeEach }) => {
beforeEach(() => {
target = getFixture();
});
QUnit.test("mount a CalendarYearRenderer", async (assert) => {
await start({});
assert.containsN(target, ".fc-month-container", 12);
const monthHeaders = target.querySelectorAll(".fc-header-toolbar .fc-center");
// check "title format"
assert.strictEqual(monthHeaders.length, 12);
const monthTitles = [
"Jan 2021",
"Feb 2021",
"Mar 2021",
"Apr 2021",
"May 2021",
"Jun 2021",
"Jul 2021",
"Aug 2021",
"Sep 2021",
"Oct 2021",
"Nov 2021",
"Dec 2021",
];
for (let i = 0; i < 12; i++) {
assert.strictEqual(monthHeaders[i].textContent, monthTitles[i]);
}
const dayHeaders = target
.querySelector(".fc-month-container")
.querySelectorAll(".fc-day-header");
// check day header format
assert.strictEqual(dayHeaders.length, 7);
const dayTitles = ["S", "M", "T", "W", "T", "F", "S"];
for (let i = 0; i < 7; i++) {
assert.strictEqual(dayHeaders[i].textContent, dayTitles[i]);
}
// check showNonCurrentDates
assert.containsN(target, ".fc-day-number", 365);
});
QUnit.test("display events", async (assert) => {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
});
await start({
props: {
createRecord(record) {
assert.step(`${record.start.toISODate()} allDay:${record.isAllDay} no event`);
},
},
services: {
popover: {
start: () => ({
add: (target, _, props) => {
assert.step(`${props.date.toISODate()} ${props.records[0].title}`);
return () => {};
},
}),
},
},
});
await clickDate(target, "2021-07-15");
assert.verifySteps(["2021-07-15 allDay:true no event"]);
await clickDate(target, "2021-07-16");
assert.verifySteps(["2021-07-16 1 day, all day in July"]);
await clickDate(target, "2021-07-17");
assert.verifySteps(["2021-07-17 allDay:true no event"]);
await clickDate(target, "2021-07-18");
assert.verifySteps(["2021-07-18 3 days, all day in July"]);
await clickDate(target, "2021-07-19");
assert.verifySteps(["2021-07-19 3 days, all day in July"]);
await clickDate(target, "2021-07-20");
assert.verifySteps(["2021-07-20 3 days, all day in July"]);
await clickDate(target, "2021-07-21");
assert.verifySteps(["2021-07-21 allDay:true no event"]);
await clickDate(target, "2021-06-28");
assert.verifySteps(["2021-06-28 allDay:true no event"]);
await clickDate(target, "2021-06-29");
assert.verifySteps(["2021-06-29 Over June and July"]);
await clickDate(target, "2021-06-30");
assert.verifySteps(["2021-06-30 Over June and July"]);
await clickDate(target, "2021-07-01");
assert.verifySteps(["2021-07-01 Over June and July"]);
await clickDate(target, "2021-07-02");
assert.verifySteps(["2021-07-02 Over June and July"]);
await clickDate(target, "2021-07-03");
assert.verifySteps(["2021-07-03 Over June and July"]);
await clickDate(target, "2021-07-04");
assert.verifySteps(["2021-07-04 allDay:true no event"]);
});
QUnit.test("select a range of date", async (assert) => {
assert.expect(3);
await start({
props: {
createRecord(record) {
assert.ok(record.isAllDay);
assert.ok(record.start.equals(luxon.DateTime.local(2021, 7, 2, 0, 0, 0, 0)));
assert.ok(record.end.equals(luxon.DateTime.local(2021, 7, 5, 0, 0, 0, 0)));
},
},
});
await selectDateRange(target, "2021-07-02", "2021-07-05");
});
QUnit.test("display correct column header for days, independent of the timezone", async (assert) => {
// Regression test: when the system tz is somewhere in a negative GMT (in our example Alaska)
// the day headers of a months were incorrectly set. (S S M T W T F) instead of (S M T W T F S)
// if the first day of the week is Sunday.
patchTimeZone(-540); // UTC-9 = Alaska
await start({});
const dayHeaders = target
.querySelector(".fc-month-container")
.querySelectorAll(".fc-day-header");
assert.deepEqual([...dayHeaders].map((el) => el.textContent), ["S", "M", "T", "W", "T", "F", "S"]);
});
});

View file

@ -1,536 +0,0 @@
/** @odoo-module **/
import { uiService } from "@web/core/ui/ui_service";
import { registry } from "@web/core/registry";
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
import { click, getFixture, mount, nextTick, triggerEvent } from "../../helpers/utils";
import { setupViewRegistries } from "@web/../tests/views/helpers";
export function makeEnv(services = {}) {
clearRegistryWithCleanup(registry.category("main_components"));
setupViewRegistries();
services = Object.assign(
{
ui: uiService,
},
services
);
for (const [key, service] of Object.entries(services)) {
registry.category("services").add(key, service, { force: true });
}
return makeTestEnv({
config: {
setDisplayName: () => {},
},
});
}
//------------------------------------------------------------------------------
export async function mountComponent(C, env, props) {
const target = getFixture();
return await mount(C, target, { env, props });
}
//------------------------------------------------------------------------------
export function makeFakeDate() {
return luxon.DateTime.local(2021, 7, 16, 8, 0, 0, 0);
}
export function makeFakeRecords() {
return {
1: {
id: 1,
title: "1 day, all day in July",
start: makeFakeDate(),
isAllDay: true,
end: makeFakeDate(),
},
2: {
id: 2,
title: "3 days, all day in July",
start: makeFakeDate().plus({ days: 2 }),
isAllDay: true,
end: makeFakeDate().plus({ days: 4 }),
},
3: {
id: 3,
title: "1 day, all day in June",
start: makeFakeDate().plus({ months: -1 }),
isAllDay: true,
end: makeFakeDate().plus({ months: -1 }),
},
4: {
id: 4,
title: "3 days, all day in June",
start: makeFakeDate().plus({ months: -1, days: 2 }),
isAllDay: true,
end: makeFakeDate().plus({ months: -1, days: 4 }),
},
5: {
id: 5,
title: "Over June and July",
start: makeFakeDate().startOf("month").plus({ days: -2 }),
isAllDay: true,
end: makeFakeDate().startOf("month").plus({ days: 2 }),
},
};
}
export const FAKE_FILTER_SECTIONS = [
{
label: "Attendees",
fieldName: "partner_ids",
avatar: {
model: "res.partner",
field: "avatar_128",
},
hasAvatar: true,
write: {
model: "filter_partner",
field: "partner_id",
},
canCollapse: true,
canAddFilter: true,
filters: [
{
type: "user",
label: "Mitchell Admin",
active: true,
value: 3,
colorIndex: 3,
recordId: null,
canRemove: false,
hasAvatar: true,
},
{
type: "all",
label: "Everybody's calendar",
active: false,
value: "all",
colorIndex: null,
recordId: null,
canRemove: false,
hasAvatar: false,
},
{
type: "record",
label: "Brandon Freeman",
active: true,
value: 4,
colorIndex: 4,
recordId: 1,
canRemove: true,
hasAvatar: true,
},
{
type: "record",
label: "Marc Demo",
active: false,
value: 6,
colorIndex: 6,
recordId: 2,
canRemove: true,
hasAvatar: true,
},
],
},
{
label: "Users",
fieldName: "user_id",
avatar: {
model: null,
field: null,
},
hasAvatar: false,
write: {
model: null,
field: null,
},
canCollapse: false,
canAddFilter: false,
filters: [
{
type: "record",
label: "Brandon Freeman",
active: false,
value: 1,
colorIndex: false,
recordId: null,
canRemove: true,
hasAvatar: true,
},
{
type: "record",
label: "Marc Demo",
active: false,
value: 2,
colorIndex: false,
recordId: null,
canRemove: true,
hasAvatar: true,
},
],
},
];
export const FAKE_FIELDS = {
id: { string: "Id", type: "integer" },
user_id: { string: "User", type: "many2one", relation: "user", default: -1 },
partner_id: {
string: "Partner",
type: "many2one",
relation: "partner",
related: "user_id.partner_id",
default: 1,
},
name: { string: "Name", type: "char" },
start_date: { string: "Start Date", type: "date" },
stop_date: { string: "Stop Date", type: "date" },
start: { string: "Start Datetime", type: "datetime" },
stop: { string: "Stop Datetime", type: "datetime" },
delay: { string: "Delay", type: "float" },
allday: { string: "Is All Day", type: "boolean" },
partner_ids: {
string: "Attendees",
type: "one2many",
relation: "partner",
default: [[6, 0, [1]]],
},
type: { string: "Type", type: "integer" },
event_type_id: { string: "Event Type", type: "many2one", relation: "event_type" },
color: { string: "Color", type: "integer", related: "event_type_id.color" },
};
function makeFakeModelState() {
return {
canCreate: true,
canDelete: true,
canEdit: true,
date: makeFakeDate(),
fieldMapping: {
date_start: "start_date",
date_stop: "stop_date",
date_delay: "delay",
all_day: "allday",
color: "color",
},
fieldNames: ["start_date", "stop_date", "color", "delay", "allday", "user_id"],
fields: FAKE_FIELDS,
filterSections: FAKE_FILTER_SECTIONS,
firstDayOfWeek: 0,
isDateHidden: false,
isTimeHidden: false,
hasAllDaySlot: true,
hasEditDialog: false,
hasQuickCreate: false,
popoverFields: {
name: { rawAttrs: {}, options: {} },
},
rangeEnd: makeFakeDate().endOf("month"),
rangeStart: makeFakeDate().startOf("month"),
records: makeFakeRecords(),
resModel: "event",
scale: "month",
scales: ["day", "week", "month", "year"],
unusualDays: [],
};
}
export function makeFakeModel(state = {}) {
return {
...makeFakeModelState(),
load() {},
createFilter() {},
createRecord() {},
unlinkFilter() {},
unlinkRecord() {},
updateFilter() {},
updateRecord() {},
...state,
};
}
// DOM Utils
//------------------------------------------------------------------------------
async function scrollTo(el, scrollParam) {
el.scrollIntoView(scrollParam);
await new Promise(window.requestAnimationFrame);
}
export function findPickedDate(target) {
return target.querySelector(".ui-datepicker-current-day");
}
export async function pickDate(target, date) {
const [year, month, day] = date.split("-");
const iMonth = parseInt(month, 10) - 1;
const iDay = parseInt(day, 10) - 1;
const el = target.querySelectorAll(
`.ui-datepicker-calendar td[data-year="${year}"][data-month="${iMonth}"]`
)[iDay];
el.scrollIntoView();
await click(el);
}
function findAllDaySlot(target, date) {
return target.querySelector(`.fc-day-grid .fc-day[data-date="${date}"]`);
}
export function findDateCell(target, date) {
return target.querySelector(`.fc-day-top[data-date="${date}"]`);
}
export function findEvent(target, eventId) {
return target.querySelector(`.o_event[data-event-id="${eventId}"]`);
}
function findDateCol(target, date) {
return target.querySelector(`.fc-day-header[data-date="${date}"]`);
}
export function findTimeRow(target, time) {
return target.querySelector(`.fc-slats [data-time="${time}"] .fc-widget-content`);
}
async function triggerEventForCalendar(el, type, position = {}) {
const rect = el.getBoundingClientRect();
const x = position.x || rect.x + rect.width / 2;
const y = position.y || rect.y + rect.height / 2;
const attrs = {
which: 1,
clientX: x,
clientY: y,
};
await triggerEvent(el, null, type, attrs);
}
export async function clickAllDaySlot(target, date) {
const el = findAllDaySlot(target, date);
await scrollTo(el);
await triggerEventForCalendar(el, "mousedown");
await triggerEventForCalendar(el, "mouseup");
await nextTick();
}
export async function clickDate(target, date) {
const el = findDateCell(target, date);
await scrollTo(el);
await triggerEventForCalendar(el, "mousedown");
await triggerEventForCalendar(el, "mouseup");
await nextTick();
}
export async function clickEvent(target, eventId) {
const el = findEvent(target, eventId);
await scrollTo(el);
await click(el);
await nextTick();
}
export async function selectTimeRange(target, startDateTime, endDateTime) {
const [startDate, startTime] = startDateTime.split(" ");
const [endDate, endTime] = endDateTime.split(" ");
const startCol = findDateCol(target, startDate);
const endCol = findDateCol(target, endDate);
const startRow = findTimeRow(target, startTime);
const endRow = findTimeRow(target, endTime);
await scrollTo(startRow);
const startColRect = startCol.getBoundingClientRect();
const startRowRect = startRow.getBoundingClientRect();
await triggerEventForCalendar(startRow, "mousedown", {
x: startColRect.x + startColRect.width / 2,
y: startRowRect.y + 1,
});
await scrollTo(endRow, false);
const endColRect = endCol.getBoundingClientRect();
const endRowRect = endRow.getBoundingClientRect();
await triggerEventForCalendar(endRow, "mousemove", {
x: endColRect.x + endColRect.width / 2,
y: endRowRect.y - 1,
});
await triggerEventForCalendar(endRow, "mouseup", {
x: endColRect.x + endColRect.width / 2,
y: endRowRect.y - 1,
});
await nextTick();
}
export async function selectDateRange(target, startDate, endDate) {
const start = findDateCell(target, startDate);
const end = findDateCell(target, endDate);
await scrollTo(start);
await triggerEventForCalendar(start, "mousedown");
await scrollTo(end);
await triggerEventForCalendar(end, "mousemove");
await triggerEventForCalendar(end, "mouseup");
await nextTick();
}
export async function selectAllDayRange(target, startDate, endDate) {
const start = findAllDaySlot(target, startDate);
const end = findAllDaySlot(target, endDate);
await scrollTo(start);
await triggerEventForCalendar(start, "mousedown");
await scrollTo(end);
await triggerEventForCalendar(end, "mousemove");
await triggerEventForCalendar(end, "mouseup");
await nextTick();
}
export async function moveEventToDate(target, eventId, date, options = {}) {
const event = findEvent(target, eventId);
const cell = findDateCell(target, date);
await scrollTo(event);
await triggerEventForCalendar(event, "mousedown");
await scrollTo(cell);
await triggerEventForCalendar(cell, "mousemove");
if (!options.disableDrop) {
await triggerEventForCalendar(cell, "mouseup");
}
await nextTick();
}
export async function moveEventToTime(target, eventId, dateTime) {
const event = findEvent(target, eventId);
const [date, time] = dateTime.split(" ");
const col = findDateCol(target, date);
const row = findTimeRow(target, time);
// Find event position
await scrollTo(event);
const eventRect = event.getBoundingClientRect();
const eventPos = {
x: eventRect.x + eventRect.width / 2,
y: eventRect.y,
};
await triggerEventForCalendar(event, "mousedown", eventPos);
// Find target position
await scrollTo(row, false);
const colRect = col.getBoundingClientRect();
const rowRect = row.getBoundingClientRect();
const toPos = {
x: colRect.x + colRect.width / 2,
y: rowRect.y - 1,
};
await triggerEventForCalendar(row, "mousemove", toPos);
await triggerEventForCalendar(row, "mouseup", toPos);
await nextTick();
}
export async function moveEventToAllDaySlot(target, eventId, date) {
const event = findEvent(target, eventId);
const slot = findAllDaySlot(target, date);
// Find event position
await scrollTo(event);
const eventRect = event.getBoundingClientRect();
const eventPos = {
x: eventRect.x + eventRect.width / 2,
y: eventRect.y,
};
await triggerEventForCalendar(event, "mousedown", eventPos);
// Find target position
await scrollTo(slot);
const slotRect = slot.getBoundingClientRect();
const toPos = {
x: slotRect.x + slotRect.width / 2,
y: slotRect.y - 1,
};
await triggerEventForCalendar(slot, "mousemove", toPos);
await triggerEventForCalendar(slot, "mouseup", toPos);
await nextTick();
}
export async function resizeEventToTime(target, eventId, dateTime) {
const event = findEvent(target, eventId);
const [date, time] = dateTime.split(" ");
const col = findDateCol(target, date);
const row = findTimeRow(target, time);
// Find event position
await scrollTo(event);
await triggerEventForCalendar(event, "mouseenter");
// Find event resizer
const resizer = event.querySelector(".fc-end-resizer");
resizer.style.display = "block";
resizer.style.width = "100%";
resizer.style.height = "1em";
resizer.style.bottom = "0";
const resizerRect = resizer.getBoundingClientRect();
const resizerPos = {
x: resizerRect.x + resizerRect.width / 2,
y: resizerRect.y + resizerRect.height / 2,
};
await triggerEventForCalendar(resizer, "mousedown", resizerPos);
// Find target position
await scrollTo(row, false);
const colRect = col.getBoundingClientRect();
const rowRect = row.getBoundingClientRect();
const toPos = {
x: colRect.x + colRect.width / 2,
y: rowRect.y - 1,
};
await triggerEventForCalendar(row, "mousemove", toPos);
await triggerEventForCalendar(row, "mouseup", toPos);
await nextTick();
}
export async function changeScale(target, scale) {
await click(target, `.o_calendar_scale_buttons .scale_button_selection`);
await click(target, `.o_calendar_scale_buttons .o_calendar_button_${scale}`);
await nextTick();
}
export async function navigate(target, direction) {
await click(target, `.o_calendar_navigation_buttons .o_calendar_button_${direction}`);
}
export function findFilterPanelSection(target, sectionName) {
return target.querySelector(`.o_calendar_filter[data-name="${sectionName}"]`);
}
export function findFilterPanelFilter(target, sectionName, filterValue) {
return findFilterPanelSection(target, sectionName).querySelector(
`.o_calendar_filter_item[data-value="${filterValue}"]`
);
}
export function findFilterPanelSectionFilter(target, sectionName) {
return findFilterPanelSection(target, sectionName).querySelector(
`.o_calendar_filter_items_checkall`
);
}
export async function toggleFilter(target, sectionName, filterValue) {
const el = findFilterPanelFilter(target, sectionName, filterValue).querySelector(`input`);
await scrollTo(el);
await click(el);
}
export async function toggleSectionFilter(target, sectionName) {
const el = findFilterPanelSectionFilter(target, sectionName).querySelector(`input`);
await scrollTo(el);
await click(el);
}