vanilla 18.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:48:09 +02:00
parent 5454004ff9
commit d7f6d2725e
979 changed files with 428093 additions and 0 deletions

View file

@ -0,0 +1,214 @@
import { describe, expect, test } from "@odoo/hoot";
import { FAKE_FIELDS } from "./calendar_test_helpers";
import { CalendarArchParser } from "@web/views/calendar/calendar_arch_parser";
describe.current.tags("headless");
const parser = new CalendarArchParser();
const DEFAULT_ARCH_RESULTS = {
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,
popoverFieldNodes: {},
scale: "week",
scales: ["day", "week", "month", "year"],
showUnusualDays: false,
};
function parseArch(arch) {
return parser.parse(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 has not defined "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([]);
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

@ -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

@ -0,0 +1,162 @@
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";
const FAKE_PROPS = {
model: FAKE_MODEL,
createRecord() {},
deleteRecord() {},
editRecord() {},
displayName: "Plop",
};
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);
});

View file

@ -0,0 +1,210 @@
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,
patchWithCleanup,
preloadBundle,
} from "@web/../tests/web_test_helpers";
import { CalendarController } from "@web/views/calendar/calendar_controller";
import { changeScale } from "./calendar_test_helpers";
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");
});
test(`Scale: scale default is fetched from sessionStorage`, async () => {
patchWithCleanup(sessionStorage, {
setItem(key, value) {
if (key === "calendar-scale") {
expect.step(`scale_${value}`);
}
},
getItem(key) {
if (key === "calendar-scale") {
return "month";
}
},
});
await mountView({
resModel: "event",
type: "calendar",
arch: `<calendar date_start="start"/>`,
});
expect(`.scale_button_selection`).toHaveText("Month");
await changeScale("year");
expect(`.scale_button_selection`).toHaveText("Year");
expect.verifySteps(["scale_year"]);
});

View file

@ -0,0 +1,137 @@
import { expect, test } from "@odoo/hoot";
import { click, queryAllTexts } from "@odoo/hoot-dom";
import { contains, mountWithCleanup, preloadBundle } from "@web/../tests/web_test_helpers";
import { FAKE_MODEL } from "./calendar_test_helpers";
import { CalendarFilterPanel } from "@web/views/calendar/filter_panel/calendar_filter_panel";
import { runAllTimers } from "@odoo/hoot-mock";
const FAKE_PROPS = {
model: FAKE_MODEL,
};
async function start(props = {}) {
await mountWithCleanup(CalendarFilterPanel, {
props: { ...FAKE_PROPS, ...props },
});
}
preloadBundle("web.fullcalendar_lib");
test(`render filter panel`, async () => {
await start({});
expect(`.o_calendar_filter`).toHaveCount(2);
expect(`.o_calendar_filter:eq(0) .o_cw_filter_label`).toHaveText("Attendees");
expect(`.o_calendar_filter:eq(0) .o_calendar_filter_item`).toHaveCount(4);
expect(`.o_calendar_filter:eq(1) .o_cw_filter_label`).toHaveText("Users");
expect(`.o_calendar_filter:eq(1) .o_calendar_filter_item`).toHaveCount(2);
});
test(`filters are correctly sorted`, async () => {
await start({});
expect(queryAllTexts`.o_calendar_filter:eq(0) .o_calendar_filter_item`).toEqual([
"Mitchell Admin",
"Brandon Freeman",
"Marc Demo",
"Everybody's calendar",
]);
expect(queryAllTexts`.o_calendar_filter:eq(1) .o_calendar_filter_item`).toEqual([
"Brandon Freeman",
"Marc Demo",
]);
});
test(`section can collapse`, async () => {
await start({});
expect(`.o_calendar_filter:eq(0) .o_cw_filter_collapse_icon`).toHaveCount(1);
expect(`.o_calendar_filter:eq(0) .o_calendar_filter_item`).toHaveCount(4);
await contains(`.o_calendar_filter:eq(0) .o_cw_filter_label`).click();
await runAllTimers();
expect(`.o_calendar_filter:eq(0) .o_calendar_filter_item`).toHaveCount(0);
await contains(`.o_calendar_filter:eq(0) .o_cw_filter_label`).click();
await runAllTimers();
expect(`.o_calendar_filter:eq(0) .o_calendar_filter_item`).toHaveCount(4);
});
test(`section cannot collapse`, async () => {
await start({});
expect(`.o_calendar_filter:eq(1) .o_cw_filter_label > i`).toHaveCount(0);
expect(`.o_calendar_filter:eq(1)`).not.toHaveClass("o_calendar_filter-collapsed");
expect(`.o_calendar_filter:eq(1) .o_calendar_filter_item`).toHaveCount(2);
await contains(`.o_calendar_filter:eq(1) .o_cw_filter_label`).click();
await runAllTimers();
expect(`.o_calendar_filter:eq(1)`).not.toHaveClass("o_calendar_filter-collapsed");
expect(`.o_calendar_filter:eq(1) .o_calendar_filter_item`).toHaveCount(2);
});
test(`filters can have avatar`, async () => {
await start({});
expect(`.o_calendar_filter:eq(0) .o_cw_filter_avatar`).toHaveCount(4);
expect(`.o_calendar_filter:eq(0) img.o_cw_filter_avatar`).toHaveCount(3);
expect(`.o_calendar_filter:eq(0) i.o_cw_filter_avatar`).toHaveCount(1);
expect(
`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(0) .o_cw_filter_avatar`
).toHaveAttribute("data-src", "/web/image/res.partner/3/avatar_128");
expect(
`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(1) .o_cw_filter_avatar`
).toHaveAttribute("data-src", "/web/image/res.partner/4/avatar_128");
expect(
`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(2) .o_cw_filter_avatar`
).toHaveAttribute("data-src", "/web/image/res.partner/6/avatar_128");
});
test(`filters cannot have avatar`, async () => {
await start({});
expect(`.o_calendar_filter:eq(1) .o_calendar_filter_item`).toHaveCount(2);
expect(`.o_calendar_filter:eq(1) .o_cw_filter_avatar`).toHaveCount(0);
});
test(`filter can have remove button`, async () => {
await start({});
expect(`.o_calendar_filter:eq(0) .o_calendar_filter_item`).toHaveCount(4);
expect(`.o_calendar_filter:eq(0) .o_calendar_filter_item .o_remove`).toHaveCount(2);
expect(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(0) .o_remove`).toHaveCount(0);
expect(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(1) .o_remove`).toHaveCount(1);
expect(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(2) .o_remove`).toHaveCount(1);
expect(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(3) .o_remove`).toHaveCount(0);
});
test(`click on remove button`, async () => {
await start({
model: {
...FAKE_MODEL,
unlinkFilter(fieldName, recordId) {
expect.step(`${fieldName} ${recordId}`);
},
},
});
await click(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(1) .o_remove`);
await click(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(2) .o_remove`);
expect.verifySteps(["partner_ids 1", "partner_ids 2"]);
});
test(`click on filter`, async () => {
await start({
model: {
...FAKE_MODEL,
updateFilters(fieldName, args) {
expect.step(`${fieldName} ${Object.keys(args)[0]} ${Object.values(args)[0]}`);
},
},
});
await click(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(0) input`);
await click(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(1) input`);
await click(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(2) input`);
await click(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(3) input`);
await click(`.o_calendar_filter:eq(0) .o_calendar_filter_item:eq(3) input`);
expect.verifySteps([
"partner_ids 3 false",
"partner_ids 4 false",
"partner_ids 6 true",
"partner_ids all true",
"partner_ids all 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

@ -0,0 +1,746 @@
import { click, drag, 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 { 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",
},
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" },
};
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 contains(`.o_time_picker_select:eq(0)`).select(selectedValue);
await contains(".o_datetime_picker .o_apply").click();
}
/**
* @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();
}
}
/**
* @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();
}
}
/**
* @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 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

@ -0,0 +1,153 @@
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("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
});