mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 17:12:06 +02:00
vanilla 18.0
This commit is contained in:
parent
5454004ff9
commit
d7f6d2725e
979 changed files with 428093 additions and 0 deletions
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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"]);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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"]);
|
||||
});
|
||||
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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
|
|
@ -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"]);
|
||||
});
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
/* global ace */
|
||||
|
||||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
editAce,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
pagerNext,
|
||||
preloadBundle,
|
||||
preventResizeObserverError,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
_rec_name = "display_name";
|
||||
|
||||
foo = fields.Text({ default: "My little Foo Value" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, foo: "yop" },
|
||||
{ id: 2, foo: "blip" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
preloadBundle("web.ace_lib");
|
||||
preventResizeObserverError();
|
||||
|
||||
test("AceEditorField on text fields works", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
expect(window).toInclude("ace", { message: "the ace library should be loaded" });
|
||||
expect(`div.ace_content`).toHaveCount(1);
|
||||
expect(".o_field_code").toHaveText(/yop/);
|
||||
});
|
||||
|
||||
test("AceEditorField mark as dirty as soon at onchange", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
|
||||
const aceEditor = queryOne`.ace_editor`;
|
||||
expect(aceEditor).toHaveText(/yop/);
|
||||
|
||||
// edit the foo field
|
||||
ace.edit(aceEditor).setValue("blip");
|
||||
await animationFrame();
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveCount(1);
|
||||
expect(`.o_form_status_indicator_buttons`).not.toHaveClass("invisible");
|
||||
|
||||
// revert edition
|
||||
ace.edit(aceEditor).setValue("yop");
|
||||
await animationFrame();
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveCount(1);
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveClass("invisible");
|
||||
});
|
||||
|
||||
test("AceEditorField on html fields works", async () => {
|
||||
Partner._fields.html_field = fields.Html();
|
||||
Partner._records.push({ id: 3, html_field: `<p>My little HTML Test</p>` });
|
||||
|
||||
onRpc(({ method }) => expect.step(method));
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 3,
|
||||
type: "form",
|
||||
arch: `<form><field name="html_field" widget="code" /></form>`,
|
||||
});
|
||||
expect(".o_field_code").toHaveText(/My little HTML Test/);
|
||||
expect.verifySteps(["get_views", "web_read"]);
|
||||
|
||||
// Modify foo and save
|
||||
await editAce("DEF");
|
||||
await clickSave();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test.tags("desktop", "focus required");
|
||||
test("AceEditorField doesn't crash when editing", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
|
||||
await contains(".ace_editor .ace_content").click();
|
||||
expect(".ace-view-editor").toHaveClass("ace_focus");
|
||||
});
|
||||
|
||||
test("AceEditorField is updated on value change", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
expect(".o_field_code").toHaveText(/yop/);
|
||||
|
||||
await pagerNext();
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(".o_field_code").toHaveText(/blip/);
|
||||
});
|
||||
|
||||
test("leaving an untouched record with an unset ace field should not write", async () => {
|
||||
for (const record of Partner._records) {
|
||||
record.foo = false;
|
||||
}
|
||||
|
||||
onRpc(({ args, method }) => {
|
||||
if (method) {
|
||||
expect.step(`${method}: ${JSON.stringify(args)}`);
|
||||
}
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
expect.verifySteps(["get_views: []", "web_read: [[1]]"]);
|
||||
|
||||
await pagerNext();
|
||||
expect.verifySteps(["web_read: [[2]]"]);
|
||||
});
|
||||
|
||||
test.tags("focus required");
|
||||
test("AceEditorField only trigger onchanges when blurred", async () => {
|
||||
Partner._onChanges.foo = () => {};
|
||||
for (const record of Partner._records) {
|
||||
record.foo = false;
|
||||
}
|
||||
|
||||
onRpc(({ args, method }) => {
|
||||
expect.step(`${method}: ${JSON.stringify(args)}`);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
type: "form",
|
||||
arch: `<form><field name="display_name"/><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
expect.verifySteps(["get_views: []", "web_read: [[1]]"]);
|
||||
|
||||
await editAce("a");
|
||||
await contains(getFixture()).focus(); // blur ace editor
|
||||
expect.verifySteps([`onchange: [[1],{"foo":"a"},["foo"],{"display_name":{},"foo":{}}]`]);
|
||||
|
||||
await clickSave();
|
||||
expect.verifySteps([`web_save: [[1],{"foo":"a"}]`]);
|
||||
});
|
||||
|
||||
test("Save and Discard buttons are displayed when necessary", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
|
||||
await editAce("a");
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveCount(1);
|
||||
expect(`.o_form_status_indicator_buttons`).not.toHaveClass("invisible");
|
||||
await clickSave();
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveCount(1);
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveClass("invisible");
|
||||
});
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
_rec_name = "display_name";
|
||||
|
||||
many2one_field = fields.Many2one({ relation: "res.partner" });
|
||||
selection_field = fields.Selection({
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
many2one_field: 4,
|
||||
selection_field: "blocked",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
many2one_field: 1,
|
||||
selection_field: "normal",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
display_name: "", // empty value
|
||||
selection_field: "done",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "fourth record",
|
||||
selection_field: "done",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("BadgeField component on a char field in list view", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `<list><field name="display_name" widget="badge"/></list>`,
|
||||
});
|
||||
|
||||
expect(`.o_field_badge[name="display_name"]:contains(first record)`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="display_name"]:contains(second record)`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="display_name"]:contains(fourth record)`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("BadgeField component on a selection field in list view", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `<list><field name="selection_field" widget="badge"/></list>`,
|
||||
});
|
||||
|
||||
expect(`.o_field_badge[name="selection_field"]:contains(Blocked)`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="selection_field"]:contains(Normal)`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="selection_field"]:contains(Done)`).toHaveCount(2);
|
||||
});
|
||||
|
||||
test("BadgeField component on a many2one field in list view", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `<list><field name="many2one_field" widget="badge"/></list>`,
|
||||
});
|
||||
|
||||
expect(`.o_field_badge[name="many2one_field"]:contains(first record)`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="many2one_field"]:contains(fourth record)`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("BadgeField component with decoration-xxx attributes", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="selection_field" widget="badge"/>
|
||||
<field name="display_name" widget="badge" decoration-danger="selection_field == 'done'" decoration-warning="selection_field == 'blocked'"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`.o_field_badge[name="display_name"]`).toHaveCount(4);
|
||||
expect(`.o_field_badge[name="display_name"] .text-bg-danger`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="display_name"] .text-bg-warning`).toHaveCount(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
MockServer,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
color = fields.Selection({
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
});
|
||||
|
||||
_records = [{ id: 1 }, { id: 2, product_id: 37 }];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
_records = [
|
||||
{ id: 37, display_name: "xphone" },
|
||||
{ id: 41, display_name: "xpad" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
test("BadgeSelectionField widget on a many2one in a new record", async () => {
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(`saved product_id: ${args[1]["product_id"]}`);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `<form><field name="product_id" widget="selection_badge"/></form>`,
|
||||
});
|
||||
|
||||
expect(`div.o_field_selection_badge`).toHaveCount(1, {
|
||||
message: "should have rendered outer div",
|
||||
});
|
||||
expect(`span.o_selection_badge`).toHaveCount(2, { message: "should have 2 possible choices" });
|
||||
expect(`span.o_selection_badge:contains(xphone)`).toHaveCount(1, {
|
||||
message: "one of them should be xphone",
|
||||
});
|
||||
expect(`span.active`).toHaveCount(0, { message: "none of the input should be checked" });
|
||||
|
||||
await contains(`span.o_selection_badge`).click();
|
||||
expect(`span.active`).toHaveCount(1, { message: "one of the input should be checked" });
|
||||
|
||||
await clickSave();
|
||||
expect.verifySteps(["saved product_id: 37"]);
|
||||
});
|
||||
|
||||
test("BadgeSelectionField widget on a selection in a new record", async () => {
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(`saved color: ${args[1]["color"]}`);
|
||||
});
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `<form><field name="color" widget="selection_badge"/></form>`,
|
||||
});
|
||||
|
||||
expect(`div.o_field_selection_badge`).toHaveCount(1, {
|
||||
message: "should have rendered outer div",
|
||||
});
|
||||
expect("span.o_selection_badge").toHaveCount(2, { message: "should have 2 possible choices" });
|
||||
expect(`span.o_selection_badge:contains(Red)`).toHaveCount(1, {
|
||||
message: "one of them should be Red",
|
||||
});
|
||||
|
||||
await contains(`span.o_selection_badge:last`).click();
|
||||
await clickSave();
|
||||
expect.verifySteps(["saved color: black"]);
|
||||
});
|
||||
|
||||
test("BadgeSelectionField widget on a selection in a readonly mode", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `<form><field name="color" widget="selection_badge" readonly="1"/></form>`,
|
||||
});
|
||||
expect(`div.o_readonly_modifier span`).toHaveCount(1, {
|
||||
message: "should have 1 possible value in readonly mode",
|
||||
});
|
||||
});
|
||||
|
||||
test("BadgeSelectionField widget on a selection unchecking selected value", async () => {
|
||||
onRpc("res.partner", "web_save", ({ args }) => {
|
||||
expect.step("web_save");
|
||||
expect(args[1]).toEqual({ color: false });
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: '<form><field name="color" widget="selection_badge"/></form>',
|
||||
});
|
||||
|
||||
expect("div.o_field_selection_badge").toHaveCount(1, {
|
||||
message: "should have rendered outer div",
|
||||
});
|
||||
expect("span.o_selection_badge").toHaveCount(2, { message: "should have 2 possible choices" });
|
||||
expect("span.o_selection_badge.active").toHaveCount(1, { message: "one is active" });
|
||||
expect("span.o_selection_badge.active").toHaveText("Red", {
|
||||
message: "the active one should be Red",
|
||||
});
|
||||
|
||||
// click again on red option and save to update the server data
|
||||
await contains("span.o_selection_badge.active").click();
|
||||
expect.verifySteps([]);
|
||||
await contains(".o_form_button_save").click();
|
||||
expect.verifySteps(["web_save"]);
|
||||
|
||||
expect(MockServer.env["res.partner"].at(-1).color).toBe(false, {
|
||||
message: "the new value should be false as we have selected same value as default",
|
||||
});
|
||||
});
|
||||
|
||||
test("BadgeSelectionField widget on a selection unchecking selected value (required field)", async () => {
|
||||
Partner._fields.color.required = true;
|
||||
onRpc("res.partner", "web_save", ({ args }) => {
|
||||
expect.step("web_save");
|
||||
expect(args[1]).toEqual({ color: "red" });
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: '<form><field name="color" widget="selection_badge"/></form>',
|
||||
});
|
||||
|
||||
expect("div.o_field_selection_badge").toHaveCount(1, {
|
||||
message: "should have rendered outer div",
|
||||
});
|
||||
expect("span.o_selection_badge").toHaveCount(2, { message: "should have 2 possible choices" });
|
||||
expect("span.o_selection_badge.active").toHaveCount(1, { message: "one is active" });
|
||||
expect("span.o_selection_badge.active").toHaveText("Red", {
|
||||
message: "the active one should be Red",
|
||||
});
|
||||
|
||||
// click again on red option and save to update the server data
|
||||
await contains("span.o_selection_badge.active").click();
|
||||
expect.verifySteps([]);
|
||||
await contains(".o_form_button_save").click();
|
||||
expect.verifySteps(["web_save"]);
|
||||
|
||||
expect(MockServer.env["res.partner"].at(-1).color).toBe("red", {
|
||||
message: "the new value should be red",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,450 @@
|
|||
import { after, expect, test } from "@odoo/hoot";
|
||||
import { click, queryOne, queryValue, setInputFiles, waitFor } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
makeServerError,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
pagerNext,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { toBase64Length } from "@web/core/utils/binary";
|
||||
import { MAX_FILENAME_SIZE_BYTES } from "@web/views/fields/binary/binary_field";
|
||||
|
||||
const BINARY_FILE =
|
||||
"R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
foo = fields.Char({ default: "My little Foo Value" });
|
||||
document = fields.Binary();
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
|
||||
_records = [{ foo: "coucou.txt", document: "coucou==\n" }];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 37, name: "xphone" },
|
||||
{ id: 41, name: "xpad" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("BinaryField is correctly rendered (readonly)", async () => {
|
||||
onRpc("/web/content", async (request) => {
|
||||
expect.step("/web/content");
|
||||
|
||||
const body = await request.text();
|
||||
expect(body).toBeInstanceOf(FormData);
|
||||
expect(body.get("field")).toBe("document", {
|
||||
message: "we should download the field document",
|
||||
});
|
||||
expect(body.get("data")).toBe("coucou==\n", {
|
||||
message: "we should download the correct data",
|
||||
});
|
||||
|
||||
return new Blob([body.get("data")], { type: "text/plain" });
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form edit="0">
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`.o_field_widget[name="document"] a > .fa-download`).toHaveCount(1, {
|
||||
message: "the binary field should be rendered as a downloadable link in readonly",
|
||||
});
|
||||
expect(`.o_field_widget[name="document"]`).toHaveText("coucou.txt", {
|
||||
message: "the binary field should display the name of the file in the link",
|
||||
});
|
||||
expect(`.o_field_char`).toHaveText("coucou.txt", {
|
||||
message: "the filename field should have the file name as value",
|
||||
});
|
||||
|
||||
// Testing the download button in the field
|
||||
// We must avoid the browser to download the file effectively
|
||||
const deferred = new Deferred();
|
||||
const downloadOnClick = (ev) => {
|
||||
const target = ev.target;
|
||||
if (target.tagName === "A" && "download" in target.attributes) {
|
||||
ev.preventDefault();
|
||||
document.removeEventListener("click", downloadOnClick);
|
||||
deferred.resolve();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", downloadOnClick);
|
||||
after(() => document.removeEventListener("click", downloadOnClick));
|
||||
|
||||
await contains(`.o_field_widget[name="document"] a`).click();
|
||||
await deferred;
|
||||
expect.verifySteps(["/web/content"]);
|
||||
});
|
||||
|
||||
test("BinaryField is correctly rendered", async () => {
|
||||
onRpc("/web/content", async (request) => {
|
||||
expect.step("/web/content");
|
||||
|
||||
const body = await request.text();
|
||||
expect(body).toBeInstanceOf(FormData);
|
||||
expect(body.get("field")).toBe("document", {
|
||||
message: "we should download the field document",
|
||||
});
|
||||
expect(body.get("data")).toBe("coucou==\n", {
|
||||
message: "we should download the correct data",
|
||||
});
|
||||
|
||||
return new Blob([body.get("data")], { type: "text/plain" });
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`.o_field_widget[name="document"] a > .fa-download`).toHaveCount(0, {
|
||||
message: "the binary field should not be rendered as a downloadable link in edit",
|
||||
});
|
||||
expect(`.o_field_widget[name="document"].o_field_binary .o_input`).toHaveValue("coucou.txt", {
|
||||
message: "the binary field should display the file name in the input edit mode",
|
||||
});
|
||||
expect(`.o_field_binary .o_clear_file_button`).toHaveCount(1, {
|
||||
message: "there shoud be a button to clear the file",
|
||||
});
|
||||
expect(`.o_field_char input`).toHaveValue("coucou.txt", {
|
||||
message: "the filename field should have the file name as value",
|
||||
});
|
||||
|
||||
// Testing the download button in the field
|
||||
// We must avoid the browser to download the file effectively
|
||||
const deferred = new Deferred();
|
||||
const downloadOnClick = (ev) => {
|
||||
const target = ev.target;
|
||||
if (target.tagName === "A" && "download" in target.attributes) {
|
||||
ev.preventDefault();
|
||||
document.removeEventListener("click", downloadOnClick);
|
||||
deferred.resolve();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", downloadOnClick);
|
||||
after(() => document.removeEventListener("click", downloadOnClick));
|
||||
|
||||
await click(`.fa-download`);
|
||||
await deferred;
|
||||
expect.verifySteps(["/web/content"]);
|
||||
|
||||
await click(`.o_field_binary .o_clear_file_button`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_binary input`).not.toBeVisible({ message: "the input should be hidden" });
|
||||
expect(`.o_field_binary .o_select_file_button`).toHaveCount(1, {
|
||||
message: "there should be a button to upload the file",
|
||||
});
|
||||
expect(`.o_field_char input`).toHaveValue("", {
|
||||
message: "the filename field should be empty since we removed the file",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(`.o_field_widget[name="document"] a > .fa-download`).toHaveCount(0, {
|
||||
message:
|
||||
"the binary field should not render as a downloadable link since we removed the file",
|
||||
});
|
||||
expect(`o_field_widget span`).toHaveCount(0, {
|
||||
message:
|
||||
"the binary field should not display a filename in the link since we removed the file",
|
||||
});
|
||||
});
|
||||
|
||||
test("BinaryField is correctly rendered (isDirty)", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// Simulate a file upload
|
||||
await click(`.o_select_file_button`);
|
||||
await animationFrame();
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await setInputFiles([file]);
|
||||
await waitFor(`.o_form_button_save:visible`);
|
||||
expect(`.o_field_widget[name="document"] .fa-download`).toHaveCount(0, {
|
||||
message:
|
||||
"the binary field should not be rendered as a downloadable since the record is dirty",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(`.o_field_widget[name="document"] .fa-download`).toHaveCount(1, {
|
||||
message:
|
||||
"the binary field should render as a downloadable link since the record is not dirty",
|
||||
});
|
||||
});
|
||||
|
||||
test("file name field is not defined", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="document" filename="foo"/></form>`,
|
||||
});
|
||||
expect(`.o_field_binary`).toHaveText("", {
|
||||
message: "there should be no text since the name field is not in the view",
|
||||
});
|
||||
expect(`.o_field_binary .fa-download`).toBeDisplayed({
|
||||
message: "download icon should be visible",
|
||||
});
|
||||
});
|
||||
|
||||
test("icons are displayed exactly once", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="document" filename="foo"/></form>`,
|
||||
});
|
||||
expect(queryOne`.o_field_binary .o_select_file_button`).toBeVisible({
|
||||
message: "only one select file icon should be visible",
|
||||
});
|
||||
expect(queryOne`.o_field_binary .o_download_file_button`).toBeVisible({
|
||||
message: "only one download file icon should be visible",
|
||||
});
|
||||
expect(queryOne`.o_field_binary .o_clear_file_button`).toBeVisible({
|
||||
message: "only one clear file icon should be visible",
|
||||
});
|
||||
});
|
||||
|
||||
test("input value is empty when clearing after uploading", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(`.o_select_file_button`);
|
||||
await animationFrame();
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await setInputFiles([file]);
|
||||
await waitFor(`.o_form_button_save:visible`);
|
||||
expect(`.o_field_binary input[type=text]`).toHaveAttribute("readonly");
|
||||
expect(`.o_field_binary input[type=text]`).toHaveValue("fake_file.txt");
|
||||
expect(`.o_field_char input[type=text]`).toHaveValue("fake_file.txt");
|
||||
|
||||
await click(`.o_clear_file_button`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_binary .o_input_file`).toHaveValue("");
|
||||
expect(`.o_field_char input`).toHaveValue("");
|
||||
});
|
||||
|
||||
test("option accepted_file_extensions", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="binary" options="{'accepted_file_extensions': '.dat,.bin'}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(`input.o_input_file`).toHaveAttribute("accept", ".dat,.bin", {
|
||||
message: "the input should have the correct ``accept`` attribute",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("readonly in create mode does not download", async () => {
|
||||
onRpc("/web/content", () => {
|
||||
expect.step("We shouldn't be getting the file.");
|
||||
});
|
||||
|
||||
Partner._onChanges.product_id = (record) => {
|
||||
record.document = "onchange==\n";
|
||||
};
|
||||
Partner._fields.document.readonly = true;
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id"/>
|
||||
<field name="document" filename="yooo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(`.o_field_many2one[name='product_id'] input`);
|
||||
await animationFrame();
|
||||
await click(`.o_field_many2one[name='product_id'] .dropdown-item`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_widget[name="document"] a`).toHaveCount(0, {
|
||||
message: "The link to download the binary should not be present",
|
||||
});
|
||||
expect(`.o_field_widget[name="document"] a > .fa-download`).toHaveCount(0, {
|
||||
message: "The download icon should not be present",
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("BinaryField in list view (formatter)", async () => {
|
||||
Partner._records[0]["document"] = BINARY_FILE;
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `<list><field name="document"/></list>`,
|
||||
});
|
||||
expect(`.o_data_row .o_data_cell`).toHaveText("93.43 Bytes");
|
||||
});
|
||||
|
||||
test("BinaryField in list view with filename", async () => {
|
||||
Partner._records[0]["document"] = BINARY_FILE;
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="document" filename="foo" widget="binary"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
expect(`.o_data_row .o_data_cell`).toHaveText("coucou.txt");
|
||||
});
|
||||
|
||||
test("new record has no download button", async () => {
|
||||
Partner._fields.document.default = BINARY_FILE;
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `<form><field name="document" filename="foo"/></form>`,
|
||||
});
|
||||
expect(`button.fa-download`).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("filename doesn't exceed 255 bytes", async () => {
|
||||
const LARGE_BINARY_FILE = BINARY_FILE.repeat(5);
|
||||
expect((LARGE_BINARY_FILE.length / 4) * 3).toBeGreaterThan(MAX_FILENAME_SIZE_BYTES, {
|
||||
message:
|
||||
"The initial binary file should be larger than max bytes that can represent the filename",
|
||||
});
|
||||
|
||||
Partner._fields.document.default = LARGE_BINARY_FILE;
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `<form><field name="document"/></form>`,
|
||||
});
|
||||
expect(queryValue(`.o_field_binary input[type=text]`)).toHaveLength(
|
||||
toBase64Length(MAX_FILENAME_SIZE_BYTES),
|
||||
{
|
||||
message: "The filename shouldn't exceed the maximum size in bytes in base64",
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("filename is updated when using the pager", async () => {
|
||||
Partner._records.push(
|
||||
{ id: 1, document: "abc", foo: "abc.txt" },
|
||||
{ id: 2, document: "def", foo: "def.txt" }
|
||||
);
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(`.o_field_binary input[type=text]`).toHaveValue("abc.txt", {
|
||||
message: `displayed value should be "abc.txt"`,
|
||||
});
|
||||
|
||||
await pagerNext();
|
||||
expect(`.o_field_binary input[type=text]`).toHaveValue("def.txt", {
|
||||
message: `displayed value should be "def.txt"`,
|
||||
});
|
||||
});
|
||||
|
||||
test("isUploading state should be set to false after upload", async () => {
|
||||
expect.errors(1);
|
||||
|
||||
Partner._records.push({ id: 1 });
|
||||
Partner._onChanges.document = (record) => {
|
||||
if (record.document) {
|
||||
throw makeServerError({ type: "ValidationError" });
|
||||
}
|
||||
};
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="document"/></form>`,
|
||||
});
|
||||
|
||||
await click(`.o_select_file_button`);
|
||||
await animationFrame();
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await setInputFiles([file]);
|
||||
await waitFor(`.o_form_button_save:visible`);
|
||||
await animationFrame();
|
||||
expect.verifyErrors([/RPC_ERROR/]);
|
||||
expect(`.o_select_file_button`).toHaveText("Upload your file");
|
||||
});
|
||||
|
||||
test("doesn't crash if value is not a string", async () => {
|
||||
class Dummy extends models.Model {
|
||||
document = fields.Binary()
|
||||
_applyComputesAndValidate() {}
|
||||
}
|
||||
defineModels([Dummy])
|
||||
Dummy._records.push({ id: 1, document: {} });
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "dummy",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_binary input").toHaveValue("");
|
||||
});
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllProperties, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
bar = fields.Boolean({ default: true });
|
||||
|
||||
_records = [
|
||||
{ id: 1, bar: true },
|
||||
{ id: 2, bar: true },
|
||||
{ id: 3, bar: true },
|
||||
{ id: 4, bar: true },
|
||||
{ id: 5, bar: false },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("FavoriteField in kanban view", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", 1]],
|
||||
type: "kanban",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="bar" widget="boolean_favorite"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a`).toHaveText("Remove from Favorites", {
|
||||
message: `the label should say "Remove from Favorites"`,
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(0, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a`).toHaveText("Add to Favorites", {
|
||||
message: `the label should say "Add to Favorites"`,
|
||||
});
|
||||
});
|
||||
|
||||
test("FavoriteField saves changes by default", async () => {
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step("save");
|
||||
expect(args).toEqual([[1], { bar: false }]);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", 1]],
|
||||
type: "kanban",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="bar" widget="boolean_favorite"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(0, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a`).toHaveText("Add to Favorites", {
|
||||
message: `the label should say "Add to Favorites"`,
|
||||
});
|
||||
expect.verifySteps(["save"]);
|
||||
});
|
||||
|
||||
test("FavoriteField does not save if autosave option is set to false", async () => {
|
||||
onRpc("web_save", () => {
|
||||
expect.step("save");
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", 1]],
|
||||
type: "kanban",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="bar" widget="boolean_favorite" options="{'autosave': False}"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(0, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a`).toHaveText("Add to Favorites", {
|
||||
message: `the label should say "Add to Favorites"`,
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("FavoriteField in form view", async () => {
|
||||
onRpc("web_save", () => {
|
||||
expect.step("save");
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" widget="boolean_favorite"/></form>`,
|
||||
});
|
||||
expect(`.o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
expect(`.o_field_widget .o_favorite > a`).toHaveText("Remove from Favorites", {
|
||||
message: `the label should say "Remove from Favorites"`,
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
expect.verifySteps(["save"]);
|
||||
expect(`.o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(0, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
expect(`.o_field_widget .o_favorite > a i.fa.fa-star-o`).toHaveCount(1, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
expect(`.o_field_widget .o_favorite > a`).toHaveText("Add to Favorites", {
|
||||
message: `the label should say "Add to Favorites"`,
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
expect.verifySteps(["save"]);
|
||||
expect(`.o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
expect(`.o_field_widget .o_favorite > a`).toHaveText("Remove from Favorites", {
|
||||
message: `the label should say "Remove from Favorites"`,
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("FavoriteField in editable list view without label", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="bar" widget="boolean_favorite" nolabel="1" options="{'autosave': False}"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
expect(`.o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
|
||||
// switch to edit mode
|
||||
await contains(`tbody td:not(.o_list_record_selector)`).click();
|
||||
expect(`.o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_data_row .o_field_widget .o_favorite > a`).click();
|
||||
expect(`.o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(0, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
|
||||
// save
|
||||
await contains(`.o_list_button_save`).click();
|
||||
expect(`.o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star-o`).toHaveCount(1, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("FavoriteField in list has a fixed width if no label", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
Partner._fields.char = fields.Char();
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="bar" widget="boolean_favorite" nolabel="1"/>
|
||||
<field name="bar" widget="boolean_favorite"/>
|
||||
<field name="char"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
const columnWidths = queryAllProperties(".o_list_table thead th", "offsetWidth");
|
||||
const columnLabels = queryAllTexts(".o_list_table thead th");
|
||||
expect(columnWidths[1]).toBe(29);
|
||||
expect(columnLabels[1]).toBe("");
|
||||
expect(columnWidths[2]).toBeGreaterThan(29);
|
||||
expect(columnLabels[2]).toBe("Bar");
|
||||
});
|
||||
|
||||
test("FavoriteField in kanban view with readonly attribute", async () => {
|
||||
onRpc("web_save", () => {
|
||||
expect.step("should not save");
|
||||
});
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", 1]],
|
||||
type: "kanban",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="bar" widget="boolean_favorite" readonly="1"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a`).toHaveClass("pe-none");
|
||||
expect(`.o_kanban_record .o_field_widget`).toHaveText("");
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
// expect nothing to change since its readonly
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should remain favorite",
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { check, click, press, uncheck } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
bar = fields.Boolean({ default: true });
|
||||
|
||||
_records = [
|
||||
{ id: 1, bar: true },
|
||||
{ id: 2, bar: true },
|
||||
{ id: 3, bar: true },
|
||||
{ id: 4, bar: true },
|
||||
{ id: 5, bar: false },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("boolean field in form view", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<label for="bar" string="Awesome checkbox"/>
|
||||
<field name="bar"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
expect(`.o_field_boolean input`).toBeEnabled();
|
||||
|
||||
await uncheck(`.o_field_boolean input`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).not.toBeChecked();
|
||||
|
||||
await clickSave();
|
||||
expect(`.o_field_boolean input`).not.toBeChecked();
|
||||
|
||||
await check(`.o_field_boolean input`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
|
||||
await uncheck(`.o_field_boolean input`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).not.toBeChecked();
|
||||
|
||||
await click(`.o_form_view label:not(.form-check-label)`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
|
||||
await click(`.o_form_view label:not(.form-check-label)`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).not.toBeChecked();
|
||||
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).not.toBeChecked();
|
||||
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
|
||||
await clickSave();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
});
|
||||
|
||||
test("boolean field in editable list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "list",
|
||||
arch: `<list editable="bottom"><field name="bar"/></list>`,
|
||||
});
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input`).toHaveCount(5);
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input:checked`).toHaveCount(4);
|
||||
|
||||
// Edit a line
|
||||
const cell = `tr.o_data_row td:not(.o_list_record_selector):first`;
|
||||
expect(`${cell} .o-checkbox input:only`).toBeChecked();
|
||||
expect(`${cell} .o-checkbox input:only`).not.toBeEnabled();
|
||||
|
||||
await click(`${cell} .o-checkbox`);
|
||||
await animationFrame();
|
||||
expect(`tr.o_data_row:nth-child(1)`).toHaveClass("o_selected_row", {
|
||||
message: "the row is now selected, in edition",
|
||||
});
|
||||
expect(`${cell} .o-checkbox input:only`).not.toBeChecked();
|
||||
expect(`${cell} .o-checkbox input:only`).toBeEnabled();
|
||||
|
||||
await click(`${cell} .o-checkbox`);
|
||||
await click(cell);
|
||||
await animationFrame();
|
||||
expect(`${cell} .o-checkbox input:only`).toBeChecked();
|
||||
expect(`${cell} .o-checkbox input:only`).toBeEnabled();
|
||||
|
||||
await click(`${cell} .o-checkbox`);
|
||||
await animationFrame();
|
||||
|
||||
await click(`.o_list_button_save`);
|
||||
await animationFrame();
|
||||
expect(`${cell} .o-checkbox input:only`).not.toBeChecked();
|
||||
expect(`${cell} .o-checkbox input:only`).not.toBeEnabled();
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input`).toHaveCount(5);
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input:checked`).toHaveCount(3);
|
||||
|
||||
// Fake-check the checkbox
|
||||
await click(cell);
|
||||
await animationFrame();
|
||||
await click(`${cell} .o-checkbox`);
|
||||
await animationFrame();
|
||||
|
||||
await click(`.o_list_button_save`);
|
||||
await animationFrame();
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input`).toHaveCount(5);
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input:checked`).toHaveCount(3);
|
||||
});
|
||||
|
||||
test("readonly boolean field", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" readonly="1"/></form>`,
|
||||
});
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
expect(`.o_field_boolean input`).not.toBeEnabled();
|
||||
|
||||
await click(`.o_field_boolean .o-checkbox`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
expect(`.o_field_boolean input`).not.toBeEnabled();
|
||||
});
|
||||
|
||||
test("onchange return value before toggle checkbox", async () => {
|
||||
Partner._onChanges.bar = (record) => {
|
||||
record["bar"] = true;
|
||||
};
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar"/></form>`,
|
||||
});
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
|
||||
await click(`.o_field_boolean .o-checkbox`);
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
bar = fields.Boolean({ string: "Bar field" });
|
||||
foo = fields.Boolean();
|
||||
|
||||
_records = [{ id: 1, bar: true, foo: false }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("BooleanIcon field in form view", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_icon" options="{'icon': 'fa-recycle'}" />
|
||||
<field name="foo" widget="boolean_icon" options="{'icon': 'fa-trash'}" />
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_boolean_icon button").toHaveCount(2);
|
||||
expect("[name='bar'] button").toHaveAttribute("data-tooltip", "Bar field");
|
||||
expect("[name='bar'] button").toHaveClass("btn-primary fa-recycle");
|
||||
expect("[name='foo'] button").toHaveClass("btn-outline-secondary fa-trash");
|
||||
|
||||
await click("[name='bar'] button");
|
||||
await animationFrame();
|
||||
expect("[name='bar'] button").toHaveClass("btn-outline-secondary fa-recycle");
|
||||
});
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { check, click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
bar = fields.Boolean({ default: true });
|
||||
|
||||
_records = [{ id: 1, bar: false }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("use BooleanToggleField in form view", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" widget="boolean_toggle"/></form>`,
|
||||
});
|
||||
expect(`.form-check.o_boolean_toggle`).toHaveCount(1);
|
||||
expect(`.o_boolean_toggle input`).toBeEnabled();
|
||||
expect(`.o_boolean_toggle input`).not.toBeChecked();
|
||||
|
||||
await check(`.o_field_widget[name='bar'] input`);
|
||||
await animationFrame();
|
||||
expect(`.o_boolean_toggle input`).toBeEnabled();
|
||||
expect(`.o_boolean_toggle input`).toBeChecked();
|
||||
});
|
||||
|
||||
test("BooleanToggleField is disabled with a readonly attribute", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" widget="boolean_toggle" readonly="1"/></form>`,
|
||||
});
|
||||
expect(`.form-check.o_boolean_toggle`).toHaveCount(1);
|
||||
expect(`.o_boolean_toggle input`).not.toBeEnabled();
|
||||
});
|
||||
|
||||
test("BooleanToggleField is disabled if readonly in editable list", async () => {
|
||||
Partner._fields.bar.readonly = true;
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="bar" widget="boolean_toggle"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
expect(`.o_boolean_toggle input`).not.toBeEnabled();
|
||||
expect(`.o_boolean_toggle input`).not.toBeChecked();
|
||||
|
||||
await click(`.o_boolean_toggle`);
|
||||
await animationFrame();
|
||||
expect(`.o_boolean_toggle input`).not.toBeEnabled();
|
||||
expect(`.o_boolean_toggle input`).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("BooleanToggleField - auto save record when field toggled", async () => {
|
||||
onRpc("web_save", () => expect.step("web_save"));
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" widget="boolean_toggle"/></form>`,
|
||||
});
|
||||
await click(`.o_field_widget[name='bar'] input`);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("BooleanToggleField - autosave option set to false", async () => {
|
||||
onRpc("web_save", () => expect.step("web_save"));
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" widget="boolean_toggle" options="{'autosave': false}"/></form>`,
|
||||
});
|
||||
await click(`.o_field_widget[name='bar'] input`);
|
||||
await animationFrame();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -0,0 +1,938 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryFirst } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Currency extends models.Model {
|
||||
digits = fields.Integer();
|
||||
symbol = fields.Char({ string: "Currency Symbol" });
|
||||
position = fields.Char({ string: "Currency Position" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "$",
|
||||
symbol: "$",
|
||||
position: "before",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "€",
|
||||
symbol: "€",
|
||||
position: "after",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
_inherit = [];
|
||||
|
||||
name = fields.Char({
|
||||
string: "Name",
|
||||
default: "My little Name Value",
|
||||
trim: true,
|
||||
});
|
||||
int_field = fields.Integer();
|
||||
partner_ids = fields.One2many({
|
||||
string: "one2many field",
|
||||
relation: "res.partner",
|
||||
});
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
|
||||
placeholder_name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
name: "yop",
|
||||
int_field: 10,
|
||||
partner_ids: [],
|
||||
placeholder_name: "Placeholder Name",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
name: "blip",
|
||||
int_field: 0,
|
||||
partner_ids: [],
|
||||
},
|
||||
{ id: 3, name: "gnap", int_field: 80 },
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
name: "abc",
|
||||
},
|
||||
{ id: 5, name: "blop", int_field: -4 },
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
color = fields.Integer({ string: "Color index" });
|
||||
name = fields.Char({ string: "Partner Type" });
|
||||
|
||||
_records = [
|
||||
{ id: 12, display_name: "gold", color: 2 },
|
||||
{ id: 14, display_name: "silver", color: 5 },
|
||||
];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product Name" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 37,
|
||||
name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
name: "xpad",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class Users extends models.Model {
|
||||
_name = "res.users";
|
||||
|
||||
name = fields.Char();
|
||||
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Aline",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Christine",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Currency, Partner, PartnerType, Product, Users]);
|
||||
|
||||
test("char field in form view", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_widget input[type='text']").toHaveCount(1, {
|
||||
message: "should have an input for the char field",
|
||||
});
|
||||
expect(".o_field_widget input[type='text']").toHaveValue("yop", {
|
||||
message: "input should contain field value in edit mode",
|
||||
});
|
||||
await fieldInput("name").edit("limbo");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input[type='text']").toHaveValue("limbo", {
|
||||
message: "the new value should be displayed",
|
||||
});
|
||||
});
|
||||
|
||||
test("setting a char field to empty string is saved as a false value", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].name).toBe(false);
|
||||
});
|
||||
await fieldInput("name").clear();
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("char field with size attribute", async () => {
|
||||
Partner._fields.name.size = 5;
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect("input").toHaveAttribute("maxlength", "5", {
|
||||
message: "maxlength attribute should have been set correctly on the input",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("char field in editable list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="name" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect("tbody td:not(.o_list_record_selector)").toHaveCount(5, {
|
||||
message: "should have 5 cells",
|
||||
});
|
||||
expect("tbody td:not(.o_list_record_selector):first").toHaveText("yop", {
|
||||
message: "value should be displayed properly as text",
|
||||
});
|
||||
|
||||
const cellSelector = "tbody td:not(.o_list_record_selector)";
|
||||
await contains(cellSelector).click();
|
||||
expect(queryFirst(cellSelector).parentElement).toHaveClass("o_selected_row", {
|
||||
message: "should be set as edit mode",
|
||||
});
|
||||
expect(`${cellSelector} input`).toHaveValue("yop", {
|
||||
message: "should have the corect value in internal input",
|
||||
});
|
||||
await fieldInput("name").edit("brolo", { confirm: false });
|
||||
|
||||
await contains(".o_list_button_save").click();
|
||||
expect(cellSelector).not.toHaveClass("o_selected_row", {
|
||||
message: "should not be in edit mode anymore",
|
||||
});
|
||||
});
|
||||
|
||||
test("char field translatable", async () => {
|
||||
Partner._fields.name.translate = true;
|
||||
|
||||
serverState.lang = "en_US";
|
||||
serverState.multiLang = true;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
let callGetFieldTranslations = 0;
|
||||
onRpc("res.lang", "get_installed", () => [
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
["es_ES", "Spanish"],
|
||||
]);
|
||||
onRpc("res.partner", "get_field_translations", () => {
|
||||
if (callGetFieldTranslations++ === 0) {
|
||||
return [
|
||||
[
|
||||
{ lang: "en_US", source: "yop", value: "yop" },
|
||||
{ lang: "fr_BE", source: "yop", value: "yop français" },
|
||||
{ lang: "es_ES", source: "yop", value: "yop español" },
|
||||
],
|
||||
{ translation_type: "char", translation_show_source: false },
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
[
|
||||
{ lang: "en_US", source: "bar", value: "bar" },
|
||||
{ lang: "fr_BE", source: "bar", value: "yop français" },
|
||||
{ lang: "es_ES", source: "bar", value: "bar" },
|
||||
],
|
||||
{ translation_type: "char", translation_show_source: false },
|
||||
];
|
||||
}
|
||||
});
|
||||
onRpc("res.partner", "update_field_translations", function ({ args, kwargs }) {
|
||||
expect(args[2]).toEqual(
|
||||
{ en_US: "bar", es_ES: false },
|
||||
{
|
||||
message:
|
||||
"the new translation value should be written and the value false voids the translation",
|
||||
}
|
||||
);
|
||||
for (const record of this.env["res.partner"].browse(args[0])) {
|
||||
record[args[1]] = args[2][kwargs.context.lang];
|
||||
}
|
||||
return true;
|
||||
});
|
||||
expect("[name=name] input").toHaveClass("o_field_translate");
|
||||
await contains("[name=name] input").click();
|
||||
expect(".o_field_char .btn.o_field_translate").toHaveCount(1, {
|
||||
message: "should have a translate button",
|
||||
});
|
||||
expect(".o_field_char .btn.o_field_translate").toHaveText("EN", {
|
||||
message: "the button should have as test the current language",
|
||||
});
|
||||
await contains(".o_field_char .btn.o_field_translate").click();
|
||||
expect(".modal").toHaveCount(1, {
|
||||
message: "a translate modal should be visible",
|
||||
});
|
||||
expect(".modal .o_translation_dialog .translation").toHaveCount(3, {
|
||||
message: "three rows should be visible",
|
||||
});
|
||||
let translations = queryAll(".modal .o_translation_dialog .translation input");
|
||||
expect(translations[0]).toHaveValue("yop", {
|
||||
message: "English translation should be filled",
|
||||
});
|
||||
expect(translations[1]).toHaveValue("yop français", {
|
||||
message: "French translation should be filled",
|
||||
});
|
||||
expect(translations[2]).toHaveValue("yop español", {
|
||||
message: "Spanish translation should be filled",
|
||||
});
|
||||
await contains(translations[0]).edit("bar");
|
||||
await contains(translations[2]).clear();
|
||||
await contains("footer .btn.btn-primary").click();
|
||||
expect(".o_field_widget.o_field_char input").toHaveValue("bar", {
|
||||
message: "the new translation should be transfered to modified record",
|
||||
});
|
||||
await fieldInput("name").edit("baz");
|
||||
await contains(".o_field_char .btn.o_field_translate").click();
|
||||
|
||||
translations = queryAll(".modal .o_translation_dialog .translation input");
|
||||
expect(translations[0]).toHaveValue("baz", {
|
||||
message: "Modified value should be used instead of translation",
|
||||
});
|
||||
expect(translations[1]).toHaveValue("yop français", {
|
||||
message: "French translation shouldn't be changed",
|
||||
});
|
||||
expect(translations[2]).toHaveValue("bar", {
|
||||
message: "Spanish translation should fallback to the English translation",
|
||||
});
|
||||
});
|
||||
|
||||
test("translation dialog should close if field is not there anymore", async () => {
|
||||
expect.assertions(4);
|
||||
// In this test, we simulate the case where the field is removed from the view
|
||||
// this can happen for example if the user click the back button of the browser.
|
||||
Partner._fields.name.translate = true;
|
||||
|
||||
serverState.lang = "en_US";
|
||||
serverState.multiLang = true;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" />
|
||||
<field name="name" invisible="int_field == 9"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
onRpc(async ({ method, model }) => {
|
||||
if (method === "get_installed" && model === "res.lang") {
|
||||
return [
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
["es_ES", "Spanish"],
|
||||
];
|
||||
}
|
||||
if (method === "get_field_translations" && model === "res.partner") {
|
||||
return [
|
||||
[
|
||||
{ lang: "en_US", source: "yop", value: "yop" },
|
||||
{ lang: "fr_BE", source: "yop", value: "valeur français" },
|
||||
{ lang: "es_ES", source: "yop", value: "yop español" },
|
||||
],
|
||||
{ translation_type: "char", translation_show_source: false },
|
||||
];
|
||||
}
|
||||
});
|
||||
expect("[name=name] input").toHaveClass("o_field_translate");
|
||||
await contains("[name=name] input").click();
|
||||
await contains(".o_field_char .btn.o_field_translate").click();
|
||||
expect(".modal").toHaveCount(1, {
|
||||
message: "a translate modal should be visible",
|
||||
});
|
||||
await fieldInput("int_field").edit("9");
|
||||
await animationFrame();
|
||||
expect("[name=name] input").toHaveCount(0, {
|
||||
message: "the field name should be invisible",
|
||||
});
|
||||
expect(".modal").toHaveCount(0, {
|
||||
message: "a translate modal should not be visible",
|
||||
});
|
||||
});
|
||||
|
||||
test("html field translatable", async () => {
|
||||
expect.assertions(5);
|
||||
Partner._fields.name.translate = true;
|
||||
|
||||
serverState.lang = "en_US";
|
||||
serverState.multiLang = true;
|
||||
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
onRpc(async ({ args, method, model }) => {
|
||||
if (method === "get_installed" && model === "res.lang") {
|
||||
return [
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
];
|
||||
}
|
||||
if (method === "get_field_translations" && model === "res.partner") {
|
||||
return [
|
||||
[
|
||||
{
|
||||
lang: "en_US",
|
||||
source: "first paragraph",
|
||||
value: "first paragraph",
|
||||
},
|
||||
{
|
||||
lang: "en_US",
|
||||
source: "second paragraph",
|
||||
value: "second paragraph",
|
||||
},
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "first paragraph",
|
||||
value: "premier paragraphe",
|
||||
},
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "second paragraph",
|
||||
value: "deuxième paragraphe",
|
||||
},
|
||||
],
|
||||
{
|
||||
translation_type: "char",
|
||||
translation_show_source: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (method === "update_field_translations" && model === "res.partner") {
|
||||
expect(args[2]).toEqual(
|
||||
{ en_US: { "first paragraph": "first paragraph modified" } },
|
||||
{
|
||||
message: "the new translation value should be written",
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// this will not affect the translate_fields effect until the record is
|
||||
// saved but is set for consistency of the test
|
||||
await fieldInput("name").edit("<p>first paragraph</p><p>second paragraph</p>");
|
||||
await contains(".o_field_char .btn.o_field_translate").click();
|
||||
expect(".modal").toHaveCount(1, {
|
||||
message: "a translate modal should be visible",
|
||||
});
|
||||
expect(".modal .o_translation_dialog .translation").toHaveCount(4, {
|
||||
message: "four rows should be visible",
|
||||
});
|
||||
const enField = queryFirst(".modal .o_translation_dialog .translation input");
|
||||
expect(enField).toHaveValue("first paragraph", {
|
||||
message: "first part of english translation should be filled",
|
||||
});
|
||||
await contains(enField).edit("first paragraph modified");
|
||||
await contains(".modal button.btn-primary").click();
|
||||
expect(".o_field_char input[type='text']").toHaveValue(
|
||||
"<p>first paragraph</p><p>second paragraph</p>",
|
||||
{
|
||||
message: "the new partial translation should not be transfered",
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("char field translatable in create mode", async () => {
|
||||
Partner._fields.name.translate = true;
|
||||
|
||||
serverState.multiLang = true;
|
||||
|
||||
await mountView({ type: "form", resModel: "res.partner" });
|
||||
|
||||
expect(".o_field_char .btn.o_field_translate").toHaveCount(1, {
|
||||
message: "should have a translate button in create mode",
|
||||
});
|
||||
});
|
||||
|
||||
test("char field does not allow html injections", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
await fieldInput("name").edit("<script>throw Error();</script>");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("<script>throw Error();</script>", {
|
||||
message: "the value should have been properly escaped",
|
||||
});
|
||||
});
|
||||
|
||||
test("char field trim (or not) characters", async () => {
|
||||
Partner._fields.foo2 = fields.Char({ trim: false });
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" />
|
||||
<field name="foo2" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await fieldInput("name").edit(" abc ");
|
||||
await fieldInput("foo2").edit(" def ");
|
||||
await clickSave();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("abc", {
|
||||
message: "Name value should have been trimmed",
|
||||
});
|
||||
expect(".o_field_widget[name='foo2'] input:only").toHaveValue(" def ");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("input field: change value before pending onchange returns", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="product_id" />
|
||||
<field name="name" />
|
||||
</list>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
let def;
|
||||
onRpc("onchange", () => def);
|
||||
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("My little Name Value", {
|
||||
message: "should contain the default value",
|
||||
});
|
||||
|
||||
def = new Deferred();
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--dropdown-item").click();
|
||||
await fieldInput("name").edit("tralala", { confirm: false });
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "should contain tralala",
|
||||
});
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "should contain the same value as before onchange",
|
||||
});
|
||||
});
|
||||
|
||||
test("input field: change value before pending onchange returns (2)", async () => {
|
||||
Partner._onChanges.int_field = (obj) => {
|
||||
if (obj.int_field === 7) {
|
||||
obj.name = "blabla";
|
||||
} else {
|
||||
obj.name = "tralala";
|
||||
}
|
||||
};
|
||||
|
||||
const def = new Deferred();
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="int_field" />
|
||||
<field name="name" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
onRpc("onchange", () => def);
|
||||
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("yop", {
|
||||
message: "should contain the correct value",
|
||||
});
|
||||
|
||||
// trigger a deferred onchange
|
||||
await fieldInput("int_field").edit("7");
|
||||
await fieldInput("name").edit("test", { confirm: false });
|
||||
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("test", {
|
||||
message: "The onchage value should not be applied because the input is in edition",
|
||||
});
|
||||
await fieldInput("name").press("Enter");
|
||||
await expect(".o_field_widget[name='name'] input").toHaveValue("test");
|
||||
await fieldInput("int_field").edit("10");
|
||||
await expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "The onchange value should be applied because the input is not in edition",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("input field: change value before pending onchange returns (with fieldDebounce)", async () => {
|
||||
// this test is exactly the same as the previous one, except that in
|
||||
// this scenario the onchange return *before* we validate the change
|
||||
// on the input field (before the "change" event is triggered).
|
||||
Partner._onChanges.product_id = (obj) => {
|
||||
obj.int_field = obj.product_id ? 7 : false;
|
||||
};
|
||||
let def;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
<field name="int_field"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
onRpc("onchange", () => def);
|
||||
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("My little Name Value", {
|
||||
message: "should contain the default value",
|
||||
});
|
||||
|
||||
def = new Deferred();
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--dropdown-item").click();
|
||||
await fieldInput("name").edit("tralala", { confirm: false });
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "should contain tralala",
|
||||
});
|
||||
expect(".o_field_widget[name='int_field'] input").toHaveValue("");
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "should contain the same value as before onchange",
|
||||
});
|
||||
expect(".o_field_widget[name='int_field'] input").toHaveValue("7", {
|
||||
message: "should contain the value returned by the onchange",
|
||||
});
|
||||
});
|
||||
|
||||
test("onchange return value before editing input", async () => {
|
||||
Partner._onChanges.name = (obj) => {
|
||||
obj.name = "yop";
|
||||
};
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("yop");
|
||||
await fieldInput("name").edit("tralala");
|
||||
await expect("[name='name'] input").toHaveValue("yop");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("input field: change value before pending onchange renaming", async () => {
|
||||
Partner._onChanges.product_id = (obj) => {
|
||||
obj.name = "on change value";
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="product_id" />
|
||||
<field name="name" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
onRpc("onchange", () => def);
|
||||
|
||||
const def = new Deferred();
|
||||
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("yop", {
|
||||
message: "should contain the correct value",
|
||||
});
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--dropdown-item").click();
|
||||
// set name before onchange
|
||||
await fieldInput("name").edit("tralala");
|
||||
await expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "should contain tralala",
|
||||
});
|
||||
|
||||
// complete the onchange
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "input should contain the same value as before onchange",
|
||||
});
|
||||
});
|
||||
|
||||
test("support autocomplete attribute", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" autocomplete="coucou"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='name'] input").toHaveAttribute("autocomplete", "coucou", {
|
||||
message: "attribute autocomplete should be set",
|
||||
});
|
||||
});
|
||||
|
||||
test("input autocomplete attribute set to none by default", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='name'] input").toHaveAttribute("autocomplete", "off", {
|
||||
message: "attribute autocomplete should be set to none by default",
|
||||
});
|
||||
});
|
||||
|
||||
test("support password attribute", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" password="True"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("yop", {
|
||||
message: "input value should be the password",
|
||||
});
|
||||
expect(".o_field_widget[name='name'] input").toHaveAttribute("type", "password", {
|
||||
message: "input should be of type password",
|
||||
});
|
||||
});
|
||||
|
||||
test("input field: readonly password", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" password="True" readonly="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_char").not.toHaveText("yop", {
|
||||
message: "password field value should be visible in read mode",
|
||||
});
|
||||
|
||||
expect(".o_field_char").toHaveText("***", {
|
||||
message: "password field value should be hidden with '*' in read mode",
|
||||
});
|
||||
});
|
||||
|
||||
test("input field: change password value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" password="True"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_char input").toHaveAttribute("type", "password", {
|
||||
message: "password field input value should with type 'password' in edit mode",
|
||||
});
|
||||
expect(".o_field_char input").toHaveValue("yop", {
|
||||
message: "password field input value should be the (hidden) password value",
|
||||
});
|
||||
});
|
||||
|
||||
test("input field: empty password", async () => {
|
||||
Partner._records[0].name = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" password="True"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_char input").toHaveAttribute("type", "password", {
|
||||
message: "password field input value should with type 'password' in edit mode",
|
||||
});
|
||||
expect(".o_field_char input").toHaveValue("", {
|
||||
message: "password field input value should be the (non-hidden, empty) password value",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("input field: set and remove value, then wait for onchange", async () => {
|
||||
Partner._onChanges.product_id = (obj) => {
|
||||
obj.name = obj.product_id ? "onchange value" : false;
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
expect(".o_field_widget[name=name] input").toHaveValue("");
|
||||
await fieldInput("name").edit("test", { confirm: false });
|
||||
await fieldInput("name").clear({ confirm: false });
|
||||
|
||||
// trigger the onchange by setting a product
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--dropdown-item").click();
|
||||
expect(".o_field_widget[name=name] input").toHaveValue("onchange value", {
|
||||
message: "input should contain correct value after onchange",
|
||||
});
|
||||
});
|
||||
|
||||
test("char field with placeholder", async () => {
|
||||
Partner._fields.name.default = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" placeholder="Placeholder" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='name'] input").toHaveAttribute("placeholder", "Placeholder", {
|
||||
message: "placeholder attribute should be set",
|
||||
});
|
||||
});
|
||||
|
||||
test("Form: placeholder_field shows as placeholder", async () => {
|
||||
Partner._records[0].name = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="placeholder_name" invisible="1" />
|
||||
<field name="name" options="{'placeholder_field': 'placeholder_name'}" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect("input").toHaveValue("", {
|
||||
message: "should have no value in input",
|
||||
});
|
||||
expect("input").toHaveAttribute("placeholder", "Placeholder Name", {
|
||||
message: "placeholder_field should be the placeholder",
|
||||
});
|
||||
});
|
||||
|
||||
test("char field: correct value is used to evaluate the modifiers", async () => {
|
||||
Partner._records[0].name = false;
|
||||
Partner._records[0].display_name = false;
|
||||
Partner._onChanges.name = (obj) => {
|
||||
if (obj.name === "a") {
|
||||
obj.display_name = false;
|
||||
} else if (obj.name === "b") {
|
||||
obj.display_name = "";
|
||||
}
|
||||
};
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" />
|
||||
<field name="display_name" invisible="'' == display_name"/>
|
||||
</form>`,
|
||||
});
|
||||
expect("[name='display_name']").toHaveCount(1);
|
||||
|
||||
await fieldInput("name").edit("a");
|
||||
await animationFrame();
|
||||
expect("[name='display_name']").toHaveCount(1);
|
||||
|
||||
await fieldInput("name").edit("b");
|
||||
await animationFrame();
|
||||
expect("[name='display_name']").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("edit a char field should display the status indicator buttons without flickering", async () => {
|
||||
Partner._records[0].partner_ids = [2];
|
||||
Partner._onChanges.name = (obj) => {
|
||||
obj.display_name = "cc";
|
||||
};
|
||||
|
||||
const def = new Deferred();
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="name"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
onRpc("onchange", () => {
|
||||
expect.step("onchange");
|
||||
return def;
|
||||
});
|
||||
expect(".o_form_status_indicator_buttons").not.toBeVisible({
|
||||
message: "form view is not dirty",
|
||||
});
|
||||
await contains(".o_data_cell").click();
|
||||
await fieldInput("name").edit("a");
|
||||
expect(".o_form_status_indicator_buttons").toBeVisible({
|
||||
message: "form view is dirty",
|
||||
});
|
||||
def.resolve();
|
||||
expect.verifySteps(["onchange"]);
|
||||
await animationFrame();
|
||||
expect(".o_form_status_indicator_buttons").toBeVisible({
|
||||
message: "form view is dirty",
|
||||
});
|
||||
expect.verifySteps(["onchange"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Color extends models.Model {
|
||||
hex_color = fields.Char({ string: "hexadecimal color" });
|
||||
text = fields.Char();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
hex_color: "#ff4444",
|
||||
},
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="hex_color" widget="color" />
|
||||
</group>
|
||||
</form>`,
|
||||
list: /* xml */ `
|
||||
<list editable="bottom">
|
||||
<field name="hex_color" widget="color" />
|
||||
</list>`,
|
||||
};
|
||||
}
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
|
||||
name = fields.Char();
|
||||
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Color, User]);
|
||||
|
||||
test("field contains a color input", async () => {
|
||||
Color._onChanges.hex_color = () => {};
|
||||
await mountView({ type: "form", resModel: "color", resId: 1 });
|
||||
|
||||
onRpc("onchange", ({ args }) => {
|
||||
expect.step(`onchange ${JSON.stringify(args)}`);
|
||||
});
|
||||
|
||||
expect(".o_field_color input[type='color']").toHaveCount(1);
|
||||
|
||||
expect(".o_field_color div").toHaveStyle(
|
||||
{ backgroundColor: "rgba(0, 0, 0, 0)" },
|
||||
{
|
||||
message: "field has the transparent background if no color value has been selected",
|
||||
}
|
||||
);
|
||||
expect(".o_field_color input").toHaveValue("#000000");
|
||||
|
||||
await contains(".o_field_color input", { visible: false }).edit("#fefefe");
|
||||
expect.verifySteps([
|
||||
'onchange [[1],{"hex_color":"#fefefe"},["hex_color"],{"hex_color":{},"display_name":{}}]',
|
||||
]);
|
||||
expect(".o_field_color input").toHaveValue("#fefefe");
|
||||
expect(".o_field_color div").toHaveStyle({ backgroundColor: "rgb(254, 254, 254)" });
|
||||
});
|
||||
|
||||
test("color field in editable list view", async () => {
|
||||
await mountView({ type: "list", resModel: "color", resId: 1 });
|
||||
|
||||
expect(".o_field_color input[type='color']").toHaveCount(2);
|
||||
await contains(".o_field_color input", { visible: false }).click();
|
||||
expect(".o_data_row").not.toHaveClass("o_selected_row");
|
||||
});
|
||||
|
||||
test("read-only color field in editable list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "color",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="hex_color" readonly="1" widget="color" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(".o_field_color input:disabled").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("color field read-only in model definition, in non-editable list", async () => {
|
||||
Color._fields.hex_color.readonly = true;
|
||||
await mountView({ type: "list", resModel: "color" });
|
||||
|
||||
expect(".o_field_color input:disabled").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("color field change via anoter field's onchange", async () => {
|
||||
Color._onChanges.text = (obj) => {
|
||||
obj.hex_color = "#fefefe";
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "color",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="text" />
|
||||
<field name="hex_color" widget="color" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
onRpc("onchange", ({ args }) => {
|
||||
expect.step(`onchange ${JSON.stringify(args)}`);
|
||||
});
|
||||
|
||||
expect(".o_field_color div").toHaveStyle(
|
||||
{ backgroundColor: "rgba(0, 0, 0, 0)" },
|
||||
{
|
||||
message: "field has the transparent background if no color value has been selected",
|
||||
}
|
||||
);
|
||||
expect(".o_field_color input").toHaveValue("#000000");
|
||||
await fieldInput("text").edit("someValue");
|
||||
expect.verifySteps([
|
||||
'onchange [[1],{"text":"someValue"},["text"],{"text":{},"hex_color":{},"display_name":{}}]',
|
||||
]);
|
||||
expect(".o_field_color input").toHaveValue("#fefefe");
|
||||
expect(".o_field_color div").toHaveStyle({ backgroundColor: "rgb(254, 254, 254)" });
|
||||
});
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll } from "@odoo/hoot-dom";
|
||||
import { contains, defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
name = fields.Char();
|
||||
int_field = fields.Integer();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "partnerName",
|
||||
int_field: 0,
|
||||
},
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
list: /* xml */ `
|
||||
<list>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
<field name="display_name" />
|
||||
</list>`,
|
||||
};
|
||||
}
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
|
||||
name = fields.Char();
|
||||
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Partner, User]);
|
||||
|
||||
test("No chosen color is a red line with a white background (color 0)", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_color_picker button.o_colorlist_item_color_0").toHaveCount(1);
|
||||
await contains(".o_field_color_picker button").click();
|
||||
expect(".o_field_color_picker button.o_colorlist_item_color_0").toHaveCount(1);
|
||||
await contains(".o_field_color_picker .o_colorlist_item_color_3").click();
|
||||
await contains(".o_field_color_picker button").click();
|
||||
expect(".o_field_color_picker button.o_colorlist_item_color_0").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("closes when color selected or outside click", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o_field_color_picker button").click();
|
||||
expect(queryAll(".o_field_color_picker button").length).toBeGreaterThan(1);
|
||||
await contains(".o_field_color_picker .o_colorlist_item_color_3").click();
|
||||
expect(".o_field_color_picker button").toHaveCount(1);
|
||||
await contains(".o_field_color_picker button").click();
|
||||
await contains(".o_field_widget[name='name'] input").click();
|
||||
expect(".o_field_color_picker button").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("color picker on list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
selectRecord() {
|
||||
expect.step("record selected to open");
|
||||
},
|
||||
});
|
||||
|
||||
await contains(".o_field_color_picker button").click();
|
||||
expect.verifySteps(["record selected to open"]);
|
||||
});
|
||||
|
||||
test("color picker in editable list view", async () => {
|
||||
Partner._records.push({
|
||||
int_field: 1,
|
||||
});
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
<field name="display_name" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(".o_data_row:nth-child(1) .o_field_color_picker button").toHaveCount(1);
|
||||
await contains(".o_data_row:nth-child(1) .o_field_color_picker button").click();
|
||||
expect(".o_data_row:nth-child(1).o_selected_row").toHaveCount(1);
|
||||
expect(".o_data_row:nth-child(1) .o_field_color_picker button").toHaveCount(12);
|
||||
await contains(
|
||||
".o_data_row:nth-child(1) .o_field_color_picker .o_colorlist_item_color_6"
|
||||
).click();
|
||||
expect(".o_data_row:nth-child(1) .o_field_color_picker button").toHaveCount(12);
|
||||
await contains(".o_data_row:nth-child(2) .o_data_cell").click();
|
||||
expect(".o_data_row:nth-child(1) .o_field_color_picker button").toHaveCount(1);
|
||||
expect(".o_data_row:nth-child(2) .o_field_color_picker button").toHaveCount(12);
|
||||
});
|
||||
|
||||
test("column widths: dont overflow color picker in list", async () => {
|
||||
Partner._fields.date_field = fields.Date({ string: "Date field" });
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="date_field"/>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
</list>`,
|
||||
domain: [["id", "<", 0]],
|
||||
});
|
||||
await contains(".o_control_panel_main_buttons .o_list_button_add", {
|
||||
visible: false,
|
||||
}).click();
|
||||
const date_column_width = queryAll(
|
||||
'.o_list_table thead th[data-name="date_field"]'
|
||||
)[0].style.width.replace("px", "");
|
||||
const int_field_column_width = queryAll(
|
||||
'.o_list_table thead th[data-name="int_field"]'
|
||||
)[0].style.width.replace("px", "");
|
||||
// Default values for date and int fields are: date: '92px', integer: '74px'
|
||||
// With the screen growing, the proportion is kept and thus int_field would remain smaller than date if
|
||||
// the color_picker wouldn't have widthInList set to '1'. With that property set, int_field size will be bigger
|
||||
// than date's one.
|
||||
expect(parseFloat(date_column_width)).toBeLessThan(parseFloat(int_field_column_width), {
|
||||
message: "colorpicker should display properly (Horizontly)",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
char_field = fields.Char({
|
||||
string: "Char",
|
||||
default: "My little Char Value",
|
||||
trim: true,
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
char_field: "char value",
|
||||
},
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="char_field" widget="CopyClipboardChar"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("Char Field: Copy to clipboard button", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_clipboard_button.o_btn_char_copy").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Show copy button even on empty field", async () => {
|
||||
Partner._records.push({
|
||||
char_field: false,
|
||||
});
|
||||
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 2 });
|
||||
|
||||
expect(".o_field_CopyClipboardChar[name='char_field'] .o_clipboard_button").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Show copy button even on readonly empty field", async () => {
|
||||
Partner._fields.char_field.readonly = true;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="char_field" widget="CopyClipboardChar" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_CopyClipboardChar[name='char_field'] .o_clipboard_button").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Display a tooltip on click", async () => {
|
||||
mockService("popover", {
|
||||
add(el, comp, params) {
|
||||
expect(params).toEqual({ tooltip: "Copied" });
|
||||
expect.step("copied tooltip");
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
patchWithCleanup(navigator.clipboard, {
|
||||
async writeText(text) {
|
||||
expect.step(text);
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await expect(".o_clipboard_button.o_btn_char_copy").toHaveCount(1);
|
||||
await contains(".o_clipboard_button", { visible: false }).click();
|
||||
expect.verifySteps(["char value", "copied tooltip"]);
|
||||
});
|
||||
|
||||
test("CopyClipboardButtonField in form view", async () => {
|
||||
patchWithCleanup(navigator.clipboard, {
|
||||
async writeText(text) {
|
||||
expect.step(text);
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="char_field" widget="CopyClipboardButton"/>
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=char_field] input").toHaveCount(0);
|
||||
expect(".o_clipboard_button.o_btn_char_copy").toHaveCount(1);
|
||||
|
||||
await contains(".o_clipboard_button.o_btn_char_copy").click();
|
||||
|
||||
expect.verifySteps(["char value"]);
|
||||
});
|
||||
|
||||
test("CopyClipboardButtonField can be disabled", async () => {
|
||||
patchWithCleanup(navigator.clipboard, {
|
||||
async writeText(text) {
|
||||
expect.step(text);
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="char_field" disabled="char_field == 'char value'" widget="CopyClipboardButton"/>
|
||||
<field name="char_field" widget="char"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_clipboard_button.o_btn_char_copy[disabled]").toHaveCount(1);
|
||||
await fieldInput("char_field").edit("another char value");
|
||||
expect(".o_clipboard_button.o_btn_char_copy[disabled]").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,544 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, press, queryAllTexts, queryOne, scroll } from "@odoo/hoot-dom";
|
||||
import { animationFrame, mockDate, mockTimeZone } from "@odoo/hoot-mock";
|
||||
import {
|
||||
assertDateTimePicker,
|
||||
getPickerCell,
|
||||
zoomOut,
|
||||
} from "@web/../tests/core/datetime/datetime_test_helpers";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
defineParams,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
date = fields.Date();
|
||||
char_field = fields.Char({ string: "Char" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
date: "2017-02-03",
|
||||
char_field: "first char field",
|
||||
},
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="date"/>
|
||||
<field name="char_field"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("toggle datepicker", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
await contains(".o_field_date input").click();
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
await fieldInput("char_field").click();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("open datepicker on Control+Enter", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
date_format: "%d/%m/%Y",
|
||||
time_format: "%H:%M:%S",
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_field_date input").toHaveCount(1);
|
||||
|
||||
await press(["ctrl", "enter"]);
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
//edit the input and open the datepicker again with ctrl+enter
|
||||
await contains(".o_field_date .o_input").click();
|
||||
await edit("09/01/1997");
|
||||
await press(["ctrl", "enter"]);
|
||||
await animationFrame();
|
||||
assertDateTimePicker({
|
||||
title: "January 1997",
|
||||
date: [
|
||||
{
|
||||
cells: [
|
||||
[0, 0, 0, 1, 2, 3, 4],
|
||||
[5, 6, 7, 8, [9], 10, 11],
|
||||
[12, 13, 14, 15, 16, 17, 18],
|
||||
[19, 20, 21, 22, 23, 24, 25],
|
||||
[26, 27, 28, 29, 30, 31, 0],
|
||||
],
|
||||
daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
weekNumbers: [1, 2, 3, 4, 5],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
test("toggle datepicker far in the future", async () => {
|
||||
Partner._records[0].date = "9999-12-31";
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
// focus another field
|
||||
await fieldInput("char_field").click();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("date field is empty if no date is set", async () => {
|
||||
Partner._records[0].date = false;
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_date input").toHaveCount(1);
|
||||
expect(".o_field_date input").toHaveValue("");
|
||||
});
|
||||
|
||||
test("set an invalid date when the field is already set", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("02/03/2017");
|
||||
await fieldInput("date").edit("invalid date");
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("02/03/2017", {
|
||||
message: "Should have been reset to the original value",
|
||||
});
|
||||
});
|
||||
|
||||
test("set an invalid date when the field is not set yet", async () => {
|
||||
Partner._records[0].date = false;
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("");
|
||||
await fieldInput("date").edit("invalid date");
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("");
|
||||
});
|
||||
|
||||
test("value should not set on first click", async () => {
|
||||
Partner._records[0].date = false;
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("");
|
||||
await contains(getPickerCell(22)).click();
|
||||
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_date_item_cell.o_selected").toHaveText("22");
|
||||
});
|
||||
|
||||
test("date field in form view (with positive time zone offset)", async () => {
|
||||
mockTimeZone(2); // should be ignored by date fields
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(args[1].date);
|
||||
});
|
||||
|
||||
expect(".o_field_date input").toHaveValue("02/03/2017");
|
||||
|
||||
// open datepicker and select another value
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
expect(".o_date_item_cell.o_selected").toHaveCount(1);
|
||||
|
||||
// select 22 Feb 2017
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await contains(getPickerCell("2017")).click();
|
||||
await contains(getPickerCell("Feb")).click();
|
||||
await contains(getPickerCell("22")).click();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
expect(".o_field_date input").toHaveValue("02/22/2017");
|
||||
|
||||
await clickSave();
|
||||
expect.verifySteps(["2017-02-22"]);
|
||||
expect(".o_field_date input").toHaveValue("02/22/2017");
|
||||
});
|
||||
|
||||
test("date field in form view (with negative time zone offset)", async () => {
|
||||
mockTimeZone(-2); // should be ignored by date fields
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_date input").toHaveValue("02/03/2017");
|
||||
});
|
||||
|
||||
test("date field dropdown doesn't dissapear on scroll", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<div class="scrollable overflow-auto" style="height: 50px;">
|
||||
<div style="height: 2000px;">
|
||||
<field name="date" />
|
||||
</div>
|
||||
</div>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
await scroll(".scrollable", { top: 50 });
|
||||
expect(".scrollable").toHaveProperty("scrollTop", 50);
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("date field with label opens datepicker on click", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<label for="date" string="What date is it" />
|
||||
<field name="date" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains("label.o_form_label").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("date field with warn_future option ", async () => {
|
||||
Partner._records[0] = { id: 1 };
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date" options="{'warn_future': true}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_date input").click();
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await contains(getPickerCell("2020")).click();
|
||||
await contains(getPickerCell("Dec")).click();
|
||||
await contains(getPickerCell("22")).click();
|
||||
expect(".fa-exclamation-triangle").toHaveCount(1);
|
||||
await fieldInput("date").clear();
|
||||
expect(".fa-exclamation-triangle").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("date field with warn_future option: do not overwrite datepicker option", async () => {
|
||||
Partner._onChanges.date = () => {};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
// Do not let the date field get the focus in the first place
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="char_field" />
|
||||
<field name="date" options="{'warn_future': true}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("02/03/2017");
|
||||
await contains(".o_form_button_create").click();
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("date field in editable list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="date"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
const cell = queryOne("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
expect(cell).toHaveText("02/03/2017");
|
||||
await contains(cell).click();
|
||||
expect(".o_field_date input").toHaveCount(1);
|
||||
expect(".o_field_date input").toBeFocused();
|
||||
expect(".o_field_date input").toHaveValue("02/03/2017");
|
||||
|
||||
// open datepicker and select another value
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await contains(getPickerCell("2017")).click();
|
||||
await contains(getPickerCell("Feb")).click();
|
||||
await contains(getPickerCell("22")).click();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
expect(".o_field_date input").toHaveValue("02/22/2017");
|
||||
|
||||
await contains(".o_list_button_save").click();
|
||||
expect("tr.o_data_row td:not(.o_list_record_selector)").toHaveText("02/22/2017");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("multi edition of date field in list view: clear date in input", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
Partner._records = [
|
||||
{ id: 1, date: "2017-02-03" },
|
||||
{ id: 2, date: "2017-02-03" },
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<list multi_edit="1">
|
||||
<field name="date"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_data_row:eq(0) .o_list_record_selector input").click();
|
||||
await contains(".o_data_row:eq(1) .o_list_record_selector input").click();
|
||||
await contains(".o_data_row:eq(0) .o_data_cell").click();
|
||||
|
||||
expect(".o_field_date input").toHaveCount(1);
|
||||
await fieldInput("date").clear();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
await contains(".modal .modal-footer .btn-primary").click();
|
||||
|
||||
expect(".o_data_row:first-child .o_data_cell").toHaveText("");
|
||||
expect(".o_data_row:nth-child(2) .o_data_cell").toHaveText("");
|
||||
});
|
||||
|
||||
test("date field remove value", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(args[1].date);
|
||||
});
|
||||
|
||||
expect(".o_field_date input").toHaveValue("02/03/2017");
|
||||
|
||||
await fieldInput("date").clear();
|
||||
expect(".o_field_date input").toHaveValue("");
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_date").toHaveText("");
|
||||
expect.verifySteps([false]);
|
||||
});
|
||||
|
||||
test("date field should select its content onclick when there is one", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
const active = document.activeElement;
|
||||
expect(active.tagName).toBe("INPUT");
|
||||
expect(active.value.slice(active.selectionStart, active.selectionEnd)).toBe("02/03/2017");
|
||||
});
|
||||
|
||||
test("date field supports custom formats", async () => {
|
||||
defineParams({ lang_parameters: { date_format: "dd-MM-yyyy" } });
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
const dateViewValue = queryOne(".o_field_date input").value;
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_field_date input").toHaveValue(dateViewValue);
|
||||
|
||||
await contains(getPickerCell("22")).click();
|
||||
|
||||
const dateEditValue = queryOne(".o_field_date input").value;
|
||||
await clickSave();
|
||||
expect(".o_field_date input").toHaveValue(dateEditValue);
|
||||
});
|
||||
|
||||
test("date field supports internationalization", async () => {
|
||||
serverState.lang = "nb_NO";
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
const dateViewForm = queryOne(".o_field_date input").value;
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_field_date input").toHaveValue(dateViewForm);
|
||||
expect(".o_zoom_out strong").toHaveText("februar 2017");
|
||||
|
||||
await contains(getPickerCell("22")).click();
|
||||
const dateEditForm = queryOne(".o_field_date input").value;
|
||||
await clickSave();
|
||||
expect(".o_field_date input").toHaveValue(dateEditForm);
|
||||
});
|
||||
|
||||
test("hit enter should update value", async () => {
|
||||
mockTimeZone(2);
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
const year = new Date().getFullYear();
|
||||
await contains(".o_field_date input").edit("01/08");
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue(`01/08/${year}`);
|
||||
await contains(".o_field_date input").edit("08/01");
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue(`08/01/${year}`);
|
||||
});
|
||||
|
||||
test("allow to use compute dates (+5d for instance)", async () => {
|
||||
mockDate({ year: 2021, month: 2, day: 15 });
|
||||
|
||||
Partner._fields.date.default = "2019-09-15";
|
||||
await mountView({ type: "form", resModel: "res.partner" });
|
||||
|
||||
expect(".o_field_date input").toHaveValue("09/15/2019");
|
||||
await fieldInput("date").edit("+5d");
|
||||
expect(".o_field_date input").toHaveValue("02/20/2021");
|
||||
|
||||
// Discard and do it again
|
||||
await contains(".o_form_button_cancel").click();
|
||||
expect(".o_field_date input").toHaveValue("09/15/2019");
|
||||
await fieldInput("date").edit("+5d");
|
||||
expect(".o_field_date input").toHaveValue("02/20/2021");
|
||||
|
||||
// Save and do it again
|
||||
await clickSave();
|
||||
expect(".o_field_date input").toHaveValue("02/20/2021");
|
||||
await fieldInput("date").edit("+5d");
|
||||
expect(".o_field_date input").toHaveValue("02/20/2021");
|
||||
});
|
||||
|
||||
test("date field with min_precision option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
// Do not let the date field get the focus in the first place
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="date" options="{'min_precision': 'months'}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(".o_field_date input");
|
||||
await animationFrame();
|
||||
expect(".o_date_item_cell").toHaveCount(12);
|
||||
expect(queryAllTexts(".o_date_item_cell")).toEqual([
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
]);
|
||||
expect(".o_date_item_cell.o_selected").toHaveText("Feb");
|
||||
|
||||
await click(getPickerCell("Jan"));
|
||||
await animationFrame();
|
||||
// The picker should be closed
|
||||
expect(".o_date_item_cell").toHaveCount(0);
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("01/01/2017");
|
||||
});
|
||||
|
||||
test("date field with max_precision option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
// Do not let the date field get the focus in the first place
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="date" options="{'max_precision': 'months'}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(".o_field_date input");
|
||||
await animationFrame();
|
||||
// Try to zoomOut twice to be in the year selector
|
||||
await zoomOut();
|
||||
// Currently in the month selector
|
||||
expect(".o_datetime_picker_header").toHaveText("2017");
|
||||
await zoomOut();
|
||||
// Stay in the month selector according to the max precision value
|
||||
expect(".o_datetime_picker_header").toHaveText("2017");
|
||||
expect(".o_date_item_cell.o_selected").toHaveText("Feb");
|
||||
|
||||
await click(getPickerCell("Jan"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("12"));
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("01/12/2017");
|
||||
});
|
||||
|
||||
test("DateField with onchange forcing a specific date", async () => {
|
||||
mockDate("2009-05-04 10:00:00", +1);
|
||||
|
||||
Partner._onChanges.date = (obj) => {
|
||||
if (obj.char_field === "force today") {
|
||||
obj.date = "2009-05-04";
|
||||
}
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="char_field"/>
|
||||
<field name="date"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_date input").toHaveValue("");
|
||||
|
||||
// enable the onchange
|
||||
await contains(".o_field_widget[name=char_field] input").edit("force today");
|
||||
|
||||
// open the picker and try to set a value different from today
|
||||
await click(".o_field_date input");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
await contains(getPickerCell("22")).click(); // 22 May 2009
|
||||
expect(".o_field_date input").toHaveValue("05/04/2009"); // value forced by the onchange
|
||||
|
||||
// do it again (the technical flow is a bit different as now the current value is already today)
|
||||
await click(".o_field_date input");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
await contains(getPickerCell("22")).click(); // 22 May 2009
|
||||
expect(".o_field_date input").toHaveValue("05/04/2009"); // value forced by the onchange
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,662 @@
|
|||
import { after, expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
click,
|
||||
edit,
|
||||
queryAll,
|
||||
queryAllProperties,
|
||||
queryAllTexts,
|
||||
resize,
|
||||
select,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { animationFrame, mockTimeZone } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
defineParams,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import {
|
||||
getPickerApplyButton,
|
||||
getPickerCell,
|
||||
getTimePickers,
|
||||
zoomOut,
|
||||
} from "@web/../tests/core/datetime/datetime_test_helpers";
|
||||
|
||||
import { resetDateFieldWidths } from "@web/views/list/column_width_hook";
|
||||
class Partner extends models.Model {
|
||||
date = fields.Date({ string: "A date", searchable: true });
|
||||
datetime = fields.Datetime({ string: "A datetime", searchable: true });
|
||||
p = fields.One2many({
|
||||
string: "one2many field",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
});
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
date: "2017-02-03",
|
||||
datetime: "2017-02-08 10:00:00",
|
||||
p: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: false,
|
||||
datetime: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
|
||||
name = fields.Char();
|
||||
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Partner, User]);
|
||||
|
||||
test("DatetimeField in form view", async () => {
|
||||
mockTimeZone(+2); // UTC+2
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
const expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone
|
||||
expect(".o_field_datetime input").toHaveValue(expectedDateString, {
|
||||
message: "the datetime should be correctly displayed",
|
||||
});
|
||||
|
||||
// datepicker should not open on focus
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
|
||||
await click(".o_field_datetime input");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
// select 22 April 2018 at 8:25
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await click(getPickerCell("2018"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("Apr"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("22"));
|
||||
await animationFrame();
|
||||
const [hourSelect, minuteSelect] = getTimePickers().at(0);
|
||||
await select("8", { target: hourSelect });
|
||||
await animationFrame();
|
||||
await select("25", { target: minuteSelect });
|
||||
await animationFrame();
|
||||
// Close the datepicker
|
||||
await click(".o_form_view_container");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0, { message: "datepicker should be closed" });
|
||||
|
||||
const newExpectedDateString = "04/22/2018 08:25:00";
|
||||
expect(".o_field_datetime input").toHaveValue(newExpectedDateString, {
|
||||
message: "the selected date should be displayed in the input",
|
||||
});
|
||||
|
||||
// save
|
||||
await clickSave();
|
||||
expect(".o_field_datetime input").toHaveValue(newExpectedDateString, {
|
||||
message: "the selected date should be displayed after saving",
|
||||
});
|
||||
});
|
||||
|
||||
test("DatetimeField only triggers fieldChange when a day is picked and when an hour/minute is selected", async () => {
|
||||
mockTimeZone(+2);
|
||||
|
||||
Partner._onChanges.datetime = () => {};
|
||||
|
||||
onRpc("onchange", () => expect.step("onchange"));
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
await click(".o_field_datetime input");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
// select 22 April 2018 at 8:25
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await click(getPickerCell("2018"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("Apr"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("22"));
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
const [hourSelect, minuteSelect] = getTimePickers().at(0);
|
||||
await select("8", { target: hourSelect });
|
||||
await animationFrame();
|
||||
await select("25", { target: minuteSelect });
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
// Close the datepicker
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
|
||||
expect(".o_field_datetime input").toHaveValue("04/22/2018 08:25:00");
|
||||
expect.verifySteps(["onchange"]);
|
||||
});
|
||||
|
||||
test("DatetimeField with datetime formatted without second", async () => {
|
||||
mockTimeZone(0);
|
||||
|
||||
Partner._fields.datetime = fields.Datetime({
|
||||
string: "A datetime",
|
||||
searchable: true,
|
||||
default: "2017-08-02 12:00:05",
|
||||
required: true,
|
||||
});
|
||||
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
date_format: "%m/%d/%Y",
|
||||
time_format: "%H:%M",
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
const expectedDateString = "08/02/2017 12:00";
|
||||
expect(".o_field_datetime input").toHaveValue(expectedDateString, {
|
||||
message: "the datetime should be correctly displayed",
|
||||
});
|
||||
|
||||
await click(".o_form_button_cancel");
|
||||
expect(".modal").toHaveCount(0, { message: "there should not be a Warning dialog" });
|
||||
});
|
||||
|
||||
test("DatetimeField in editable list view", async () => {
|
||||
mockTimeZone(+2);
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list editable="bottom"><field name="datetime"/></list>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone
|
||||
expect("tr.o_data_row td:not(.o_list_record_selector):first").toHaveText(expectedDateString, {
|
||||
message: "the datetime should be correctly displayed",
|
||||
});
|
||||
|
||||
// switch to edit mode
|
||||
await click(".o_data_row .o_data_cell");
|
||||
await animationFrame();
|
||||
expect(".o_field_datetime input").toHaveCount(1, {
|
||||
message: "the view should have a date input for editable mode",
|
||||
});
|
||||
expect(".o_field_datetime input").toBeFocused({
|
||||
message: "date input should have the focus",
|
||||
});
|
||||
|
||||
expect(".o_field_datetime input").toHaveValue(expectedDateString, {
|
||||
message: "the date should be correct in edit mode",
|
||||
});
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
|
||||
await click(".o_field_datetime input");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
// select 22 April 2018 at 8:25
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await click(getPickerCell("2018"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("Apr"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("22"));
|
||||
await animationFrame();
|
||||
const [hourSelect, minuteSelect] = getTimePickers().at(0);
|
||||
await select("8", { target: hourSelect });
|
||||
await animationFrame();
|
||||
await select("25", { target: minuteSelect });
|
||||
await animationFrame();
|
||||
// Apply changes
|
||||
|
||||
await click(getPickerApplyButton());
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(0, { message: "datepicker should be closed" });
|
||||
|
||||
const newExpectedDateString = "04/22/2018 08:25:00";
|
||||
|
||||
expect(".o_field_datetime input:first").toHaveValue(newExpectedDateString, {
|
||||
message: "the date should be correct in edit mode",
|
||||
});
|
||||
// save
|
||||
|
||||
await click(".o_list_button_save");
|
||||
await animationFrame();
|
||||
expect("tr.o_data_row td:not(.o_list_record_selector):first").toHaveText(
|
||||
newExpectedDateString,
|
||||
{ message: "the selected datetime should be displayed after saving" }
|
||||
);
|
||||
});
|
||||
test.tags("desktop");
|
||||
test("multi edition of DatetimeField in list view: edit date in input", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list multi_edit="1"><field name="datetime"/></list>',
|
||||
});
|
||||
|
||||
// select two records and edit them
|
||||
await click(".o_data_row:eq(0) .o_list_record_selector input");
|
||||
await animationFrame();
|
||||
await click(".o_data_row:eq(1) .o_list_record_selector input");
|
||||
await animationFrame();
|
||||
|
||||
await click(".o_data_row:eq(0) .o_data_cell");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_datetime input").toHaveCount(1);
|
||||
|
||||
await click(".o_field_datetime input");
|
||||
await edit("10/02/2019 09:00:00", { confirm: "Enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
|
||||
await click(".modal .modal-footer .btn-primary");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:first-child .o_data_cell:first").toHaveText("10/02/2019 09:00:00");
|
||||
expect(".o_data_row:nth-child(2) .o_data_cell:first").toHaveText("10/02/2019 09:00:00");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("multi edition of DatetimeField in list view: clear date in input", async () => {
|
||||
Partner._records[1].datetime = "2017-02-08 10:00:00";
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list multi_edit="1"><field name="datetime"/></list>',
|
||||
});
|
||||
|
||||
// select two records and edit them
|
||||
await click(".o_data_row:eq(0) .o_list_record_selector input");
|
||||
await animationFrame();
|
||||
await click(".o_data_row:eq(1) .o_list_record_selector input");
|
||||
await animationFrame();
|
||||
await click(".o_data_row:eq(0) .o_data_cell");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_datetime input").toHaveCount(1);
|
||||
|
||||
await click(".o_field_datetime input");
|
||||
await animationFrame();
|
||||
await edit("", { confirm: "Enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
|
||||
await click(".modal .modal-footer .btn-primary");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:first-child .o_data_cell:first").toHaveText("");
|
||||
expect(".o_data_row:nth-child(2) .o_data_cell:first").toHaveText("");
|
||||
});
|
||||
|
||||
test("DatetimeField remove value", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
mockTimeZone(+2);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].datetime).toBe(false, { message: "the correct value should be saved" });
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_datetime input:first").toHaveValue("02/08/2017 12:00:00", {
|
||||
message: "the date should be correct in edit mode",
|
||||
});
|
||||
|
||||
await click(".o_field_datetime input");
|
||||
await edit("");
|
||||
await animationFrame();
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_datetime input:first").toHaveValue("", {
|
||||
message: "should have an empty input",
|
||||
});
|
||||
|
||||
// save
|
||||
await clickSave();
|
||||
expect(".o_field_datetime:first").toHaveText("", {
|
||||
message: "the selected date should be displayed after saving",
|
||||
});
|
||||
});
|
||||
|
||||
test("DatetimeField with date/datetime widget (with day change) does not care about widget", async () => {
|
||||
mockTimeZone(-4);
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
Partner._records[0].p = [2];
|
||||
Partner._records[1].datetime = "2017-02-08 02:00:00"; // UTC
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="p">
|
||||
<list><field name="datetime" /></list>
|
||||
<form><field name="datetime" widget="date" /></form>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "02/07/2017 22:00:00"; // local time zone
|
||||
expect(".o_field_widget[name='p'] .o_data_cell").toHaveText(expectedDateString, {
|
||||
message: "the datetime (datetime widget) should be correctly displayed in list view",
|
||||
});
|
||||
|
||||
// switch to form view
|
||||
await click(".o_field_widget[name='p'] .o_data_cell");
|
||||
await animationFrame();
|
||||
expect(".modal .o_field_date[name='datetime'] input").toHaveValue("02/07/2017 22:00:00", {
|
||||
message: "the datetime (date widget) should be correctly displayed in form view",
|
||||
});
|
||||
});
|
||||
|
||||
test("DatetimeField with date/datetime widget (without day change) does not care about widget", async () => {
|
||||
mockTimeZone(-4);
|
||||
|
||||
Partner._records[0].p = [2];
|
||||
Partner._records[1].datetime = "2017-02-08 10:00:00"; // without timezone
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="p">
|
||||
<list><field name="datetime" /></list>
|
||||
<form><field name="datetime" widget="date" /></form>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "02/08/2017 06:00:00"; // with timezone
|
||||
expect(".o_field_widget[name='p'] .o_data_cell:first").toHaveText(expectedDateString, {
|
||||
message: "the datetime (datetime widget) should be correctly displayed in list view",
|
||||
});
|
||||
|
||||
// switch to form view
|
||||
await click(".o_field_widget[name='p'] .o_data_cell");
|
||||
await animationFrame();
|
||||
expect(".modal .o_field_date[name='datetime'] input:first").toHaveValue("02/08/2017 06:00:00", {
|
||||
message: "the datetime (date widget) should be correctly displayed in form view",
|
||||
});
|
||||
});
|
||||
|
||||
test("datetime field: hit enter should update value", async () => {
|
||||
// This test verifies that the field datetime is correctly computed when:
|
||||
// - we press enter to validate our entry
|
||||
// - we click outside the field to validate our entry
|
||||
// - we save
|
||||
mockTimeZone(+2);
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Enter a beginning of date and press enter to validate
|
||||
await click(".o_field_datetime input");
|
||||
await edit("01/08/22 14:30:40", { confirm: "Enter" });
|
||||
|
||||
const datetimeValue = `01/08/2022 14:30:40`;
|
||||
|
||||
expect(".o_field_datetime input:first").toHaveValue(datetimeValue);
|
||||
|
||||
// Click outside the field to check that the field is not changed
|
||||
await click(document.body);
|
||||
expect(".o_field_datetime input:first").toHaveValue(datetimeValue);
|
||||
|
||||
// Save and check that it's still ok
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_datetime input:first").toHaveValue(datetimeValue);
|
||||
});
|
||||
|
||||
test("DateTimeField with label opens datepicker on click", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<label for="datetime" string="When is it" />
|
||||
<field name="datetime" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click("label.o_form_label");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1, { message: "datepicker should be opened" });
|
||||
});
|
||||
|
||||
test("datetime field: use picker with arabic numbering system", async () => {
|
||||
defineParams({ lang: "ar_001" }); // Select Arab language
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form string="Partners"><field name="datetime" /></form>`,
|
||||
});
|
||||
|
||||
expect("[name=datetime] input:first").toHaveValue("٠٢/٠٨/٢٠١٧ ١١:٠٠:٠٠");
|
||||
|
||||
await click("[name=datetime] input");
|
||||
await animationFrame();
|
||||
await select(45, { target: getTimePickers()[0][1] });
|
||||
await animationFrame();
|
||||
expect("[name=datetime] input:first").toHaveValue("٠٢/٠٨/٢٠١٧ ١١:٤٥:٠٠");
|
||||
});
|
||||
|
||||
test("datetime field in list view with show_seconds option", async () => {
|
||||
mockTimeZone(+2);
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list>
|
||||
<field name="datetime" widget="datetime" options="{'show_seconds': false}" string="show_seconds as false"/>
|
||||
<field name="datetime" widget="datetime" string="show_seconds as true"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_row:first .o_field_datetime")).toEqual([
|
||||
"02/08/2017 12:00",
|
||||
"02/08/2017 12:00:00",
|
||||
]);
|
||||
});
|
||||
|
||||
test("edit a datetime field in form view with show_seconds option", async () => {
|
||||
mockTimeZone(+2);
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="datetime" widget="datetime" options="{'show_seconds': false}" string="show_seconds as false"/>
|
||||
<field name="datetime" widget="datetime" string="show_seconds as true"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const [dateField1, dateField2] = queryAll(".o_input.cursor-pointer");
|
||||
await click(dateField1);
|
||||
await animationFrame();
|
||||
expect(".o_time_picker_select").toHaveCount(3); // 3rd 'o_time_picker_select' is for the seconds
|
||||
await edit("02/08/2017 11:00:00", { confirm: "Enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(dateField1).toHaveValue("02/08/2017 11:00", {
|
||||
message: "seconds should be hidden for showSeconds false",
|
||||
});
|
||||
|
||||
expect(dateField2).toHaveValue("02/08/2017 11:00:00", {
|
||||
message: "seconds should be visible for showSeconds true",
|
||||
});
|
||||
});
|
||||
|
||||
test("datetime field (with widget) in kanban with show_time option", async () => {
|
||||
mockTimeZone(+2);
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="datetime" widget="datetime" options="{'show_time': false}"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_kanban_record:first").toHaveText("02/08/2017");
|
||||
});
|
||||
|
||||
test("datetime field in list with show_time option", async () => {
|
||||
mockTimeZone(+2);
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="datetime" options="{'show_time': false}"/>
|
||||
<field name="datetime" />
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
const dates = queryAll(".o_field_cell");
|
||||
|
||||
expect(dates[0]).toHaveText("02/08/2017", {
|
||||
message: "for date field only date should be visible with date widget",
|
||||
});
|
||||
expect(dates[1]).toHaveText("02/08/2017 12:00:00", {
|
||||
message: "for datetime field only date should be visible with date widget",
|
||||
});
|
||||
await click(dates[0]);
|
||||
await animationFrame();
|
||||
expect(".o_field_datetime input:first").toHaveValue("02/08/2017 12:00:00", {
|
||||
message: "for datetime field both date and time should be visible with datetime widget",
|
||||
});
|
||||
});
|
||||
|
||||
test("datetime field in form view with condensed option", async () => {
|
||||
mockTimeZone(-2); // UTC-2
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="datetime" options="{'condensed': true}"/>
|
||||
<field name="datetime" options="{'condensed': true}" readonly="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "2/8/2017 8:00:00"; // 10:00:00 without timezone
|
||||
expect(".o_field_datetime input").toHaveValue(expectedDateString);
|
||||
expect(".o_field_datetime.o_readonly_modifier").toHaveText(expectedDateString);
|
||||
});
|
||||
|
||||
test("datetime field in kanban view with condensed option", async () => {
|
||||
mockTimeZone(-2); // UTC-2
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="datetime" options="{'condensed': true}"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "2/8/2017 8:00:00"; // 10:00:00 without timezone
|
||||
expect(".o_kanban_record:first").toHaveText(expectedDateString);
|
||||
});
|
||||
|
||||
test("list datetime: column widths (show_time=false)", async () => {
|
||||
await resize({ width: 800 });
|
||||
document.body.style.fontFamily = "sans-serif";
|
||||
resetDateFieldWidths();
|
||||
after(resetDateFieldWidths);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list>
|
||||
<field name="datetime" widget="datetime" options="{'show_time': false }" />
|
||||
<field name="display_name" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_row:eq(0) .o_data_cell")).toEqual(["02/08/2017", "partner,1"]);
|
||||
expect(queryAllProperties(".o_list_table thead th", "offsetWidth")).toEqual([40, 83, 677]);
|
||||
});
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { Deferred, press, waitFor, waitUntil } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { onWillStart } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { DynamicPlaceholderPopover } from "@web/views/fields/dynamic_placeholder_popover";
|
||||
|
||||
class Partner extends models.Model {
|
||||
char = fields.Char();
|
||||
placeholder = fields.Char({ default: "partner" });
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, char: "yop", product_id: 37 },
|
||||
{ id: 2, char: "blip", product_id: false },
|
||||
{ id: 4, char: "abc", product_id: 41 },
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<field name="placeholder" invisible="1"/>
|
||||
<sheet>
|
||||
<group>
|
||||
<field
|
||||
name="char"
|
||||
options="{
|
||||
'dynamic_placeholder': true,
|
||||
'dynamic_placeholder_model_reference_field': 'placeholder'
|
||||
}"
|
||||
/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product Name" });
|
||||
|
||||
_records = [
|
||||
{ id: 37, name: "xphone" },
|
||||
{ id: 41, name: "xpad" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
onRpc("mail_allowed_qweb_expressions", () => []);
|
||||
|
||||
test("dynamic placeholder close with click out", async () => {
|
||||
await mountView({ type: "form", resModel: "partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
await contains(".o_content").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
await contains(".o_model_field_selector_popover_item_relation").click();
|
||||
await contains(".o_content").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("dynamic placeholder close with escape", async () => {
|
||||
await mountView({ type: "form", resModel: "partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
press("Escape");
|
||||
await animationFrame();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
await contains(".o_model_field_selector_popover_item_relation").click();
|
||||
press("Escape");
|
||||
await animationFrame();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("dynamic placeholder close when clicking on the cross", async () => {
|
||||
await mountView({ type: "form", resModel: "partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
await contains(".o_model_field_selector_popover_close").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
await contains(".o_model_field_selector_popover_item_relation").click();
|
||||
await contains(".o_model_field_selector_popover_close").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("correctly cache model qweb variables and don't prevent opening of other popovers", async () => {
|
||||
const def = new Deferred();
|
||||
let willStarts = 0;
|
||||
patchWithCleanup(DynamicPlaceholderPopover.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
onWillStart(() => {
|
||||
willStarts++;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("partner", "mail_allowed_qweb_expressions", async () => {
|
||||
expect.step("mail_allowed_qweb_expressions");
|
||||
await def;
|
||||
return [];
|
||||
});
|
||||
|
||||
await mountView({ type: "form", resModel: "partner", resId: 1 });
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
await waitUntil(() => willStarts === 1);
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
await waitUntil(() => willStarts === 2);
|
||||
|
||||
def.resolve();
|
||||
await waitFor(".o_model_field_selector_popover");
|
||||
expect(willStarts).toBe(2);
|
||||
expect.verifySteps(["mail_allowed_qweb_expressions"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "../../web_test_helpers";
|
||||
import { queryAllTexts, queryFirst } from "@odoo/hoot-dom";
|
||||
|
||||
class Contact extends models.Model {
|
||||
email = fields.Char();
|
||||
}
|
||||
|
||||
defineModels([Contact]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("in form view", async () => {
|
||||
Contact._records = [{ id: 1, email: "john.doe@odoo.com" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
resId: 1,
|
||||
arch: `<form><field name="email" widget="email"/></form>`,
|
||||
});
|
||||
expect(`.o_field_email input[type="email"]`).toHaveCount(1);
|
||||
expect(`.o_field_email input[type="email"]`).toHaveValue("john.doe@odoo.com");
|
||||
expect(`.o_field_email a`).toHaveCount(1);
|
||||
expect(`.o_field_email a`).toHaveAttribute("href", "mailto:john.doe@odoo.com");
|
||||
expect(`.o_field_email a`).toHaveAttribute("target", "_blank");
|
||||
await fieldInput("email").edit("new@odoo.com");
|
||||
expect(`.o_field_email input[type="email"]`).toHaveValue("new@odoo.com");
|
||||
});
|
||||
|
||||
test("in editable list view", async () => {
|
||||
Contact._records = [
|
||||
{ id: 1, email: "john.doe@odoo.com" },
|
||||
{ id: 2, email: "jane.doe@odoo.com" },
|
||||
];
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "contact",
|
||||
arch: '<list editable="bottom"><field name="email" widget="email"/></list>',
|
||||
});
|
||||
expect(`tbody td:not(.o_list_record_selector) a`).toHaveCount(2);
|
||||
expect(`.o_field_email a`).toHaveCount(2);
|
||||
expect(queryAllTexts(`tbody td:not(.o_list_record_selector) a`)).toEqual([
|
||||
"john.doe@odoo.com",
|
||||
"jane.doe@odoo.com",
|
||||
]);
|
||||
expect(".o_field_email a:first").toHaveAttribute("href", "mailto:john.doe@odoo.com");
|
||||
let cell = queryFirst("tbody td:not(.o_list_record_selector)");
|
||||
await contains(cell).click();
|
||||
expect(cell.parentElement).toHaveClass("o_selected_row");
|
||||
expect(`.o_field_email input[type="email"]`).toHaveValue("john.doe@odoo.com");
|
||||
await fieldInput("email").edit("new@odoo.com");
|
||||
await contains(getFixture()).click();
|
||||
cell = queryFirst("tbody td:not(.o_list_record_selector)");
|
||||
expect(cell.parentElement).not.toHaveClass("o_selected_row");
|
||||
expect(queryAllTexts(`tbody td:not(.o_list_record_selector) a`)).toEqual([
|
||||
"new@odoo.com",
|
||||
"jane.doe@odoo.com",
|
||||
]);
|
||||
expect(".o_field_email a:first").toHaveAttribute("href", "mailto:new@odoo.com");
|
||||
});
|
||||
|
||||
test("with empty value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
arch: `<form><field name="email" widget="email" placeholder="Placeholder"/></form>`,
|
||||
});
|
||||
expect(`.o_field_email input`).toHaveValue("");
|
||||
});
|
||||
|
||||
test("with placeholder", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
arch: `<form><field name="email" widget="email" placeholder="Placeholder"/></form>`,
|
||||
});
|
||||
expect(`.o_field_email input`).toHaveAttribute("placeholder", "Placeholder");
|
||||
});
|
||||
|
||||
test("trim user value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
arch: '<form><field name="email" widget="email"/></form>',
|
||||
});
|
||||
|
||||
await fieldInput("email").edit(" hello@gmail.com ");
|
||||
await contains(getFixture()).click();
|
||||
expect(`.o_field_email input`).toHaveValue("hello@gmail.com");
|
||||
});
|
||||
|
||||
test("onchange scenario with readonly", async () => {
|
||||
Contact._fields.phone = fields.Char({
|
||||
onChange: (record) => {
|
||||
record.email = "onchange@domain.ext";
|
||||
},
|
||||
});
|
||||
Contact._records = [{ id: 1, email: "default@domain.ext" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
resId: 1,
|
||||
arch: `<form><field name="phone"/><field name="email" widget="email" readonly="1"/></form>`,
|
||||
});
|
||||
expect(`.o_field_email`).toHaveText("default@domain.ext");
|
||||
await fieldInput("phone").edit("047412345");
|
||||
expect(`.o_field_email`).toHaveText("onchange@domain.ext");
|
||||
});
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { contains, defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Program extends models.Model {
|
||||
type = fields.Selection({
|
||||
required: true,
|
||||
selection: [
|
||||
["coupon", "Coupons"],
|
||||
["promotion", "Promotion"],
|
||||
["gift_card", "Gift card"],
|
||||
],
|
||||
});
|
||||
available_types = fields.Json({
|
||||
required: true,
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 1, type: "coupon", available_types: "['coupon', 'promotion']" },
|
||||
{ id: 2, type: "gift_card", available_types: "['gift_card', 'promotion']" },
|
||||
];
|
||||
}
|
||||
defineModels([Program]);
|
||||
|
||||
// Note: the `toHaveCount` always check for one more as there will be an invisible empty option every time.
|
||||
|
||||
test(`FilterableSelectionField test whitelist`, async () => {
|
||||
await mountView({
|
||||
resModel: "program",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="type" widget="filterable_selection" options="{'whitelisted_values': ['coupons', 'promotion']}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(`select option`).toHaveCount(3);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"coupon"']`).toHaveCount(1);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"promotion"']`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(`FilterableSelectionField test blacklist`, async () => {
|
||||
await mountView({
|
||||
resModel: "program",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="type" widget="filterable_selection" options="{'blacklisted_values': ['gift_card']}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(`select option`).toHaveCount(3);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"coupon"']`).toHaveCount(1);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"promotion"']`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(`FilterableSelectionField test with invalid value`, async () => {
|
||||
// The field should still display the current value in the list
|
||||
await mountView({
|
||||
resModel: "program",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="type" widget="filterable_selection" options="{'blacklisted_values': ['gift_card']}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 2,
|
||||
});
|
||||
expect(`select option`).toHaveCount(4);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"gift_card"']`).toHaveCount(1);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"coupon"']`).toHaveCount(1);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"promotion"']`).toHaveCount(1);
|
||||
|
||||
await contains(`.o_field_widget[name="type"] select`).select(`"coupon"`);
|
||||
expect(`select option`).toHaveCount(3);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"gift_card"']`).toHaveCount(0);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"coupon"']`).toHaveCount(1);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"promotion"']`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(`FilterableSelectionField test whitelist_fname`, async () => {
|
||||
await mountView({
|
||||
resModel: "program",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="available_types" invisible="1"/>
|
||||
<field name="type" widget="filterable_selection" options="{'whitelist_fname': 'available_types'}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(`select option`).toHaveCount(3);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"coupon"']`).toHaveCount(1);
|
||||
expect(`.o_field_widget[name="type"] select option[value='"promotion"']`).toHaveCount(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
defineParams,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
qux = fields.Float();
|
||||
|
||||
_records = [{ id: 1, qux: 9.1 }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("FloatFactorField in form view", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
onRpc("partner", "web_save", ({ args }) => {
|
||||
// 2.3 / 0.5 = 4.6
|
||||
expect(args[1].qux).toBe(4.6, { message: "the correct float value should be saved" });
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="qux" widget="float_factor" options="{'factor': 0.5}" digits="[16,2]" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='qux'] input").toHaveValue("4.55", {
|
||||
message: "The value should be rendered correctly in the input.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name='qux'] input").edit("2.3");
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("2.30", {
|
||||
message: "The new value should be saved and displayed properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("FloatFactorField comma as decimal point", async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
// patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "" });
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
decimal_point: ",",
|
||||
thousands_sep: "",
|
||||
},
|
||||
});
|
||||
onRpc("partner", "web_save", ({ args }) => {
|
||||
// 2.3 / 0.5 = 4.6
|
||||
expect(args[1].qux).toBe(4.6);
|
||||
expect.step("save");
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="qux" widget="float_factor" options="{'factor': 0.5}" digits="[16,2]" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name='qux'] input").edit("2,3");
|
||||
await clickSave();
|
||||
|
||||
expect.verifySteps(["save"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,459 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
defineParams,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
class Partner extends models.Model {
|
||||
float_field = fields.Float({ string: "Float field" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, float_field: 0.36 },
|
||||
{ id: 2, float_field: 0 },
|
||||
{ id: 3, float_field: -3.89859 },
|
||||
{ id: 4, float_field: 0 },
|
||||
{ id: 5, float_field: 9.1 },
|
||||
{ id: 100, float_field: 2.034567e3 },
|
||||
{ id: 101, float_field: 3.75675456e6 },
|
||||
{ id: 102, float_field: 6.67543577586e12 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("human readable format 1", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 101,
|
||||
arch: `<form><field name="float_field" options="{'human_readable': 'true'}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("4M", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T).",
|
||||
});
|
||||
});
|
||||
|
||||
test("human readable format 2", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 100,
|
||||
arch: `<form><field name="float_field" options="{'human_readable': 'true', 'decimals': 1}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("2.0k", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T).",
|
||||
});
|
||||
});
|
||||
|
||||
test("human readable format 3", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 102,
|
||||
arch: `<form><field name="float_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("6.6754T", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T).",
|
||||
});
|
||||
});
|
||||
|
||||
test("still human readable when readonly", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 102,
|
||||
arch: `<form><field readonly="true" name="float_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget span").toHaveText("6.6754T", {
|
||||
message: "The value should be rendered in human readable format when input is readonly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("unset field should be set to 0", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget").not.toHaveClass("o_field_empty", {
|
||||
message: "Non-set float field should be considered as 0.00",
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("0.00", {
|
||||
message: "Non-set float field should be considered as 0.",
|
||||
});
|
||||
});
|
||||
|
||||
test("use correct digit precision from field definition", async () => {
|
||||
Partner._fields.float_field = fields.Float({ string: "Float field", digits: [0, 1] });
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_float input").toHaveValue("0.4", {
|
||||
message: "should contain a number rounded to 1 decimal",
|
||||
});
|
||||
});
|
||||
|
||||
test("use correct digit precision from options", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 1] }" /></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_float input").toHaveValue("0.4", {
|
||||
message: "should contain a number rounded to 1 decimal",
|
||||
});
|
||||
});
|
||||
|
||||
test("use correct digit precision from field attrs", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="float_field" digits="[0, 1]" /></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_float input").toHaveValue("0.4", {
|
||||
message: "should contain a number rounded to 1 decimal",
|
||||
});
|
||||
});
|
||||
|
||||
test("with 'step' option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="float_field" options="{'type': 'number', 'step': 0.3}"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveAttribute("step", "0.3", {
|
||||
message: 'Integer field with option type must have a step attribute equals to "3".',
|
||||
});
|
||||
});
|
||||
|
||||
test("basic flow in form view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget").not.toHaveClass("o_field_empty", {
|
||||
message: "Float field should be considered set for value 0.",
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("0.000", {
|
||||
message: "The value should be displayed properly.",
|
||||
});
|
||||
|
||||
await contains('div[name="float_field"] input').edit("108.2451938598598");
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("108.245", {
|
||||
message: "The value should have been formatted on blur.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("18.8958938598598");
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("18.896", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("use a formula", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=20+3*2");
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("26.000", {
|
||||
message: "The new value should be calculated properly.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=2**3");
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("8.000", {
|
||||
message: "The new value should be calculated properly.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=2^3");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("8.000", {
|
||||
message: "The new value should be calculated properly.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=100/3");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("33.333", {
|
||||
message: "The new value should be calculated properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("use incorrect formula", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=abc", { confirm: false });
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget[name=float_field]").toHaveClass("o_field_invalid", {
|
||||
message: "fload field should be displayed as invalid",
|
||||
});
|
||||
expect(".o_form_editable").toHaveCount(1, { message: "form view should still be editable" });
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=3:2?+4", { confirm: false });
|
||||
await clickSave();
|
||||
|
||||
expect(".o_form_editable").toHaveCount(1, { message: "form view should still be editable" });
|
||||
expect(".o_field_widget[name=float_field]").toHaveClass("o_field_invalid", {
|
||||
message: "float field should be displayed as invalid",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("float field in editable list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="float_field" widget="float" digits="[5,3]" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
// switch to edit mode
|
||||
await contains("tr.o_data_row td:not(.o_list_record_selector)").click();
|
||||
|
||||
expect('div[name="float_field"] input').toHaveCount(1, {
|
||||
message: "The view should have 1 input for editable float.",
|
||||
});
|
||||
|
||||
await contains('div[name="float_field"] input').edit("108.2458938598598", { confirm: "blur" });
|
||||
expect(".o_field_widget:eq(0)").toHaveText("108.246", {
|
||||
message: "The value should have been formatted on blur.",
|
||||
});
|
||||
|
||||
await contains("tr.o_data_row td:not(.o_list_record_selector)").click();
|
||||
await contains('div[name="float_field"] input').edit("18.8958938598598", { confirm: false });
|
||||
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
|
||||
expect(".o_field_widget:eq(0)").toHaveText("18.896", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("float field with type number option", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
grouping: [3, 0],
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" options="{'type': 'number'}"/>
|
||||
</form>`,
|
||||
resId: 4,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveAttribute("type", "number", {
|
||||
message: 'Float field with option type must have a type attribute equals to "number".',
|
||||
});
|
||||
await contains(".o_field_widget input").fill("123456.7890", { instantly: true });
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue(123456.789, {
|
||||
message:
|
||||
"Float value must be not formatted if input type is number. (but the trailing 0 is gone)",
|
||||
});
|
||||
});
|
||||
|
||||
test("float field with type number option and comma decimal separator", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
thousands_sep: ".",
|
||||
decimal_point: ",",
|
||||
grouping: [3, 0],
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" options="{'type': 'number'}"/>
|
||||
</form>`,
|
||||
resId: 4,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveAttribute("type", "number", {
|
||||
message: 'Float field with option type must have a type attribute equals to "number".',
|
||||
});
|
||||
await contains(".o_field_widget[name=float_field] input").fill("123456.789", {
|
||||
instantly: true,
|
||||
});
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue(123456.789, {
|
||||
message: "Float value must be not formatted if input type is number.",
|
||||
});
|
||||
});
|
||||
|
||||
test("float field without type number option", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
grouping: [3, 0],
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
resId: 4,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveAttribute("type", "text", {
|
||||
message: "Float field with option type must have a text type (default type).",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("123456.7890");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("123,456.79", {
|
||||
message: "Float value must be formatted if input type isn't number.",
|
||||
});
|
||||
});
|
||||
|
||||
test("field with enable_formatting option as false", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
grouping: [3, 0],
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="float_field" options="{'enable_formatting': false}"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("0.36", {
|
||||
message: "Integer value must not be formatted",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("123456.789");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("123456.789", {
|
||||
message: "Integer value must be not formatted if input type is number.",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("field with enable_formatting option as false in editable list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="float_field" widget="float" digits="[5,3]" options="{'enable_formatting': false}" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
// switch to edit mode
|
||||
await contains("tr.o_data_row td:not(.o_list_record_selector)").click();
|
||||
|
||||
expect('div[name="float_field"] input').toHaveCount(1, {
|
||||
message: "The view should have 1 input for editable float.",
|
||||
});
|
||||
|
||||
await contains('div[name="float_field"] input').edit("108.2458938598598", {
|
||||
confirm: "blur",
|
||||
});
|
||||
expect(".o_field_widget:eq(0)").toHaveText("108.2458938598598", {
|
||||
message: "The value should not be formatted on blur.",
|
||||
});
|
||||
|
||||
await contains("tr.o_data_row td:not(.o_list_record_selector)").click();
|
||||
await contains('div[name="float_field"] input').edit("18.8958938598598", {
|
||||
confirm: false,
|
||||
});
|
||||
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
|
||||
expect(".o_field_widget:eq(0)").toHaveText("18.8958938598598", {
|
||||
message: "The new value should not be rounded as well.",
|
||||
});
|
||||
});
|
||||
|
||||
test("float_field field with placeholder", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="float_field" placeholder="Placeholder"/></form>',
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name='float_field'] input").clear();
|
||||
expect(".o_field_widget[name='float_field'] input").toHaveAttribute(
|
||||
"placeholder",
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
test("float field can be updated by another field/widget", async () => {
|
||||
class MyWidget extends Component {
|
||||
static template = xml`<button t-on-click="onClick">do it</button>`;
|
||||
static props = ["*"];
|
||||
onClick() {
|
||||
const val = this.props.record.data.float_field;
|
||||
this.props.record.update({ float_field: val + 1 });
|
||||
}
|
||||
}
|
||||
const myWidget = {
|
||||
component: MyWidget,
|
||||
};
|
||||
registry.category("view_widgets").add("wi", myWidget);
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field"/>
|
||||
<field name="float_field"/>
|
||||
<widget name="wi"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("40");
|
||||
|
||||
expect(".o_field_widget[name=float_field] input:eq(0)").toHaveValue("40.00");
|
||||
expect(".o_field_widget[name=float_field] input:eq(1)").toHaveValue("40.00");
|
||||
|
||||
await contains(".o_widget button").click();
|
||||
|
||||
expect(".o_field_widget[name=float_field] input:eq(0)").toHaveValue("41.00");
|
||||
expect(".o_field_widget[name=float_field] input:eq(1)").toHaveValue("41.00");
|
||||
});
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
qux = fields.Float();
|
||||
|
||||
_records = [{ id: 5, qux: 9.1 }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("FloatTimeField in form view", async () => {
|
||||
expect.assertions(4);
|
||||
onRpc("partner", "web_save", ({ args }) => {
|
||||
// 48 / 60 = 0.8
|
||||
expect(args[1].qux).toBe(-11.8, {
|
||||
message: "the correct float value should be saved",
|
||||
});
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 5,
|
||||
});
|
||||
|
||||
// 9 + 0.1 * 60 = 9.06
|
||||
expect(".o_field_float_time[name=qux] input").toHaveValue("09:06", {
|
||||
message: "The value should be rendered correctly in the input.",
|
||||
});
|
||||
|
||||
await contains(".o_field_float_time[name=qux] input").edit("-11:48");
|
||||
expect(".o_field_float_time[name=qux] input").toHaveValue("-11:48", {
|
||||
message: "The new value should be displayed properly in the input.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("-11:48", {
|
||||
message: "The new value should be saved and displayed properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("FloatTimeField value formatted on blur", async () => {
|
||||
expect.assertions(4);
|
||||
onRpc("partner", "web_save", ({ args }) => {
|
||||
expect(args[1].qux).toBe(9.5, {
|
||||
message: "the correct float value should be saved",
|
||||
});
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</form>`,
|
||||
resId: 5,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("09:06", {
|
||||
message: "The formatted time value should be displayed properly.",
|
||||
});
|
||||
|
||||
await contains(".o_field_float_time[name=qux] input").edit("9.5");
|
||||
expect(".o_field_float_time[name=qux] input").toHaveValue("09:30", {
|
||||
message: "The new value should be displayed properly in the input.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("09:30", {
|
||||
message: "The new value should be saved and displayed properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("FloatTimeField with invalid value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_float_time[name=qux] input").edit("blabla");
|
||||
await clickSave();
|
||||
expect(".o_notification_title").toHaveText("Invalid fields:");
|
||||
expect(".o_notification_content").toHaveInnerHTML("<ul><li>Qux</li></ul>");
|
||||
expect(".o_notification_bar").toHaveClass("bg-danger");
|
||||
expect(".o_field_float_time[name=qux]").toHaveClass("o_field_invalid");
|
||||
|
||||
await contains(".o_field_float_time[name=qux] input").edit("6.5");
|
||||
expect(".o_field_float_time[name=qux] input").not.toHaveClass("o_field_invalid", {
|
||||
message: "date field should not be displayed as invalid now",
|
||||
});
|
||||
});
|
||||
|
||||
test("float_time field with placeholder", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time" placeholder="Placeholder"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name='qux'] input").clear();
|
||||
expect(".o_field_widget[name='qux'] input").toHaveAttribute("placeholder", "Placeholder");
|
||||
});
|
||||
|
||||
test("float_time field does not have an inputmode attribute", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time" placeholder="Placeholder"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='qux'] input").not.toHaveAttribute("inputmode");
|
||||
});
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryText } from "@odoo/hoot-dom";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
float_field = fields.Float({ string: "Float field" });
|
||||
|
||||
_records = [{ id: 1, float_field: 0.44444 }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("basic flow in form view", async () => {
|
||||
onRpc("partner", "web_save", ({ args }) => {
|
||||
// 1.000 / 0.125 = 8
|
||||
expect.step(args[1].float_field.toString());
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="float_toggle" options="{'factor': 0.125, 'range': [0, 1, 0.75, 0.5, 0.25]}" digits="[5,3]"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget").toHaveText("0.056", {
|
||||
message: "The formatted time value should be displayed properly.",
|
||||
});
|
||||
expect("button.o_field_float_toggle").toHaveText("0.056", {
|
||||
message: "The value should be rendered correctly on the button.",
|
||||
});
|
||||
|
||||
await contains("button.o_field_float_toggle").click();
|
||||
|
||||
expect("button.o_field_float_toggle").toHaveText("0.000", {
|
||||
message: "The value should be rendered correctly on the button.",
|
||||
});
|
||||
|
||||
// note, 0 will not be written, it's kept in the _changes of the datapoint.
|
||||
// because save has not been clicked.
|
||||
|
||||
await contains("button.o_field_float_toggle").click();
|
||||
|
||||
expect("button.o_field_float_toggle").toHaveText("1.000", {
|
||||
message: "The value should be rendered correctly on the button.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget").toHaveText("1.000", {
|
||||
message: "The new value should be saved and displayed properly.",
|
||||
});
|
||||
|
||||
expect.verifySteps(["8"]);
|
||||
});
|
||||
|
||||
test("kanban view (readonly) with option force_button", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="float_field" widget="float_toggle" options="{'force_button': true}"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
expect("button.o_field_float_toggle").toHaveCount(1, {
|
||||
message: "should have rendered toggle button",
|
||||
});
|
||||
|
||||
const value = queryText("button.o_field_float_toggle");
|
||||
await contains("button.o_field_float_toggle").click();
|
||||
expect("button.o_field_float_toggle").not.toHaveText(value, {
|
||||
message: "float_field field value should be changed",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { patchTranslations, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { markup } from "@odoo/owl";
|
||||
import { currencies } from "@web/core/currency";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import {
|
||||
formatFloat,
|
||||
formatFloatFactor,
|
||||
formatFloatTime,
|
||||
formatJson,
|
||||
formatInteger,
|
||||
formatMany2one,
|
||||
formatMany2oneReference,
|
||||
formatMonetary,
|
||||
formatPercentage,
|
||||
formatReference,
|
||||
formatText,
|
||||
formatX2many,
|
||||
} from "@web/views/fields/formatters";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
beforeEach(() => {
|
||||
patchTranslations();
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: ".",
|
||||
thousandsSep: ",",
|
||||
grouping: [3, 0],
|
||||
});
|
||||
});
|
||||
|
||||
test("formatFloat", () => {
|
||||
expect(formatFloat(false)).toBe("");
|
||||
});
|
||||
|
||||
test("formatFloatFactor", () => {
|
||||
expect(formatFloatFactor(false)).toBe("");
|
||||
expect(formatFloatFactor(6000)).toBe("6,000.00");
|
||||
expect(formatFloatFactor(6000, { factor: 3 })).toBe("18,000.00");
|
||||
expect(formatFloatFactor(6000, { factor: 0.5 })).toBe("3,000.00");
|
||||
});
|
||||
|
||||
test("formatFloatTime", () => {
|
||||
expect(formatFloatTime(2)).toBe("02:00");
|
||||
expect(formatFloatTime(3.5)).toBe("03:30");
|
||||
expect(formatFloatTime(0.25)).toBe("00:15");
|
||||
expect(formatFloatTime(0.58)).toBe("00:35");
|
||||
expect(formatFloatTime(2 / 60, { displaySeconds: true })).toBe("00:02:00");
|
||||
expect(formatFloatTime(2 / 60 + 1 / 3600, { displaySeconds: true })).toBe("00:02:01");
|
||||
expect(formatFloatTime(2 / 60 + 2 / 3600, { displaySeconds: true })).toBe("00:02:02");
|
||||
expect(formatFloatTime(2 / 60 + 3 / 3600, { displaySeconds: true })).toBe("00:02:03");
|
||||
expect(formatFloatTime(0.25, { displaySeconds: true })).toBe("00:15:00");
|
||||
expect(formatFloatTime(0.25 + 15 / 3600, { displaySeconds: true })).toBe("00:15:15");
|
||||
expect(formatFloatTime(0.25 + 45 / 3600, { displaySeconds: true })).toBe("00:15:45");
|
||||
expect(formatFloatTime(56 / 3600, { displaySeconds: true })).toBe("00:00:56");
|
||||
expect(formatFloatTime(-0.5)).toBe("-00:30");
|
||||
|
||||
const options = { noLeadingZeroHour: true };
|
||||
expect(formatFloatTime(2, options)).toBe("2:00");
|
||||
expect(formatFloatTime(3.5, options)).toBe("3:30");
|
||||
expect(formatFloatTime(3.5, { ...options, displaySeconds: true })).toBe("3:30:00");
|
||||
expect(formatFloatTime(3.5 + 15 / 3600, { ...options, displaySeconds: true })).toBe("3:30:15");
|
||||
expect(formatFloatTime(3.5 + 45 / 3600, { ...options, displaySeconds: true })).toBe("3:30:45");
|
||||
expect(formatFloatTime(56 / 3600, { ...options, displaySeconds: true })).toBe("0:00:56");
|
||||
expect(formatFloatTime(-0.5, options)).toBe("-0:30");
|
||||
});
|
||||
|
||||
test("formatJson", () => {
|
||||
expect(formatJson(false)).toBe("");
|
||||
expect(formatJson({})).toBe("{}");
|
||||
expect(formatJson({ 1: 111 })).toBe('{"1":111}');
|
||||
expect(formatJson({ 9: 11, 666: 42 })).toBe('{"9":11,"666":42}');
|
||||
});
|
||||
|
||||
test("formatInteger", () => {
|
||||
expect(formatInteger(false)).toBe("");
|
||||
expect(formatInteger(0)).toBe("0");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 3, 3, 3] });
|
||||
expect(formatInteger(1000000)).toBe("1,000,000");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 2, -1] });
|
||||
expect(formatInteger(106500)).toBe("1,06,500");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [1, 2, -1] });
|
||||
expect(formatInteger(106500)).toBe("106,50,0");
|
||||
|
||||
const options = { grouping: [2, 0], thousandsSep: "€" };
|
||||
expect(formatInteger(6000, options)).toBe("60€00");
|
||||
});
|
||||
|
||||
test("formatMany2one", () => {
|
||||
expect(formatMany2one(false)).toBe("");
|
||||
expect(formatMany2one([false, "M2O value"])).toBe("M2O value");
|
||||
expect(formatMany2one([1, false])).toBe("Unnamed");
|
||||
expect(formatMany2one([1, "M2O value"])).toBe("M2O value");
|
||||
expect(formatMany2one([1, "M2O value"], { escape: true })).toBe("M2O%20value");
|
||||
});
|
||||
|
||||
test("formatText", () => {
|
||||
expect(formatText(false)).toBe("");
|
||||
expect(formatText("value")).toBe("value");
|
||||
expect(formatText(1)).toBe("1");
|
||||
expect(formatText(1.5)).toBe("1.5");
|
||||
expect(formatText(markup("<p>This is a Test</p>"))).toBe("<p>This is a Test</p>");
|
||||
expect(formatText([1, 2, 3, 4, 5])).toBe("1,2,3,4,5");
|
||||
expect(formatText({ a: 1, b: 2 })).toBe("[object Object]");
|
||||
});
|
||||
|
||||
test("formatX2many", () => {
|
||||
// Results are cast as strings since they're lazy translated.
|
||||
expect(String(formatX2many({ currentIds: [] }))).toBe("No records");
|
||||
expect(String(formatX2many({ currentIds: [1] }))).toBe("1 record");
|
||||
expect(String(formatX2many({ currentIds: [1, 3] }))).toBe("2 records");
|
||||
});
|
||||
|
||||
test("formatMonetary", () => {
|
||||
patchWithCleanup(currencies, {
|
||||
10: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "€",
|
||||
},
|
||||
11: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$",
|
||||
},
|
||||
12: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "&",
|
||||
},
|
||||
});
|
||||
|
||||
expect(formatMonetary(false)).toBe("");
|
||||
|
||||
const field = {
|
||||
type: "monetary",
|
||||
currency_field: "c_x",
|
||||
};
|
||||
let data = {
|
||||
c_x: [11],
|
||||
c_y: 12,
|
||||
};
|
||||
expect(formatMonetary(200, { field, currencyId: 10, data })).toBe("200.00\u00a0€");
|
||||
expect(formatMonetary(200, { field, data })).toBe("$\u00a0200.00");
|
||||
expect(formatMonetary(200, { field, currencyField: "c_y", data })).toBe("200.00\u00a0&");
|
||||
|
||||
const floatField = { type: "float" };
|
||||
data = {
|
||||
currency_id: [11],
|
||||
};
|
||||
expect(formatMonetary(200, { field: floatField, data })).toBe("$\u00a0200.00");
|
||||
});
|
||||
|
||||
test("formatPercentage", () => {
|
||||
expect(formatPercentage(false)).toBe("0%");
|
||||
expect(formatPercentage(0)).toBe("0%");
|
||||
expect(formatPercentage(0.5)).toBe("50%");
|
||||
|
||||
expect(formatPercentage(1)).toBe("100%");
|
||||
|
||||
expect(formatPercentage(-0.2)).toBe("-20%");
|
||||
expect(formatPercentage(2.5)).toBe("250%");
|
||||
|
||||
expect(formatPercentage(0.125)).toBe("12.5%");
|
||||
expect(formatPercentage(0.666666)).toBe("66.67%");
|
||||
expect(formatPercentage(125)).toBe("12500%");
|
||||
|
||||
expect(formatPercentage(50, { humanReadable: true })).toBe("5k%");
|
||||
expect(formatPercentage(0.5, { noSymbol: true })).toBe("50");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 0], decimalPoint: ",", thousandsSep: "." });
|
||||
expect(formatPercentage(0.125)).toBe("12,5%");
|
||||
expect(formatPercentage(0.666666)).toBe("66,67%");
|
||||
});
|
||||
|
||||
test("formatReference", () => {
|
||||
expect(formatReference(false)).toBe("");
|
||||
const value = { resModel: "product", resId: 2, displayName: "Chair" };
|
||||
expect(formatReference(value)).toBe("Chair");
|
||||
});
|
||||
|
||||
test("formatMany2oneReference", () => {
|
||||
expect(formatMany2oneReference(false)).toBe("");
|
||||
expect(formatMany2oneReference({ resId: 9, displayName: "Chair" })).toBe("Chair");
|
||||
});
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { onMounted } from "@odoo/owl";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { GaugeField } from "@web/views/fields/gauge/gauge_field";
|
||||
import { setupChartJsForTests } from "../graph/graph_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer({ string: "int_field" });
|
||||
another_int_field = fields.Integer({ string: "another_int_field" });
|
||||
_records = [
|
||||
{ id: 1, int_field: 10, another_int_field: 45 },
|
||||
{ id: 2, int_field: 4, another_int_field: 10 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
setupChartJsForTests();
|
||||
|
||||
test("GaugeField in kanban view", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<field name="another_int_field"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="int_field" widget="gauge" options="{'max_field': 'another_int_field'}"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
|
||||
expect(".o_field_widget[name=int_field] .oe_gauge canvas").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_gauge_value")).toEqual(["10", "4"]);
|
||||
});
|
||||
|
||||
test("GaugeValue supports max_value option", async () => {
|
||||
patchWithCleanup(GaugeField.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
onMounted(() => {
|
||||
expect.step("gauge mounted");
|
||||
expect(this.chart.config.options.plugins.tooltip.callbacks.label({})).toBe(
|
||||
"Max: 120"
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Partner._records = Partner._records.slice(0, 1);
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div>
|
||||
<field name="int_field" widget="gauge" options="{'max_value': 120}"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
expect.verifySteps(["gauge mounted"]);
|
||||
expect(".o_field_widget[name=int_field] .oe_gauge canvas").toHaveCount(1);
|
||||
expect(".o_gauge_value").toHaveText("10");
|
||||
});
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, queryFirst } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
MockServer,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
display_name = fields.Char({ string: "Displayed name", searchable: true });
|
||||
p = fields.One2many({ string: "one2many field", relation: "partner", searchable: true });
|
||||
sequence = fields.Integer({ string: "Sequence", searchable: true });
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
p: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
p: [],
|
||||
sequence: 4,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
sequence: 9,
|
||||
},
|
||||
];
|
||||
}
|
||||
defineModels([Partner]);
|
||||
|
||||
test("HandleField in x2m", async () => {
|
||||
Partner._records[0].p = [2, 4];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="p">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("td span.o_row_handle").toHaveText("", {
|
||||
message: "handle should not have any content",
|
||||
});
|
||||
expect(queryFirst("td span.o_row_handle")).toBeVisible({
|
||||
message: "handle should be invisible",
|
||||
});
|
||||
|
||||
expect("span.o_row_handle").toHaveCount(2, { message: "should have 2 handles" });
|
||||
|
||||
expect(queryFirst("td")).toHaveClass("o_handle_cell", {
|
||||
message: "column widget should be displayed in css class",
|
||||
});
|
||||
await click("td:eq(1)");
|
||||
await animationFrame();
|
||||
|
||||
expect("td:eq(0) span.o_row_handle").toHaveCount(1, {
|
||||
message: "content of the cell should have been replaced",
|
||||
});
|
||||
});
|
||||
|
||||
test("HandleField with falsy values", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list>
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(".o_row_handle:visible").toHaveCount(MockServer.env["partner"].length, {
|
||||
message: "there should be a visible handle for each record",
|
||||
});
|
||||
});
|
||||
|
||||
test("HandleField in a readonly one2many", async () => {
|
||||
Partner._records[0].p = [1, 2, 4];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="p" readonly="1">
|
||||
<list editable="top">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_row_handle").toHaveCount(3, {
|
||||
message: "there should be 3 handles, one for each row",
|
||||
});
|
||||
expect(queryFirst("td span.o_row_handle")).not.toBeVisible({
|
||||
message: "handle should be invisible",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,333 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, pointerDown, queryAll, queryFirst } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
const RED_TEXT = /* html */ `<div class="kek" style="color:red">some text</div>`;
|
||||
const GREEN_TEXT = /* html */ `<div class="kek" style="color:green">hello</div>`;
|
||||
const BLUE_TEXT = /* html */ `<div class="kek" style="color:blue">hello world</div>`;
|
||||
|
||||
class Partner extends models.Model {
|
||||
txt = fields.Html({ string: "txt", trim: true });
|
||||
_records = [{ id: 1, txt: RED_TEXT }];
|
||||
}
|
||||
defineModels([Partner]);
|
||||
|
||||
test("html fields are correctly rendered in form view (readonly)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="txt" readonly="1" /></form>`,
|
||||
});
|
||||
|
||||
expect("div.kek").toHaveCount(1);
|
||||
expect(".o_field_html .kek").toHaveStyle({ color: "rgb(255, 0, 0)" });
|
||||
expect(".o_field_html").toHaveText("some text");
|
||||
});
|
||||
|
||||
test("html field with required attribute", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="txt" required="1"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_html textarea").toHaveCount(1, { message: "should have a text area" });
|
||||
await click(".o_field_html textarea");
|
||||
await edit("");
|
||||
await animationFrame();
|
||||
expect(".o_field_html textarea").toHaveValue("");
|
||||
|
||||
await clickSave();
|
||||
expect(".o_notification_title").toHaveText("Invalid fields:");
|
||||
expect(queryFirst(".o_notification_content")).toHaveInnerHTML("<ul><li>txt</li></ul>");
|
||||
});
|
||||
|
||||
test("html fields are correctly rendered (edit)", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="txt" /></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_html textarea").toHaveCount(1, { message: "should have a text area" });
|
||||
expect(".o_field_html textarea").toHaveValue(RED_TEXT);
|
||||
await click(".o_field_html textarea");
|
||||
await edit(GREEN_TEXT);
|
||||
await animationFrame();
|
||||
expect(".o_field_html textarea").toHaveValue(GREEN_TEXT);
|
||||
expect(".o_field_html .kek").toHaveCount(0);
|
||||
|
||||
await edit(BLUE_TEXT);
|
||||
await animationFrame();
|
||||
expect(".o_field_html textarea").toHaveValue(BLUE_TEXT);
|
||||
});
|
||||
|
||||
test("html fields are correctly rendered in list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list editable="top"><field name="txt"/></list>`,
|
||||
});
|
||||
expect(".o_data_row [name='txt']").toHaveText("some text");
|
||||
expect(".o_data_row [name='txt'] .kek").toHaveStyle({ color: "rgb(255, 0, 0)" });
|
||||
|
||||
await click(".o_data_row [name='txt']");
|
||||
await animationFrame();
|
||||
expect(".o_data_row [name='txt'] textarea").toHaveValue(
|
||||
'<div class="kek" style="color:red">some text</div>'
|
||||
);
|
||||
});
|
||||
|
||||
test("html field displays an empty string for the value false in list view", async () => {
|
||||
Partner._records[0].txt = false;
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list editable="top"><field name="txt"/></list>`,
|
||||
});
|
||||
|
||||
expect(".o_data_row [name='txt']").toHaveText("");
|
||||
|
||||
await click(".o_data_row [name='txt']");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row [name='txt'] textarea").toHaveValue("");
|
||||
});
|
||||
|
||||
test("html fields are correctly rendered in kanban view", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban class="o_kanban_test">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="txt"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
expect(".kek").toHaveText("some text");
|
||||
expect(".kek").toHaveStyle({ color: "rgb(255, 0, 0)" });
|
||||
});
|
||||
|
||||
test("field html translatable", async () => {
|
||||
expect.assertions(10);
|
||||
|
||||
Partner._fields.txt = fields.Html({ string: "txt", trim: true, translate: true });
|
||||
|
||||
serverState.lang = "en_US";
|
||||
serverState.multiLang = true;
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
onRpc("get_field_translations", function ({ args }) {
|
||||
expect(args).toEqual([[1], "txt"], {
|
||||
message: "should translate the txt field of the record",
|
||||
});
|
||||
return [
|
||||
[
|
||||
{ lang: "en_US", source: "first paragraph", value: "first paragraph" },
|
||||
{
|
||||
lang: "en_US",
|
||||
source: "second paragraph",
|
||||
value: "second paragraph",
|
||||
},
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "first paragraph",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "second paragraph",
|
||||
value: "deuxième paragraphe",
|
||||
},
|
||||
],
|
||||
{ translation_type: "char", translation_show_source: true },
|
||||
];
|
||||
});
|
||||
onRpc("get_installed", () => {
|
||||
return [
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
];
|
||||
});
|
||||
onRpc("update_field_translations", ({ args }) => {
|
||||
expect(args).toEqual(
|
||||
[
|
||||
[1],
|
||||
"txt",
|
||||
{
|
||||
en_US: { "first paragraph": "first paragraph modified" },
|
||||
fr_BE: {
|
||||
"first paragraph": "premier paragraphe modifié",
|
||||
"second paragraph": "deuxième paragraphe modifié",
|
||||
},
|
||||
},
|
||||
],
|
||||
{ message: "the new translation value should be written" }
|
||||
);
|
||||
return [];
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="txt" widget="html"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("[name=txt] textarea").toHaveClass("o_field_translate");
|
||||
await contains("[name=txt] textarea").click();
|
||||
expect(".o_field_html .btn.o_field_translate").toHaveCount(1, {
|
||||
message: "should have a translate button",
|
||||
});
|
||||
expect(".o_field_html .btn.o_field_translate").toHaveText("EN", {
|
||||
message: "the button should have as test the current language",
|
||||
});
|
||||
|
||||
await click(".o_field_html .btn.o_field_translate");
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1, { message: "a translate modal should be visible" });
|
||||
expect(".translation").toHaveCount(4, { message: "four rows should be visible" });
|
||||
|
||||
const translations = queryAll(".modal .o_translation_dialog .translation input");
|
||||
|
||||
const enField1 = translations[0];
|
||||
expect(enField1).toHaveValue("first paragraph", {
|
||||
message: "first part of english translation should be filled",
|
||||
});
|
||||
await click(enField1);
|
||||
await edit("first paragraph modified");
|
||||
|
||||
const frField1 = translations[2];
|
||||
expect(frField1).toHaveValue("", {
|
||||
message: "first part of french translation should not be filled",
|
||||
});
|
||||
await click(frField1);
|
||||
await edit("premier paragraphe modifié");
|
||||
|
||||
const frField2 = translations[3];
|
||||
expect(frField2).toHaveValue("deuxième paragraphe", {
|
||||
message: "second part of french translation should be filled",
|
||||
});
|
||||
|
||||
await click(frField2);
|
||||
await edit("deuxième paragraphe modifié");
|
||||
|
||||
await click(".modal button.btn-primary"); // save
|
||||
await animationFrame();
|
||||
});
|
||||
|
||||
test("html fields: spellcheck is disabled on blur", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="txt" /></form>`,
|
||||
});
|
||||
|
||||
const textarea = queryFirst(".o_field_html textarea");
|
||||
expect(textarea).toHaveProperty("spellcheck", true, {
|
||||
message: "by default, spellcheck is enabled",
|
||||
});
|
||||
await click(textarea);
|
||||
|
||||
await edit("nev walue");
|
||||
await pointerDown(document.body);
|
||||
await animationFrame();
|
||||
expect(textarea).toHaveProperty("spellcheck", false, {
|
||||
message: "spellcheck is disabled once the field has lost its focus",
|
||||
});
|
||||
|
||||
await pointerDown(textarea);
|
||||
|
||||
expect(textarea).toHaveProperty("spellcheck", true, {
|
||||
message: "spellcheck is re-enabled once the field is focused",
|
||||
});
|
||||
});
|
||||
|
||||
test("Setting an html field to empty string is saved as a false value", async () => {
|
||||
expect.assertions(1);
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].txt).toBe(false, { message: "the txt value should be false" });
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="txt" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(".o_field_widget[name=txt] textarea");
|
||||
await edit("");
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("html field: correct value is used to evaluate the modifiers", async () => {
|
||||
Partner._fields.foo = fields.Char({
|
||||
string: "foo",
|
||||
onChange: (obj) => {
|
||||
if (obj.foo === "a") {
|
||||
obj.txt = false;
|
||||
} else if (obj.foo === "b") {
|
||||
obj.txt = "";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Partner._records[0].foo = false;
|
||||
Partner._records[0].txt = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="txt" invisible="'' == txt"/>
|
||||
</form>`,
|
||||
});
|
||||
expect("[name='txt'] textarea").toHaveCount(1);
|
||||
|
||||
await click("[name='foo'] input");
|
||||
await edit("a", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect("[name='txt'] textarea").toHaveCount(1);
|
||||
|
||||
await edit("b", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect("[name='txt'] textarea").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, queryFirst } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Report extends models.Model {
|
||||
int_field = fields.Integer();
|
||||
html_field = fields.Html();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
html_field: /* html */ `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { color : rgb(255, 0, 0); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="nice_div"><p>Some content</p></div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Report]);
|
||||
|
||||
test("IframeWrapperField in form view with onchange", async () => {
|
||||
Report._onChanges.int_field = (record) => {
|
||||
record.html_field = record.html_field.replace("Some content", "New content");
|
||||
};
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "report",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="int_field"/>
|
||||
<field name="html_field" widget="iframe_wrapper"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect("iframe:iframe .nice_div:first").toHaveInnerHTML("<p>Some content</p>");
|
||||
expect("iframe:iframe .nice_div p:first").toHaveStyle({
|
||||
color: "rgb(255, 0, 0)",
|
||||
});
|
||||
await click(".o_field_widget[name=int_field] input");
|
||||
await edit(264, { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect(queryFirst("iframe:iframe .nice_div")).toHaveInnerHTML("<p>New content</p>");
|
||||
});
|
||||
|
|
@ -0,0 +1,879 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
click,
|
||||
edit,
|
||||
manuallyDispatchProgrammaticEvent,
|
||||
queryAll,
|
||||
queryFirst,
|
||||
setInputFiles,
|
||||
waitFor,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers, mockDate } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
pagerNext,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
const MY_IMAGE =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
|
||||
const PRODUCT_IMAGE =
|
||||
"R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7";
|
||||
|
||||
function getUnique(target) {
|
||||
const src = target.dataset.src;
|
||||
return new URL(src).searchParams.get("unique");
|
||||
}
|
||||
|
||||
async function setFiles(files, name = "document") {
|
||||
await click("input[type=file]", { visible: false });
|
||||
await setInputFiles(files);
|
||||
await waitFor(`div[name=${name}] img[data-src^="data:image/"]`, { timeout: 1000 });
|
||||
}
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
timmy = fields.Many2many({ relation: "partner.type" });
|
||||
foo = fields.Char();
|
||||
document = fields.Binary();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "first record", timmy: [], document: "coucou==" },
|
||||
{ id: 2, name: "second record", timmy: [] },
|
||||
{ id: 4, name: "aaa" },
|
||||
];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
_name = "partner.type";
|
||||
|
||||
name = fields.Char();
|
||||
color = fields.Integer();
|
||||
|
||||
_records = [
|
||||
{ id: 12, name: "gold", color: 2 },
|
||||
{ id: 14, name: "silver", color: 5 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, PartnerType]);
|
||||
|
||||
test("ImageField is correctly rendered", async () => {
|
||||
expect.assertions(10);
|
||||
|
||||
Partner._records[0].write_date = "2017-02-08 10:00:00";
|
||||
Partner._records[0].document = MY_IMAGE;
|
||||
|
||||
onRpc("web_read", ({ kwargs }) => {
|
||||
expect(kwargs.specification).toEqual(
|
||||
{
|
||||
display_name: {},
|
||||
document: {},
|
||||
write_date: {},
|
||||
},
|
||||
{
|
||||
message:
|
||||
"The fields document, name and write_date should be present when reading an image",
|
||||
}
|
||||
);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='document']").toHaveClass("o_field_image", {
|
||||
message: "the widget should have the correct class",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveCount(1, {
|
||||
message: "the widget should contain an image",
|
||||
});
|
||||
expect('div[name="document"] img').toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
{ message: "the image should have the correct src" }
|
||||
);
|
||||
expect(".o_field_widget[name='document'] img").toHaveClass("img-fluid", {
|
||||
message: "the image should have the correct class",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveAttribute("width", "90", {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveStyle(
|
||||
{
|
||||
maxWidth: "90px",
|
||||
width: "90px",
|
||||
height: "90px",
|
||||
},
|
||||
{
|
||||
message: "the image should correctly set its attributes",
|
||||
}
|
||||
);
|
||||
expect(".o_field_image .o_select_file_button").toHaveCount(1, {
|
||||
message: "the image can be edited",
|
||||
});
|
||||
expect(".o_field_image .o_clear_file_button").toHaveCount(1, {
|
||||
message: "the image can be deleted",
|
||||
});
|
||||
expect("input.o_input_file").toHaveAttribute("accept", "image/*", {
|
||||
message:
|
||||
'the default value for the attribute "accept" on the "image" widget must be "image/*"',
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField with img_class option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'img_class': 'my_custom_class'}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_image img").toHaveClass("my_custom_class");
|
||||
});
|
||||
|
||||
test("ImageField with alt attribute", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" alt="something"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='document'] img").toHaveAttribute("alt", "something", {
|
||||
message: "the image should correctly set its alt attribute",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField on a many2one", async () => {
|
||||
Partner._fields.parent_id = fields.Many2one({ relation: "partner" });
|
||||
Partner._records[1].parent_id = 1;
|
||||
|
||||
mockDate("2017-02-06 10:00:00");
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="parent_id" widget="image" options="{'preview_image': 'document'}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=parent_id] img").toHaveCount(1);
|
||||
expect('div[name="parent_id"] img').toHaveAttribute(
|
||||
"data-src",
|
||||
`${getOrigin()}/web/image/partner/1/document?unique=1486375200000`
|
||||
);
|
||||
expect(".o_field_widget[name='parent_id'] img").toHaveAttribute("alt", "first record");
|
||||
});
|
||||
|
||||
test("url should not use the record last updated date when the field is related", async () => {
|
||||
Partner._fields.related = fields.Binary({ related: "parent_id.document" });
|
||||
Partner._fields.parent_id = fields.Many2one({ relation: "partner" });
|
||||
Partner._records[1].parent_id = 1;
|
||||
Partner._records[0].write_date = "2017-02-04 10:00:00";
|
||||
Partner._records[0].document = "3 kb";
|
||||
|
||||
mockDate("2017-02-06 10:00:00");
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="related" widget="image" readonly="0"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const initialUnique = Number(getUnique(queryFirst('div[name="related"] img')));
|
||||
expect(DateTime.fromMillis(initialUnique).hasSame(DateTime.fromISO("2017-02-06"), "days")).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
await click(".o_field_widget[name='foo'] input");
|
||||
await edit("grrr");
|
||||
await animationFrame();
|
||||
|
||||
expect(Number(getUnique(queryFirst('div[name="related"] img')))).toBe(initialUnique);
|
||||
|
||||
mockDate("2017-02-09 10:00:00");
|
||||
|
||||
await click("input[type=file]", { visible: false });
|
||||
await setFiles(
|
||||
new File(
|
||||
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file.png",
|
||||
{ type: "png" }
|
||||
),
|
||||
"related"
|
||||
);
|
||||
|
||||
expect("div[name=related] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave();
|
||||
|
||||
const unique = Number(getUnique(queryFirst('div[name="related"] img')));
|
||||
expect(DateTime.fromMillis(unique).hasSame(DateTime.fromISO("2017-02-09"), "days")).toBe(true);
|
||||
});
|
||||
|
||||
test("url should use the record last updated date when the field is related on the same model", async () => {
|
||||
Partner._fields.related = fields.Binary({ related: "document" });
|
||||
Partner._records[0].write_date = "2017-02-04 10:00:00"; // 1486202400000
|
||||
Partner._records[0].document = "3 kb";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="related" widget="image"/>
|
||||
</form>`,
|
||||
});
|
||||
expect('div[name="related"] img').toHaveAttribute(
|
||||
"data-src",
|
||||
`${getOrigin()}/web/image/partner/1/related?unique=1486202400000`
|
||||
);
|
||||
});
|
||||
|
||||
test("ImageField is correctly replaced when given an incorrect value", async () => {
|
||||
Partner._records[0].document = "incorrect_base64_value";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`div[name="document"] img`).toHaveAttribute(
|
||||
"data-src",
|
||||
"data:image/png;base64,incorrect_base64_value",
|
||||
{
|
||||
message: "the image has the invalid src by default",
|
||||
}
|
||||
);
|
||||
|
||||
// As GET requests can't occur in tests, we must generate an error
|
||||
// on the img element to check whether the data-src is replaced with
|
||||
// a placeholder, here knowing that the GET request would fail
|
||||
manuallyDispatchProgrammaticEvent(queryFirst('div[name="document"] img'), "error");
|
||||
await animationFrame();
|
||||
|
||||
expect('.o_field_widget[name="document"]').toHaveClass("o_field_image", {
|
||||
message: "the widget should have the correct class",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveCount(1, {
|
||||
message: "the widget should contain an image",
|
||||
});
|
||||
expect('div[name="document"] img').toHaveAttribute(
|
||||
"data-src",
|
||||
"/web/static/img/placeholder.png",
|
||||
{ message: "the image should have the correct src" }
|
||||
);
|
||||
expect(".o_field_widget[name='document'] img").toHaveClass("img-fluid", {
|
||||
message: "the image should have the correct class",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveAttribute("width", "90", {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveStyle("maxWidth: 90px", {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
|
||||
expect(".o_field_image .o_select_file_button").toHaveCount(1, {
|
||||
message: "the image can be edited",
|
||||
});
|
||||
expect(".o_field_image .o_clear_file_button").toHaveCount(0, {
|
||||
message: "the image cannot be deleted as it has not been uploaded",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField preview is updated when an image is uploaded", async () => {
|
||||
const imageFile = new File(
|
||||
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file.png",
|
||||
{ type: "png" }
|
||||
);
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect('div[name="document"] img').toHaveAttribute(
|
||||
"data-src",
|
||||
"data:image/png;base64,coucou==",
|
||||
{ message: "the image should have the initial src" }
|
||||
);
|
||||
// Whitebox: replace the event target before the event is handled by the field so that we can modify
|
||||
// the files that it will take into account. This relies on the fact that it reads the files from
|
||||
// event.target and not from a direct reference to the input element.
|
||||
await click(".o_select_file_button");
|
||||
await setInputFiles(imageFile);
|
||||
// It can take some time to encode the data as a base64 url
|
||||
await runAllTimers();
|
||||
// Wait for a render
|
||||
await animationFrame();
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
{ message: "the image should have the new src" }
|
||||
);
|
||||
});
|
||||
|
||||
test("clicking save manually after uploading new image should change the unique of the image src", async () => {
|
||||
Partner._onChanges.foo = () => {};
|
||||
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
|
||||
|
||||
// 1659692220000, 1659695820000
|
||||
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
|
||||
let index = 0;
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
args[1].write_date = lastUpdates[index];
|
||||
args[1].document = "4 kb";
|
||||
index++;
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="document" widget="image" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
|
||||
await click("input[type=file]", { visible: false });
|
||||
await setFiles(
|
||||
new File(
|
||||
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file.png",
|
||||
{ type: "png" }
|
||||
)
|
||||
);
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await click(".o_field_widget[name='foo'] input");
|
||||
await edit("grrr");
|
||||
await animationFrame();
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave();
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659692220000");
|
||||
|
||||
// Change the image again. After clicking save, it should have the correct new url.
|
||||
await click("input[type=file]", { visible: false });
|
||||
await setFiles(
|
||||
new File(
|
||||
[Uint8Array.from([...atob(PRODUCT_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file2.gif",
|
||||
{ type: "gif" }
|
||||
)
|
||||
);
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/gif;base64,${PRODUCT_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave();
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659695820000");
|
||||
});
|
||||
|
||||
test("save record with image field modified by onchange", async () => {
|
||||
Partner._onChanges.foo = (data) => {
|
||||
data.document = MY_IMAGE;
|
||||
};
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
|
||||
|
||||
// 1659692220000
|
||||
const lastUpdates = ["2022-08-05 09:37:00"];
|
||||
let index = 0;
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
args[1].write_date = lastUpdates[index];
|
||||
args[1].document = "3 kb";
|
||||
index++;
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="document" widget="image" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
await click("[name='foo'] input");
|
||||
await edit("grrr", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave();
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659692220000");
|
||||
});
|
||||
|
||||
test("ImageField: option accepted_file_extensions", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'accepted_file_extensions': '.png,.jpeg'}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
// The view must be in edit mode
|
||||
expect("input.o_input_file").toHaveAttribute("accept", ".png,.jpeg", {
|
||||
message: "the input should have the correct ``accept`` attribute",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField: set 0 width/height in the size option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [0, 0]}" />
|
||||
<field name="document" widget="image" options="{'size': [0, 50]}" />
|
||||
<field name="document" widget="image" options="{'size': [50, 0]}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
const imgs = queryAll(".o_field_widget img");
|
||||
|
||||
expect([imgs[0].attributes.width, imgs[0].attributes.height]).toEqual([undefined, undefined], {
|
||||
message: "if both size are set to 0, both attributes are undefined",
|
||||
});
|
||||
|
||||
expect([imgs[1].attributes.width, imgs[1].attributes.height.value]).toEqual([undefined, "50"], {
|
||||
message: "if only the width is set to 0, the width attribute is not set on the img",
|
||||
});
|
||||
expect([
|
||||
imgs[1].style.width,
|
||||
imgs[1].style.maxWidth,
|
||||
imgs[1].style.height,
|
||||
imgs[1].style.maxHeight,
|
||||
]).toEqual(["auto", "100%", "", "50px"], {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
|
||||
expect([imgs[2].attributes.width.value, imgs[2].attributes.height]).toEqual(["50", undefined], {
|
||||
message: "if only the height is set to 0, the height attribute is not set on the img",
|
||||
});
|
||||
expect([
|
||||
imgs[2].style.width,
|
||||
imgs[2].style.maxWidth,
|
||||
imgs[2].style.height,
|
||||
imgs[2].style.maxHeight,
|
||||
]).toEqual(["", "50px", "auto", "100%"], {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField: zoom and zoom_delay options (readonly)", async () => {
|
||||
Partner._records[0].document = MY_IMAGE;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'zoom_delay': 600}" readonly="1" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
// data-tooltip attribute is used by the tooltip service
|
||||
expect(".o_field_image img").toHaveAttribute(
|
||||
"data-tooltip-info",
|
||||
`{"url":"data:image/png;base64,${MY_IMAGE}"}`,
|
||||
{ message: "shows a tooltip on hover" }
|
||||
);
|
||||
expect(".o_field_image img").toHaveAttribute("data-tooltip-delay", "600", {
|
||||
message: "tooltip has the right delay",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField: zoom and zoom_delay options (edit)", async () => {
|
||||
Partner._records[0].document = "3 kb";
|
||||
Partner._records[0].write_date = "2022-08-05 08:37:00";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'zoom_delay': 600}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_field_image img").toHaveAttribute(
|
||||
"data-tooltip-info",
|
||||
`{"url":"${getOrigin()}/web/image/partner/1/document?unique=1659688620000"}`,
|
||||
{ message: "tooltip show the full image from the field value" }
|
||||
);
|
||||
expect(".o_field_image img").toHaveAttribute("data-tooltip-delay", "600", {
|
||||
message: "tooltip has the right delay",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField displays the right images with zoom and preview_image options (readonly)", async () => {
|
||||
Partner._records[0].document = "3 kb";
|
||||
Partner._records[0].write_date = "2022-08-05 08:37:00";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'preview_image': 'document_preview', 'zoom_delay': 600}" readonly="1" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(".o_field_image img").toHaveAttribute(
|
||||
"data-tooltip-info",
|
||||
`{"url":"${getOrigin()}/web/image/partner/1/document?unique=1659688620000"}`,
|
||||
{ message: "tooltip show the full image from the field value" }
|
||||
);
|
||||
expect(".o_field_image img").toHaveAttribute("data-tooltip-delay", "600", {
|
||||
message: "tooltip has the right delay",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField in subviews is loaded correctly", async () => {
|
||||
Partner._records[0].write_date = "2017-02-08 10:00:00";
|
||||
Partner._records[0].document = MY_IMAGE;
|
||||
PartnerType._fields.image = fields.Binary({});
|
||||
PartnerType._records[0].image = PRODUCT_IMAGE;
|
||||
Partner._records[0].timmy = [12];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}" />
|
||||
<field name="timmy" widget="many2many" mode="kanban">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name" />
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<field name="image" widget="image" />
|
||||
</form>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`img[data-src="data:image/png;base64,${MY_IMAGE}"]`).toHaveCount(1);
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
|
||||
|
||||
// Actual flow: click on an element of the m2m to get its form view
|
||||
await click(".o_kanban_record:not(.o_kanban_ghost)");
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(1, { message: "The modal should have opened" });
|
||||
|
||||
expect(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("ImageField in x2many list is loaded correctly", async () => {
|
||||
PartnerType._fields.image = fields.Binary({});
|
||||
PartnerType._records[0].image = PRODUCT_IMAGE;
|
||||
Partner._records[0].timmy = [12];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many">
|
||||
<list>
|
||||
<field name="image" widget="image" />
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect("tr.o_data_row").toHaveCount(1, {
|
||||
message: "There should be one record in the many2many",
|
||||
});
|
||||
expect(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`).toHaveCount(1, {
|
||||
message: "The list's image is in the DOM",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField with required attribute", async () => {
|
||||
onRpc("create", () => {
|
||||
throw new Error("Should not do a create RPC with unset required image field");
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect(".o_form_view .o_form_editable").toHaveCount(1, {
|
||||
message: "form view should still be editable",
|
||||
});
|
||||
expect(".o_field_widget").toHaveClass("o_field_invalid", {
|
||||
message: "image field should be displayed as invalid",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField is reset when changing record", async () => {
|
||||
const imageData = Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
const imageFile = new File([imageData], "fake_file.png", { type: "png" });
|
||||
expect("img[alt='Binary file']").toHaveAttribute(
|
||||
"data-src",
|
||||
"/web/static/img/placeholder.png",
|
||||
{ message: "image field should not be set" }
|
||||
);
|
||||
|
||||
await setFiles(imageFile);
|
||||
expect("img[alt='Binary file']").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
{
|
||||
message: "image field should be set",
|
||||
}
|
||||
);
|
||||
|
||||
await clickSave();
|
||||
await click(".o_control_panel_main_buttons .o_form_button_create");
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect("img[alt='Binary file']").toHaveAttribute(
|
||||
"data-src",
|
||||
"/web/static/img/placeholder.png",
|
||||
{ message: "image field should be reset" }
|
||||
);
|
||||
|
||||
await setFiles(imageFile);
|
||||
expect("img[alt='Binary file']").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
{
|
||||
message: "image field should be set",
|
||||
}
|
||||
);
|
||||
});
|
||||
test("unique in url doesn't change on onchange", async () => {
|
||||
Partner._onChanges.foo = () => {};
|
||||
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00";
|
||||
|
||||
onRpc(({ method, args }) => {
|
||||
expect.step(method);
|
||||
if (method === "web_save") {
|
||||
args[1].write_date = "2022-08-05 09:37:00"; // 1659692220000
|
||||
}
|
||||
});
|
||||
await mountView({
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect.verifySteps(["get_views", "web_read"]);
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
|
||||
expect.verifySteps([]);
|
||||
// same unique as before
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
|
||||
await click(".o_field_widget[name='foo'] input");
|
||||
await edit("grrr", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect.verifySteps(["onchange"]);
|
||||
// also same unique
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
|
||||
await clickSave();
|
||||
expect.verifySteps(["web_save"]);
|
||||
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659692220000");
|
||||
});
|
||||
|
||||
test("unique in url change on record change", async () => {
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00";
|
||||
|
||||
const rec2 = Partner._records.find((rec) => rec.id === 2);
|
||||
rec2.document = "3 kb";
|
||||
rec2.write_date = "2022-08-05 09:37:00";
|
||||
|
||||
await mountView({
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
await pagerNext();
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659692220000");
|
||||
});
|
||||
|
||||
test("unique in url does not change on record change if reload option is set to false", async () => {
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00";
|
||||
|
||||
await mountView({
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" options="{'reload': false}" />
|
||||
<field name="write_date" readonly="0"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
await click("div[name='write_date'] > div > input");
|
||||
await edit("2022-08-05 08:39:00", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
await clickSave();
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
});
|
||||
|
||||
test("convert image to webp", async () => {
|
||||
onRpc("ir.attachment", "create_unique", ({ args }) => {
|
||||
// This RPC call is done two times - once for storing webp and once for storing jpeg
|
||||
// This handles first RPC call to store webp
|
||||
if (!args[0][0].res_id) {
|
||||
// Here we check the image data we pass and generated data.
|
||||
// Also we check the file type
|
||||
expect(args[0][0].datas).not.toBe(imageData);
|
||||
expect(args[0][0].mimetype).toBe("image/webp");
|
||||
return [1];
|
||||
}
|
||||
// This handles second RPC call to store jpeg
|
||||
expect(args[0][0].datas).not.toBe(imageData);
|
||||
expect(args[0][0].mimetype).toBe("image/jpeg");
|
||||
return true;
|
||||
});
|
||||
|
||||
const imageData = Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" options="{'convert_to_webp': True}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
const imageFile = new File([imageData], "fake_file.jpeg", { type: "jpeg" });
|
||||
expect("img[alt='Binary file']").toHaveAttribute(
|
||||
"data-src",
|
||||
"/web/static/img/placeholder.png",
|
||||
{ message: "image field should not be set" }
|
||||
);
|
||||
await setFiles(imageFile);
|
||||
});
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
|
||||
const FR_FLAG_URL = "/base/static/img/country_flags/fr.png";
|
||||
const EN_FLAG_URL = "/base/static/img/country_flags/gb.png";
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
foo = fields.Char();
|
||||
p = fields.One2many({ relation: "partner" });
|
||||
timmy = fields.Many2many({ relation: "partner.type" });
|
||||
|
||||
_records = [{ id: 1, foo: FR_FLAG_URL, timmy: [] }];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
name = fields.Char();
|
||||
color = fields.Integer();
|
||||
|
||||
_records = [
|
||||
{ id: 12, display_name: "gold", color: 2 },
|
||||
{ id: 14, display_name: "silver", color: 5 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, PartnerType]);
|
||||
|
||||
test("image fields are correctly rendered", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(`div[name="foo"]`).toHaveClass("o_field_image_url", {
|
||||
message: "the widget should have the correct class",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveCount(1, {
|
||||
message: "the widget should contain an image",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveAttribute("data-src", FR_FLAG_URL, {
|
||||
message: "the image should have the correct src",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveClass("img-fluid", {
|
||||
message: "the image should have the correct class",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveAttribute("width", "90", {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveStyle("maxWidth: 90px", {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageUrlField in subviews are loaded correctly", async () => {
|
||||
PartnerType._fields.image = fields.Char();
|
||||
PartnerType._records[0].image = EN_FLAG_URL;
|
||||
Partner._records[0].timmy = [12];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
<field name="timmy" widget="many2many" mode="kanban">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<field name="image" widget="image_url"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(`img[data-src="${FR_FLAG_URL}"]`).toHaveCount(1, {
|
||||
message: "The view's image is in the DOM",
|
||||
});
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1, {
|
||||
message: "There should be one record in the many2many",
|
||||
});
|
||||
|
||||
// Actual flow: click on an element of the m2m to get its form view
|
||||
await click(".o_kanban_record:not(.o_kanban_ghost)");
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(1, { message: "The modal should have opened" });
|
||||
expect(`img[data-src="${EN_FLAG_URL}"]`).toHaveCount(1, {
|
||||
message: "The dialog's image is in the DOM",
|
||||
});
|
||||
});
|
||||
|
||||
test("image fields in x2many list are loaded correctly", async () => {
|
||||
PartnerType._fields.image = fields.Char();
|
||||
PartnerType._records[0].image = EN_FLAG_URL;
|
||||
Partner._records[0].timmy = [12];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many">
|
||||
<list>
|
||||
<field name="image" widget="image_url"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect("tr.o_data_row").toHaveCount(1, {
|
||||
message: "There should be one record in the many2many",
|
||||
});
|
||||
expect(`img[data-src="${EN_FLAG_URL}"]`).toHaveCount(1, {
|
||||
message: "The list's image is in the DOM",
|
||||
});
|
||||
});
|
||||
|
||||
test("image url fields in kanban don't stop opening record", async () => {
|
||||
patchWithCleanup(KanbanController.prototype, {
|
||||
openRecord() {
|
||||
expect.step("open record");
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo" widget="image_url"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(".o_kanban_record");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["open record"]);
|
||||
});
|
||||
|
||||
test("image fields with empty value", async () => {
|
||||
Partner._records[0].foo = false;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(`div[name="foo"]`).toHaveClass("o_field_image_url", {
|
||||
message: "the widget should have the correct class",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveCount(0, {
|
||||
message: "the widget should not contain an image",
|
||||
});
|
||||
});
|
||||
|
||||
test("onchange update image fields", async () => {
|
||||
const srcTest = "/my/test/src";
|
||||
Partner._onChanges.name = (record) => {
|
||||
record.foo = srcTest;
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(`div[name="foo"] > img`).toHaveAttribute("data-src", FR_FLAG_URL, {
|
||||
message: "the image should have the correct src",
|
||||
});
|
||||
|
||||
await click(`[name="name"] input`);
|
||||
await edit("test", { confirm: "enter" });
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(`div[name="foo"] > img`).toHaveAttribute("data-src", srcTest, {
|
||||
message: "the image should have the onchange src",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Product extends models.Model {
|
||||
price = fields.Integer();
|
||||
}
|
||||
|
||||
defineModels([Product]);
|
||||
|
||||
test("human readable format 1", async () => {
|
||||
Product._records = [{ id: 1, price: 3.756754e6 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'human_readable': 'true'}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("4M", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T)",
|
||||
});
|
||||
});
|
||||
|
||||
test("human readable format 2", async () => {
|
||||
Product._records = [{ id: 1, price: 2.034e3 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'human_readable': 'true', 'decimals': 1}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("2.0k", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T)",
|
||||
});
|
||||
});
|
||||
|
||||
test("human readable format 3", async () => {
|
||||
Product._records = [{ id: 1, price: 6.67543577586e12 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("6.6754T", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T)",
|
||||
});
|
||||
});
|
||||
|
||||
test("still human readable when readonly", async () => {
|
||||
Product._records = [{ id: 1, price: 6.67543577586e12 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" readonly="true" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget span").toHaveText("6.6754T");
|
||||
});
|
||||
|
||||
test("should be 0 when unset", async () => {
|
||||
Product._records = [{ id: 1 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
expect(".o_field_widget input").not.toHaveClass("o_field_empty");
|
||||
expect(".o_field_widget input").toHaveValue("0");
|
||||
});
|
||||
|
||||
test("basic form view flow", async () => {
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("10");
|
||||
await fieldInput("price").edit("30");
|
||||
expect(".o_field_widget input").toHaveValue("30");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("30");
|
||||
});
|
||||
|
||||
test("no need to focus out of the input to save the record after correcting an invalid input", async () => {
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("10");
|
||||
await fieldInput("price").edit("a");
|
||||
expect(".o_field_widget input").toHaveValue("a");
|
||||
expect(".o_form_status_indicator span i.fa-warning").toHaveCount(1);
|
||||
expect(".o_form_button_save[disabled]").toHaveCount(1);
|
||||
await fieldInput("price").edit("1");
|
||||
expect(".o_field_widget input").toHaveValue("1");
|
||||
expect(".o_form_status_indicator span i.fa-warning").toHaveCount(0);
|
||||
expect(".o_form_button_save[disabled]").toHaveCount(0);
|
||||
await clickSave(); // makes sure there is an enabled save button
|
||||
});
|
||||
|
||||
test("rounded when using formula in form view", async () => {
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
await fieldInput("price").edit("=100/3");
|
||||
expect(".o_field_widget input").toHaveValue("33");
|
||||
});
|
||||
|
||||
test("with input type 'number' option", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'type': 'number'}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveAttribute("type", "number");
|
||||
await fieldInput("price").edit("1234567890");
|
||||
expect(".o_field_widget input").toHaveValue(1234567890, {
|
||||
message: "Integer value must be not formatted if input type is number",
|
||||
});
|
||||
});
|
||||
|
||||
test("with 'step' option", async () => {
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'type': 'number', 'step': 3}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveAttribute("step", "3");
|
||||
});
|
||||
|
||||
test("without input type option", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveAttribute("type", "text");
|
||||
await fieldInput("price").edit("1234567890");
|
||||
expect(".o_field_widget input").toHaveValue("1,234,567,890");
|
||||
});
|
||||
|
||||
test("is formatted by default", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 8069 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'enable_formatting': 'false'}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("8,069");
|
||||
});
|
||||
|
||||
test("basic flow in editable list view", async () => {
|
||||
Product._records = [{ id: 1 }, { id: 2, price: 10 }];
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "product",
|
||||
arch: '<list editable="bottom"><field name="price"/></list>',
|
||||
});
|
||||
const zeroValues = queryAllTexts("td").filter((text) => text === "0");
|
||||
expect(zeroValues).toHaveLength(1, {
|
||||
message: "Unset integer values should not be rendered as zeros",
|
||||
});
|
||||
await contains("td.o_data_cell").click();
|
||||
expect('.o_field_widget[name="price"] input').toHaveCount(1);
|
||||
await contains('.o_field_widget[name="price"] input').edit("-28");
|
||||
expect("td.o_data_cell:first").toHaveText("-28");
|
||||
expect('.o_field_widget[name="price"] input').toHaveValue("10");
|
||||
await contains(getFixture()).click();
|
||||
expect(queryAllTexts("td.o_data_cell")).toEqual(["-28", "10"]);
|
||||
});
|
||||
|
||||
test("with placeholder", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: `<form><field name="price" placeholder="Placeholder"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveAttribute("placeholder", "Placeholder");
|
||||
});
|
||||
|
||||
test("with enable_formatting option as false", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 8069 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'enable_formatting': false}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("8069");
|
||||
await fieldInput("price").edit("1234567890");
|
||||
expect(".o_field_widget input").toHaveValue("1234567890");
|
||||
});
|
||||
|
||||
test("value is formatted on Enter", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("0");
|
||||
|
||||
await fieldInput("price").edit("1000", { confirm: "Enter" });
|
||||
expect(".o_field_widget input").toHaveValue("1,000");
|
||||
});
|
||||
|
||||
test("value is formatted on Enter (even if same value)", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 8069 }];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("8,069");
|
||||
|
||||
await fieldInput("price").edit("8069", { confirm: "Enter" });
|
||||
expect(".o_field_widget input").toHaveValue("8,069");
|
||||
});
|
||||
|
||||
test("value is formatted on click out (even if same value)", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 8069 }];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("8,069");
|
||||
|
||||
await fieldInput("price").edit("8069", { confirm: false });
|
||||
expect(".o_field_widget input").toHaveValue("8069");
|
||||
|
||||
await contains(".o_control_panel").click();
|
||||
expect(".o_field_widget input").toHaveValue("8,069");
|
||||
});
|
||||
|
||||
test("Value should not be a boolean when enable_formatting is false", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "product",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="id" options="{'enable_formatting': false}"/>
|
||||
<field name="price"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
await contains(`.o_list_button_add`).click();
|
||||
expect(".o_selected_row .o_field_integer").toHaveText("");
|
||||
});
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, press } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
findComponent,
|
||||
models,
|
||||
mountView,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
|
||||
const graph_values = [
|
||||
{ value: 300, label: "5-11 Dec" },
|
||||
{ value: 500, label: "12-18 Dec" },
|
||||
{ value: 100, label: "19-25 Dec" },
|
||||
];
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer({
|
||||
string: "int_field",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
});
|
||||
selection = fields.Selection({
|
||||
string: "Selection",
|
||||
searchable: true,
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
});
|
||||
graph_data = fields.Text({ string: "Graph Data" });
|
||||
graph_type = fields.Selection({
|
||||
string: "Graph Type",
|
||||
selection: [
|
||||
["line", "Line"],
|
||||
["bar", "Bar"],
|
||||
],
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
int_field: 10,
|
||||
selection: "blocked",
|
||||
graph_type: "bar",
|
||||
graph_data: JSON.stringify([
|
||||
{
|
||||
color: "blue",
|
||||
title: "Partner 1",
|
||||
values: graph_values,
|
||||
key: "A key",
|
||||
area: true,
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
int_field: 0,
|
||||
selection: "normal",
|
||||
graph_type: "line",
|
||||
graph_data: JSON.stringify([
|
||||
{
|
||||
color: "red",
|
||||
title: "Partner 0",
|
||||
values: graph_values,
|
||||
key: "A key",
|
||||
area: true,
|
||||
},
|
||||
]),
|
||||
},
|
||||
];
|
||||
}
|
||||
defineModels([Partner]);
|
||||
|
||||
// Kanban
|
||||
// WOWL remove this helper and user the control panel instead
|
||||
const reload = async (kanban, params = {}) => {
|
||||
kanban.env.searchModel.reload(params);
|
||||
kanban.env.searchModel.search();
|
||||
await animationFrame();
|
||||
};
|
||||
|
||||
test.tags("desktop");
|
||||
test("JournalDashboardGraphField is rendered correctly", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<field name="graph_type"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "in", [1, 2]]],
|
||||
});
|
||||
expect(".o_dashboard_graph canvas").toHaveCount(2, {
|
||||
message: "there should be two graphs rendered",
|
||||
});
|
||||
expect(".o_kanban_record:nth-child(1) .o_graph_barchart").toHaveCount(1, {
|
||||
message: "graph of first record should be a barchart",
|
||||
});
|
||||
expect(".o_kanban_record:nth-child(2) .o_graph_linechart").toHaveCount(1, {
|
||||
message: "graph of second record should be a linechart",
|
||||
});
|
||||
|
||||
// reload kanban
|
||||
await click("input.o_searchview_input");
|
||||
await press("Enter");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dashboard_graph canvas").toHaveCount(2, {
|
||||
message: "there should be two graphs rendered",
|
||||
});
|
||||
});
|
||||
|
||||
test("rendering of a JournalDashboardGraphField in an updated grouped kanban view", async () => {
|
||||
const view = await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<field name="graph_type"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "in", [1, 2]]],
|
||||
});
|
||||
const kanban = findComponent(view, (component) => component instanceof KanbanController);
|
||||
expect(".o_dashboard_graph canvas").toHaveCount(2, {
|
||||
message: "there should be two graph rendered",
|
||||
});
|
||||
await reload(kanban, { groupBy: ["selection"], domain: [["int_field", "=", 10]] });
|
||||
|
||||
expect(".o_dashboard_graph canvas").toHaveCount(1, {
|
||||
message: "there should be one graph rendered",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
foo = fields.Char();
|
||||
selection = fields.Selection({
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
});
|
||||
_records = [
|
||||
{
|
||||
foo: "yop",
|
||||
selection: "blocked",
|
||||
},
|
||||
{
|
||||
foo: "blip",
|
||||
selection: "normal",
|
||||
},
|
||||
{
|
||||
foo: "abc",
|
||||
selection: "done",
|
||||
},
|
||||
];
|
||||
}
|
||||
defineModels([Partner]);
|
||||
|
||||
test("LabelSelectionField in form view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="label_selection"
|
||||
options="{'classes': {'normal': 'secondary', 'blocked': 'warning','done': 'success'}}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveCount(1, {
|
||||
message: "should have a warning status label since selection is the second, blocked state",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveCount(0, {
|
||||
message: "should not have a default status since selection is the second, blocked state",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveCount(0, {
|
||||
message: "should not have a success status since selection is the second, blocked state",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveText("Blocked", {
|
||||
message: "the label should say 'Blocked' since this is the label value for that state",
|
||||
});
|
||||
});
|
||||
|
||||
test("LabelSelectionField in editable list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list editable="bottom">
|
||||
<field name="foo"/>
|
||||
<field name="selection" widget="label_selection"
|
||||
options="{'classes': {'normal': 'secondary', 'blocked': 'warning','done': 'success'}}"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget .badge:not(:empty)").toHaveCount(3, {
|
||||
message: "should have three visible status labels",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveCount(1, {
|
||||
message: "should have one warning status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveText("Blocked", {
|
||||
message: "the warning label should read 'Blocked'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveCount(1, {
|
||||
message: "should have one default status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveText("Normal", {
|
||||
message: "the default label should read 'Normal'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveCount(1, {
|
||||
message: "should have one success status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveText("Done", {
|
||||
message: "the success label should read 'Done'",
|
||||
});
|
||||
|
||||
// switch to edit mode and check the result
|
||||
await click("tbody td:not(.o_list_record_selector)");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_widget .badge:not(:empty)").toHaveCount(3, {
|
||||
message: "should have three visible status labels",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveCount(1, {
|
||||
message: "should have one warning status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveText("Blocked", {
|
||||
message: "the warning label should read 'Blocked'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveCount(1, {
|
||||
message: "should have one default status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveText("Normal", {
|
||||
message: "the default label should read 'Normal'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveCount(1, {
|
||||
message: "should have one success status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveText("Done", {
|
||||
message: "the success label should read 'Done'",
|
||||
});
|
||||
|
||||
// save and check the result
|
||||
await click(".o_control_panel_main_buttons .o_list_button_save");
|
||||
await animationFrame();
|
||||
expect(".o_field_widget .badge:not(:empty)").toHaveCount(3, {
|
||||
message: "should have three visible status labels",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveCount(1, {
|
||||
message: "should have one warning status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveText("Blocked", {
|
||||
message: "the warning label should read 'Blocked'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveCount(1, {
|
||||
message: "should have one default status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveText("Normal", {
|
||||
message: "the default label should read 'Normal'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveCount(1, {
|
||||
message: "should have one success status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveText("Done", {
|
||||
message: "the success label should read 'Done'",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { setInputFiles } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
MockServer,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Turtle extends models.Model {
|
||||
picture_ids = fields.Many2many({
|
||||
string: "Pictures",
|
||||
relation: "ir.attachment",
|
||||
});
|
||||
_records = [{ id: 1, picture_ids: [17] }];
|
||||
}
|
||||
|
||||
class IrAttachment extends models.Model {
|
||||
_name = "ir.attachment";
|
||||
name = fields.Char();
|
||||
mimetype = fields.Char();
|
||||
_records = [{ id: 17, name: "Marley&Me.jpg", mimetype: "jpg" }];
|
||||
}
|
||||
|
||||
defineModels([Turtle, IrAttachment]);
|
||||
|
||||
test("widget many2many_binary", async () => {
|
||||
expect.assertions(17);
|
||||
|
||||
mockService("http", () => ({
|
||||
post(route, { ufile }) {
|
||||
expect(route).toBe("/web/binary/upload_attachment");
|
||||
expect(ufile[0].name).toBe("fake_file.tiff", {
|
||||
message: "file is correctly uploaded to the server",
|
||||
});
|
||||
const ids = MockServer.env["ir.attachment"].create(
|
||||
ufile.map(({ name }) => ({ name, mimetype: "text/plain" }))
|
||||
);
|
||||
return JSON.stringify(MockServer.env["ir.attachment"].read(ids));
|
||||
},
|
||||
}));
|
||||
|
||||
IrAttachment._views.list = '<list string="Pictures"><field name="name"/></list>';
|
||||
onRpc((args) => {
|
||||
if (args.method !== "get_views") {
|
||||
expect.step(args.route);
|
||||
}
|
||||
if (args.method === "web_read" && args.model === "turtle") {
|
||||
expect(args.kwargs.specification).toEqual({
|
||||
display_name: {},
|
||||
picture_ids: {
|
||||
fields: {
|
||||
mimetype: {},
|
||||
name: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (args.method === "web_save" && args.model === "turtle") {
|
||||
expect(args.kwargs.specification).toEqual({
|
||||
display_name: {},
|
||||
picture_ids: {
|
||||
fields: {
|
||||
mimetype: {},
|
||||
name: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (args.method === "web_read" && args.model === "ir.attachment") {
|
||||
expect(args.kwargs.specification).toEqual({
|
||||
mimetype: {},
|
||||
name: {},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="picture_ids" widget="many2many_binary" options="{'accepted_file_extensions': 'image/*'}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget .oe_fileupload").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachments").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attach").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attach").toHaveText("Pictures");
|
||||
|
||||
expect("input.o_input_file").toHaveAttribute("accept", "image/*");
|
||||
expect.verifySteps(["/web/dataset/call_kw/turtle/web_read"]);
|
||||
|
||||
// Set and trigger the change of a file for the input
|
||||
const file = new File(["fake_file"], "fake_file.tiff", { type: "text/plain" });
|
||||
await contains(".o_file_input_trigger").click();
|
||||
await setInputFiles([file]);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_attachment:nth-child(2) .caption a:eq(0)").toHaveText("fake_file.tiff", {
|
||||
message: 'value of attachment should be "fake_file.tiff"',
|
||||
});
|
||||
expect(".o_attachment:nth-child(2) .caption.small a").toHaveText("TIFF", {
|
||||
message: "file extension should be correct",
|
||||
});
|
||||
expect(".o_attachment:nth-child(2) .o_image.o_hover").toHaveAttribute(
|
||||
"data-mimetype",
|
||||
"text/plain",
|
||||
{ message: "preview displays the right mimetype" }
|
||||
);
|
||||
|
||||
// delete the attachment
|
||||
await contains("div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete").click();
|
||||
|
||||
await clickSave();
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachments").toHaveCount(1);
|
||||
expect.verifySteps([
|
||||
"/web/dataset/call_kw/ir.attachment/web_read",
|
||||
"/web/dataset/call_kw/turtle/web_save",
|
||||
]);
|
||||
});
|
||||
|
||||
test("widget many2many_binary displays notification on error", async () => {
|
||||
expect.assertions(12);
|
||||
|
||||
mockService("http", () => ({
|
||||
post(route, { ufile }) {
|
||||
expect(route).toBe("/web/binary/upload_attachment");
|
||||
expect([ufile[0].name, ufile[1].name]).toEqual(["good_file.txt", "bad_file.txt"], {
|
||||
message: "files are correctly sent to the server",
|
||||
});
|
||||
const ids = MockServer.env["ir.attachment"].create({
|
||||
name: ufile[0].name,
|
||||
mimetype: "text/plain",
|
||||
});
|
||||
return JSON.stringify([
|
||||
...MockServer.env["ir.attachment"].read(ids),
|
||||
{
|
||||
name: ufile[1].name,
|
||||
mimetype: "text/plain",
|
||||
error: `Error on file: ${ufile[1].name}`,
|
||||
},
|
||||
]);
|
||||
},
|
||||
}));
|
||||
|
||||
IrAttachment._views.list = '<list string="Pictures"><field name="name"/></list>';
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="picture_ids" widget="many2many_binary" options="{'accepted_file_extensions': 'image/*'}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget .oe_fileupload").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachments").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attach").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete").toHaveCount(1);
|
||||
|
||||
// Set and trigger the import of 2 files in the input
|
||||
const files = [
|
||||
new File(["good_file"], "good_file.txt", { type: "text/plain" }),
|
||||
new File(["bad_file"], "bad_file.txt", { type: "text/plain" }),
|
||||
];
|
||||
await contains(".o_file_input_trigger").click();
|
||||
await setInputFiles(files);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_attachment:nth-child(2) .caption a:eq(0)").toHaveText("good_file.txt", {
|
||||
message: 'value of attachment should be "good_file.txt"',
|
||||
});
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachments").toHaveCount(1);
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification_title").toHaveText("Uploading error");
|
||||
expect(".o_notification_content").toHaveText("Error on file: bad_file.txt");
|
||||
expect(".o_notification_bar").toHaveClass("bg-danger");
|
||||
});
|
||||
|
|
@ -0,0 +1,496 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer({ sortable: true });
|
||||
timmy = fields.Many2many({ string: "pokemon", relation: "partner.type" });
|
||||
p = fields.One2many({
|
||||
string: "one2many field",
|
||||
relation: "partner",
|
||||
relation_field: "trululu",
|
||||
});
|
||||
trululu = fields.Many2one({ relation: "partner" });
|
||||
_records = [{ id: 1, int_field: 10, p: [1] }];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
name = fields.Char();
|
||||
_records = [
|
||||
{ id: 12, name: "gold" },
|
||||
{ id: 14, name: "silver" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, PartnerType]);
|
||||
|
||||
test("Many2ManyCheckBoxesField", async () => {
|
||||
Partner._records[0].timmy = [12];
|
||||
const commands = [[[4, 14]], [[3, 12]]];
|
||||
onRpc("web_save", (args) => {
|
||||
expect.step("web_save");
|
||||
expect(args.args[1].timmy).toEqual(commands.shift());
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").not.toBeChecked();
|
||||
|
||||
expect("div.o_field_widget div.form-check input:disabled").toHaveCount(0);
|
||||
|
||||
// add a m2m value by clicking on input
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
await runAllTimers();
|
||||
await clickSave();
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(2);
|
||||
|
||||
// remove a m2m value by clinking on label
|
||||
await contains("div.o_field_widget div.form-check > label").click();
|
||||
await runAllTimers();
|
||||
await clickSave();
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").not.toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").toBeChecked();
|
||||
|
||||
expect.verifySteps(["web_save", "web_save"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField (readonly)", async () => {
|
||||
Partner._records[0].timmy = [12];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" readonly="True" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2, {
|
||||
message: "should have fetched and displayed the 2 values of the many2many",
|
||||
});
|
||||
expect("div.o_field_widget div.form-check input:disabled").toHaveCount(2, {
|
||||
message: "the checkboxes should be disabled",
|
||||
});
|
||||
|
||||
await contains("div.o_field_widget div.form-check > label:eq(1)").click();
|
||||
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").not.toBeChecked();
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField does not read added record", async () => {
|
||||
Partner._records[0].timmy = [];
|
||||
onRpc((args) => {
|
||||
expect.step(args.method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(0);
|
||||
|
||||
await contains("div.o_field_widget div.form-check input").click();
|
||||
await runAllTimers();
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(1);
|
||||
|
||||
await clickSave();
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(1);
|
||||
|
||||
expect.verifySteps(["get_views", "web_read", "name_search", "web_save"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField: start non empty, then remove twice", async () => {
|
||||
Partner._records[0].timmy = [12, 14];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains("div.o_field_widget div.form-check input:eq(0)").click();
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
await runAllTimers();
|
||||
await clickSave();
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").not.toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").not.toBeChecked();
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField: many2many read, field context is properly sent", async () => {
|
||||
onRpc((args) => {
|
||||
expect.step(args.method);
|
||||
if (args.method === "web_read") {
|
||||
expect(args.kwargs.specification.timmy.context).toEqual({ hello: "world" });
|
||||
} else if (args.method === "name_search") {
|
||||
expect(args.kwargs.context.hello).toEqual("world");
|
||||
}
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes" context="{ 'hello': 'world' }" />
|
||||
</form>`,
|
||||
});
|
||||
expect.verifySteps(["get_views", "web_read", "name_search"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField: values are updated when domain changes", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field" />
|
||||
<field name="timmy" widget="many2many_checkboxes" domain="[['id', '>', int_field]]" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='int_field'] input").toHaveValue("10");
|
||||
expect(".o_field_widget[name='timmy'] .form-check").toHaveCount(2);
|
||||
expect(".o_field_widget[name='timmy']").toHaveText("gold\nsilver");
|
||||
|
||||
await contains(".o_field_widget[name='int_field'] input").edit(13);
|
||||
expect(".o_field_widget[name='timmy'] .form-check").toHaveCount(1);
|
||||
expect(".o_field_widget[name='timmy']").toHaveText("silver");
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField with 40+ values", async () => {
|
||||
// 40 is the default limit for x2many fields. However, the many2many_checkboxes is a
|
||||
// special field that fetches its data through the fetchSpecialData mechanism, and it
|
||||
// uses the name_search server-side limit of 100. This test comes with a fix for a bug
|
||||
// that occurred when the user (un)selected a checkbox that wasn't in the 40 first checkboxes,
|
||||
// because the piece of data corresponding to that checkbox hadn't been processed by the
|
||||
// BasicModel, whereas the code handling the change assumed it had.
|
||||
expect.assertions(3);
|
||||
|
||||
const records = [];
|
||||
for (let id = 1; id <= 90; id++) {
|
||||
records.push({
|
||||
id,
|
||||
name: `type ${id}`,
|
||||
});
|
||||
}
|
||||
PartnerType._records = records;
|
||||
Partner._records[0].timmy = records.map((r) => r.id);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].timmy).toEqual([[3, records[records.length - 1].id]]);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='timmy'] input[type='checkbox']:checked").toHaveCount(90);
|
||||
|
||||
// toggle the last value
|
||||
await contains(".o_field_widget[name='timmy'] input[type='checkbox']:last").click();
|
||||
await runAllTimers();
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget[name='timmy'] input[type='checkbox']:last").not.toBeChecked();
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField with 100+ values", async () => {
|
||||
// The many2many_checkboxes widget limits the displayed values to 100 (this is the
|
||||
// server-side name_search limit). This test encodes a scenario where there are more than
|
||||
// 100 records in the co-model, and all values in the many2many relationship aren't
|
||||
// displayed in the widget (due to the limit). If the user (un)selects a checkbox, we don't
|
||||
// want to remove all values that aren't displayed from the relation.
|
||||
expect.assertions(5);
|
||||
|
||||
const records = [];
|
||||
for (let id = 1; id < 150; id++) {
|
||||
records.push({
|
||||
id,
|
||||
name: `type ${id}`,
|
||||
});
|
||||
}
|
||||
PartnerType._records = records;
|
||||
Partner._records[0].timmy = records.map((r) => r.id);
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].timmy).toEqual([[3, records[0].id]]);
|
||||
expect.step("web_save");
|
||||
});
|
||||
onRpc("name_search", () => {
|
||||
expect.step("name_search");
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='timmy'] input[type='checkbox']").toHaveCount(100);
|
||||
expect(".o_field_widget[name='timmy'] input[type='checkbox']").toBeChecked();
|
||||
|
||||
// toggle the first value
|
||||
await contains(".o_field_widget[name='timmy'] input[type='checkbox']").click();
|
||||
await runAllTimers();
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget[name='timmy'] input[type='checkbox']:first").not.toBeChecked();
|
||||
expect.verifySteps(["name_search", "web_save"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField in a one2many", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
PartnerType._records.push({ id: 15, name: "bronze" });
|
||||
Partner._records[0].timmy = [14, 15];
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1]).toEqual({
|
||||
p: [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
{
|
||||
timmy: [
|
||||
[4, 12],
|
||||
[3, 14],
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<list><field name="id"/></list>
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await contains(".o_data_cell").click();
|
||||
|
||||
// edit the timmy field by (un)checking boxes on the widget
|
||||
await contains(".modal .form-check-input:eq(0)").click();
|
||||
expect(".modal .form-check-input:eq(0)").toBeChecked();
|
||||
await contains(".modal .form-check-input:eq(1)").click();
|
||||
expect(".modal .form-check-input:eq(1)").not.toBeChecked();
|
||||
|
||||
await contains(".modal .o_form_button_save").click();
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField with default values", async () => {
|
||||
expect.assertions(7);
|
||||
|
||||
Partner._fields.timmy = fields.Many2many({
|
||||
string: "pokemon",
|
||||
relation: "partner.type",
|
||||
default: [[4, 3]],
|
||||
});
|
||||
PartnerType._records.push({ id: 3, name: "bronze" });
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].timmy).toEqual([[4, 12]], {
|
||||
message: "correct values should have been sent to create",
|
||||
});
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_form_view .form-check input:eq(0)").not.toBeChecked();
|
||||
expect(".o_form_view .form-check input:eq(1)").not.toBeChecked();
|
||||
expect(".o_form_view .form-check input:eq(2)").toBeChecked();
|
||||
|
||||
await contains(".o_form_view .form-check input:checked").click();
|
||||
await contains(".o_form_view .form-check input:eq(0)").click();
|
||||
await contains(".o_form_view .form-check input:eq(0)").click();
|
||||
await contains(".o_form_view .form-check input:eq(0)").click();
|
||||
await runAllTimers();
|
||||
|
||||
expect(".o_form_view .form-check input:eq(0)").toBeChecked();
|
||||
expect(".o_form_view .form-check input:eq(1)").not.toBeChecked();
|
||||
expect(".o_form_view .form-check input:eq(2)").not.toBeChecked();
|
||||
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField batches successive changes", async () => {
|
||||
Partner._fields.timmy = fields.Many2many({
|
||||
string: "pokemon",
|
||||
relation: "partner.type",
|
||||
onChange: () => {},
|
||||
});
|
||||
Partner._records[0].timmy = [];
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(0);
|
||||
|
||||
await contains("div.o_field_widget div.form-check input:eq(0)").click();
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
// checkboxes are updated directly
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(2);
|
||||
// but no onchanges has been fired yet
|
||||
expect.verifySteps(["get_views", "web_read", "name_search"]);
|
||||
await runAllTimers();
|
||||
expect.verifySteps(["onchange"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField sends batched changes on save", async () => {
|
||||
Partner._fields.timmy = fields.Many2many({
|
||||
string: "pokemon",
|
||||
relation: "partner.type",
|
||||
onChange: () => {},
|
||||
});
|
||||
Partner._records[0].timmy = [];
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(0);
|
||||
|
||||
await contains("div.o_field_widget div.form-check input:eq(0)").click();
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
// checkboxes are updated directly
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(2);
|
||||
// but no onchanges has been fired yet
|
||||
expect.verifySteps(["get_views", "web_read", "name_search"]);
|
||||
await runAllTimers();
|
||||
// save
|
||||
await clickSave();
|
||||
expect.verifySteps(["onchange", "web_save"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField in a notebook tab", async () => {
|
||||
Partner._records[0].timmy = [];
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<notebook>
|
||||
<page string="Page 1">
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</page>
|
||||
<page string="Page 2">
|
||||
<field name="int_field" />
|
||||
</page>
|
||||
</notebook>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget[name=timmy]").toHaveCount(1);
|
||||
expect("div.o_field_widget[name=timmy] div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget[name=timmy] div.form-check input:checked").toHaveCount(0);
|
||||
|
||||
await contains("div.o_field_widget div.form-check input:eq(0)").click();
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
// checkboxes are updated directly
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(2);
|
||||
// go to the other tab
|
||||
await contains(".o_notebook .nav-link:eq(1)").click();
|
||||
expect("div.o_field_widget[name=timmy]").toHaveCount(0);
|
||||
expect("div.o_field_widget[name=int_field]").toHaveCount(1);
|
||||
// save
|
||||
await clickSave();
|
||||
expect.verifySteps(["get_views", "web_read", "name_search", "web_save"]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,434 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { press, queryAllTexts, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char({ string: "Displayed name" });
|
||||
_records = [
|
||||
{ id: 1, name: "first record" },
|
||||
{ id: 2, name: "second record" },
|
||||
{ id: 4, name: "aaa" },
|
||||
];
|
||||
}
|
||||
|
||||
class Turtle extends models.Model {
|
||||
name = fields.Char({ string: "Displayed name" });
|
||||
partner_ids = fields.Many2many({ string: "Partner", relation: "partner" });
|
||||
_records = [
|
||||
{ id: 1, name: "leonardo", partner_ids: [] },
|
||||
{ id: 2, name: "donatello", partner_ids: [2, 4] },
|
||||
{ id: 3, name: "raphael" },
|
||||
];
|
||||
}
|
||||
|
||||
onRpc("has_group", () => {
|
||||
return true;
|
||||
});
|
||||
|
||||
defineModels([Partner, Turtle]);
|
||||
|
||||
test("widget many2many_tags_avatar", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(queryAllTexts("[name='partner_ids'] .o_tag")).toEqual([]);
|
||||
expect("[name='partner_ids'] .o_input_dropdown input").toHaveValue("");
|
||||
|
||||
await contains("[name='partner_ids'] .o_input_dropdown input").fill("first record");
|
||||
await runAllTimers();
|
||||
expect(queryAllTexts("[name='partner_ids'] .o_tag")).toEqual(["first record"]);
|
||||
expect("[name='partner_ids'] .o_input_dropdown input").toHaveValue("");
|
||||
|
||||
await contains("[name='partner_ids'] .o_input_dropdown input").fill("abc");
|
||||
await runAllTimers();
|
||||
expect(queryAllTexts("[name='partner_ids'] .o_tag")).toEqual(["first record", "abc"]);
|
||||
expect("[name='partner_ids'] .o_input_dropdown input").toHaveValue("");
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar img src", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
});
|
||||
|
||||
expect(".o_field_many2many_tags_avatar.o_field_widget .o_avatar img").toHaveCount(2);
|
||||
expect(
|
||||
`.o_field_many2many_tags_avatar.o_field_widget .o_avatar:nth-child(1) img[data-src='${getOrigin()}/web/image/partner/2/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar in list view", async () => {
|
||||
for (let id = 5; id <= 15; id++) {
|
||||
Partner._records.push({
|
||||
id,
|
||||
name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
|
||||
Turtle._records.push({
|
||||
id: 4,
|
||||
name: "crime master gogo",
|
||||
partner_ids: [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
|
||||
});
|
||||
Turtle._records[0].partner_ids = [1];
|
||||
Turtle._records[1].partner_ids = [1, 2, 4, 5, 6, 7];
|
||||
Turtle._records[2].partner_ids = [1, 2, 4, 5, 7];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</list>`,
|
||||
});
|
||||
expect(
|
||||
`.o_data_row:nth-child(1) .o_field_many2many_tags_avatar .o_avatar img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/1/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_data_row .o_many2many_tags_avatar_cell .o_field_many2many_tags_avatar:eq(0)"
|
||||
).toHaveText("first record");
|
||||
expect(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img"
|
||||
).toHaveCount(4);
|
||||
expect(
|
||||
".o_data_row:nth-child(3) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img"
|
||||
).toHaveCount(5);
|
||||
expect(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveText("+2");
|
||||
expect(
|
||||
`.o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:nth-child(1) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/1/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
`.o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:nth-child(2) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/2/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
`.o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:nth-child(3) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/4/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
`.o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:nth-child(4) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/5/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_data_row:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveCount(0);
|
||||
expect(
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img"
|
||||
).toHaveCount(4);
|
||||
expect(
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveText("+9");
|
||||
|
||||
// check data-tooltip attribute (used by the tooltip service)
|
||||
const tag = queryOne(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
);
|
||||
expect(tag).toHaveAttribute("data-tooltip-template", "web.TagsList.Tooltip");
|
||||
const tooltipInfo = JSON.parse(tag.dataset["tooltipInfo"]);
|
||||
expect(tooltipInfo.tags.map((tag) => tag.text).join(" ")).toBe("record 6 record 7", {
|
||||
message: "shows a tooltip on hover",
|
||||
});
|
||||
|
||||
await contains(".o_data_row .o_many2many_tags_avatar_cell:eq(0)").click();
|
||||
await contains(
|
||||
".o_data_row .o_many2many_tags_avatar_cell:eq(0) .o-autocomplete--input"
|
||||
).click();
|
||||
await contains(".o-autocomplete--dropdown-item:eq(1)").click();
|
||||
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
|
||||
expect(".o_data_row:eq(0) .o_field_many2many_tags_avatar .o_avatar img").toHaveCount(2);
|
||||
|
||||
// Select the first row and enter edit mode on the x2many field.
|
||||
await contains(".o_data_row:nth-child(1) .o_list_record_selector input").click();
|
||||
await contains(".o_data_row:nth-child(1) .o_data_cell").click();
|
||||
|
||||
// Only the first row should have tags with delete buttons.
|
||||
expect(".o_data_row:nth-child(1) .o_field_tags span .o_delete").toHaveCount(2);
|
||||
expect(".o_data_row:nth-child(2) .o_field_tags span .o_delete").toHaveCount(0);
|
||||
expect(".o_data_row:nth-child(3) .o_field_tags span .o_delete").toHaveCount(0);
|
||||
expect(".o_data_row:nth-child(4) .o_field_tags span .o_delete").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar list view - don't crash on keyboard navigation", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "turtle",
|
||||
arch: /*xml*/ `
|
||||
<list editable="bottom">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
// Select the 2nd row and enter edit mode on the x2many field.
|
||||
await contains(".o_data_row:nth-child(2) .o_list_record_selector input").click();
|
||||
await contains(".o_data_row:nth-child(2) .o_data_cell").click();
|
||||
|
||||
// Pressing left arrow should focus on the right-most (second) tag.
|
||||
await press("arrowleft");
|
||||
expect(".o_data_row:nth-child(2) .o_field_tags span:nth-child(2):first").toBeFocused();
|
||||
|
||||
// Pressing left arrow again should not crash and should focus on the first tag.
|
||||
await press("arrowleft");
|
||||
expect(".o_data_row:nth-child(2) .o_field_tags span:nth-child(1):first").toBeFocused();
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar in kanban view", async () => {
|
||||
expect.assertions(21);
|
||||
|
||||
for (let id = 5; id <= 15; id++) {
|
||||
Partner._records.push({
|
||||
id,
|
||||
name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
|
||||
Turtle._records.push({
|
||||
id: 4,
|
||||
name: "crime master gogo",
|
||||
partner_ids: [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
|
||||
});
|
||||
Turtle._records[0].partner_ids = [1];
|
||||
Turtle._records[1].partner_ids = [1, 2, 4];
|
||||
Turtle._records[2].partner_ids = [1, 2, 4, 5];
|
||||
Turtle._views = {
|
||||
form: '<form><field name="name"/></form>',
|
||||
};
|
||||
Partner._views = {
|
||||
list: '<list><field name="name"/></list>',
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name"/>
|
||||
<footer>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
selectRecord(recordId) {
|
||||
expect(recordId).toBe(1, {
|
||||
message: "should call its selectRecord prop with the clicked record",
|
||||
});
|
||||
},
|
||||
});
|
||||
expect(".o_kanban_record:eq(0) .o_field_many2many_tags_avatar .o_quick_assign").toHaveCount(1);
|
||||
|
||||
expect(
|
||||
".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_avatar img"
|
||||
).toHaveCount(3);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_avatar img"
|
||||
).toHaveCount(2);
|
||||
expect(
|
||||
`.o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_avatar:nth-child(1) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/5/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
`.o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_avatar:nth-child(2) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/4/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveText("+2");
|
||||
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_avatar img"
|
||||
).toHaveCount(2);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveText("9+");
|
||||
expect(".o_field_many2many_tags_avatar .o_field_many2many_selection").toHaveCount(0);
|
||||
await contains(".o_kanban_record:nth-child(3) .o_field_tags > .o_m2m_avatar_empty").click();
|
||||
await animationFrame();
|
||||
expect(".o-overlay-container input").toBeFocused();
|
||||
expect(".o-overlay-container .o_tag").toHaveCount(4);
|
||||
// delete inside the popover
|
||||
await contains(".o-overlay-container .o_tag .o_delete:eq(0)", {
|
||||
visible: false,
|
||||
displayed: true,
|
||||
}).click();
|
||||
expect(".o-overlay-container .o_tag").toHaveCount(3);
|
||||
expect(".o_kanban_record:nth-child(3) .o_tag").toHaveCount(3);
|
||||
// select first non selected input
|
||||
await contains(".o-overlay-container .o-autocomplete--dropdown-item:eq(4)").click();
|
||||
expect(".o-overlay-container .o_tag").toHaveCount(4);
|
||||
expect(".o_kanban_record:nth-child(3) .o_tag").toHaveCount(2);
|
||||
// load more
|
||||
await contains(".o-overlay-container .o_m2o_dropdown_option_search_more").click();
|
||||
// first non already selected item
|
||||
await contains(".o_dialog .o_list_table .o_data_row .o_data_cell:eq(3)").click();
|
||||
expect(".o-overlay-container .o_tag").toHaveCount(5);
|
||||
expect(".o_kanban_record:nth-child(3) .o_tag").toHaveCount(2);
|
||||
expect(
|
||||
`.o_kanban_record:nth-child(2) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/4/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
await contains(".o_kanban_record .o_field_many2many_tags_avatar img.o_m2m_avatar").click();
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar add/remove tags in kanban view", async () => {
|
||||
onRpc("web_save", ({ args }) => {
|
||||
const command = args[1].partner_ids[0];
|
||||
expect.step(`web_save: ${command[0]}-${command[1]}`);
|
||||
});
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name"/>
|
||||
<footer>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
await contains(".o_kanban_record:eq(0) .o_quick_assign", { visible: false }).click();
|
||||
// add and directly remove an item
|
||||
await contains(".o_popover .o-autocomplete--dropdown-item:eq(0)").click();
|
||||
await contains(".o_popover .o_tag .o_delete", { visible: false }).click();
|
||||
expect.verifySteps(["web_save: 4-1", "web_save: 3-1"]);
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar delete tag", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
resId: 2,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_many2many_tags_avatar.o_field_widget .o_tag").toHaveCount(2);
|
||||
|
||||
await contains(".o_field_many2many_tags_avatar.o_field_widget .o_tag .o_delete", {
|
||||
visible: false,
|
||||
}).click();
|
||||
expect(".o_field_many2many_tags_avatar.o_field_widget .o_tag").toHaveCount(1);
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_many2many_tags_avatar.o_field_widget .o_tag").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar quick add tags and close in kanban view with keyboard navigation", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name"/>
|
||||
<footer>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
await contains(".o_kanban_record:eq(0) .o_quick_assign", { visible: false }).click();
|
||||
// add and directly close the dropdown
|
||||
await press("Tab");
|
||||
await press("Enter");
|
||||
await animationFrame();
|
||||
expect(".o_kanban_record:eq(0) .o_field_many2many_tags_avatar .o_tag").toHaveCount(1);
|
||||
expect(".o_kanban_record:eq(0) .o_field_many2many_tags_avatar .o_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar in kanban view missing access rights", async () => {
|
||||
expect.assertions(1);
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<kanban edit="0" create="0">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name"/>
|
||||
<footer>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
expect(".o_kanban_record:eq(0) .o_field_many2many_tags_avatar .o_quick_assign").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Many2ManyTagsAvatarField: make sure that the arch context is passed to the form view call", async () => {
|
||||
Partner._views = {
|
||||
form: `<form><field name="name"/></form>`,
|
||||
};
|
||||
onRpc("onchange", (args) => {
|
||||
if (args.model === "partner" && args.kwargs.context.append_coucou === "test_value") {
|
||||
expect.step("onchange with context given");
|
||||
}
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "turtle",
|
||||
arch: `<list editable="top">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar" context="{ 'append_coucou': 'test_value' }"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains("div[name=partner_ids]").click();
|
||||
await contains(`div[name="partner_ids"] input`).edit("A new partner", { confirm: false });
|
||||
await runAllTimers();
|
||||
await contains(".o_m2o_dropdown_option_create_edit").click();
|
||||
|
||||
expect(".modal .o_form_view").toHaveCount(1);
|
||||
expect.verifySteps(["onchange with context given"]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,410 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { runAllTimers } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
clickFieldDropdown,
|
||||
clickFieldDropdownItem,
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer();
|
||||
user_id = fields.Many2one({ string: "Users", relation: "res.users" });
|
||||
_records = [
|
||||
{ id: 1, user_id: 1 },
|
||||
{ id: 2, user_id: 2 },
|
||||
{ id: 3, user_id: 1 },
|
||||
{ id: 4, user_id: false },
|
||||
];
|
||||
}
|
||||
|
||||
class Users extends models.Model {
|
||||
_name = "res.users";
|
||||
name = fields.Char();
|
||||
partner_ids = fields.One2many({ relation: "partner", relation_field: "user_id" });
|
||||
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Aline",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Christine",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Users]);
|
||||
|
||||
test.tags("desktop");
|
||||
test("basic form view flow", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=user_id] input").toHaveValue("Aline");
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/1/avatar_128"]').toHaveCount(1);
|
||||
expect(".o_field_many2one_avatar > div").toHaveCount(1);
|
||||
|
||||
expect(".o_input_dropdown").toHaveCount(1);
|
||||
expect(".o_input_dropdown input").toHaveValue("Aline");
|
||||
expect(".o_external_button").toHaveCount(1);
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/1/avatar_128"]').toHaveCount(1);
|
||||
|
||||
await clickFieldDropdown("user_id");
|
||||
expect(".o_field_many2one_selection .o_avatar_many2x_autocomplete").toHaveCount(3);
|
||||
await clickFieldDropdownItem("user_id", "Christine");
|
||||
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/2/avatar_128"]').toHaveCount(1);
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget[name=user_id] input").toHaveValue("Christine");
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/2/avatar_128"]').toHaveCount(1);
|
||||
|
||||
await contains('.o_field_widget[name="user_id"] input').clear({ confirm: "blur" });
|
||||
|
||||
expect(".o_m2o_avatar > img").toHaveCount(0);
|
||||
expect(".o_m2o_avatar > .o_m2o_avatar_empty").toHaveCount(1);
|
||||
await clickSave();
|
||||
|
||||
expect(".o_m2o_avatar > img").toHaveCount(0);
|
||||
expect(".o_m2o_avatar > .o_m2o_avatar_empty").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("onchange in form view flow", async () => {
|
||||
Partner._fields.int_field = fields.Integer({
|
||||
onChange: (obj) => {
|
||||
if (obj.int_field === 1) {
|
||||
obj.user_id = [2, "Christine"];
|
||||
} else if (obj.int_field === 2) {
|
||||
obj.user_id = false;
|
||||
} else {
|
||||
obj.user_id = [1, "Aline"]; // default value
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field"/>
|
||||
<field name="user_id" widget="many2one_avatar" readonly="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=user_id]").toHaveText("Aline");
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/1/avatar_128"]').toHaveCount(1);
|
||||
|
||||
await contains("div[name=int_field] input").edit(1);
|
||||
|
||||
expect(".o_field_widget[name=user_id]").toHaveText("Christine");
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/2/avatar_128"]').toHaveCount(1);
|
||||
|
||||
await contains("div[name=int_field] input").edit(2);
|
||||
|
||||
expect(".o_field_widget[name=user_id]").toHaveText("");
|
||||
expect(".o_m2o_avatar > img").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("basic list view flow", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list><field name="user_id" widget="many2one_avatar"/></list>',
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell[name='user_id']")).toEqual([
|
||||
"Aline",
|
||||
"Christine",
|
||||
"Aline",
|
||||
"",
|
||||
]);
|
||||
const imgs = queryAll(".o_m2o_avatar > img");
|
||||
expect(imgs[0]).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
expect(imgs[1]).toHaveAttribute("data-src", "/web/image/res.users/2/avatar_128");
|
||||
expect(imgs[2]).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
});
|
||||
|
||||
test("basic flow in editable list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list editable="top"><field name="user_id" widget="many2one_avatar"/></list>',
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell[name='user_id']")).toEqual([
|
||||
"Aline",
|
||||
"Christine",
|
||||
"Aline",
|
||||
"",
|
||||
]);
|
||||
|
||||
const imgs = queryAll(".o_m2o_avatar > img");
|
||||
expect(imgs[0]).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
expect(imgs[1]).toHaveAttribute("data-src", "/web/image/res.users/2/avatar_128");
|
||||
expect(imgs[2]).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
|
||||
await contains(".o_data_row .o_data_cell:eq(0)").click();
|
||||
|
||||
expect(".o_m2o_avatar > img:eq(0)").toHaveAttribute(
|
||||
"data-src",
|
||||
"/web/image/res.users/1/avatar_128"
|
||||
);
|
||||
});
|
||||
|
||||
test("Many2OneAvatar with placeholder", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="user_id" widget="many2one_avatar" placeholder="Placeholder"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='user_id'] input").toHaveAttribute("placeholder", "Placeholder");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("click on many2one_avatar in a list view (multi_edit='1')", async () => {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
expect.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list multi_edit="1">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_data_row:eq(0) .o_list_record_selector input").click();
|
||||
await contains(".o_data_row .o_data_cell [name='user_id']").click();
|
||||
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
|
||||
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("click on many2one_avatar in an editable list view", async () => {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
expect.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_data_row .o_data_cell [name='user_id']").click();
|
||||
expect(".o_selected_row").toHaveCount(0);
|
||||
|
||||
expect.verifySteps(["openRecord"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("click on many2one_avatar in an editable list view (editable top)", async () => {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
expect.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_data_row:eq(0) .o_list_record_selector input").click();
|
||||
await contains(".o_data_row .o_data_cell [name='user_id']").click();
|
||||
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
|
||||
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("readonly many2one_avatar in form view should contain a link", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="user_id" widget="many2one_avatar" readonly="1"/></form>`,
|
||||
});
|
||||
|
||||
expect("[name='user_id'] a").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("readonly many2one_avatar in list view should not contain a link", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `<list><field name="user_id" widget="many2one_avatar"/></list>`,
|
||||
});
|
||||
|
||||
expect("[name='user_id'] a").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("cancelling create dialog should clear value in the field", async () => {
|
||||
Users._views = {
|
||||
form: `
|
||||
<form>
|
||||
<field name="name" />
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_data_cell:eq(0)").click();
|
||||
await contains(".o_field_widget[name=user_id] input").edit("yy", { confirm: false });
|
||||
await runAllTimers();
|
||||
await clickFieldDropdownItem("user_id", "Create and edit...");
|
||||
|
||||
await contains(".o_form_button_cancel").click();
|
||||
expect(".o_field_widget[name=user_id] input").toHaveValue("");
|
||||
expect(".o_field_widget[name=user_id] span.o_m2o_avatar_empty").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("widget many2one_avatar in kanban view (load more dialog)", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
for (let id = 3; id <= 12; id++) {
|
||||
Users._records.push({
|
||||
id,
|
||||
display_name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
|
||||
Users._views = {
|
||||
list: '<list><field name="display_name"/></list>',
|
||||
};
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<footer>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
// open popover
|
||||
await contains(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > a.o_quick_assign"
|
||||
).click();
|
||||
|
||||
// load more
|
||||
await contains(".o-overlay-container .o_m2o_dropdown_option_search_more").click();
|
||||
await contains(".o_dialog .o_list_table .o_data_row .o_data_cell").click();
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
});
|
||||
|
||||
test("widget many2one_avatar in kanban view", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<footer>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
expect(
|
||||
".o_kanban_record:nth-child(1) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
|
||||
).toHaveCount(1);
|
||||
// open popover
|
||||
await contains(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
|
||||
).click();
|
||||
expect(".o-overlay-container input").toBeFocused();
|
||||
// select first input
|
||||
await contains(".o-overlay-container .o-autocomplete--dropdown-item").click();
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("widget many2one_avatar in kanban view without access rights", async () => {
|
||||
expect.assertions(2);
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban edit="0" create="0">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<footer>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
expect(
|
||||
".o_kanban_record:nth-child(1) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { mockUserAgent, mockVibrate, runAllTimers } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
getKwArgs,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import * as BarcodeScanner from "@web/core/barcode/barcode_dialog";
|
||||
|
||||
class Product extends models.Model {
|
||||
_name = "product.product";
|
||||
name = fields.Char();
|
||||
barcode = fields.Char();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 111,
|
||||
name: "product_cable_management_box",
|
||||
barcode: "601647855631",
|
||||
},
|
||||
{
|
||||
id: 112,
|
||||
name: "product_n95_mask",
|
||||
barcode: "601647855632",
|
||||
},
|
||||
{
|
||||
id: 113,
|
||||
name: "product_surgical_mask",
|
||||
barcode: "601647855633",
|
||||
},
|
||||
];
|
||||
// to allow the search in barcode too
|
||||
name_search() {
|
||||
const result = super.name_search(...arguments);
|
||||
const kwargs = getKwArgs(arguments, "name", "domain");
|
||||
for (const record of this) {
|
||||
if (record.barcode === kwargs.name) {
|
||||
result.push([record.id, record.name]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class SaleOrderLine extends models.Model {
|
||||
id = fields.Integer();
|
||||
product_id = fields.Many2one({
|
||||
relation: "product.product",
|
||||
});
|
||||
}
|
||||
|
||||
defineModels([Product, SaleOrderLine]);
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserAgent("android");
|
||||
mockVibrate((pattern) => expect.step(`vibrate:${pattern}`));
|
||||
});
|
||||
|
||||
test("Many2OneBarcode component should display the barcode icon", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order.line",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id" widget="many2one_barcode"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(".o_barcode").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("barcode button with single results", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = Product._records[0];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => selectedRecordTest.barcode,
|
||||
});
|
||||
|
||||
onRpc("sale.order.line", "web_save", (args) => {
|
||||
const selectedId = args.args[1]["product_id"];
|
||||
expect(selectedId).toBe(selectedRecordTest.id, {
|
||||
message: `product id selected ${selectedId}, should be ${selectedRecordTest.id} (${selectedRecordTest.barcode})`,
|
||||
});
|
||||
return args.parent();
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order.line",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id" options="{'can_scan_barcode': True}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_barcode").toHaveCount(1);
|
||||
|
||||
await contains(".o_barcode").click();
|
||||
await clickSave();
|
||||
|
||||
expect.verifySteps(["vibrate:100"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("barcode button with multiple results", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = Product._records[1];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => "mask",
|
||||
});
|
||||
|
||||
onRpc("sale.order.line", "web_save", (args) => {
|
||||
const selectedId = args.args[1]["product_id"];
|
||||
expect(selectedId).toBe(selectedRecordTest.id, {
|
||||
message: `product id selected ${selectedId}, should be ${selectedRecordTest.id} (${selectedRecordTest.barcode})`,
|
||||
});
|
||||
return args.parent();
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order.line",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id" options="{'can_scan_barcode': True}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_barcode").toHaveCount(1);
|
||||
|
||||
await contains(".o_barcode").click();
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete--dropdown-menu").toHaveCount(1);
|
||||
|
||||
expect(
|
||||
".o-autocomplete--dropdown-menu .o-autocomplete--dropdown-item.ui-menu-item:not(.o_m2o_dropdown_option)"
|
||||
).toHaveCount(2);
|
||||
|
||||
await contains(
|
||||
".o-autocomplete--dropdown-menu .o-autocomplete--dropdown-item:nth-child(1)"
|
||||
).click();
|
||||
await clickSave();
|
||||
expect.verifySteps(["vibrate:100"]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,232 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { runAllTimers } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
clickFieldDropdownItem,
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
selectFieldDropdownItem,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
model = fields.Char({
|
||||
string: "Resource Model",
|
||||
});
|
||||
res_id = fields.Many2oneReference({
|
||||
string: "Resource Id",
|
||||
model_field: "model",
|
||||
relation: "partner.type",
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 1, model: "partner.type", res_id: 10 },
|
||||
{ id: 2, res_id: false },
|
||||
];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
id = fields.Integer();
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 10, name: "gold" },
|
||||
{ id: 14, name: "silver" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, PartnerType]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("Many2OneReferenceField in form view", async () => {
|
||||
mockService("action", {
|
||||
doAction() {
|
||||
expect.step("doAction");
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("get_formview_action", ({ model, args }) => {
|
||||
expect.step(`opening ${model} ${args[0][0]}`);
|
||||
return false;
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("gold");
|
||||
expect(".o_field_widget[name=res_id] .o_external_button").toHaveCount(1);
|
||||
|
||||
await contains(".o_field_widget[name=res_id] .o_external_button", { visible: false }).click();
|
||||
expect.verifySteps(["opening partner.type 10", "doAction"]);
|
||||
});
|
||||
|
||||
test("Many2OneReferenceField in list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<list>
|
||||
<field name="model" column_invisible="1"/>
|
||||
<field name="res_id"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual(["gold", ""]);
|
||||
});
|
||||
|
||||
test("Many2OneReferenceField with no_open option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id" options="{'no_open': 1}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("gold");
|
||||
expect(".o_field_widget[name=res_id] .o_external_button").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Many2OneReferenceField edition: unset", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args).toEqual([[2], { model: "partner.type", res_id: 14 }]);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model"/>
|
||||
<field name="res_id"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=res_id] input").toHaveCount(0);
|
||||
|
||||
await contains(".o_field_widget[name=model] input").edit("partner.type");
|
||||
|
||||
expect(".o_field_widget[name=res_id] input").toHaveCount(1);
|
||||
|
||||
await selectFieldDropdownItem("res_id", "silver");
|
||||
expect(".o_field_widget[name=res_id] input").toHaveValue("silver");
|
||||
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Many2OneReferenceField set value with search more", async () => {
|
||||
PartnerType._views = {
|
||||
list: `<list><field name="name"/></list>`,
|
||||
};
|
||||
PartnerType._records = [
|
||||
{ id: 1, name: "type 1" },
|
||||
{ id: 2, name: "type 2" },
|
||||
{ id: 3, name: "type 3" },
|
||||
{ id: 4, name: "type 4" },
|
||||
{ id: 5, name: "type 5" },
|
||||
{ id: 6, name: "type 6" },
|
||||
{ id: 7, name: "type 7" },
|
||||
{ id: 8, name: "type 8" },
|
||||
{ id: 9, name: "type 9" },
|
||||
];
|
||||
Partner._records[0].res_id = 1;
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("type 1");
|
||||
await selectFieldDropdownItem("res_id", "Search More...");
|
||||
expect(".o_dialog .o_list_view").toHaveCount(1);
|
||||
await contains(".o_data_row .o_data_cell:eq(6)").click();
|
||||
expect(".o_dialog .o_list_view").toHaveCount(0);
|
||||
expect(".o_field_widget input").toHaveValue("type 7");
|
||||
expect.verifySteps([
|
||||
"get_views", // form view
|
||||
"web_read", // partner id 1
|
||||
"name_search", // many2one
|
||||
"get_views", // Search More...
|
||||
"web_search_read", // SelectCreateDialog
|
||||
"has_group",
|
||||
"web_read", // read selected value
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Many2OneReferenceField: quick create a value", async () => {
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("gold");
|
||||
|
||||
await contains(".o_field_widget[name='res_id'] input").edit("new value", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(
|
||||
".o_field_widget[name='res_id'] .dropdown-menu .o_m2o_dropdown_option_create"
|
||||
).toHaveCount(1);
|
||||
await clickFieldDropdownItem("res_id", `Create "new value"`);
|
||||
expect(".o_field_widget input").toHaveValue("new value");
|
||||
expect.verifySteps(["get_views", "web_read", "name_search", "name_create"]);
|
||||
});
|
||||
|
||||
test("Many2OneReferenceField with no_create option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id" options="{'no_create': 1}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name='res_id'] input").edit("new value", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(
|
||||
".o_field_widget[name='res_id'] .dropdown-menu .o_m2o_dropdown_option_create"
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
model = fields.Char({
|
||||
string: "Resource Model",
|
||||
});
|
||||
res_id = fields.Many2oneReference({
|
||||
string: "Resource Id",
|
||||
model_field: "model",
|
||||
relation: "partner.type",
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 1, model: "partner.type", res_id: 10 },
|
||||
{ id: 2, res_id: false },
|
||||
];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 10, name: "gold" },
|
||||
{ id: 14, name: "silver" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, PartnerType]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("Many2OneReferenceIntegerField in form view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="res_id" widget="many2one_reference_integer"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("10");
|
||||
});
|
||||
|
||||
test("Many2OneReferenceIntegerField in list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<list><field name="res_id" widget="many2one_reference_integer"/></list>',
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual(["10", ""]);
|
||||
});
|
||||
|
||||
test("Many2OneReferenceIntegerField: unset value in form view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: '<form><field name="res_id" widget="many2one_reference_integer"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("");
|
||||
});
|
||||
|
|
@ -0,0 +1,798 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryAllTexts, queryFirst } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
int_field = fields.Integer();
|
||||
float_field = fields.Float({
|
||||
digits: [16, 1],
|
||||
});
|
||||
p = fields.One2many({ relation: "partner" });
|
||||
currency_id = fields.Many2one({ relation: "res.currency" });
|
||||
monetary_field = fields.Monetary({ currency_field: "currency_id" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, int_field: 10, float_field: 0.44444 },
|
||||
{ id: 2, int_field: 0, float_field: 0, currency_id: 2 },
|
||||
{ id: 3, int_field: 80, float_field: -3.89859 },
|
||||
{ id: 4, int_field: 0, float_field: 0 },
|
||||
{ id: 5, int_field: -4, float_field: 9.1, monetary_field: 9.1, currency_id: 1 },
|
||||
{ id: 6, float_field: 3.9, monetary_field: 4.2, currency_id: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
class Currency extends models.Model {
|
||||
_name = "res.currency";
|
||||
|
||||
name = fields.Char();
|
||||
symbol = fields.Char({ string: "Currency Sumbol" });
|
||||
position = fields.Selection({
|
||||
selection: [
|
||||
["after", "A"],
|
||||
["before", "B"],
|
||||
],
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "USD", symbol: "$", position: "before" },
|
||||
{ id: 2, name: "EUR", symbol: "€", position: "after" },
|
||||
{
|
||||
id: 3,
|
||||
name: "VEF",
|
||||
symbol: "Bs.F",
|
||||
position: "after",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Currency]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("basic flow in form view - float field", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_monetary > div.text-nowrap").toHaveCount(1);
|
||||
expect(".o_field_widget input").toHaveValue("9.10", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_monetary input").edit("108.2458938598598");
|
||||
expect(".o_field_widget input").toHaveValue("108.25", {
|
||||
message: "The new value should be rounded properly after the blur",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("108.25", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("basic flow in form view - monetary field", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("9.10", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_monetary input").edit("108.2458938598598");
|
||||
expect(".o_field_widget input").toHaveValue("108.25", {
|
||||
message: "The new value should be rounded properly after the blur",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("108.25", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("rounding using formula in form view - float field", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// Test computation and rounding
|
||||
await contains(".o_field_monetary input").edit("=100/3");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("33.33", {
|
||||
message: "The new value should be calculated and rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("rounding using formula in form view - monetary field", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// Test computation and rounding
|
||||
await contains(".o_field_monetary input").edit("=100/3");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("33.33", {
|
||||
message: "The new value should be calculated and rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("with currency digits != 2 - float field", async () => {
|
||||
serverState.currencies = [
|
||||
{ id: 1, name: "USD", symbol: "$", position: "before" },
|
||||
{ id: 2, name: "EUR", symbol: "€", position: "after" },
|
||||
{
|
||||
id: 3,
|
||||
name: "VEF",
|
||||
symbol: "Bs.F",
|
||||
position: "after",
|
||||
digits: [0, 4],
|
||||
},
|
||||
];
|
||||
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 99.1234,
|
||||
currency_id: 3,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("99.1234", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
expect(".o_field_widget .o_input span:eq(1)").toHaveText("Bs.F", {
|
||||
message: "The input should be superposed with a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget input").edit("99.111111111");
|
||||
expect(".o_field_widget input").toHaveValue("99.1111", {
|
||||
message: "The value should should be formatted on blur.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("99.1111", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("with currency digits != 2 - monetary field", async () => {
|
||||
serverState.currencies = [
|
||||
{ id: 1, name: "USD", symbol: "$", position: "before" },
|
||||
{ id: 2, name: "EUR", symbol: "€", position: "after" },
|
||||
{
|
||||
id: 3,
|
||||
name: "VEF",
|
||||
symbol: "Bs.F",
|
||||
position: "after",
|
||||
digits: [0, 4],
|
||||
},
|
||||
];
|
||||
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 99.1234,
|
||||
monetary_field: 99.1234,
|
||||
currency_id: 3,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("99.1234", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
expect(".o_field_widget .o_input span:eq(1)").toHaveText("Bs.F", {
|
||||
message: "The input should be superposed with a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget input").edit("99.111111111");
|
||||
expect(".o_field_widget input").toHaveValue("99.1111", {
|
||||
message: "The value should should be formatted on blur.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("99.1111", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("basic flow in editable list view - float field", async () => {
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 9.1,
|
||||
monetary_field: 9.1,
|
||||
currency_id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
float_field: 15.3,
|
||||
monetary_field: 15.3,
|
||||
currency_id: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
float_field: 0,
|
||||
monetary_field: 0,
|
||||
currency_id: 1,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
float_field: 5.0,
|
||||
monetary_field: 5.0,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
const dollarValues = queryAll("td:contains($)");
|
||||
expect(dollarValues).toHaveLength(2, { message: "Only 2 line has dollar as a currency." });
|
||||
|
||||
const euroValues = queryAll("td:contains(€)");
|
||||
expect(euroValues).toHaveLength(1, { message: "Only 1 line has euro as a currency." });
|
||||
|
||||
const noCurrencyValues = queryAll("td.o_data_cell").filter(
|
||||
(x) => !(x.textContent.includes("€") || x.textContent.includes("$"))
|
||||
);
|
||||
expect(noCurrencyValues).toHaveLength(1, { message: "Only 1 line has no currency." });
|
||||
|
||||
// switch to edit mode
|
||||
const dollarCell = queryFirst("td.o_field_cell");
|
||||
await contains(dollarCell).click();
|
||||
|
||||
expect(dollarCell.children).toHaveLength(1, {
|
||||
message: "The cell td should only contain the special div of monetary widget.",
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveCount(1, {
|
||||
message: "The view should have 1 input for editable monetary float.",
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("9.10", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget input").edit("108.2458938598598", { confirm: "blur" });
|
||||
expect(dollarCell).toHaveText("$ 108.25", { message: "The new value should be correct" });
|
||||
});
|
||||
|
||||
test("basic flow in editable list view - monetary field", async () => {
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 9.1,
|
||||
monetary_field: 9.1,
|
||||
currency_id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
float_field: 15.3,
|
||||
monetary_field: 15.3,
|
||||
currency_id: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
float_field: 0,
|
||||
monetary_field: 0,
|
||||
currency_id: 1,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
float_field: 5.0,
|
||||
monetary_field: 5.0,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
const dollarValues = queryAll("td:contains($)");
|
||||
expect(dollarValues).toHaveLength(2, { message: "Only 2 line has dollar as a currency." });
|
||||
|
||||
const euroValues = queryAll("td:contains(€)");
|
||||
expect(euroValues).toHaveLength(1, { message: "Only 1 line has euro as a currency." });
|
||||
|
||||
const noCurrencyValues = queryAll("td.o_data_cell").filter(
|
||||
(x) => !(x.textContent.includes("€") || x.textContent.includes("$"))
|
||||
);
|
||||
expect(noCurrencyValues).toHaveLength(1, { message: "Only 1 line has no currency." });
|
||||
|
||||
// switch to edit mode
|
||||
const dollarCell = queryFirst("td.o_field_cell");
|
||||
await contains(dollarCell).click();
|
||||
|
||||
expect(dollarCell.children).toHaveLength(1, {
|
||||
message: "The cell td should only contain the special div of monetary widget.",
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveCount(1, {
|
||||
message: "The view should have 1 input for editable monetary float.",
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("9.10", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget input").edit("108.2458938598598", { confirm: "blur" });
|
||||
expect(dollarCell).toHaveText("$ 108.25", { message: "The new value should be correct" });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("changing currency updates the field - float field", async () => {
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 4.2,
|
||||
monetary_field: 4.2,
|
||||
currency_id: 1,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_many2one_selection input").click();
|
||||
await contains(".o-autocomplete--dropdown-item:contains(EUR)").click();
|
||||
expect(".o_field_widget .o_input span:eq(1)").toHaveText("€", {
|
||||
message:
|
||||
"The input should be preceded by a span containing the currency symbol added on blur.",
|
||||
});
|
||||
expect(".o_field_monetary input").toHaveValue("4.20");
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_monetary input").toHaveValue("4.20", {
|
||||
message: "The new value should still be correct.",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("changing currency updates the field - monetary field", async () => {
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 4.2,
|
||||
monetary_field: 4.2,
|
||||
currency_id: 1,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_many2one_selection input").click();
|
||||
await contains(".o-autocomplete--dropdown-item:contains(EUR)").click();
|
||||
|
||||
expect(".o_field_widget .o_input span:eq(1)").toHaveText("€", {
|
||||
message:
|
||||
"The input should be preceded by a span containing the currency symbol added on blur.",
|
||||
});
|
||||
expect(".o_field_monetary input").toHaveValue("4.20");
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_monetary input").toHaveValue("4.20", {
|
||||
message: "The new value should still be correct.",
|
||||
});
|
||||
});
|
||||
|
||||
test("MonetaryField with monetary field given in options", async () => {
|
||||
Partner._fields.company_currency_id = fields.Many2one({
|
||||
string: "Company Currency",
|
||||
relation: "res.currency",
|
||||
});
|
||||
Partner._records[4].company_currency_id = 2;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form edit="0">
|
||||
<sheet>
|
||||
<field name="monetary_field" options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="company_currency_id"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 5,
|
||||
});
|
||||
|
||||
expect(".o_field_monetary").toHaveText("9.10 €", {
|
||||
message: "field monetary should be formatted with correct currency",
|
||||
});
|
||||
});
|
||||
|
||||
test("should keep the focus when being edited in x2many lists", async () => {
|
||||
Partner._fields.currency_id.default = 1;
|
||||
Partner._fields.m2m = fields.Many2many({
|
||||
relation: "partner",
|
||||
default: [[4, 2]],
|
||||
});
|
||||
Partner._views = {
|
||||
list: `
|
||||
<list editable="bottom">
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</list>`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="p"/>
|
||||
<field name="m2m"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// test the monetary field inside the one2many
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
await contains(".o_field_widget[name=float_field] input").edit("22", { confirm: "blur" });
|
||||
|
||||
expect(".o_field_widget[name=p] .o_field_widget[name=float_field] span").toHaveInnerHTML(
|
||||
"$ 22.00",
|
||||
{ type: "html" }
|
||||
);
|
||||
|
||||
// test the monetary field inside the many2many
|
||||
await contains(".o_field_widget[name=m2m] .o_data_cell").click();
|
||||
await contains(".o_field_widget[name=float_field] input").edit("22", { confirm: "blur" });
|
||||
expect(".o_field_widget[name=m2m] .o_field_widget[name=float_field] span").toHaveInnerHTML(
|
||||
"22.00 €",
|
||||
{ type: "html" }
|
||||
);
|
||||
});
|
||||
|
||||
test("MonetaryField with currency set by an onchange", async () => {
|
||||
// this test ensures that the monetary field can be re-rendered with and
|
||||
// without currency (which can happen as the currency can be set by an
|
||||
// onchange)
|
||||
Partner._onChanges = {
|
||||
int_field: function (obj) {
|
||||
obj.currency_id = obj.int_field ? 2 : null;
|
||||
},
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="int_field"/>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_control_panel_main_buttons .o_list_button_add").click();
|
||||
expect(".o_selected_row .o_field_widget[name=float_field] input").toHaveCount(1, {
|
||||
message: "monetary field should have been rendered correctly (without currency)",
|
||||
});
|
||||
expect(".o_selected_row .o_field_widget[name=float_field] span").toHaveCount(2, {
|
||||
message: "monetary field should have been rendered correctly (without currency)",
|
||||
});
|
||||
|
||||
// set a value for int_field -> should set the currency and re-render float_field
|
||||
await contains(".o_field_widget[name=int_field] input").edit("7", { confirm: "blur" });
|
||||
await contains(".o_field_cell[name=int_field]").click();
|
||||
expect(".o_selected_row .o_field_widget[name=float_field] input").toHaveCount(1, {
|
||||
message: "monetary field should have been re-rendered correctly (with currency)",
|
||||
});
|
||||
expect(
|
||||
queryAllTexts(".o_selected_row .o_field_widget[name=float_field] .o_input span")
|
||||
).toEqual(["0.00", "€"], {
|
||||
message: "monetary field should have been re-rendered correctly (with currency)",
|
||||
});
|
||||
await contains(".o_field_widget[name=float_field] input").click();
|
||||
expect(".o_field_widget[name=float_field] input").toBeFocused({
|
||||
message: "focus should be on the float_field field's input",
|
||||
});
|
||||
|
||||
// unset the value of int_field -> should unset the currency and re-render float_field
|
||||
await contains(".o_field_widget[name=int_field]").click();
|
||||
await contains(".o_field_widget[name=int_field] input").edit("0", { confirm: "blur" });
|
||||
await contains(".o_field_cell[name=int_field]").click();
|
||||
expect(".o_selected_row .o_field_widget[name=float_field] input").toHaveCount(1, {
|
||||
message: "monetary field should have been re-rendered correctly (without currency)",
|
||||
});
|
||||
expect(".o_selected_row .o_field_widget[name=float_field] span").toHaveCount(2, {
|
||||
message: "monetary field should have been re-rendered correctly (without currency)",
|
||||
});
|
||||
await contains(".o_field_widget[name=float_field] input").click();
|
||||
expect(".o_field_widget[name=float_field] input").toBeFocused({
|
||||
message: "focus should be on the float_field field's input",
|
||||
});
|
||||
});
|
||||
|
||||
test("float widget on monetary field", async () => {
|
||||
Partner._fields.monetary = fields.Monetary({ currency_field: "currency_id" });
|
||||
Partner._records[0].monetary = 9.99;
|
||||
Partner._records[0].currency_id = 1;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form edit="0">
|
||||
<sheet>
|
||||
<field name="monetary" widget="float"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=monetary]").toHaveText("9.99", {
|
||||
message: "value should be correctly formatted (with the float formatter)",
|
||||
});
|
||||
});
|
||||
|
||||
test("float field with monetary widget and decimal precision", async () => {
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: -8.89859,
|
||||
currency_id: 1,
|
||||
},
|
||||
];
|
||||
serverState.currencies = [
|
||||
{ id: 1, name: "USD", symbol: "$", position: "before", digits: [0, 4] },
|
||||
{ id: 2, name: "EUR", symbol: "€", position: "after" },
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="float_field" widget="monetary" options="{'field_digits': True}"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("-8.9", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_monetary input").edit("109.2458938598598");
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("109.2", {
|
||||
message: "The value should should be formatted on blur.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("109.2", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("MonetaryField without currency symbol", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="float_field" widget="monetary" options="{'no_symbol': True}" />
|
||||
<field name="currency_id" invisible="1" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// Non-breaking space between the currency and the amount
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("9.10", {
|
||||
message: "The currency symbol is not displayed",
|
||||
});
|
||||
});
|
||||
|
||||
test("monetary field with placeholder", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary" placeholder="Placeholder"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o_field_widget[name='float_field'] input").clear();
|
||||
expect(".o_field_widget[name='float_field'] input").toHaveAttribute(
|
||||
"placeholder",
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
test("required monetary field with zero value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field" required="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_form_editable").toHaveCount(1);
|
||||
expect("[name=monetary_field] input").toHaveValue("0.00");
|
||||
});
|
||||
|
||||
test("uses 'currency_id' as currency field by default", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
resId: 6,
|
||||
});
|
||||
|
||||
expect(".o_form_editable").toHaveCount(1);
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
});
|
||||
|
||||
test("automatically uses currency_field if defined", async () => {
|
||||
Partner._fields.custom_currency_id = fields.Many2one({
|
||||
string: "Currency",
|
||||
relation: "res.currency",
|
||||
});
|
||||
Partner._fields.monetary_field.currency_field = "custom_currency_id";
|
||||
Partner._records[5].custom_currency_id = 1;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="custom_currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
resId: 6,
|
||||
});
|
||||
|
||||
expect(".o_form_editable").toHaveCount(1);
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
});
|
||||
|
||||
test("monetary field with pending onchange", async () => {
|
||||
const def = new Deferred();
|
||||
Partner._onChanges = {
|
||||
async name(record) {
|
||||
record.float_field = 132;
|
||||
},
|
||||
};
|
||||
onRpc("onchange", async () => {
|
||||
await def;
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="name"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name='name'] input").edit("test", { confirm: "blur" });
|
||||
await contains(".o_field_widget[name='float_field'] input").edit("1", { confirm: false });
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_field_monetary .o_monetary_ghost_value").toHaveText("1");
|
||||
});
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { click, keyDown, pointerDown, queryAll, queryFirst } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineModels,
|
||||
defineParams,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
mountWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { useNumpadDecimal } from "@web/views/fields/numpad_decimal_hook";
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer();
|
||||
qux = fields.Float({ digits: [16, 1] });
|
||||
currency_id = fields.Many2one({ relation: "currency" });
|
||||
float_factor_field = fields.Float();
|
||||
percentage = fields.Float();
|
||||
monetary = fields.Monetary({ currency_field: "" });
|
||||
progressbar = fields.Integer();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
int_field: 10,
|
||||
qux: 0.44444,
|
||||
float_factor_field: 9.99,
|
||||
percentage: 0.99,
|
||||
monetary: 9.99,
|
||||
currency_id: 1,
|
||||
progressbar: 69,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class Currency extends models.Model {
|
||||
digits = fields.Float();
|
||||
symbol = fields.Char();
|
||||
position = fields.Char();
|
||||
|
||||
_records = [{ id: 1, display_name: "$", symbol: "$", position: "before" }];
|
||||
}
|
||||
|
||||
defineModels([Partner, Currency]);
|
||||
|
||||
beforeEach(() => {
|
||||
defineParams({ lang_parameters: { decimal_point: ",", thousands_sep: "." } });
|
||||
});
|
||||
|
||||
test("Numeric fields: fields with keydown on numpad decimal key", async () => {
|
||||
defineParams({ lang_parameters: { decimal_point: "🇧🇪" } });
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="float_factor_field" options="{'factor': 0.5}" widget="float_factor"/>
|
||||
<field name="qux"/>
|
||||
<field name="int_field"/>
|
||||
<field name="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="percentage" widget="percentage"/>
|
||||
<field name="progressbar" widget="progressbar" options="{'editable': true, 'max_value': 'qux', 'edit_max_value': true}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Dispatch numpad "dot" and numpad "comma" keydown events to all inputs and check
|
||||
// Numpad "comma" is specific to some countries (Brazil...)
|
||||
await click(".o_field_float_factor input");
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_float_factor input").toHaveValue("5🇧🇪00🇧🇪🇧🇪");
|
||||
|
||||
await click(".o_field_float input");
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_float input").toHaveValue("0🇧🇪4🇧🇪🇧🇪");
|
||||
|
||||
await click(".o_field_integer input");
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_integer input").toHaveValue("10🇧🇪🇧🇪");
|
||||
|
||||
await click(".o_field_monetary input");
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_monetary input").toHaveValue("9🇧🇪99🇧🇪🇧🇪");
|
||||
|
||||
await click(".o_field_percentage input");
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_percentage input").toHaveValue("99🇧🇪🇧🇪");
|
||||
|
||||
await click(".o_field_progressbar input");
|
||||
await animationFrame();
|
||||
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_progressbar input").toHaveValue("0🇧🇪44🇧🇪🇧🇪");
|
||||
});
|
||||
|
||||
test("Numeric fields: NumpadDecimal key is different from the decimalPoint", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="float_factor_field" options="{'factor': 0.5}" widget="float_factor"/>
|
||||
<field name="qux"/>
|
||||
<field name="int_field"/>
|
||||
<field name="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="percentage" widget="percentage"/>
|
||||
<field name="progressbar" widget="progressbar" options="{'editable': true, 'max_value': 'qux', 'edit_max_value': true}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Get all inputs
|
||||
const floatFactorField = queryFirst(".o_field_float_factor input");
|
||||
const floatInput = queryFirst(".o_field_float input");
|
||||
const integerInput = queryFirst(".o_field_integer input");
|
||||
const monetaryInput = queryFirst(".o_field_monetary input");
|
||||
const percentageInput = queryFirst(".o_field_percentage input");
|
||||
const progressbarInput = queryFirst(".o_field_progressbar input");
|
||||
|
||||
/**
|
||||
* Common assertion steps are extracted in this procedure.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {HTMLInputElement} params.el
|
||||
* @param {[number, number]} params.selectionRange
|
||||
* @param {string} params.expectedValue
|
||||
* @param {string} params.msg
|
||||
*/
|
||||
async function testInputElementOnNumpadDecimal(params) {
|
||||
const { el, selectionRange, expectedValue, msg } = params;
|
||||
|
||||
await pointerDown(el);
|
||||
await animationFrame();
|
||||
el.setSelectionRange(...selectionRange);
|
||||
const [event] = await keyDown(".", { code: "NumpadDecimal" });
|
||||
if (event.defaultPrevented) {
|
||||
expect.step("preventDefault");
|
||||
}
|
||||
await animationFrame();
|
||||
|
||||
// dispatch an extra keydown event and expect that it's not default prevented
|
||||
const [extraEvent] = await keyDown("1", { code: "Digit1" });
|
||||
if (extraEvent.defaultPrevented) {
|
||||
throw new Error("should not be default prevented");
|
||||
}
|
||||
await animationFrame();
|
||||
|
||||
// Selection range should be at +2 from the specified selection start (separator + character).
|
||||
expect(el.selectionStart).toBe(selectionRange[0] + 2);
|
||||
expect(el.selectionEnd).toBe(selectionRange[0] + 2);
|
||||
await animationFrame();
|
||||
// NumpadDecimal event should be default prevented
|
||||
expect.verifySteps(["preventDefault"]);
|
||||
expect(el).toHaveValue(expectedValue, { message: msg });
|
||||
}
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: floatFactorField,
|
||||
selectionRange: [1, 3],
|
||||
expectedValue: "5,10",
|
||||
msg: "Float factor field from 5,00 to 5,10",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: floatInput,
|
||||
selectionRange: [0, 2],
|
||||
expectedValue: ",14",
|
||||
msg: "Float field from 0,4 to ,14",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: integerInput,
|
||||
selectionRange: [1, 2],
|
||||
expectedValue: "1,1",
|
||||
msg: "Integer field from 10 to 1,1",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: monetaryInput,
|
||||
selectionRange: [0, 3],
|
||||
expectedValue: ",19",
|
||||
msg: "Monetary field from 9,99 to ,19",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: percentageInput,
|
||||
selectionRange: [1, 1],
|
||||
expectedValue: "9,19",
|
||||
msg: "Percentage field from 99 to 9,19",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: progressbarInput,
|
||||
selectionRange: [1, 3],
|
||||
expectedValue: "0,14",
|
||||
msg: "Progressbar field 2 from 0,44 to 0,14",
|
||||
});
|
||||
});
|
||||
|
||||
test("useNumpadDecimal should synchronize handlers on input elements", async () => {
|
||||
/**
|
||||
* Takes an array of input elements and asserts that each has the correct event listener.
|
||||
* @param {HTMLInputElement[]} inputEls
|
||||
*/
|
||||
async function testInputElements(inputEls) {
|
||||
for (const inputEl of inputEls) {
|
||||
await pointerDown(inputEl);
|
||||
await animationFrame();
|
||||
const [event] = await keyDown(".", { code: "NumpadDecimal" });
|
||||
if (event.defaultPrevented) {
|
||||
expect.step("preventDefault");
|
||||
}
|
||||
await animationFrame();
|
||||
|
||||
// dispatch an extra keydown event and expect that it's not default prevented
|
||||
const [extraEvent] = await keyDown("1", { code: "Digit1" });
|
||||
if (extraEvent.defaultPrevented) {
|
||||
throw new Error("should not be default prevented");
|
||||
}
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps(["preventDefault"]);
|
||||
}
|
||||
}
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<main t-ref="numpadDecimal">
|
||||
<input type="text" placeholder="input 1" />
|
||||
<input t-if="state.showOtherInput" type="text" placeholder="input 2" />
|
||||
</main>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useNumpadDecimal();
|
||||
this.state = useState({ showOtherInput: false });
|
||||
}
|
||||
}
|
||||
|
||||
const comp = await mountWithCleanup(MyComponent);
|
||||
await animationFrame();
|
||||
|
||||
// Initially, only one input should be rendered.
|
||||
expect("main > input").toHaveCount(1);
|
||||
await testInputElements(queryAll("main > input"));
|
||||
|
||||
// We show the second input by manually updating the state.
|
||||
comp.state.showOtherInput = true;
|
||||
await animationFrame();
|
||||
|
||||
// The second input should also be able to handle numpad decimal.
|
||||
expect("main > input").toHaveCount(2);
|
||||
await testInputElements(queryAll("main > input"));
|
||||
});
|
||||
|
||||
test("select all content on focus", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="monetary"/></form>`,
|
||||
});
|
||||
|
||||
const input = queryFirst(".o_field_widget[name='monetary'] input");
|
||||
await pointerDown(input);
|
||||
await animationFrame();
|
||||
expect(input.selectionStart).toBe(0);
|
||||
expect(input.selectionEnd).toBe(4);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,177 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { makeMockEnv, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { nbsp } from "@web/core/utils/strings";
|
||||
import {
|
||||
parseFloat,
|
||||
parseFloatTime,
|
||||
parseInteger,
|
||||
parseMonetary,
|
||||
parsePercentage,
|
||||
} from "@web/views/fields/parsers";
|
||||
|
||||
beforeEach(makeMockEnv);
|
||||
|
||||
test("parseFloat", () => {
|
||||
expect(parseFloat("")).toBe(0);
|
||||
expect(parseFloat("0")).toBe(0);
|
||||
expect(parseFloat("100.00")).toBe(100);
|
||||
expect(parseFloat("-100.00")).toBe(-100);
|
||||
expect(parseFloat("1,000.00")).toBe(1000);
|
||||
expect(parseFloat("1,000,000.00")).toBe(1000000);
|
||||
expect(parseFloat("1,234.567")).toBe(1234.567);
|
||||
expect(() => parseFloat("1.000.000")).toThrow();
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
expect(parseFloat("1.234,567")).toBe(1234.567);
|
||||
|
||||
// Can evaluate expression from locale with decimal point different from ".".
|
||||
expect(parseFloat("=1.000,1 + 2.000,2")).toBe(3000.3);
|
||||
expect(parseFloat("=1.000,00 + 11.121,00")).toBe(12121);
|
||||
expect(parseFloat("=1000,00 + 11122,00")).toBe(12122);
|
||||
expect(parseFloat("=1000 + 11123")).toBe(12123);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: false });
|
||||
expect(parseFloat("1234,567")).toBe(1234.567);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: nbsp });
|
||||
expect(parseFloat("9 876,543")).toBe(9876.543);
|
||||
expect(parseFloat("1 234 567,89")).toBe(1234567.89);
|
||||
expect(parseFloat(`98${nbsp}765 432,1`)).toBe(98765432.1);
|
||||
});
|
||||
|
||||
test("parseFloatTime", () => {
|
||||
expect(parseFloatTime("0")).toBe(0);
|
||||
expect(parseFloatTime("100")).toBe(100);
|
||||
expect(parseFloatTime("100.00")).toBe(100);
|
||||
expect(parseFloatTime("7:15")).toBe(7.25);
|
||||
expect(parseFloatTime("-4:30")).toBe(-4.5);
|
||||
expect(parseFloatTime(":")).toBe(0);
|
||||
expect(parseFloatTime("1:")).toBe(1);
|
||||
expect(parseFloatTime(":12")).toBe(0.2);
|
||||
|
||||
expect(() => parseFloatTime("a:1")).toThrow();
|
||||
expect(() => parseFloatTime("1:a")).toThrow();
|
||||
expect(() => parseFloatTime("1:1:")).toThrow();
|
||||
expect(() => parseFloatTime(":1:1")).toThrow();
|
||||
});
|
||||
|
||||
test("parseInteger", () => {
|
||||
expect(parseInteger("")).toBe(0);
|
||||
expect(parseInteger("0")).toBe(0);
|
||||
expect(parseInteger("100")).toBe(100);
|
||||
expect(parseInteger("-100")).toBe(-100);
|
||||
expect(parseInteger("1,000")).toBe(1000);
|
||||
expect(parseInteger("1,000,000")).toBe(1000000);
|
||||
expect(parseInteger("-2,147,483,648")).toBe(-2147483648);
|
||||
expect(parseInteger("2,147,483,647")).toBe(2147483647);
|
||||
expect(() => parseInteger("1.000.000")).toThrow();
|
||||
expect(() => parseInteger("1,234.567")).toThrow();
|
||||
expect(() => parseInteger("-2,147,483,649")).toThrow();
|
||||
expect(() => parseInteger("2,147,483,648")).toThrow();
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
|
||||
expect(parseInteger("1.000.000")).toBe(1000000);
|
||||
expect(() => parseInteger("1.234,567")).toThrow();
|
||||
// fallback to en localization
|
||||
expect(parseInteger("1,000,000")).toBe(1000000);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: false });
|
||||
expect(parseInteger("1000000")).toBe(1000000);
|
||||
});
|
||||
|
||||
test("parsePercentage", () => {
|
||||
expect(parsePercentage("")).toBe(0);
|
||||
expect(parsePercentage("0")).toBe(0);
|
||||
expect(parsePercentage("0.5")).toBe(0.005);
|
||||
expect(parsePercentage("1")).toBe(0.01);
|
||||
expect(parsePercentage("100")).toBe(1);
|
||||
expect(parsePercentage("50%")).toBe(0.5);
|
||||
expect(() => parsePercentage("50%40")).toThrow();
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
|
||||
expect(parsePercentage("1.234,56")).toBe(12.3456);
|
||||
expect(parsePercentage("6,02")).toBe(0.0602);
|
||||
});
|
||||
|
||||
test("parsers fallback on english localisation", () => {
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: ",",
|
||||
thousandsSep: ".",
|
||||
});
|
||||
|
||||
expect(parseInteger("1,000,000")).toBe(1000000);
|
||||
expect(parseFloat("1,000,000.50")).toBe(1000000.5);
|
||||
});
|
||||
|
||||
test("parseMonetary", () => {
|
||||
expect(parseMonetary("")).toBe(0);
|
||||
expect(parseMonetary("0")).toBe(0);
|
||||
expect(parseMonetary("100.00\u00a0€")).toBe(100);
|
||||
expect(parseMonetary("-100.00")).toBe(-100);
|
||||
expect(parseMonetary("1,000.00")).toBe(1000);
|
||||
expect(parseMonetary(".1")).toBe(0.1);
|
||||
expect(parseMonetary("1,000,000.00")).toBe(1000000);
|
||||
expect(parseMonetary("$\u00a0125.00")).toBe(125);
|
||||
expect(parseMonetary("1,000.00\u00a0€")).toBe(1000);
|
||||
|
||||
expect(parseMonetary("\u00a0")).toBe(0);
|
||||
expect(parseMonetary("1\u00a0")).toBe(1);
|
||||
expect(parseMonetary("\u00a01")).toBe(1);
|
||||
|
||||
expect(parseMonetary("12.00 €")).toBe(12);
|
||||
expect(parseMonetary("$ 12.00")).toBe(12);
|
||||
expect(parseMonetary("1\u00a0$")).toBe(1);
|
||||
expect(parseMonetary("$\u00a01")).toBe(1);
|
||||
|
||||
expect(() => parseMonetary("1$\u00a01")).toThrow();
|
||||
expect(() => parseMonetary("$\u00a012.00\u00a034")).toThrow();
|
||||
|
||||
// nbsp as thousands separator
|
||||
patchWithCleanup(localization, { thousandsSep: "\u00a0", decimalPoint: "," });
|
||||
expect(parseMonetary("1\u00a0000,06\u00a0€")).toBe(1000.06);
|
||||
expect(parseMonetary("$\u00a01\u00a0000,07")).toBe(1000.07);
|
||||
expect(parseMonetary("1000000,08")).toBe(1000000.08);
|
||||
expect(parseMonetary("$ -1\u00a0000,09")).toBe(-1000.09);
|
||||
|
||||
// symbol not separated from the value
|
||||
expect(parseMonetary("1\u00a0000,08€")).toBe(1000.08);
|
||||
expect(parseMonetary("€1\u00a0000,09")).toBe(1000.09);
|
||||
expect(parseMonetary("$1\u00a0000,10")).toBe(1000.1);
|
||||
expect(parseMonetary("$-1\u00a0000,11")).toBe(-1000.11);
|
||||
|
||||
// any symbol
|
||||
expect(parseMonetary("1\u00a0000,11EUROS")).toBe(1000.11);
|
||||
expect(parseMonetary("EUR1\u00a0000,12")).toBe(1000.12);
|
||||
expect(parseMonetary("DOL1\u00a0000,13")).toBe(1000.13);
|
||||
expect(parseMonetary("1\u00a0000,14DOLLARS")).toBe(1000.14);
|
||||
expect(parseMonetary("DOLLARS+1\u00a0000,15")).toBe(1000.15);
|
||||
expect(parseMonetary("EURO-1\u00a0000,16DOGE")).toBe(-1000.16);
|
||||
|
||||
// comma as decimal point and dot as thousands separator
|
||||
patchWithCleanup(localization, { thousandsSep: ".", decimalPoint: "," });
|
||||
expect(parseMonetary("10,08")).toBe(10.08);
|
||||
expect(parseMonetary("")).toBe(0);
|
||||
expect(parseMonetary("0")).toBe(0);
|
||||
expect(parseMonetary("100,12\u00a0€")).toBe(100.12);
|
||||
expect(parseMonetary("-100,12")).toBe(-100.12);
|
||||
expect(parseMonetary("1.000,12")).toBe(1000.12);
|
||||
expect(parseMonetary(",1")).toBe(0.1);
|
||||
expect(parseMonetary("1.000.000,12")).toBe(1000000.12);
|
||||
expect(parseMonetary("$\u00a0125,12")).toBe(125.12);
|
||||
expect(parseMonetary("1.000,00\u00a0€")).toBe(1000);
|
||||
expect(parseMonetary(",")).toBe(0);
|
||||
expect(parseMonetary("1\u00a0")).toBe(1);
|
||||
expect(parseMonetary("\u00a01")).toBe(1);
|
||||
expect(parseMonetary("12,34 €")).toBe(12.34);
|
||||
expect(parseMonetary("$ 12,34")).toBe(12.34);
|
||||
|
||||
// Can evaluate expression
|
||||
expect(parseMonetary("=1.000,1 + 2.000,2")).toBe(3000.3);
|
||||
expect(parseMonetary("=1.000,00 + 11.121,00")).toBe(12121);
|
||||
expect(parseMonetary("=1000,00 + 11122,00")).toBe(12122);
|
||||
expect(parseMonetary("=1000 + 11123")).toBe(12123);
|
||||
});
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { test, expect } from "@odoo/hoot";
|
||||
import { click, setInputFiles, queryOne, waitFor } from "@odoo/hoot-dom";
|
||||
|
||||
const getIframeSrc = () => queryOne(".o_field_widget iframe.o_pdfview_iframe").dataset.src;
|
||||
|
||||
const getIframeProtocol = () => getIframeSrc().match(/\?file=(\w+)%3A/)[1];
|
||||
|
||||
const getIframeViewerParams = () =>
|
||||
decodeURIComponent(getIframeSrc().match(/%2Fweb%2Fcontent%3F(.*)#page/)[1]);
|
||||
|
||||
class Partner extends models.Model {
|
||||
document = fields.Binary({ string: "Binary" });
|
||||
_records = [
|
||||
{
|
||||
document: "coucou==\n",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("PdfViewerField without data", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
expect(".o_field_widget").toHaveClass("o_field_pdf_viewer");
|
||||
expect(".o_select_file_button:not(.o_hidden)").toHaveCount(1);
|
||||
expect(".o_pdfview_iframe").toHaveCount(0);
|
||||
expect(`input[type="file"]`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("PdfViewerField: basic rendering", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget").toHaveClass("o_field_pdf_viewer");
|
||||
expect(".o_select_file_button").toHaveCount(1);
|
||||
expect(".o_field_widget iframe.o_pdfview_iframe").toHaveCount(1);
|
||||
expect(getIframeProtocol()).toBe("https");
|
||||
expect(getIframeViewerParams()).toBe("model=partner&field=document&id=1");
|
||||
});
|
||||
|
||||
test("PdfViewerField: upload rendering", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1]).toEqual({ document: btoa("test") });
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
|
||||
expect("iframe.o_pdfview_iframe").toHaveCount(0);
|
||||
const file = new File(["test"], "test.pdf", { type: "application/pdf" });
|
||||
await click(".o_field_pdf_viewer input[type=file]");
|
||||
await setInputFiles(file);
|
||||
await waitFor("iframe.o_pdfview_iframe");
|
||||
expect(getIframeProtocol()).toBe("blob");
|
||||
await clickSave();
|
||||
expect(getIframeProtocol()).toBe("blob");
|
||||
});
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
import { test, expect } from "@odoo/hoot";
|
||||
import { queryOne } from "@odoo/hoot-dom";
|
||||
|
||||
class Partner extends models.Model {
|
||||
foo = fields.Char({
|
||||
string: "Foo",
|
||||
default: "My little Foo Value",
|
||||
trim: true,
|
||||
});
|
||||
int_field = fields.Integer();
|
||||
float_field = fields.Float();
|
||||
_records = [
|
||||
{ id: 1, foo: "yop", int_field: 10 },
|
||||
{ id: 2, foo: "gnap", int_field: 80 },
|
||||
{ id: 3, foo: "blip", float_field: 33.3333 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("PercentPieField in form view with value < 50%", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie").toHaveCount(1);
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_value").toHaveText("10%", {
|
||||
message: "should have 10% as pie value since int_field=10",
|
||||
});
|
||||
|
||||
expect(
|
||||
queryOne(".o_field_percent_pie.o_field_widget .o_pie").style.background.replaceAll(
|
||||
/\s+/g,
|
||||
" "
|
||||
)
|
||||
).toBe(
|
||||
"conic-gradient( var(--PercentPieField-color-active) 0% 10%, var(--PercentPieField-color-static) 0% 100% )",
|
||||
{ message: "pie should have a background computed for its value of 10%" }
|
||||
);
|
||||
});
|
||||
|
||||
test("PercentPieField in form view with value > 50%", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
});
|
||||
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie").toHaveCount(1);
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_value").toHaveText("80%", {
|
||||
message: "should have 80% as pie value since int_field=80",
|
||||
});
|
||||
expect(
|
||||
queryOne(".o_field_percent_pie.o_field_widget .o_pie").style.background.replaceAll(
|
||||
/\s+/g,
|
||||
" "
|
||||
)
|
||||
).toBe(
|
||||
"conic-gradient( var(--PercentPieField-color-active) 0% 80%, var(--PercentPieField-color-static) 0% 100% )",
|
||||
{ message: "pie should have a background computed for its value of 80%" }
|
||||
);
|
||||
});
|
||||
|
||||
test("PercentPieField in form view with float value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="float_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 3,
|
||||
});
|
||||
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie").toHaveCount(1);
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_value").toHaveText("33.33%", {
|
||||
message:
|
||||
"should have 33.33% as pie value since float_field=33.3333 and its value is rounded to 2 decimals",
|
||||
});
|
||||
expect(
|
||||
queryOne(".o_field_percent_pie.o_field_widget .o_pie").style.background.replaceAll(
|
||||
/\s+/g,
|
||||
" "
|
||||
)
|
||||
).toBe(
|
||||
"conic-gradient( var(--PercentPieField-color-active) 0% 33.3333%, var(--PercentPieField-color-static) 0% 100% )",
|
||||
{ message: "pie should have a background computed for its value of 33.3333%" }
|
||||
);
|
||||
});
|
||||
|
||||
test("hide the string when the PercentPieField widget is used in the view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie").toHaveCount(1);
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_text").not.toBeVisible();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("show the string when the PercentPieField widget is used in a button with the class oe_stat_button", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<div name="button_box" class="oe_button_box">
|
||||
<button type="object" class="oe_stat_button">
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</button>
|
||||
</div>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie").toHaveCount(1);
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_text").toBeVisible();
|
||||
});
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { clear, click, edit } from "@odoo/hoot-dom";
|
||||
|
||||
class Partner extends models.Model {
|
||||
float_field = fields.Float({
|
||||
string: "Float_field",
|
||||
digits: [0, 1],
|
||||
});
|
||||
_records = [{ float_field: 0.44444 }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("PercentageField in form view", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].float_field).toBe(0.24);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="float_field" widget="percentage"/></form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("44.4");
|
||||
expect(".o_field_widget[name=float_field] span").toHaveText("%", {
|
||||
message: "The input should be followed by a span containing the percentage symbol.",
|
||||
});
|
||||
|
||||
await click("[name='float_field'] input");
|
||||
await edit("24");
|
||||
expect("[name='float_field'] input").toHaveValue("24");
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("24");
|
||||
});
|
||||
|
||||
test("percentage field with placeholder", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="float_field" widget="percentage" placeholder="Placeholder"/></form>`,
|
||||
});
|
||||
|
||||
await click(".o_field_widget[name='float_field'] input");
|
||||
await clear();
|
||||
|
||||
expect(".o_field_widget[name='float_field'] input").toHaveProperty(
|
||||
"placeholder",
|
||||
"Placeholder"
|
||||
);
|
||||
expect(".o_field_widget[name='float_field'] input").toHaveAttribute(
|
||||
"placeholder",
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
test("PercentageField in form view without rounding error", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="float_field" widget="percentage"/></form>`,
|
||||
});
|
||||
|
||||
await click("[name='float_field'] input");
|
||||
await edit("28");
|
||||
|
||||
expect("[name='float_field'] input").toHaveValue("28");
|
||||
});
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, pointerDown, queryFirst, queryOne } from "@odoo/hoot-dom";
|
||||
import { getNextTabableElement } from "@web/core/utils/ui";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
class Partner extends models.Model {
|
||||
foo = fields.Char({ default: "My little Foo Value", trim: true });
|
||||
name = fields.Char();
|
||||
|
||||
_records = [{ foo: "yop" }, { foo: "blip" }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("PhoneField in form view on normal screens (readonly)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
mode: "readonly",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(".o_field_phone a").toHaveCount(1);
|
||||
expect(".o_field_phone a").toHaveText("yop");
|
||||
expect(".o_field_phone a").toHaveAttribute("href", "tel:yop");
|
||||
});
|
||||
|
||||
test("PhoneField in form view on normal screens (edit)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(`input[type="tel"]`).toHaveCount(1);
|
||||
expect(`input[type="tel"]`).toHaveValue("yop");
|
||||
expect(".o_field_phone a").toHaveCount(1);
|
||||
expect(".o_field_phone a").toHaveText("Call");
|
||||
expect(".o_field_phone a").toHaveAttribute("href", "tel:yop");
|
||||
|
||||
// change value in edit mode
|
||||
await click(`input[type="tel"]`);
|
||||
await edit("new");
|
||||
await animationFrame();
|
||||
// save
|
||||
await clickSave();
|
||||
expect(`input[type="tel"]`).toHaveValue("new");
|
||||
});
|
||||
|
||||
test("PhoneField in editable list view on normal screens", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list editable="bottom"><field name="foo" widget="phone"/></list>',
|
||||
});
|
||||
expect("tbody td:not(.o_list_record_selector).o_data_cell").toHaveCount(2);
|
||||
expect("tbody td:not(.o_list_record_selector) a:first").toHaveText("yop");
|
||||
expect(".o_field_widget a.o_form_uri").toHaveCount(2);
|
||||
|
||||
// Edit a line and check the result
|
||||
const cell = queryFirst("tbody td:not(.o_list_record_selector)");
|
||||
await click(cell);
|
||||
await animationFrame();
|
||||
expect(cell.parentElement).toHaveClass("o_selected_row");
|
||||
expect(`tbody td:not(.o_list_record_selector) input`).toHaveValue("yop");
|
||||
|
||||
await click(`tbody td:not(.o_list_record_selector) input`);
|
||||
await edit("new");
|
||||
await animationFrame();
|
||||
await click(".o_control_panel_main_buttons .o_list_button_save");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_selected_row").toHaveCount(0);
|
||||
expect("tbody td:not(.o_list_record_selector) a:first").toHaveText("new");
|
||||
expect(".o_field_widget a.o_form_uri").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("use TAB to navigate to a PhoneField", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await pointerDown(".o_field_widget[name=name] input");
|
||||
|
||||
expect(".o_field_widget[name=name] input").toBeFocused();
|
||||
expect(queryOne`[name="foo"] input:only`).toBe(getNextTabableElement());
|
||||
});
|
||||
|
||||
test("phone field with placeholder", async () => {
|
||||
Partner._fields.foo.default = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone" placeholder="Placeholder"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='foo'] input").toHaveProperty("placeholder", "Placeholder");
|
||||
});
|
||||
|
||||
test("unset and readonly PhoneField", async () => {
|
||||
Partner._fields.foo.default = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone" readonly="1" placeholder="Placeholder"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='foo'] a").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("href is correctly formatted", async () => {
|
||||
Partner._records[0].foo = "+12 345 67 89 00";
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
mode: "readonly",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_phone a").toHaveText("+12 345 67 89 00");
|
||||
expect(".o_field_phone a").toHaveAttribute("href", "tel:+12345678900");
|
||||
});
|
||||
|
||||
test("New record, fill in phone field, then click on call icon and save", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" required="1"/>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=name] input").edit("TEST");
|
||||
await contains(".o_field_widget[name=foo] input").edit("+12345678900");
|
||||
|
||||
await click(`input[type="tel"]`);
|
||||
|
||||
expect(`.o_form_status_indicator_buttons`).not.toHaveClass("invisible");
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget[name=name] input").toHaveValue("TEST");
|
||||
expect(".o_field_widget[name=foo] input").toHaveValue("+12345678900");
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveClass("invisible");
|
||||
});
|
||||
|
|
@ -0,0 +1,453 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, hover, leave, press, queryAll, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
foo = fields.Char({ string: "Foo" });
|
||||
id = fields.Integer({ string: "Sequence" });
|
||||
selection = fields.Selection({
|
||||
string: "Selection",
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
foo: "yop",
|
||||
selection: "blocked",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
foo: "blip",
|
||||
selection: "normal",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
foo: "abc",
|
||||
selection: "done",
|
||||
},
|
||||
{ id: 3, foo: "gnap" },
|
||||
{ id: 5, foo: "blop" },
|
||||
];
|
||||
}
|
||||
defineModels([Partner]);
|
||||
|
||||
test("PriorityField when not set", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget .o_priority:not(.o_field_empty)").toHaveCount(1, {
|
||||
message: "widget should be considered set, even though there is no value for this field",
|
||||
});
|
||||
expect(".o_field_widget .o_priority a.o_priority_star").toHaveCount(2, {
|
||||
message:
|
||||
"should have two stars for representing each possible value: no star, one star and two stars",
|
||||
});
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star").toHaveCount(0, {
|
||||
message: "should have no full star since there is no value",
|
||||
});
|
||||
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star-o").toHaveCount(2, {
|
||||
message: "should have two empty stars since there is no value",
|
||||
});
|
||||
});
|
||||
|
||||
test("PriorityField tooltip", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// check data-tooltip attribute (used by the tooltip service)
|
||||
const stars = queryAll(".o_field_widget .o_priority a.o_priority_star");
|
||||
expect(stars[0]).toHaveAttribute("data-tooltip", "Selection: Blocked");
|
||||
expect(stars[1]).toHaveAttribute("data-tooltip", "Selection: Done");
|
||||
});
|
||||
|
||||
test("PriorityField in form view", async () => {
|
||||
expect.assertions(8);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args).toEqual([[1], { selection: "done" }]);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget .o_priority:not(.o_field_empty)").toHaveCount(1);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star").toHaveCount(2);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star").toHaveCount(1);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star-o").toHaveCount(1);
|
||||
|
||||
// click on the second star in edit mode
|
||||
await click(".o_field_widget .o_priority a.o_priority_star.fa-star-o:last");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_widget .o_priority a.o_priority_star").toHaveCount(2);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star").toHaveCount(2);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star-o").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("PriorityField hover a star in form view", async () => {
|
||||
expect.assertions(10);
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget .o_priority:not(.o_field_empty)").toHaveCount(1);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star").toHaveCount(2);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star").toHaveCount(1);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star-o").toHaveCount(1);
|
||||
|
||||
// hover last star
|
||||
const star = ".o_field_widget .o_priority a.o_priority_star.fa-star-o:last";
|
||||
await hover(star);
|
||||
await animationFrame();
|
||||
expect(".o_field_widget .o_priority a.o_priority_star").toHaveCount(2);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star").toHaveCount(2, {
|
||||
message: "should temporary have two full stars since we are hovering the third value",
|
||||
});
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star-o").toHaveCount(0, {
|
||||
message: "should temporary have no empty star since we are hovering the third value",
|
||||
});
|
||||
|
||||
await leave(star);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_widget .o_priority a.o_priority_star").toHaveCount(2);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star").toHaveCount(1);
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star-o").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("PriorityField can write after adding a record -- kanban", async () => {
|
||||
Partner._fields.selection = fields.Selection({
|
||||
string: "Selection",
|
||||
selection: [
|
||||
["0", 0],
|
||||
["1", 1],
|
||||
],
|
||||
});
|
||||
Partner._records[0].selection = "0";
|
||||
Partner._views[["form", "myquickview"]] = /* xml */ `<form/>`;
|
||||
onRpc("web_save", ({ args }) => expect.step(`web_save ${JSON.stringify(args)}`));
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", 1]],
|
||||
groupBy: ["foo"],
|
||||
arch: /* xml */ `
|
||||
<kanban on_create="quick_create" quick_create_view="myquickview">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="selection" widget="priority"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
expect(".o_kanban_record .fa-star").toHaveCount(0);
|
||||
|
||||
await click(".o_priority a.o_priority_star.fa-star-o");
|
||||
// wait for web_save
|
||||
await animationFrame();
|
||||
expect.verifySteps(['web_save [[1],{"selection":"1"}]']);
|
||||
expect(".o_kanban_record .fa-star").toHaveCount(1);
|
||||
await click(".o_control_panel_main_buttons .o-kanban-button-new");
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
await click(".o_kanban_quick_create .o_kanban_add");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["web_save [[],{}]"]);
|
||||
await click(".o_priority a.o_priority_star.fa-star-o");
|
||||
await animationFrame();
|
||||
expect.verifySteps([`web_save [[6],{"selection":"1"}]`]);
|
||||
expect(".o_kanban_record .fa-star").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("PriorityField in editable list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list editable="bottom"><field name="selection" widget="priority" /></list>`,
|
||||
});
|
||||
|
||||
expect(".o_data_row:first-child .o_priority:not(.o_field_empty)").toHaveCount(1);
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star").toHaveCount(2, {
|
||||
message:
|
||||
"should have two stars for representing each possible value: no star, one star and two stars",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star").toHaveCount(1, {
|
||||
message: "should have one full star since the value is the second value",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star-o").toHaveCount(1, {
|
||||
message: "should have one empty star since the value is the second value",
|
||||
});
|
||||
|
||||
// switch to edit mode and check the result
|
||||
await click("tbody td:not(.o_list_record_selector)");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star").toHaveCount(2, {
|
||||
message:
|
||||
"should have two stars for representing each possible value: no star, one star and two stars",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star").toHaveCount(1, {
|
||||
message: "should have one full star since the value is the second value",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star-o").toHaveCount(1, {
|
||||
message: "should have one empty star since the value is the second value",
|
||||
});
|
||||
|
||||
// save
|
||||
await click(".o_control_panel_main_buttons .o_list_button_save");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star").toHaveCount(2, {
|
||||
message:
|
||||
"should have two stars for representing each possible value: no star, one star and two stars",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star").toHaveCount(1, {
|
||||
message: "should have one full star since the value is the second value",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star-o").toHaveCount(1, {
|
||||
message: "should have one empty star since the value is the second value",
|
||||
});
|
||||
|
||||
// click on the first star in readonly mode
|
||||
await click(".o_priority a.o_priority_star.fa-star");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star").toHaveCount(2, {
|
||||
message: "should still have two stars",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star").toHaveCount(0, {
|
||||
message: "should now have no full star since the value is the first value",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star-o").toHaveCount(2, {
|
||||
message: "should now have two empty stars since the value is the first value",
|
||||
});
|
||||
|
||||
// re-enter edit mode to force re-rendering the widget to check if the value was correctly saved
|
||||
await click("tbody td:not(.o_list_record_selector)");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star").toHaveCount(2, {
|
||||
message: "should still have two stars",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star").toHaveCount(0, {
|
||||
message: "should now have no full star since the value is the first value",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star-o").toHaveCount(2, {
|
||||
message: "should now have two empty stars since the value is the first value",
|
||||
});
|
||||
|
||||
// Click on second star in edit mode
|
||||
await click(".o_priority a.o_priority_star.fa-star-o:last");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:last-child .o_priority a.o_priority_star").toHaveCount(2, {
|
||||
message: "should still have two stars",
|
||||
});
|
||||
expect(".o_data_row:last-child .o_priority a.o_priority_star.fa-star").toHaveCount(2, {
|
||||
message: "should now have two full stars since the value is the third value",
|
||||
});
|
||||
expect(".o_data_row:last-child .o_priority a.o_priority_star.fa-star-o").toHaveCount(0, {
|
||||
message: "should now have no empty star since the value is the third value",
|
||||
});
|
||||
|
||||
// save
|
||||
await click(".o_control_panel_main_buttons .o_list_button_save");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:last-child .o_priority a.o_priority_star").toHaveCount(2, {
|
||||
message: "should still have two stars",
|
||||
});
|
||||
expect(".o_data_row:last-child .o_priority a.o_priority_star.fa-star").toHaveCount(2, {
|
||||
message: "should now have two full stars since the value is the third value",
|
||||
});
|
||||
expect(".o_data_row:last-child .o_priority a.o_priority_star.fa-star-o").toHaveCount(0, {
|
||||
message: "should now have no empty star since the value is the third value",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("PriorityField hover in editable list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list editable="bottom"><field name="selection" widget="priority" /></list>`,
|
||||
});
|
||||
|
||||
expect(".o_data_row:first-child .o_priority:not(.o_field_empty)").toHaveCount(1);
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star").toHaveCount(2, {
|
||||
message:
|
||||
"should have two stars for representing each possible value: no star, one star and two stars",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star").toHaveCount(1, {
|
||||
message: "should have one full star since the value is the second value",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star-o").toHaveCount(1, {
|
||||
message: "should have one empty star since the value is the second value",
|
||||
});
|
||||
|
||||
// hover last star
|
||||
const star = ".o_data_row:first-child .o_priority a.o_priority_star.fa-star-o:last";
|
||||
await hover(star);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star").toHaveCount(2);
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star").toHaveCount(2, {
|
||||
message: "should temporary have two full stars since we are hovering the third value",
|
||||
});
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star-o").toHaveCount(0, {
|
||||
message: "should temporary have no empty star since we are hovering the third value",
|
||||
});
|
||||
|
||||
await leave(star);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star").toHaveCount(2);
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star").toHaveCount(1);
|
||||
expect(".o_data_row:first-child .o_priority a.o_priority_star.fa-star-o").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("PriorityField with readonly attribute", async () => {
|
||||
onRpc("write", () => {
|
||||
expect.step("write");
|
||||
throw new Error("should not save");
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: '<form><field name="selection" widget="priority" readonly="1"/></form>',
|
||||
});
|
||||
|
||||
expect("span.o_priority_star.fa.fa-star-o").toHaveCount(2, {
|
||||
message: "stars of priority widget should rendered with span tag if readonly",
|
||||
});
|
||||
await hover(".o_priority_star.fa-star-o:last");
|
||||
await animationFrame();
|
||||
expect.step("hover");
|
||||
expect(".o_field_widget .o_priority a.o_priority_star.fa-star").toHaveCount(0, {
|
||||
message: "should have no full stars on hover since the field is readonly",
|
||||
});
|
||||
await click(".o_priority_star.fa-star-o:last");
|
||||
await animationFrame();
|
||||
expect.step("click");
|
||||
expect("span.o_priority_star.fa.fa-star-o").toHaveCount(2, {
|
||||
message: "should still have two stars",
|
||||
});
|
||||
expect.verifySteps(["hover", "click"]);
|
||||
});
|
||||
|
||||
test('PriorityField edited by the smart action "Set priority..."', async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="selection" widget="priority"/></form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect("a.fa-star").toHaveCount(1);
|
||||
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
const idx = queryAllTexts(".o_command").indexOf("Set priority...\nALT + R");
|
||||
expect(idx).toBeGreaterThan(-1);
|
||||
await click(queryAll(".o_command")[idx]);
|
||||
await animationFrame();
|
||||
expect(queryAllTexts(".o_command")).toEqual(["Normal", "Blocked", "Done"]);
|
||||
await click("#o_command_2");
|
||||
await animationFrame();
|
||||
expect("a.fa-star").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("PriorityField - auto save record when field toggled", async () => {
|
||||
onRpc("web_save", () => expect.step("web_save"));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
await click(".o_field_widget .o_priority a.o_priority_star.fa-star-o:last");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("PriorityField - prevent auto save with autosave option", async () => {
|
||||
onRpc("write", () => expect.step("write"));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority" options="{'autosave': False}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(".o_field_widget .o_priority a.o_priority_star.fa-star-o:last");
|
||||
await animationFrame();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, queryOne, queryText, queryValue } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
defineParams,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char({ string: "Display Name" });
|
||||
int_field = fields.Integer({
|
||||
string: "int_field",
|
||||
});
|
||||
int_field2 = fields.Integer({
|
||||
string: "int_field",
|
||||
});
|
||||
int_field3 = fields.Integer({
|
||||
string: "int_field",
|
||||
});
|
||||
float_field = fields.Float({
|
||||
string: "Float_field",
|
||||
digits: [16, 1],
|
||||
});
|
||||
_records = [
|
||||
{
|
||||
int_field: 10,
|
||||
float_field: 0.44444,
|
||||
},
|
||||
];
|
||||
}
|
||||
defineModels([Partner]);
|
||||
|
||||
test("ProgressBarField: max_value should update", async () => {
|
||||
expect.assertions(3);
|
||||
Partner._records[0].float_field = 2;
|
||||
Partner._onChanges.name = (record) => {
|
||||
record.int_field = 999;
|
||||
record.float_field = 5;
|
||||
};
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1]).toEqual(
|
||||
{ int_field: 999, float_field: 5, name: "new name" },
|
||||
{ message: "New value of progress bar saved" }
|
||||
);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="name" />
|
||||
<field name="float_field" invisible="1" />
|
||||
<field name="int_field" widget="progressbar" options="{'current_value': 'int_field', 'max_value': 'float_field'}" />
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_progressbar").toHaveText("10\n/\n2");
|
||||
await click(".o_field_widget[name=name] input");
|
||||
await edit("new name", { confirm: "enter" });
|
||||
await clickSave();
|
||||
await animationFrame();
|
||||
expect(".o_progressbar").toHaveText("999\n/\n5");
|
||||
});
|
||||
|
||||
test("ProgressBarField: value should update in edit mode when typing in input", async () => {
|
||||
expect.assertions(4);
|
||||
Partner._records[0].int_field = 99;
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].int_field).toBe(69);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(queryValue(".o_progressbar_value .o_input") + queryText(".o_progressbar")).toBe("99%", {
|
||||
message: "Initial value should be correct",
|
||||
});
|
||||
await click(".o_progressbar_value .o_input");
|
||||
// wait for apply dom change
|
||||
await animationFrame();
|
||||
await edit("69", { confirm: "enter" });
|
||||
expect(".o_progressbar_value .o_input").toHaveValue("69", {
|
||||
message: "New value should be different after focusing out of the field",
|
||||
});
|
||||
// wait for apply dom change
|
||||
await animationFrame();
|
||||
await clickSave();
|
||||
// wait for rpc
|
||||
await animationFrame();
|
||||
expect(".o_progressbar_value .o_input").toHaveValue("69", {
|
||||
message: "New value is still displayed after save",
|
||||
});
|
||||
});
|
||||
|
||||
test("ProgressBarField: value should update in edit mode when typing in input with field max value", async () => {
|
||||
expect.assertions(4);
|
||||
Partner._records[0].int_field = 99;
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].int_field).toBe(69);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="float_field" invisible="1" />
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true, 'max_value': 'float_field'}" />
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(".o_form_view .o_form_editable").toHaveCount(1, { message: "Form in edit mode" });
|
||||
expect(queryValue(".o_progressbar_value .o_input") + queryText(".o_progressbar")).toBe(
|
||||
"99/\n0",
|
||||
{ message: "Initial value should be correct" }
|
||||
);
|
||||
|
||||
await click(".o_progressbar_value .o_input");
|
||||
await animationFrame();
|
||||
await edit("69", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
await clickSave();
|
||||
await animationFrame();
|
||||
expect(queryValue(".o_progressbar_value .o_input") + queryText(".o_progressbar")).toBe(
|
||||
"69/\n0",
|
||||
{ message: "New value should be different than initial after click" }
|
||||
);
|
||||
});
|
||||
|
||||
test("ProgressBarField: max value should update in edit mode when typing in input with field max value", async () => {
|
||||
expect.assertions(5);
|
||||
Partner._records[0].int_field = 99;
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].float_field).toBe(69);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="float_field" invisible="1" />
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true, 'max_value': 'float_field', 'edit_max_value': true}" />
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(queryText(".o_progressbar") + queryValue(".o_progressbar_value .o_input")).toBe(
|
||||
"99\n/0",
|
||||
{ message: "Initial value should be correct" }
|
||||
);
|
||||
expect(".o_form_view .o_form_editable").toHaveCount(1, { message: "Form in edit mode" });
|
||||
queryOne(".o_progressbar input").focus();
|
||||
await animationFrame();
|
||||
|
||||
expect(queryText(".o_progressbar") + queryValue(".o_progressbar_value .o_input")).toBe(
|
||||
"99\n/0.44",
|
||||
{ message: "Initial value is not formatted when focused" }
|
||||
);
|
||||
|
||||
await click(".o_progressbar_value .o_input");
|
||||
await edit("69", { confirm: "enter" });
|
||||
await clickSave();
|
||||
|
||||
expect(queryText(".o_progressbar") + queryValue(".o_progressbar_value .o_input")).toBe(
|
||||
"99\n/69",
|
||||
{ message: "New value should be different than initial after click" }
|
||||
);
|
||||
});
|
||||
|
||||
test("ProgressBarField: Standard readonly mode is readonly", async () => {
|
||||
Partner._records[0].int_field = 99;
|
||||
|
||||
onRpc(({ method }) => expect.step(method));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form edit="0">
|
||||
<field name="float_field" invisible="1"/>
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true, 'max_value': 'float_field', 'edit_max_value': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_progressbar").toHaveText("99\n/\n0", {
|
||||
message: "Initial value should be correct",
|
||||
});
|
||||
|
||||
await click(".o_progress");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_progressbar_value .o_input").toHaveCount(0, {
|
||||
message: "no input in readonly mode",
|
||||
});
|
||||
expect.verifySteps(["get_views", "web_read"]);
|
||||
});
|
||||
|
||||
test("ProgressBarField: field is editable in kanban", async () => {
|
||||
expect.assertions(7);
|
||||
Partner._records[0].int_field = 99;
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].int_field).toBe(69);
|
||||
});
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="int_field" title="ProgressBarTitle" widget="progressbar" options="{'editable': true, 'max_value': 'float_field'}" />
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_progressbar_value .o_input").toHaveValue("99", {
|
||||
message: "Initial input value should be correct",
|
||||
});
|
||||
expect(".o_progressbar_value span").toHaveText("100", {
|
||||
message: "Initial max value should be correct",
|
||||
});
|
||||
expect(".o_progressbar_title").toHaveText("ProgressBarTitle");
|
||||
|
||||
await click(".o_progressbar_value .o_input");
|
||||
await edit("69", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_progressbar_value .o_input").toHaveValue("69");
|
||||
expect(".o_progressbar_value span").toHaveText("100", {
|
||||
message: "Max value is still the same be correct",
|
||||
});
|
||||
expect(".o_progressbar_title").toHaveText("ProgressBarTitle");
|
||||
});
|
||||
|
||||
test("force readonly in kanban", async (assert) => {
|
||||
expect.assertions(2);
|
||||
Partner._records[0].int_field = 99;
|
||||
onRpc("web_save", () => {
|
||||
throw new Error("Not supposed to write");
|
||||
});
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true, 'max_value': 'float_field', 'readonly': True}" />
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(".o_progressbar").toHaveText("99\n/\n100");
|
||||
expect(".o_progressbar_value .o_input").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("ProgressBarField: readonly and editable attrs/options in kanban", async () => {
|
||||
expect.assertions(4);
|
||||
Partner._records[0].int_field = 29;
|
||||
Partner._records[0].int_field2 = 59;
|
||||
Partner._records[0].int_field3 = 99;
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="int_field" readonly="1" widget="progressbar" options="{'max_value': 'float_field'}" />
|
||||
<field name="int_field2" widget="progressbar" options="{'max_value': 'float_field'}" />
|
||||
<field name="int_field3" widget="progressbar" options="{'editable': true, 'max_value': 'float_field'}" />
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect("[name='int_field'] .o_progressbar_value .o_input").toHaveCount(0, {
|
||||
message: "the field is still in readonly since there is readonly attribute",
|
||||
});
|
||||
expect("[name='int_field2'] .o_progressbar_value .o_input").toHaveCount(0, {
|
||||
message: "the field is still in readonly since there is readonly attribute",
|
||||
});
|
||||
expect("[name='int_field3'] .o_progressbar_value .o_input").toHaveCount(1, {
|
||||
message: "the field is still in readonly since there is readonly attribute",
|
||||
});
|
||||
|
||||
await click(".o_field_progressbar[name='int_field3'] .o_progressbar_value .o_input");
|
||||
await edit("69", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect(".o_field_progressbar[name='int_field3'] .o_progressbar_value .o_input").toHaveValue(
|
||||
"69",
|
||||
{ message: "New value should be different than initial after click" }
|
||||
);
|
||||
});
|
||||
|
||||
test("ProgressBarField: write float instead of int works, in locale", async () => {
|
||||
expect.assertions(4);
|
||||
Partner._records[0].int_field = 99;
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
decimal_point: ":",
|
||||
thousands_sep: "#",
|
||||
},
|
||||
});
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].int_field).toBe(1037);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(queryValue(".o_progressbar_value .o_input") + queryText(".o_progressbar")).toBe("99%", {
|
||||
message: "Initial value should be correct",
|
||||
});
|
||||
|
||||
expect(".o_form_view .o_form_editable").toHaveCount(1, { message: "Form in edit mode" });
|
||||
|
||||
await click(".o_field_widget input");
|
||||
await animationFrame();
|
||||
await edit("1#037:9", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
await clickSave();
|
||||
await animationFrame();
|
||||
expect(".o_progressbar_value .o_input").toHaveValue("1k", {
|
||||
message: "New value should be different than initial after click",
|
||||
});
|
||||
});
|
||||
|
||||
test("ProgressBarField: write gibberish instead of int throws warning", async () => {
|
||||
Partner._records[0].int_field = 99;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_progressbar_value .o_input").toHaveValue("99", {
|
||||
message: "Initial value in input is correct",
|
||||
});
|
||||
|
||||
await click(".o_progressbar_value .o_input");
|
||||
await animationFrame();
|
||||
await edit("trente sept virgule neuf", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
await click(".o_form_button_save");
|
||||
await animationFrame();
|
||||
expect(".o_form_status_indicator span.text-danger").toHaveCount(1, {
|
||||
message: "The form has not been saved",
|
||||
});
|
||||
expect(".o_form_button_save").toHaveProperty("disabled", true, {
|
||||
message: "save button is disabled",
|
||||
});
|
||||
});
|
||||
|
||||
test("ProgressBarField: color is correctly set when value > max value", async () => {
|
||||
Partner._records[0].float_field = 101;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="float_field" widget="progressbar" options="{'overflow_class': 'bg-warning'}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(".o_progressbar .bg-warning").toHaveCount(1, {
|
||||
message: "As the value has excedded the max value, the color should be set to bg-warning",
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,253 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { check, click, queryRect } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
bar = fields.Boolean({ default: true });
|
||||
int_field = fields.Integer();
|
||||
trululu = fields.Many2one({ relation: "partner" });
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
color = fields.Selection({
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
});
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
bar: true,
|
||||
int_field: 10,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
display_name: "third record",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
display_name = fields.Char();
|
||||
_records = [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
test("radio field on a many2one in a new record", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="product_id" widget="radio"/></form>`,
|
||||
});
|
||||
|
||||
expect("div.o_radio_item").toHaveCount(2);
|
||||
expect("input.o_radio_input").toHaveCount(2);
|
||||
expect(".o_field_radio:first").toHaveText("xphone\nxpad");
|
||||
expect("input.o_radio_input:checked").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("required radio field on a many2one", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="product_id" widget="radio" required="1"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_radio input:checked").toHaveCount(0);
|
||||
await clickSave();
|
||||
expect(".o_notification_title:first").toHaveText("Invalid fields:");
|
||||
expect(".o_notification_content:first").toHaveProperty(
|
||||
"innerHTML",
|
||||
"<ul><li>Product</li></ul>"
|
||||
);
|
||||
expect(".o_notification_bar:first").toHaveClass("bg-danger");
|
||||
});
|
||||
|
||||
test("radio field change value by onchange", async () => {
|
||||
Partner._fields.bar = fields.Boolean({
|
||||
default: true,
|
||||
onChange: (obj) => {
|
||||
obj.product_id = obj.bar ? [41] : [37];
|
||||
obj.color = obj.bar ? "red" : "black";
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar" />
|
||||
<field name="product_id" widget="radio" />
|
||||
<field name="color" widget="radio" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(".o_field_boolean input[type='checkbox']");
|
||||
await animationFrame();
|
||||
expect("input.o_radio_input[data-value='37']").toBeChecked();
|
||||
expect("input.o_radio_input[data-value='black']").toBeChecked();
|
||||
|
||||
await click(".o_field_boolean input[type='checkbox']");
|
||||
await animationFrame();
|
||||
expect("input.o_radio_input[data-value='41']").toBeChecked();
|
||||
expect("input.o_radio_input[data-value='red']").toBeChecked();
|
||||
});
|
||||
|
||||
test("radio field on a selection in a new record", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="color" widget="radio"/></form>`,
|
||||
});
|
||||
|
||||
expect("div.o_radio_item").toHaveCount(2);
|
||||
expect("input.o_radio_input").toHaveCount(2, { message: "should have 2 possible choices" });
|
||||
expect(".o_field_radio").toHaveText("Red\nBlack");
|
||||
|
||||
// click on 2nd option
|
||||
await click("input.o_radio_input:eq(1)");
|
||||
await animationFrame();
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect("input.o_radio_input[data-value=black]").toBeChecked({
|
||||
message: "should have saved record with correct value",
|
||||
});
|
||||
});
|
||||
|
||||
test("two radio field with same selection", async () => {
|
||||
Partner._fields.color_2 = { ...Partner._fields.color };
|
||||
Partner._records[0].color = "black";
|
||||
Partner._records[0].color_2 = "black";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="color" widget="radio"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="color_2" widget="radio"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect("[name='color'] input.o_radio_input[data-value=black]").toBeChecked();
|
||||
expect("[name='color_2'] input.o_radio_input[data-value=black]").toBeChecked();
|
||||
|
||||
// click on Red
|
||||
await click("[name='color_2'] label");
|
||||
await animationFrame();
|
||||
|
||||
expect("[name='color'] input.o_radio_input[data-value=black]").toBeChecked();
|
||||
expect("[name='color_2'] input.o_radio_input[data-value=red]").toBeChecked();
|
||||
});
|
||||
|
||||
test("radio field has o_horizontal or o_vertical class", async () => {
|
||||
Partner._fields.color2 = Partner._fields.color;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="color" widget="radio" />
|
||||
<field name="color2" widget="radio" options="{'horizontal': True}" />
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_field_radio > div.o_vertical").toHaveCount(1, {
|
||||
message: "should have o_vertical class",
|
||||
});
|
||||
|
||||
const verticalRadio = ".o_field_radio > div.o_vertical:first";
|
||||
expect(`${verticalRadio} .o_radio_item:first`).toHaveRect({
|
||||
right: queryRect(`${verticalRadio} .o_radio_item:last`).right,
|
||||
});
|
||||
expect(".o_field_radio > div.o_horizontal").toHaveCount(1, {
|
||||
message: "should have o_horizontal class",
|
||||
});
|
||||
const horizontalRadio = ".o_field_radio > div.o_horizontal:first";
|
||||
expect(`${horizontalRadio} .o_radio_item:first`).toHaveRect({
|
||||
top: queryRect(`${horizontalRadio} .o_radio_item:last`).top,
|
||||
});
|
||||
});
|
||||
|
||||
test("radio field with numerical keys encoded as strings", async () => {
|
||||
Partner._fields.selection = fields.Selection({
|
||||
selection: [
|
||||
["0", "Red"],
|
||||
["1", "Black"],
|
||||
],
|
||||
});
|
||||
|
||||
onRpc("partner", "web_save", ({ args }) => expect.step(args[1].selection));
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="selection" widget="radio"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget").toHaveText("Red\nBlack");
|
||||
expect(".o_radio_input:checked").toHaveCount(0);
|
||||
|
||||
await check("input.o_radio_input:last");
|
||||
await animationFrame();
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget").toHaveText("Red\nBlack");
|
||||
expect(".o_radio_input[data-value='1']").toBeChecked();
|
||||
|
||||
expect.verifySteps(["1"]);
|
||||
});
|
||||
|
||||
test("radio field is empty", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: /* xml */ `
|
||||
<form edit="0">
|
||||
<field name="trululu" widget="radio" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=trululu]").toHaveClass("o_field_empty");
|
||||
expect(".o_radio_input").toHaveCount(3);
|
||||
expect(".o_radio_input:disabled").toHaveCount(3);
|
||||
expect(".o_radio_input:checked").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,989 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { click, edit, press, queryAllValues, queryFirst, select } from "@odoo/hoot-dom";
|
||||
import { animationFrame, Deferred, runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
mountViewInDialog,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
foo = fields.Char({ default: "My little Foo Value" });
|
||||
bar = fields.Boolean({ default: true });
|
||||
int_field = fields.Integer();
|
||||
p = fields.One2many({
|
||||
relation: "partner",
|
||||
relation_field: "trululu",
|
||||
});
|
||||
turtles = fields.One2many({
|
||||
relation: "turtle",
|
||||
relation_field: "turtle_trululu",
|
||||
});
|
||||
trululu = fields.Many2one({ relation: "partner" });
|
||||
color = fields.Selection({
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
});
|
||||
reference = fields.Reference({
|
||||
selection: [
|
||||
["product", "Product"],
|
||||
["partner.type", "Partner Type"],
|
||||
["partner", "Partner"],
|
||||
],
|
||||
});
|
||||
reference_char = fields.Char();
|
||||
model_id = fields.Many2one({ relation: "ir.model" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "first record",
|
||||
bar: true,
|
||||
foo: "yop",
|
||||
int_field: 10,
|
||||
p: [],
|
||||
turtles: [2],
|
||||
trululu: 4,
|
||||
reference: "product,37",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "second record",
|
||||
bar: true,
|
||||
foo: "blip",
|
||||
int_field: 9,
|
||||
p: [],
|
||||
trululu: 1,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "aaa",
|
||||
bar: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 37, name: "xphone" },
|
||||
{ id: 41, name: "xpad" },
|
||||
];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 12, name: "gold" },
|
||||
{ id: 14, name: "silver" },
|
||||
];
|
||||
}
|
||||
|
||||
class Turtle extends models.Model {
|
||||
name = fields.Char();
|
||||
turtle_trululu = fields.Many2one({ relation: "partner" });
|
||||
turtle_ref = fields.Reference({
|
||||
selection: [
|
||||
["product", "Product"],
|
||||
["partner", "Partner"],
|
||||
],
|
||||
});
|
||||
partner_ids = fields.Many2many({ relation: "partner" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "leonardo", partner_ids: [] },
|
||||
{ id: 2, name: "donatello", partner_ids: [2, 4] },
|
||||
{ id: 3, name: "raphael", partner_ids: [], turtle_ref: "product,37" },
|
||||
];
|
||||
}
|
||||
|
||||
class IrModel extends models.Model {
|
||||
_name = "ir.model";
|
||||
|
||||
name = fields.Char();
|
||||
model = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 17, name: "Partner", model: "partner" },
|
||||
{ id: 20, name: "Product", model: "product" },
|
||||
{ id: 21, name: "Partner Type", model: "partner.type" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product, PartnerType, Turtle, IrModel]);
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("ReferenceField can quick create models", async () => {
|
||||
onRpc(({ method }) => expect.step(method));
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="reference" /></form>`,
|
||||
});
|
||||
|
||||
await click("select");
|
||||
await select("partner");
|
||||
await animationFrame();
|
||||
await click(".o_field_widget[name='reference'] input");
|
||||
await edit("new partner");
|
||||
await runAllTimers();
|
||||
await click(".o_field_widget[name='reference'] .o_m2o_dropdown_option_create");
|
||||
await animationFrame();
|
||||
|
||||
await clickSave();
|
||||
|
||||
// The name_create method should have been called
|
||||
expect.verifySteps([
|
||||
"get_views",
|
||||
"onchange",
|
||||
"name_search", // for the select
|
||||
"name_search", // for the spawned many2one
|
||||
"name_create",
|
||||
"web_save",
|
||||
]);
|
||||
});
|
||||
|
||||
test("ReferenceField respects no_quick_create", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="reference" options="{'no_quick_create': 1}" /></form>`,
|
||||
});
|
||||
|
||||
await click("select");
|
||||
await select("partner");
|
||||
await animationFrame();
|
||||
await click(".o_field_widget[name='reference'] input");
|
||||
await edit("new partner");
|
||||
await runAllTimers();
|
||||
expect(".ui-autocomplete .o_m2o_dropdown_option").toHaveCount(1, {
|
||||
message: "Dropdown should be opened and have only one item",
|
||||
});
|
||||
expect(".ui-autocomplete .o_m2o_dropdown_option").toHaveClass(
|
||||
"o_m2o_dropdown_option_create_edit"
|
||||
);
|
||||
});
|
||||
|
||||
test("ReferenceField in modal readonly mode", async () => {
|
||||
Partner._records[0].p = [2];
|
||||
Partner._records[1].trululu = 1;
|
||||
Partner._records[1].reference = "product,41";
|
||||
|
||||
Partner._views[["form", false]] = /* xml */ `
|
||||
<form>
|
||||
<field name="display_name" />
|
||||
<field name="reference" />
|
||||
</form>
|
||||
`;
|
||||
Partner._views[["list", false]] = /* xml */ `
|
||||
<list>
|
||||
<field name="display_name"/>
|
||||
<field name="reference" />
|
||||
</list>
|
||||
`;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form edit="0">
|
||||
<field name="reference" />
|
||||
<field name="p" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// Current Form
|
||||
expect(".o_field_widget[name=reference] .o_form_uri").toHaveText("xphone", {
|
||||
message: "the field reference of the form should have the right value",
|
||||
});
|
||||
expect(queryFirst(".o_data_cell")).toHaveText("second record", {
|
||||
message: "the list should have one record",
|
||||
});
|
||||
await click(".o_data_cell");
|
||||
await animationFrame();
|
||||
|
||||
// In modal
|
||||
expect(".modal-lg").toHaveCount(1);
|
||||
expect(".modal-lg .o_field_widget[name=reference] .o_form_uri").toHaveText("xpad", {
|
||||
message: "The field reference in the modal should have the right value",
|
||||
});
|
||||
});
|
||||
|
||||
test("ReferenceField in modal write mode", async () => {
|
||||
Partner._records[0].p = [2];
|
||||
Partner._records[1].trululu = 1;
|
||||
Partner._records[1].reference = "product,41";
|
||||
|
||||
Partner._views[["form", false]] = /* xml */ `
|
||||
<form>
|
||||
<field name="display_name" />
|
||||
<field name="reference" />
|
||||
</form>
|
||||
`;
|
||||
Partner._views[["list", false]] = /* xml */ `
|
||||
<list>
|
||||
<field name="display_name"/>
|
||||
<field name="reference" />
|
||||
</list>
|
||||
`;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="reference" />
|
||||
<field name="p" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// Current Form
|
||||
expect(".o_field_widget[name=reference] option:checked").toHaveText("Product", {
|
||||
message: "The reference field's model should be Product",
|
||||
});
|
||||
expect(".o_field_widget[name=reference] .o-autocomplete--input").toHaveValue("xphone", {
|
||||
message: "The reference field's record should be xphone",
|
||||
});
|
||||
|
||||
await click(".o_data_cell");
|
||||
await animationFrame();
|
||||
|
||||
// In modal
|
||||
expect(".modal-lg").toHaveCount(1, { message: "there should be one modal opened" });
|
||||
expect(".modal-lg .o_field_widget[name=reference] option:checked").toHaveText("Product", {
|
||||
message: "The reference field's model should be Product",
|
||||
});
|
||||
expect(".modal-lg .o_field_widget[name=reference] .o-autocomplete--input").toHaveValue("xpad", {
|
||||
message: "The reference field's record should be xpad",
|
||||
});
|
||||
});
|
||||
|
||||
test("reference in form view", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
Product._views[["form", false]] = /* xml */ `
|
||||
<form>
|
||||
<field name="display_name" />
|
||||
</form>
|
||||
`;
|
||||
|
||||
onRpc(({ args, method, model }) => {
|
||||
if (method === "get_formview_action") {
|
||||
expect(args[0]).toEqual([37], {
|
||||
message: "should call get_formview_action with correct id",
|
||||
});
|
||||
return {
|
||||
res_id: 17,
|
||||
type: "ir.actions.act_window",
|
||||
target: "current",
|
||||
res_model: "res.partner",
|
||||
};
|
||||
}
|
||||
if (method === "get_formview_id") {
|
||||
expect(args[0]).toEqual([37], {
|
||||
message: "should call get_formview_id with correct id",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (method === "name_search") {
|
||||
expect(model).toBe("partner.type", {
|
||||
message: "the name_search should be done on the newly set model",
|
||||
});
|
||||
}
|
||||
if (method === "web_save") {
|
||||
expect(model).toBe("partner", { message: "should write on the current model" });
|
||||
expect(args).toEqual([[1], { reference: "partner.type,12" }], {
|
||||
message: "should write the correct value",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
expect(action.res_id).toBe(17, {
|
||||
message: "should do a do_action with correct parameters",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await mountViewInDialog({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="reference" string="custom label"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_field_many2one_selection").toHaveCount(1, {
|
||||
message: "should contain one many2one",
|
||||
});
|
||||
expect(".o_field_widget select").toHaveValue("product", {
|
||||
message: "widget should contain one select with the model",
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("xphone", {
|
||||
message: "widget should contain one input with the record",
|
||||
});
|
||||
|
||||
expect(queryAllValues(".o_field_widget select > option")).toEqual(
|
||||
["", "product", "partner.type", "partner"],
|
||||
{
|
||||
message: "the options should be correctly set",
|
||||
}
|
||||
);
|
||||
|
||||
await click(".o_external_button");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dialog:not(.o_inactive_modal) .modal-title").toHaveText("Open: custom label", {
|
||||
message: "dialog title should display the custom string label",
|
||||
});
|
||||
|
||||
await click(".o_dialog:not(.o_inactive_modal) .o_form_button_cancel");
|
||||
await animationFrame();
|
||||
|
||||
await select("partner.type", { target: ".o_field_widget select" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("", {
|
||||
message: "many2one value should be reset after model change",
|
||||
});
|
||||
|
||||
await click(".o_field_widget[name=reference] input");
|
||||
await animationFrame();
|
||||
await click(".o_field_widget[name=reference] .ui-menu-item");
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget[name=reference] input").toHaveValue("gold", {
|
||||
message: "should contain a link with the new value",
|
||||
});
|
||||
});
|
||||
|
||||
test("Many2One 'Search more...' updates on resModel change", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
Product._views[["list", false]] = /* xml */ `<list><field name="display_name"/></list>`;
|
||||
Product._views[["search", false]] = /* xml */ `<search/>`;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="reference"/></form>`,
|
||||
});
|
||||
|
||||
// Selecting a relation
|
||||
await click("div.o_field_reference select.o_input");
|
||||
await select("partner.type");
|
||||
|
||||
// Selecting another relation
|
||||
await click("div.o_field_reference select.o_input");
|
||||
await select("product");
|
||||
await animationFrame();
|
||||
|
||||
// Opening the Search More... option
|
||||
await click("div.o_field_reference input.o_input");
|
||||
await animationFrame();
|
||||
await click("div.o_field_reference .o_m2o_dropdown_option_search_more");
|
||||
await animationFrame();
|
||||
|
||||
expect(queryFirst("div.modal td.o_data_cell")).toHaveText("xphone", {
|
||||
message: "The search more should lead to the values of product.",
|
||||
});
|
||||
});
|
||||
|
||||
test("computed reference field changed by onchange to 'False,0' value", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
Partner._onChanges.bar = (obj) => {
|
||||
if (!obj.bar) {
|
||||
obj.reference_char = "False,0";
|
||||
}
|
||||
};
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1]).toEqual({
|
||||
bar: false,
|
||||
reference_char: "False,0",
|
||||
});
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<field name="reference_char" widget="reference"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// trigger the onchange to set a value for the reference field
|
||||
await click(".o_field_boolean input");
|
||||
await animationFrame();
|
||||
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("interact with reference field changed by onchange", async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
Partner._onChanges.bar = (obj) => {
|
||||
if (!obj.bar) {
|
||||
obj.reference = "partner,1";
|
||||
}
|
||||
};
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1]).toEqual({
|
||||
bar: false,
|
||||
reference: "partner,4",
|
||||
});
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<field name="reference"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// trigger the onchange to set a value for the reference field
|
||||
await click(".o_field_boolean input");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_widget[name=reference] select").toHaveValue("partner");
|
||||
|
||||
// manually update reference field
|
||||
queryFirst(".o_field_widget[name=reference] input").tabIndex = 0;
|
||||
await click(".o_field_widget[name=reference] input");
|
||||
await edit("aaa");
|
||||
await runAllTimers();
|
||||
await click(".ui-autocomplete .ui-menu-item");
|
||||
|
||||
// save
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("default_get and onchange with a reference field", async () => {
|
||||
Partner._fields.reference = fields.Reference({
|
||||
selection: [
|
||||
["product", "Product"],
|
||||
["partner.type", "Partner Type"],
|
||||
["partner", "Partner"],
|
||||
],
|
||||
default: "product,37",
|
||||
});
|
||||
Partner._onChanges.int_field = (obj) => {
|
||||
if (obj.int_field) {
|
||||
obj.reference = "partner.type," + obj.int_field;
|
||||
}
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" />
|
||||
<field name="reference" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='reference'] select").toHaveValue("product", {
|
||||
message: "reference field model should be correctly set",
|
||||
});
|
||||
expect(".o_field_widget[name='reference'] input").toHaveValue("xphone", {
|
||||
message: "reference field value should be correctly set",
|
||||
});
|
||||
|
||||
// trigger onchange
|
||||
await click(".o_field_widget[name=int_field] input");
|
||||
await edit(12, { confirm: "enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_widget[name='reference'] select").toHaveValue("partner.type", {
|
||||
message: "reference field model should be correctly set",
|
||||
});
|
||||
expect(".o_field_widget[name='reference'] input").toHaveValue("gold", {
|
||||
message: "reference field value should be correctly set",
|
||||
});
|
||||
});
|
||||
|
||||
test("default_get a reference field in a x2m", async () => {
|
||||
Partner._fields.turtles = fields.One2many({
|
||||
relation: "turtle",
|
||||
relation_field: "turtle_trululu",
|
||||
default: [[0, 0, { turtle_ref: "product,37" }]],
|
||||
});
|
||||
Turtle._views[["form", false]] = /* xml */ `
|
||||
<form>
|
||||
<field name="display_name" />
|
||||
<field name="turtle_ref" />
|
||||
</form>
|
||||
`;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="turtles">
|
||||
<list>
|
||||
<field name="turtle_ref" />
|
||||
</list>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect('.o_field_widget[name="turtles"] .o_data_row').toHaveText("xphone", {
|
||||
message: "the default value should be correctly handled",
|
||||
});
|
||||
});
|
||||
|
||||
test("ReferenceField on char field, reset by onchange", async () => {
|
||||
Partner._records[0].foo = "product,37";
|
||||
Partner._onChanges.int_field = (obj) => (obj.foo = "product," + obj.int_field);
|
||||
let nbNameGet = 0;
|
||||
onRpc("product", "read", ({ args }) => {
|
||||
if (args[1].length === 1 && args[1][0] === "display_name") {
|
||||
nbNameGet++;
|
||||
}
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" />
|
||||
<field name="foo" widget="reference" readonly="1" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(nbNameGet).toBe(1, { message: "the first name_get should have been done" });
|
||||
expect(".o_field_widget[name=foo]").toHaveText("xphone", {
|
||||
message: "foo field should be correctly set",
|
||||
});
|
||||
// trigger onchange
|
||||
await click(".o_field_widget[name=int_field] input");
|
||||
await edit(41, { confirm: "enter" });
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(nbNameGet).toBe(2, { message: "the second name_get should have been done" });
|
||||
expect(".o_field_widget[name=foo]").toHaveText("xpad", {
|
||||
message: "foo field should have been updated",
|
||||
});
|
||||
});
|
||||
|
||||
test("reference and list navigation", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list editable="bottom">
|
||||
<field name="reference" />
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
// edit first row
|
||||
await click(".o_data_row .o_data_cell");
|
||||
await animationFrame();
|
||||
expect(".o_data_row [name='reference'] input").toBeFocused();
|
||||
|
||||
await press("Tab");
|
||||
await animationFrame();
|
||||
expect(".o_data_row:nth-child(2) [name='reference'] select").toBeFocused();
|
||||
});
|
||||
|
||||
test("ReferenceField with model_field option", async () => {
|
||||
Partner._records[0].reference = false;
|
||||
Partner._records[0].model_id = 20;
|
||||
Partner._records[1].name = "John Smith";
|
||||
Product._records[0].name = "Product 1";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="model_id" />
|
||||
<field name="reference" options="{'model_field': 'model_id'}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect("select").toHaveCount(0, {
|
||||
message: "the selection list of the reference field should not exist.",
|
||||
});
|
||||
expect(".o_field_widget[name='reference'] input").toHaveValue("", {
|
||||
message: "no record should be selected in the reference field",
|
||||
});
|
||||
|
||||
await click(".o_field_widget[name='reference'] input");
|
||||
await edit("Product 1");
|
||||
await runAllTimers();
|
||||
await click(".ui-autocomplete .ui-menu-item:first-child");
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='reference'] input").toHaveValue("Product 1", {
|
||||
message: "the Product 1 record should be selected in the reference field",
|
||||
});
|
||||
|
||||
await click(".o_field_widget[name='model_id'] input");
|
||||
await edit("Partner");
|
||||
await runAllTimers();
|
||||
await click(".ui-autocomplete .ui-menu-item:first-child");
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='reference'] input").toHaveValue("", {
|
||||
message: "no record should be selected in the reference field",
|
||||
});
|
||||
|
||||
await click(".o_field_widget[name='reference'] input");
|
||||
await edit("John");
|
||||
await runAllTimers();
|
||||
await click(".ui-autocomplete .ui-menu-item:first-child");
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='reference'] input").toHaveValue("John Smith", {
|
||||
message: "the John Smith record should be selected in the reference field",
|
||||
});
|
||||
});
|
||||
|
||||
test("ReferenceField with model_field option (model_field not synchronized with reference)", async () => {
|
||||
// Checks that the data is not modified even though it is not synchronized.
|
||||
// Not synchronized = model_id contains a different model than the one used in reference.
|
||||
Partner._records[0].reference = "partner,1";
|
||||
Partner._records[0].model_id = 20;
|
||||
Partner._records[0].name = "John Smith";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="model_id" />
|
||||
<field name="reference" options="{'model_field': 'model_id'}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect("select").toHaveCount(0, {
|
||||
message: "the selection list of the reference field should not exist.",
|
||||
});
|
||||
expect(".o_field_widget[name='model_id'] input").toHaveValue("Product", {
|
||||
message: "the Product model should be selected in the model_id field",
|
||||
});
|
||||
expect(".o_field_widget[name='reference'] input").toHaveValue("John Smith", {
|
||||
message: "the John Smith record should be selected in the reference field",
|
||||
});
|
||||
});
|
||||
|
||||
test("Reference field with default value in list view", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
onRpc(({ method, args }) => {
|
||||
if (method === "onchange") {
|
||||
return {
|
||||
value: {
|
||||
reference: {
|
||||
id: { id: 2, model: "partner" },
|
||||
name: "second record",
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (method === "web_save") {
|
||||
expect(args[1].reference).toBe("partner,2");
|
||||
}
|
||||
});
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list string="Test" editable="top">
|
||||
<field name="reference"/>
|
||||
<field name="name"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
await click(".o_control_panel_main_buttons .o_list_button_add");
|
||||
await animationFrame();
|
||||
await click('.o_list_char[name="name"] input');
|
||||
await edit("Blabla");
|
||||
await runAllTimers();
|
||||
await click(".o_control_panel_main_buttons .o_list_button_save");
|
||||
await animationFrame();
|
||||
});
|
||||
|
||||
test("ReferenceField with model_field option (tree list in form view)", async () => {
|
||||
Turtle._records[0].partner_ids = [1];
|
||||
Partner._records[0].reference = "product,41";
|
||||
Partner._records[0].model_id = 20;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="name" />
|
||||
<field name="model_id" />
|
||||
<field name="reference" options="{'model_field': 'model_id'}" class="reference_field" />
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".reference_field").toHaveText("xpad");
|
||||
|
||||
// Select the second product without changing the model
|
||||
await click(".o_list_table .reference_field");
|
||||
await animationFrame();
|
||||
|
||||
await click(".o_list_table .reference_field input");
|
||||
await animationFrame();
|
||||
|
||||
// Enter to select it
|
||||
await press("Enter");
|
||||
await animationFrame();
|
||||
|
||||
expect(".reference_field input").toHaveValue("xphone", {
|
||||
message: "should have selected the first product",
|
||||
});
|
||||
});
|
||||
|
||||
test("edit a record containing a ReferenceField with model_field option (list in form view)", async () => {
|
||||
Turtle._records[0].partner_ids = [1];
|
||||
Partner._records[0].reference = "product,41";
|
||||
Partner._records[0].model_id = 20;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="name" />
|
||||
<field name="model_id" />
|
||||
<field name="reference" options='{"model_field": "model_id"}'/>
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_list_table [name='name']").toHaveText("first record");
|
||||
expect(".o_list_table [name='reference']").toHaveText("xpad");
|
||||
|
||||
await click(".o_list_table .o_data_cell");
|
||||
await animationFrame();
|
||||
|
||||
await click(".o_list_table [name='name'] input");
|
||||
await edit("plop");
|
||||
await animationFrame();
|
||||
await click(".o_form_view");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_list_table [name='name']").toHaveText("plop");
|
||||
expect(".o_list_table [name='reference']").toHaveText("xpad");
|
||||
});
|
||||
|
||||
test("Change model field of a ReferenceField then select an invalid value (tree list in form view)", async () => {
|
||||
Turtle._records[0].partner_ids = [1];
|
||||
Partner._records[0].reference = "product,41";
|
||||
Partner._records[0].model_id = 20;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="name" />
|
||||
<field name="model_id"/>
|
||||
<field name="reference" required="true" options="{'model_field': 'model_id'}" class="reference_field" />
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".reference_field").toHaveText("xpad");
|
||||
expect(".o_list_many2one").toHaveText("Product");
|
||||
|
||||
await click(".o_list_table td.o_list_many2one");
|
||||
await animationFrame();
|
||||
await click(".o_list_table .o_list_many2one input");
|
||||
await animationFrame();
|
||||
//Select the "Partner" option, different from original "Product"
|
||||
await click(
|
||||
".o_list_table .o_list_many2one .o_input_dropdown .dropdown-item:contains(Partner)"
|
||||
);
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".reference_field input").toHaveValue("");
|
||||
expect(".o_list_many2one input").toHaveValue("Partner");
|
||||
//Void the associated, required, "reference" field and make sure the form marks the field as required
|
||||
await click(".o_list_table .reference_field input");
|
||||
const textInput = queryFirst(".o_list_table .reference_field input");
|
||||
textInput.setSelectionRange(0, textInput.value.length);
|
||||
await click(".o_list_table .reference_field input");
|
||||
await press("Backspace");
|
||||
await click(".o_form_view_container");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_list_table .reference_field.o_field_invalid").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("model selector is displayed only when it should be", async () => {
|
||||
//The model selector should be only displayed if
|
||||
//there is no hide_model=True options AND no model_field specified
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="reference" options="{'model_field': 'model_id'}" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="reference" options="{'model_field': 'model_id', 'hide_model': True}" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="reference" options="{'hide_model': True}" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="reference" />
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_inner_group:eq(0) select").toHaveCount(0, {
|
||||
message:
|
||||
"the selection list of the reference field should not exist when model_field is specified.",
|
||||
});
|
||||
expect(".o_inner_group:eq(1) select").toHaveCount(0, {
|
||||
message:
|
||||
"the selection list of the reference field should not exist when model_field is specified and hide_model=True.",
|
||||
});
|
||||
expect(".o_inner_group:eq(2) select").toHaveCount(0, {
|
||||
message: "the selection list of the reference field should not exist when hide_model=True.",
|
||||
});
|
||||
expect(".o_inner_group:eq(3) select").toHaveCount(1, {
|
||||
message:
|
||||
"the selection list of the reference field should exist when hide_model=False and no model_field specified.",
|
||||
});
|
||||
});
|
||||
|
||||
test("reference field should await fetch model before render", async () => {
|
||||
Partner._records[0].model_id = 20;
|
||||
|
||||
const def = new Deferred();
|
||||
onRpc(async (args) => {
|
||||
if (args.method === "read" && args.model === "ir.model") {
|
||||
await def;
|
||||
}
|
||||
});
|
||||
mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="model_id" invisible="1"/>
|
||||
<field name="reference" options="{'model_field': 'model_id'}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await animationFrame();
|
||||
expect(".o_form_view").toHaveCount(0);
|
||||
def.resolve();
|
||||
|
||||
await animationFrame();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("do not ask for display_name if field is invisible", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
onRpc("web_read", ({ kwargs }) => {
|
||||
expect(kwargs.specification).toEqual({
|
||||
display_name: {},
|
||||
reference: {
|
||||
fields: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="reference" invisible="1"/></form>`,
|
||||
});
|
||||
});
|
||||
|
||||
test("reference char with list view pager navigation", async () => {
|
||||
Partner._records[0].reference_char = "product,37";
|
||||
Partner._records[1].reference_char = "product,41";
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
arch: `<form edit="0"><field name="reference_char" widget="reference" string="Record"/></form>`,
|
||||
});
|
||||
expect(".o_field_reference").toHaveText("xphone");
|
||||
await click(".o_pager_next");
|
||||
await animationFrame();
|
||||
expect(".o_field_reference").toHaveText("xpad");
|
||||
});
|
||||
|
|
@ -0,0 +1,429 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { click, edit, queryAll, queryAllTexts, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame, mockDate } from "@odoo/hoot-mock";
|
||||
import { getPickerCell } from "@web/../tests/core/datetime/datetime_test_helpers";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
date = fields.Date({ string: "A date", searchable: true });
|
||||
datetime = fields.Datetime({ string: "A datetime", searchable: true });
|
||||
}
|
||||
beforeEach(() => {
|
||||
onRpc("has_group", () => true);
|
||||
});
|
||||
defineModels([Partner]);
|
||||
|
||||
test("RemainingDaysField on a date field in list view", async () => {
|
||||
mockDate("2017-10-08 15:35:11");
|
||||
|
||||
Partner._records = [
|
||||
{ id: 1, date: "2017-10-08" }, // today
|
||||
{ id: 2, date: "2017-10-09" }, // tomorrow
|
||||
{ id: 3, date: "2017-10-07" }, // yesterday
|
||||
{ id: 4, date: "2017-10-10" }, // + 2 days
|
||||
{ id: 5, date: "2017-10-05" }, // - 3 days
|
||||
{ id: 6, date: "2018-02-08" }, // + 4 months (diff >= 100 days)
|
||||
{ id: 7, date: "2017-06-08" }, // - 4 months (diff >= 100 days)
|
||||
{ id: 8, date: false },
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list><field name="date" widget="remaining_days" /></list>`,
|
||||
});
|
||||
|
||||
const cells = queryAll(".o_data_cell");
|
||||
expect(cells[0]).toHaveText("Today");
|
||||
expect(cells[1]).toHaveText("Tomorrow");
|
||||
expect(cells[2]).toHaveText("Yesterday");
|
||||
expect(cells[3]).toHaveText("In 2 days");
|
||||
expect(cells[4]).toHaveText("3 days ago");
|
||||
expect(cells[5]).toHaveText("02/08/2018");
|
||||
expect(cells[6]).toHaveText("06/08/2017");
|
||||
expect(cells[7]).toHaveText("");
|
||||
|
||||
expect(queryOne(".o_field_widget > div", { root: cells[0] })).toHaveAttribute(
|
||||
"title",
|
||||
"10/08/2017"
|
||||
);
|
||||
expect(queryOne(".o_field_widget > div", { root: cells[0] })).toHaveClass([
|
||||
"fw-bold",
|
||||
"text-warning",
|
||||
]);
|
||||
expect(queryOne(".o_field_widget > div", { root: cells[1] })).not.toHaveClass([
|
||||
"fw-bold",
|
||||
"text-warning",
|
||||
"text-danger",
|
||||
]);
|
||||
expect(queryOne(".o_field_widget > div", { root: cells[2] })).toHaveClass([
|
||||
"fw-bold",
|
||||
"text-danger",
|
||||
]);
|
||||
expect(queryOne(".o_field_widget > div", { root: cells[3] })).not.toHaveClass([
|
||||
"fw-bold",
|
||||
"text-warning",
|
||||
"text-danger",
|
||||
]);
|
||||
expect(queryOne(".o_field_widget > div", { root: cells[4] })).toHaveClass([
|
||||
"fw-bold",
|
||||
"text-danger",
|
||||
]);
|
||||
expect(queryOne(".o_field_widget > div", { root: cells[5] })).not.toHaveClass([
|
||||
"fw-bold",
|
||||
"text-warning",
|
||||
"text-danger",
|
||||
]);
|
||||
expect(queryOne(".o_field_widget > div", { root: cells[6] })).toHaveClass([
|
||||
"fw-bold",
|
||||
"text-danger",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("RemainingDaysField on a date field in multi edit list view", async () => {
|
||||
mockDate("2017-10-08 15:35:11"); // October 8 2017, 15:35:11
|
||||
|
||||
Partner._records = [
|
||||
{ id: 1, date: "2017-10-08" }, // today
|
||||
{ id: 2, date: "2017-10-09" }, // tomorrow
|
||||
{ id: 8, date: false },
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list multi_edit="1"><field name="date" widget="remaining_days" /></list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell").slice(0, 2)).toEqual(["Today", "Tomorrow"]);
|
||||
|
||||
// select two records and edit them
|
||||
await click(".o_data_row:eq(0) .o_list_record_selector input:first");
|
||||
await animationFrame();
|
||||
await click(".o_data_row:eq(1) .o_list_record_selector input:first");
|
||||
await animationFrame();
|
||||
|
||||
await click(".o_data_row:eq(0) .o_data_cell:first");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_remaining_days input").toHaveCount(1);
|
||||
|
||||
await click(".o_field_remaining_days input");
|
||||
await edit("10/10/2017", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(".modal .o_field_widget").toHaveText("In 2 days", {
|
||||
message: "should have 'In 2 days' value to change",
|
||||
});
|
||||
await click(".modal .modal-footer .btn-primary");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:eq(0) .o_data_cell:first").toHaveText("In 2 days", {
|
||||
message: "should have 'In 2 days' as date field value",
|
||||
});
|
||||
expect(".o_data_row:eq(1) .o_data_cell:first").toHaveText("In 2 days", {
|
||||
message: "should have 'In 2 days' as date field value",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("RemainingDaysField, enter wrong value manually in multi edit list view", async () => {
|
||||
mockDate("2017-10-08 15:35:11"); // October 8 2017, 15:35:11
|
||||
Partner._records = [
|
||||
{ id: 1, date: "2017-10-08" }, // today
|
||||
{ id: 2, date: "2017-10-09" }, // tomorrow
|
||||
{ id: 8, date: false },
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list multi_edit="1"><field name="date" widget="remaining_days" /></list>`,
|
||||
});
|
||||
|
||||
const cells = queryAll(".o_data_cell");
|
||||
const rows = queryAll(".o_data_row");
|
||||
|
||||
expect(cells[0]).toHaveText("Today");
|
||||
expect(cells[1]).toHaveText("Tomorrow");
|
||||
|
||||
// select two records and edit them
|
||||
await click(".o_list_record_selector input", { root: rows[0] });
|
||||
await animationFrame();
|
||||
await click(".o_list_record_selector input", { root: rows[1] });
|
||||
await animationFrame();
|
||||
|
||||
await click(".o_data_cell", { root: rows[0] });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_remaining_days input").toHaveCount(1);
|
||||
|
||||
await click(".o_field_remaining_days input");
|
||||
await edit("blabla", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(0);
|
||||
expect(cells[0]).toHaveText("Today");
|
||||
expect(cells[1]).toHaveText("Tomorrow");
|
||||
});
|
||||
|
||||
test("RemainingDaysField on a date field in form view", async () => {
|
||||
mockDate("2017-10-08 15:35:11"); // October 8 2017, 15:35:11
|
||||
Partner._records = [
|
||||
{ id: 1, date: "2017-10-08" }, // today
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="date" widget="remaining_days" /></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("10/08/2017");
|
||||
|
||||
expect(".o_form_editable").toHaveCount(1);
|
||||
expect("div.o_field_widget[name='date'] input").toHaveCount(1);
|
||||
|
||||
await click(".o_field_remaining_days input");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1, { message: "datepicker should be opened" });
|
||||
|
||||
await click(getPickerCell("9"));
|
||||
await animationFrame();
|
||||
await click(".o_form_button_save");
|
||||
await animationFrame();
|
||||
expect(".o_field_widget input").toHaveValue("10/09/2017");
|
||||
});
|
||||
|
||||
test("RemainingDaysField on a date field on a new record in form", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date" widget="remaining_days" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_form_editable .o_field_widget[name='date'] input").toHaveCount(1);
|
||||
await click(".o_field_widget[name='date'] input");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("RemainingDaysField in form view (readonly)", async () => {
|
||||
mockDate("2017-10-08 15:35:11"); // October 8 2017, 15:35:11
|
||||
Partner._records = [
|
||||
{ id: 1, date: "2017-10-08", datetime: "2017-10-08 10:00:00" }, // today
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="date" widget="remaining_days" readonly="1" />
|
||||
<field name="datetime" widget="remaining_days" readonly="1" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='date']").toHaveText("Today");
|
||||
expect(".o_field_widget[name='date'] > div ").toHaveClass(["fw-bold", "text-warning"]);
|
||||
expect(".o_field_widget[name='datetime']").toHaveText("Today");
|
||||
expect(".o_field_widget[name='datetime'] > div ").toHaveClass(["fw-bold", "text-warning"]);
|
||||
});
|
||||
|
||||
test("RemainingDaysField on a datetime field in form view", async () => {
|
||||
mockDate("2017-10-08 15:35:11"); // October 8 2017, 15:35:11
|
||||
Partner._records = [
|
||||
{ id: 1, datetime: "2017-10-08 10:00:00" }, // today
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="datetime" widget="remaining_days" /></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("10/08/2017 11:00:00");
|
||||
expect("div.o_field_widget[name='datetime'] input").toHaveCount(1);
|
||||
|
||||
await click(".o_field_widget input");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1, { message: "datepicker should be opened" });
|
||||
|
||||
await click(getPickerCell("9"));
|
||||
await animationFrame();
|
||||
await click(".o_form_button_save");
|
||||
await animationFrame();
|
||||
expect(".o_field_widget input").toHaveValue("10/09/2017 11:00:00");
|
||||
});
|
||||
|
||||
test("RemainingDaysField on a datetime field in list view in UTC", async () => {
|
||||
mockDate("2017-10-08 15:35:11", 0); // October 8 2017, 15:35:11
|
||||
Partner._records = [
|
||||
{ id: 1, datetime: "2017-10-08 20:00:00" }, // today
|
||||
{ id: 2, datetime: "2017-10-09 08:00:00" }, // tomorrow
|
||||
{ id: 3, datetime: "2017-10-07 18:00:00" }, // yesterday
|
||||
{ id: 4, datetime: "2017-10-10 22:00:00" }, // + 2 days
|
||||
{ id: 5, datetime: "2017-10-05 04:00:00" }, // - 3 days
|
||||
{ id: 6, datetime: "2018-02-08 04:00:00" }, // + 4 months (diff >= 100 days)
|
||||
{ id: 7, datetime: "2017-06-08 04:00:00" }, // - 4 months (diff >= 100 days)
|
||||
{ id: 8, datetime: false },
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list><field name="datetime" widget="remaining_days" /></list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual([
|
||||
"Today",
|
||||
"Tomorrow",
|
||||
"Yesterday",
|
||||
"In 2 days",
|
||||
"3 days ago",
|
||||
"02/08/2018",
|
||||
"06/08/2017",
|
||||
"",
|
||||
]);
|
||||
|
||||
expect(".o_data_cell .o_field_widget div:first").toHaveAttribute("title", "10/08/2017");
|
||||
|
||||
const cells = queryAll(".o_data_cell div div");
|
||||
expect(cells[0]).toHaveClass(["fw-bold", "text-warning"]);
|
||||
expect(cells[1]).not.toHaveClass(["fw-bold", "text-warning", "text-danger"]);
|
||||
expect(cells[2]).toHaveClass(["fw-bold", "text-danger"]);
|
||||
expect(cells[3]).not.toHaveClass(["fw-bold", "text-warning", "text-danger"]);
|
||||
expect(cells[4]).toHaveClass(["fw-bold", "text-danger"]);
|
||||
expect(cells[5]).not.toHaveClass(["fw-bold", "text-warning", "text-danger"]);
|
||||
expect(cells[6]).toHaveClass(["fw-bold", "text-danger"]);
|
||||
});
|
||||
|
||||
test("RemainingDaysField on a datetime field in list view in UTC+6", async () => {
|
||||
mockDate("2017-10-08 15:35:11", +6); // October 8 2017, 15:35:11, UTC+6
|
||||
|
||||
Partner._records = [
|
||||
{ id: 1, datetime: "2017-10-08 20:00:00" }, // tomorrow
|
||||
{ id: 2, datetime: "2017-10-09 08:00:00" }, // tomorrow
|
||||
{ id: 3, datetime: "2017-10-07 18:30:00" }, // today
|
||||
{ id: 4, datetime: "2017-10-07 12:00:00" }, // yesterday
|
||||
{ id: 5, datetime: "2017-10-09 20:00:00" }, // + 2 days
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list><field name="datetime" widget="remaining_days" /></list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual([
|
||||
"Tomorrow",
|
||||
"Tomorrow",
|
||||
"Today",
|
||||
"Yesterday",
|
||||
"In 2 days",
|
||||
]);
|
||||
expect(".o_data_cell .o_field_widget div:first").toHaveAttribute("title", "10/09/2017");
|
||||
});
|
||||
|
||||
test("RemainingDaysField on a date field in list view in UTC-6", async () => {
|
||||
mockDate("2017-10-08 15:35:11", -6); // October 8 2017, 15:35:11, UTC-6
|
||||
|
||||
Partner._records = [
|
||||
{ id: 1, date: "2017-10-08" }, // today
|
||||
{ id: 2, date: "2017-10-09" }, // tomorrow
|
||||
{ id: 3, date: "2017-10-07" }, // yesterday
|
||||
{ id: 4, date: "2017-10-10" }, // + 2 days
|
||||
{ id: 5, date: "2017-10-05" }, // - 3 days
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list><field name="date" widget="remaining_days" /></list>`,
|
||||
});
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual([
|
||||
"Today",
|
||||
"Tomorrow",
|
||||
"Yesterday",
|
||||
"In 2 days",
|
||||
"3 days ago",
|
||||
]);
|
||||
expect(".o_data_cell .o_field_widget div:first").toHaveAttribute("title", "10/08/2017");
|
||||
});
|
||||
|
||||
test("RemainingDaysField on a datetime field in list view in UTC-8", async () => {
|
||||
mockDate("2017-10-08 15:35:11", -8); // October 8 2017, 15:35:11, UTC-8
|
||||
|
||||
Partner._records = [
|
||||
{ id: 1, datetime: "2017-10-08 20:00:00" }, // today
|
||||
{ id: 2, datetime: "2017-10-09 07:00:00" }, // today
|
||||
{ id: 3, datetime: "2017-10-09 10:00:00" }, // tomorrow
|
||||
{ id: 4, datetime: "2017-10-08 06:00:00" }, // yesterday
|
||||
{ id: 5, datetime: "2017-10-07 02:00:00" }, // - 2 days
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list><field name="datetime" widget="remaining_days" /></list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual([
|
||||
"Today",
|
||||
"Today",
|
||||
"Tomorrow",
|
||||
"Yesterday",
|
||||
"2 days ago",
|
||||
]);
|
||||
});
|
||||
|
||||
test("RemainingDaysField with custom decoration classes", async () => {
|
||||
mockDate("2017-10-08 15:35:11");
|
||||
|
||||
Partner._records = [
|
||||
{ id: 1, date: "2017-10-08" }, // today
|
||||
{ id: 2, date: "2017-10-09" }, // tomorrow
|
||||
{ id: 3, date: "2017-10-07" }, // yesterday
|
||||
{ id: 4, date: "2017-10-10" }, // + 2 days
|
||||
{ id: 5, date: "2017-10-05" }, // - 3 days
|
||||
{ id: 6, date: "2018-02-08" }, // + 4 months (diff >= 100 days)
|
||||
{ id: 7, date: "2017-06-08" }, // - 4 months (diff >= 100 days)
|
||||
{ id: 8, date: false },
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list>
|
||||
<field
|
||||
name="date"
|
||||
widget="remaining_days"
|
||||
options="{
|
||||
'classes': {
|
||||
'muted': 'days < -30',
|
||||
'danger': 'days < 0',
|
||||
'success': 'days == 0',
|
||||
'warning': 'days > 30',
|
||||
'info': 'days >= 2'
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
const cells = queryAll(".o_data_cell div div");
|
||||
expect(cells[0]).toHaveClass("text-success");
|
||||
expect(cells[1]).not.toHaveAttribute("class");
|
||||
expect(cells[2]).toHaveClass("text-danger");
|
||||
expect(cells[3]).toHaveClass("text-info");
|
||||
expect(cells[4]).toHaveClass("text-danger");
|
||||
expect(cells[5]).toHaveClass("text-warning");
|
||||
expect(cells[6]).toHaveClass("text-muted");
|
||||
expect(cells[7]).not.toHaveAttribute("class");
|
||||
});
|
||||
|
|
@ -0,0 +1,467 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
click,
|
||||
pointerDown,
|
||||
queryAll,
|
||||
queryAllValues,
|
||||
queryFirst,
|
||||
queryOne,
|
||||
select,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
display_name = fields.Char({ string: "Displayed name" });
|
||||
int_field = fields.Integer({ string: "int_field" });
|
||||
trululu = fields.Many2one({ string: "Trululu", relation: "partner" });
|
||||
product_id = fields.Many2one({ string: "Product", relation: "product" });
|
||||
color = fields.Selection({
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
string: "Color",
|
||||
});
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
int_field: 10,
|
||||
trululu: 4,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
int_field: 9,
|
||||
trululu: 1,
|
||||
product_id: 37,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product Name" });
|
||||
_records = [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
];
|
||||
}
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
test("SelectionField in a list view", async () => {
|
||||
Partner._records.forEach((r) => (r.color = "red"));
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list string="Colors" editable="top"><field name="color"/></list>',
|
||||
});
|
||||
|
||||
expect(".o_data_row").toHaveCount(3);
|
||||
await click(".o_data_cell");
|
||||
await animationFrame();
|
||||
const td = queryFirst("tbody tr.o_selected_row td:not(.o_list_record_selector)");
|
||||
expect(queryOne("select", { root: td })).toHaveCount(1, {
|
||||
message: "td should have a child 'select'",
|
||||
});
|
||||
expect(td.children).toHaveCount(1, { message: "select tag should be only child of td" });
|
||||
});
|
||||
|
||||
test("SelectionField, edition and on many2one field", async () => {
|
||||
Partner._onChanges.product_id = () => {};
|
||||
Partner._records[0].product_id = 37;
|
||||
Partner._records[0].trululu = false;
|
||||
onRpc(({ method }) => expect.step(method));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="product_id" widget="selection" />
|
||||
<field name="trululu" widget="selection" />
|
||||
<field name="color" widget="selection" />
|
||||
</form>`,
|
||||
});
|
||||
expect("select").toHaveCount(3);
|
||||
expect(".o_field_widget[name='product_id'] select option[value='37']").toHaveCount(1, {
|
||||
message: "should have fetched xphone option",
|
||||
});
|
||||
expect(".o_field_widget[name='product_id'] select option[value='41']").toHaveCount(1, {
|
||||
message: "should have fetched xpad option",
|
||||
});
|
||||
expect(".o_field_widget[name='product_id'] select").toHaveValue("37", {
|
||||
message: "should have correct product_id value",
|
||||
});
|
||||
expect(".o_field_widget[name='trululu'] select").toHaveValue("false", {
|
||||
message: "should not have any value in trululu field",
|
||||
});
|
||||
|
||||
await click(".o_field_widget[name='product_id'] select");
|
||||
await select("41");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_widget[name='product_id'] select").toHaveValue("41", {
|
||||
message: "should have a value of xphone",
|
||||
});
|
||||
expect(".o_field_widget[name='color'] select").toHaveValue('"red"', {
|
||||
message: "should have correct value in color field",
|
||||
});
|
||||
|
||||
expect.verifySteps(["get_views", "web_read", "name_search", "name_search", "onchange"]);
|
||||
});
|
||||
|
||||
test("unset selection field with 0 as key", async () => {
|
||||
// The server doesn't make a distinction between false value (the field
|
||||
// is unset), and selection 0, as in that case the value it returns is
|
||||
// false. So the client must convert false to value 0 if it exists.
|
||||
Partner._fields.selection = fields.Selection({
|
||||
selection: [
|
||||
[0, "Value O"],
|
||||
[1, "Value 1"],
|
||||
],
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ '<form edit="0"><field name="selection" /></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget").toHaveText("Value O", {
|
||||
message: "the displayed value should be 'Value O'",
|
||||
});
|
||||
expect(".o_field_widget").not.toHaveClass("o_field_empty", {
|
||||
message: "should not have class o_field_empty",
|
||||
});
|
||||
});
|
||||
|
||||
test("unset selection field with string keys", async () => {
|
||||
// The server doesn't make a distinction between false value (the field
|
||||
// is unset), and selection 0, as in that case the value it returns is
|
||||
// false. So the client must convert false to value 0 if it exists. In
|
||||
// this test, it doesn't exist as keys are strings.
|
||||
Partner._fields.selection = fields.Selection({
|
||||
selection: [
|
||||
["0", "Value O"],
|
||||
["1", "Value 1"],
|
||||
],
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ '<form edit="0"><field name="selection" /></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget").toHaveText("", { message: "there should be no displayed value" });
|
||||
expect(".o_field_widget").toHaveClass("o_field_empty", {
|
||||
message: "should have class o_field_empty",
|
||||
});
|
||||
});
|
||||
|
||||
test("unset selection on a many2one field", async () => {
|
||||
expect.assertions(1);
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].trululu).toBe(false, {
|
||||
message: "should send 'false' as trululu value",
|
||||
});
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ '<form><field name="trululu" widget="selection" /></form>',
|
||||
});
|
||||
|
||||
await click(".o_form_view select");
|
||||
await select("false");
|
||||
await animationFrame();
|
||||
await clickSave();
|
||||
await animationFrame();
|
||||
});
|
||||
|
||||
test("field selection with many2ones and special characters", async () => {
|
||||
// edit the partner with id=4
|
||||
Partner._records[2].display_name = "<span>hey</span>";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ '<form><field name="trululu" widget="selection" /></form>',
|
||||
});
|
||||
|
||||
expect("select option[value='4']").toHaveText("<span>hey</span>");
|
||||
});
|
||||
|
||||
test("required selection widget should not have blank option", async () => {
|
||||
Partner._fields.feedback_value = fields.Selection({
|
||||
required: true,
|
||||
selection: [
|
||||
["good", "Good"],
|
||||
["bad", "Bad"],
|
||||
],
|
||||
default: "good",
|
||||
string: "Good",
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="feedback_value" />
|
||||
<field name="color" required="feedback_value == 'bad'" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(queryAll(".o_field_widget[name='color'] option").map((n) => n.style.display)).toEqual([
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
]);
|
||||
|
||||
expect(
|
||||
queryAll(".o_field_widget[name='feedback_value'] option").map((n) => n.style.display)
|
||||
).toEqual(["none", "", ""]);
|
||||
|
||||
// change value to update widget modifier values
|
||||
await click(".o_field_widget[name='feedback_value'] select");
|
||||
await select('"bad"');
|
||||
await animationFrame();
|
||||
expect(queryAll(".o_field_widget[name='color'] option").map((n) => n.style.display)).toEqual([
|
||||
"none",
|
||||
"",
|
||||
"",
|
||||
]);
|
||||
});
|
||||
|
||||
test("required selection widget should have only one blank option", async () => {
|
||||
Partner._fields.feedback_value = fields.Selection({
|
||||
required: true,
|
||||
selection: [
|
||||
["good", "Good"],
|
||||
["bad", "Bad"],
|
||||
],
|
||||
default: "good",
|
||||
string: "Good",
|
||||
});
|
||||
|
||||
Partner._fields.color = fields.Selection({
|
||||
selection: [
|
||||
[false, ""],
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
string: "Color",
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="feedback_value" />
|
||||
<field name="color" required="feedback_value == 'bad'" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='color'] option").toHaveCount(3, {
|
||||
message: "Three options in non required field (one blank option)",
|
||||
});
|
||||
|
||||
// change value to update widget modifier values
|
||||
await click(".o_field_widget[name='feedback_value'] select");
|
||||
await select('"bad"');
|
||||
await animationFrame();
|
||||
|
||||
expect(queryAll(".o_field_widget[name='color'] option").map((n) => n.style.display)).toEqual([
|
||||
"none",
|
||||
"",
|
||||
"",
|
||||
]);
|
||||
});
|
||||
|
||||
test("selection field with placeholder", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="trululu" widget="selection" placeholder="Placeholder"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='trululu'] select option:first").toHaveText("Placeholder");
|
||||
expect(".o_field_widget[name='trululu'] select option:first").toHaveValue("false");
|
||||
});
|
||||
|
||||
test("SelectionField in kanban view", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="color" widget="selection" />
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "=", 1]],
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='color'] select").toHaveCount(1, {
|
||||
message: "SelectionKanbanField widget applied to selection field",
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='color'] option").toHaveCount(3, {
|
||||
message: "Three options are displayed (one blank option)",
|
||||
});
|
||||
|
||||
expect(queryAllValues(".o_field_widget[name='color'] option")).toEqual([
|
||||
"false",
|
||||
'"red"',
|
||||
'"black"',
|
||||
]);
|
||||
});
|
||||
|
||||
test("SelectionField - auto save record in kanban view", async () => {
|
||||
onRpc("web_save", ({ method }) => expect.step(method));
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="color" widget="selection" />
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "=", 1]],
|
||||
});
|
||||
await click(".o_field_widget[name='color'] select");
|
||||
await select('"black"');
|
||||
await animationFrame();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("SelectionField don't open form view on click in kanban view", async function (assert) {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="color" widget="selection" />
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "=", 1]],
|
||||
selectRecord: () => {
|
||||
expect.step("selectRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await click(".o_field_widget[name='color'] select");
|
||||
await animationFrame();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("SelectionField is disabled if field readonly", async () => {
|
||||
Partner._fields.color = fields.Selection({
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
string: "Color",
|
||||
readonly: true,
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="color" widget="selection" />
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
domain: [["id", "=", 1]],
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='color'] span").toHaveCount(1, {
|
||||
message: "field should be readonly",
|
||||
});
|
||||
});
|
||||
|
||||
test("SelectionField is disabled with a readonly attribute", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="color" widget="selection" readonly="1" />
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
domain: [["id", "=", 1]],
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='color'] span").toHaveCount(1, {
|
||||
message: "field should be readonly",
|
||||
});
|
||||
});
|
||||
|
||||
test("SelectionField in kanban view with handle widget", async () => {
|
||||
// When records are draggable, most pointerdown events are default prevented. This test
|
||||
// comes with a fix that blacklists "select" elements, i.e. pointerdown events on such
|
||||
// elements aren't default prevented, because if they were, the select element can't be
|
||||
// opened. The test is a bit artificial but there's no other way to test the scenario, as
|
||||
// using editSelect simply triggers a "change" event, which obviously always works.
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="color" widget="selection"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
const events = await pointerDown(".o_kanban_record .o_field_widget[name=color] select");
|
||||
await animationFrame();
|
||||
expect(events.get("pointerdown").defaultPrevented).toBe(false);
|
||||
});
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
import { NameAndSignature } from "@web/core/signature/name_and_signature";
|
||||
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { click, drag, edit, queryFirst, waitFor } from "@odoo/hoot-dom";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
function getUnique(target) {
|
||||
const src = target.dataset.src;
|
||||
return new URL(src).searchParams.get("unique");
|
||||
}
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
product_id = fields.Many2one({
|
||||
string: "Product Name",
|
||||
relation: "product",
|
||||
});
|
||||
sign = fields.Binary({ string: "Signature" });
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Pop's Chock'lit",
|
||||
product_id: 7,
|
||||
},
|
||||
];
|
||||
}
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product Name" });
|
||||
_records = [
|
||||
{
|
||||
id: 7,
|
||||
name: "Veggie Burger",
|
||||
},
|
||||
];
|
||||
}
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
test("signature can be drawn", async () => {
|
||||
onRpc("/web/sign/get_fonts/", () => ({}));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="sign" widget="signature" /></form>`,
|
||||
});
|
||||
|
||||
expect("div[name=sign] img.o_signature").toHaveCount(0);
|
||||
expect("div[name=sign] div.o_signature svg").toHaveCount(1, {
|
||||
message: "should have a valid signature widget",
|
||||
});
|
||||
|
||||
// Click on the widget to open signature modal
|
||||
await click("div[name=sign] div.o_signature");
|
||||
await waitFor(".modal .modal-body");
|
||||
expect(".modal .modal-body .o_web_sign_name_and_signature").toHaveCount(1);
|
||||
expect(".modal .btn.btn-primary:not([disabled])").toHaveCount(0);
|
||||
|
||||
// Use a drag&drop simulation to draw a signature
|
||||
const { drop } = await drag(".modal .o_web_sign_signature", {
|
||||
position: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
},
|
||||
relative: true,
|
||||
});
|
||||
await drop(".modal .o_web_sign_signature", {
|
||||
position: {
|
||||
x: 10, // Arbitrary value
|
||||
y: 10, // Arbitrary value
|
||||
},
|
||||
relative: true,
|
||||
});
|
||||
await animationFrame(); // await owl rendering
|
||||
expect(".modal .btn.btn-primary:not([disabled])").toHaveCount(1);
|
||||
|
||||
// Click on "Adopt and Sign" button
|
||||
await click(".modal .btn.btn-primary:not([disabled])");
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(0);
|
||||
|
||||
// The signature widget should now display the signature img
|
||||
expect("div[name=sign] div.o_signature svg").toHaveCount(0);
|
||||
expect("div[name=sign] img.o_signature").toHaveCount(1);
|
||||
|
||||
const signImgSrc = queryFirst("div[name=sign] img.o_signature").dataset.src;
|
||||
expect(signImgSrc).not.toMatch("placeholder");
|
||||
expect(signImgSrc).toMatch(/^data:image\/png;base64,/);
|
||||
});
|
||||
|
||||
test("Set simple field in 'full_name' node option", async () => {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
expect.step(this.props.signature.name);
|
||||
},
|
||||
});
|
||||
onRpc("/web/sign/get_fonts/", () => ({}));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="sign" widget="signature" options="{'full_name': 'name'}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div[name=sign] div.o_signature svg").toHaveCount(1, {
|
||||
message: "should have a valid signature widget",
|
||||
});
|
||||
// Click on the widget to open signature modal
|
||||
await click("div[name=sign] div.o_signature");
|
||||
await animationFrame();
|
||||
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(1, {
|
||||
message: 'should open a modal with "Auto" button',
|
||||
});
|
||||
expect(".o_web_sign_auto_button").toHaveClass("active", {
|
||||
message: "'Auto' panel is visible by default",
|
||||
});
|
||||
expect.verifySteps(["Pop's Chock'lit"]);
|
||||
});
|
||||
|
||||
test("Set m2o field in 'full_name' node option", async () => {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
expect.step(this.props.signature.name);
|
||||
},
|
||||
});
|
||||
onRpc("/web/sign/get_fonts/", () => ({}));
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="product_id"/>
|
||||
<field name="sign" widget="signature" options="{'full_name': 'product_id'}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div[name=sign] div.o_signature svg").toHaveCount(1, {
|
||||
message: "should have a valid signature widget",
|
||||
});
|
||||
|
||||
// Click on the widget to open signature modal
|
||||
await click("div[name=sign] div.o_signature");
|
||||
await waitFor(".modal .modal-body");
|
||||
|
||||
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(1, {
|
||||
message: 'should open a modal with "Auto" button',
|
||||
});
|
||||
expect.verifySteps(["Veggie Burger"]);
|
||||
});
|
||||
|
||||
test("Set size (width and height) in node option", async () => {
|
||||
Partner._fields.sign2 = fields.Binary({ string: "Signature" });
|
||||
Partner._fields.sign3 = fields.Binary({ string: "Signature" });
|
||||
onRpc("/web/sign/get_fonts/", () => ({}));
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="sign" widget="signature" options="{'size': [150,'']}" />
|
||||
<field name="sign2" widget="signature" options="{'size': ['',100]}" />
|
||||
<field name="sign3" widget="signature" options="{'size': [120,130]}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_signature").toHaveCount(3);
|
||||
|
||||
expect("[name='sign'] .o_signature").toHaveStyle({
|
||||
width: "150px",
|
||||
height: "50px",
|
||||
});
|
||||
expect("[name='sign2'] .o_signature").toHaveStyle({
|
||||
width: "300px",
|
||||
height: "100px",
|
||||
});
|
||||
expect("[name='sign3'] .o_signature").toHaveStyle({
|
||||
width: "120px",
|
||||
height: "40px",
|
||||
});
|
||||
});
|
||||
|
||||
test("clicking save manually after changing signature should change the unique of the image src", async () => {
|
||||
Partner._fields.foo = fields.Char({
|
||||
onChange() {},
|
||||
});
|
||||
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.sign = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
|
||||
const fillSignatureField = async (lineToX, lineToY) => {
|
||||
await click(".o_field_signature img", { visible: false });
|
||||
await waitFor(".modal .modal-body");
|
||||
expect(".modal canvas").toHaveCount(1);
|
||||
const { drop } = await drag(".modal .o_web_sign_signature", {
|
||||
position: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
},
|
||||
relative: true,
|
||||
});
|
||||
await drop(".modal .o_web_sign_signature", {
|
||||
position: {
|
||||
x: lineToX,
|
||||
y: lineToY,
|
||||
},
|
||||
relative: true,
|
||||
});
|
||||
await animationFrame();
|
||||
await click(".modal-footer .btn-primary");
|
||||
await animationFrame();
|
||||
};
|
||||
// 1659692220000, 1659695820000
|
||||
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
|
||||
let index = 0;
|
||||
|
||||
onRpc("/web/sign/get_fonts/", () => ({}));
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step("web_save");
|
||||
args[1].write_date = lastUpdates[index];
|
||||
args[1].sign = "4 kb";
|
||||
index++;
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="sign" widget="signature" />
|
||||
</form>`,
|
||||
});
|
||||
expect(getUnique(queryFirst(".o_field_signature img"))).toBe("1659688620000");
|
||||
|
||||
await fillSignatureField(0, 2);
|
||||
await click(".o_field_widget[name='foo'] input");
|
||||
await edit("grrr", { confirm: "Enter" });
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
await clickSave();
|
||||
expect.verifySteps(["web_save"]);
|
||||
expect(getUnique(queryFirst(".o_field_signature img"))).toBe("1659692220000");
|
||||
|
||||
await fillSignatureField(2, 0);
|
||||
await clickSave();
|
||||
expect.verifySteps(["web_save"]);
|
||||
expect(getUnique(queryFirst(".o_field_signature img"))).toBe("1659695820000");
|
||||
});
|
||||
|
||||
test("save record with signature field modified by onchange", async () => {
|
||||
const MYB64 = `iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABRJREFUGFdjZGD438DAwNjACGMAACQlBAMW7JulAAAAAElFTkSuQmCC`;
|
||||
|
||||
Partner._fields.foo = fields.Char({
|
||||
onChange(data) {
|
||||
data.sign = MYB64;
|
||||
},
|
||||
});
|
||||
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.sign = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
|
||||
|
||||
// 1659692220000, 1659695820000
|
||||
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
|
||||
let index = 0;
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step("web_save");
|
||||
args[1].write_date = lastUpdates[index];
|
||||
args[1].sign = "4 kb";
|
||||
index++;
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="sign" widget="signature" />
|
||||
</form>`,
|
||||
});
|
||||
expect(getUnique(queryFirst(".o_field_signature img"))).toBe("1659688620000");
|
||||
await click("[name='foo'] input");
|
||||
await edit("grrr", { confirm: "Enter" });
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(queryFirst("div[name=sign] img").dataset.src).toBe(`data:image/png;base64,${MYB64}`);
|
||||
|
||||
await clickSave();
|
||||
expect(getUnique(queryFirst(".o_field_signature img"))).toBe("1659692220000");
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("signature field should render initials", async () => {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
expect.step(this.getCleanedName());
|
||||
},
|
||||
});
|
||||
onRpc("/web/sign/get_fonts/", () => ({}));
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="product_id"/>
|
||||
<field name="sign" widget="signature" options="{'full_name': 'product_id', 'type': 'initial'}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div[name=sign] div.o_signature svg").toHaveCount(1, {
|
||||
message: "should have a valid signature widget",
|
||||
});
|
||||
|
||||
// Click on the widget to open signature modal
|
||||
await click("div[name=sign] div.o_signature");
|
||||
await animationFrame();
|
||||
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(1, {
|
||||
message: 'should open a modal with "Auto" button',
|
||||
});
|
||||
expect.verifySteps(["V.B."]);
|
||||
});
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { contains, defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
foo = fields.Char({ default: "My little Foo Value" });
|
||||
int_field = fields.Integer({ string: "int_field" });
|
||||
qux = fields.Float({ digits: [16, 1] });
|
||||
monetary = fields.Monetary({ currency_field: "" });
|
||||
|
||||
_records = [{ id: 1, foo: "yop", int_field: 10, qux: 0.44444, monetary: 9.999999 }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("StatInfoField formats decimal precision", async () => {
|
||||
// sometimes the round method can return numbers such as 14.000001
|
||||
// when asked to round a number to 2 decimals, as such is the behaviour of floats.
|
||||
// we check that even in that eventuality, only two decimals are displayed
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<button class="oe_stat_button" name="items" icon="fa-gear">
|
||||
<field name="qux" widget="statinfo" />
|
||||
</button>
|
||||
<button class="oe_stat_button" name="money" icon="fa-money">
|
||||
<field name="monetary" widget="statinfo" />
|
||||
</button>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// formatFloat renders according to this.field.digits
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_value:eq(0)").toHaveText("0.4", {
|
||||
message: "Default precision should be [16,1]",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_value:eq(1)").toHaveText("10.00", {
|
||||
message: "Currency decimal precision should be 2",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("StatInfoField in form view on desktop", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" name="items" type="object" icon="fa-gear">
|
||||
<field name="int_field" widget="statinfo" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_info").toHaveCount(1, {
|
||||
message: "should have one stat button",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_value").toHaveText("10", {
|
||||
message: "should have 10 as value",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_text").toHaveText("int_field", {
|
||||
message: "should have 'int_field' as text",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("StatInfoField in form view on mobile", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" name="items" type="object" icon="fa-gear">
|
||||
<field name="int_field" widget="statinfo" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await contains(".o-form-buttonbox .o_button_more").click();
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_info").toHaveCount(1, {
|
||||
message: "should have one stat button",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_value").toHaveText("10", {
|
||||
message: "should have 10 as value",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_text").toHaveText("int_field", {
|
||||
message: "should have 'int_field' as text",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("StatInfoField in form view with specific label_field on desktop", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" name="items" type="object" icon="fa-gear">
|
||||
<field string="Useful stat button" name="int_field" widget="statinfo" options="{'label_field': 'foo'}" />
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<field name="foo" invisible="1" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_info").toHaveCount(1, {
|
||||
message: "should have one stat button",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_value").toHaveText("10", {
|
||||
message: "should have 10 as value",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_text").toHaveText("yop", {
|
||||
message: "should have 'yop' as text, since it is the value of field foo",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("StatInfoField in form view with specific label_field on mobile", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" name="items" type="object" icon="fa-gear">
|
||||
<field string="Useful stat button" name="int_field" widget="statinfo" options="{'label_field': 'foo'}" />
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<field name="foo" invisible="1" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await contains(".o-form-buttonbox .o_button_more").click();
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_info").toHaveCount(1, {
|
||||
message: "should have one stat button",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_value").toHaveText("10", {
|
||||
message: "should have 10 as value",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_text").toHaveText("yop", {
|
||||
message: "should have 'yop' as text, since it is the value of field foo",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("StatInfoField in form view with no label on desktop", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" name="items" type="object" icon="fa-gear">
|
||||
<field string="Useful stat button" name="int_field" widget="statinfo" nolabel="1" />
|
||||
</button>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_info").toHaveCount(1, {
|
||||
message: "should have one stat button",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_value").toHaveText("10", {
|
||||
message: "should have 10 as value",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_text").toHaveCount(0, {
|
||||
message: "should not have any label",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("StatInfoField in form view with no label on mobile", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" name="items" type="object" icon="fa-gear">
|
||||
<field string="Useful stat button" name="int_field" widget="statinfo" nolabel="1" />
|
||||
</button>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
await contains(".o-form-buttonbox .o_button_more").click();
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_info").toHaveCount(1, {
|
||||
message: "should have one stat button",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_value").toHaveText("10", {
|
||||
message: "should have 10 as value",
|
||||
});
|
||||
expect("button.oe_stat_button .o_field_widget .o_stat_text").toHaveCount(0, {
|
||||
message: "should not have any label",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,528 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, press, queryAll, queryAllTexts, queryFirst } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
foo = fields.Char({ string: "Foo" });
|
||||
sequence = fields.Integer({ string: "Sequence", searchable: true });
|
||||
selection = fields.Selection({
|
||||
string: "Selection",
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 1, foo: "yop", selection: "blocked" },
|
||||
{ id: 2, foo: "blip", selection: "normal" },
|
||||
{ id: 4, foo: "abc", selection: "done" },
|
||||
{ id: 3, foo: "gnap" },
|
||||
{ id: 5, foo: "blop" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("StateSelectionField in form view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="state_selection"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status.o_status_red").toHaveCount(1, {
|
||||
message: "should have one red status since selection is the second, blocked state",
|
||||
});
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status.o_status_green").toHaveCount(0, {
|
||||
message: "should not have one green status since selection is the second, blocked state",
|
||||
});
|
||||
expect(".o-dropdown--menu").toHaveCount(0, { message: "there should not be a dropdown" });
|
||||
|
||||
// Click on the status button to make the dropdown appear
|
||||
await click(".o_field_widget.o_field_state_selection .o_status");
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu").toHaveCount(1, { message: "there should be a dropdown" });
|
||||
expect(".o-dropdown--menu .dropdown-item").toHaveCount(3, {
|
||||
message: "there should be three options in the dropdown",
|
||||
});
|
||||
expect(".o-dropdown--menu .dropdown-item:nth-child(2)").toHaveClass("active", {
|
||||
message: "current value has a checkmark",
|
||||
});
|
||||
|
||||
// Click on the first option, "Normal"
|
||||
await click(".o-dropdown--menu .dropdown-item");
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu").toHaveCount(0, {
|
||||
message: "there should not be a dropdown anymore",
|
||||
});
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status.o_status_red").toHaveCount(0, {
|
||||
message: "should not have one red status since selection is the first, normal state",
|
||||
});
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status.o_status_green").toHaveCount(0, {
|
||||
message: "should not have one green status since selection is the first, normal state",
|
||||
});
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status").toHaveCount(1, {
|
||||
message: "should have one grey status since selection is the first, normal state",
|
||||
});
|
||||
|
||||
expect(".o-dropdown--menu").toHaveCount(0, { message: "there should still not be a dropdown" });
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status.o_status_red").toHaveCount(0, {
|
||||
message: "should still not have one red status since selection is the first, normal state",
|
||||
});
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status.o_status_green").toHaveCount(0, {
|
||||
message:
|
||||
"should still not have one green status since selection is the first, normal state",
|
||||
});
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status").toHaveCount(1, {
|
||||
message: "should still have one grey status since selection is the first, normal state",
|
||||
});
|
||||
|
||||
// Click on the status button to make the dropdown appear
|
||||
await click(".o_field_widget.o_field_state_selection .o_status");
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu .dropdown-item").toHaveCount(3, {
|
||||
message: "there should be three options in the dropdown",
|
||||
});
|
||||
|
||||
// Click on the last option, "Done"
|
||||
await click(".o-dropdown--menu .dropdown-item:last-child");
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu").toHaveCount(0, {
|
||||
message: "there should not be a dropdown anymore",
|
||||
});
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status.o_status_red").toHaveCount(0, {
|
||||
message: "should not have one red status since selection is the third, done state",
|
||||
});
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status.o_status_green").toHaveCount(1, {
|
||||
message: "should have one green status since selection is the third, done state",
|
||||
});
|
||||
|
||||
// save
|
||||
await click(".o_form_button_save");
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu").toHaveCount(0, {
|
||||
message: "there should still not be a dropdown anymore",
|
||||
});
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status.o_status_red").toHaveCount(0, {
|
||||
message: "should still not have one red status since selection is the third, done state",
|
||||
});
|
||||
expect(".o_field_widget.o_field_state_selection span.o_status.o_status_green").toHaveCount(1, {
|
||||
message: "should still have one green status since selection is the third, done state",
|
||||
});
|
||||
});
|
||||
|
||||
test("StateSelectionField with readonly modifier", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="selection" widget="state_selection" readonly="1"/></form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_state_selection").toHaveClass("o_readonly_modifier");
|
||||
expect(".dropdown-menu:visible").not.toHaveCount();
|
||||
await click(".o_field_state_selection span.o_status");
|
||||
await animationFrame();
|
||||
expect(".dropdown-menu:visible").not.toHaveCount();
|
||||
});
|
||||
|
||||
test("StateSelectionField for form view with hide_label option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="selection" widget="state_selection" options="{'hide_label': False}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(".o_status_label").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("StateSelectionField for list view with hide_label option", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
Partner._fields.graph_type = fields.Selection({
|
||||
type: "selection",
|
||||
selection: [
|
||||
["line", "Line"],
|
||||
["bar", "Bar"],
|
||||
],
|
||||
});
|
||||
Partner._records[0].graph_type = "bar";
|
||||
Partner._records[1].graph_type = "line";
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list>
|
||||
<field name="graph_type" widget="state_selection" options="{'hide_label': True}"/>
|
||||
<field name="selection" widget="state_selection" options="{'hide_label': False}"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_state_selection_cell .o_field_state_selection span.o_status").toHaveCount(10, {
|
||||
message: "should have ten status selection widgets",
|
||||
});
|
||||
const selector =
|
||||
".o_state_selection_cell .o_field_state_selection[name=selection] span.o_status_label";
|
||||
expect(selector).toHaveCount(5, { message: "should have five label on selection widgets" });
|
||||
expect(`${selector}:contains("Done")`).toHaveCount(1, {
|
||||
message: "should have one Done status label",
|
||||
});
|
||||
expect(`${selector}:contains("Normal")`).toHaveCount(3, {
|
||||
message: "should have three Normal status label",
|
||||
});
|
||||
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection[name=graph_type] span.o_status"
|
||||
).toHaveCount(5, { message: "should have five status selection widgets" });
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection[name=graph_type] span.o_status_label"
|
||||
).toHaveCount(0, { message: "should not have status label in selection widgets" });
|
||||
});
|
||||
|
||||
test("StateSelectionField in editable list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list editable="bottom">
|
||||
<field name="foo"/>
|
||||
<field name="selection" widget="state_selection"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_state_selection_cell .o_field_state_selection span.o_status").toHaveCount(5, {
|
||||
message: "should have five status selection widgets",
|
||||
});
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red"
|
||||
).toHaveCount(1, { message: "should have one red status" });
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green"
|
||||
).toHaveCount(1, { message: "should have one green status" });
|
||||
expect(".o-dropdown--menu").toHaveCount(0, { message: "there should not be a dropdown" });
|
||||
|
||||
// Click on the status button to make the dropdown appear
|
||||
let cell = queryFirst("tbody td.o_state_selection_cell");
|
||||
await click(".o_state_selection_cell .o_field_state_selection span.o_status");
|
||||
await animationFrame();
|
||||
expect(cell.parentElement).not.toHaveClass("o_selected_row", {
|
||||
message: "should not be in edit mode since we clicked on the state selection widget",
|
||||
});
|
||||
expect(".o-dropdown--menu").toHaveCount(1, { message: "there should be a dropdown" });
|
||||
expect(".o-dropdown--menu .dropdown-item").toHaveCount(3, {
|
||||
message: "there should be three options in the dropdown",
|
||||
});
|
||||
|
||||
// Click on the first option, "Normal"
|
||||
await click(".o-dropdown--menu .dropdown-item");
|
||||
await animationFrame();
|
||||
expect(".o_state_selection_cell .o_field_state_selection span.o_status").toHaveCount(5, {
|
||||
message: "should still have five status selection widgets",
|
||||
});
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red"
|
||||
).toHaveCount(0, { message: "should now have no red status" });
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green"
|
||||
).toHaveCount(1, { message: "should still have one green status" });
|
||||
expect(".o-dropdown--menu").toHaveCount(0, { message: "there should not be a dropdown" });
|
||||
expect("tr.o_selected_row").toHaveCount(0, { message: "should not be in edit mode" });
|
||||
|
||||
// switch to edit mode and check the result
|
||||
cell = queryFirst("tbody td.o_state_selection_cell");
|
||||
await click(cell);
|
||||
await animationFrame();
|
||||
expect(cell.parentElement).toHaveClass("o_selected_row", {
|
||||
message: "should now be in edit mode",
|
||||
});
|
||||
expect(".o_state_selection_cell .o_field_state_selection span.o_status").toHaveCount(5, {
|
||||
message: "should still have five status selection widgets",
|
||||
});
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red"
|
||||
).toHaveCount(0, { message: "should now have no red status" });
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green"
|
||||
).toHaveCount(1, { message: "should still have one green status" });
|
||||
expect(".o-dropdown--menu").toHaveCount(0, { message: "there should not be a dropdown" });
|
||||
|
||||
// Click on the status button to make the dropdown appear
|
||||
await click(".o_state_selection_cell .o_field_state_selection span.o_status");
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu").toHaveCount(1, { message: "there should be a dropdown" });
|
||||
expect(".o-dropdown--menu .dropdown-item").toHaveCount(3, {
|
||||
message: "there should be three options in the dropdown",
|
||||
});
|
||||
|
||||
// Click on another row
|
||||
const lastCell = queryAll("tbody td.o_state_selection_cell")[4];
|
||||
await click(lastCell);
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu").toHaveCount(0, {
|
||||
message: "there should not be a dropdown anymore",
|
||||
});
|
||||
const firstCell = queryFirst("tbody td.o_state_selection_cell");
|
||||
expect(firstCell.parentElement).not.toHaveClass("o_selected_row", {
|
||||
message: "first row should not be in edit mode anymore",
|
||||
});
|
||||
expect(lastCell.parentElement).toHaveClass("o_selected_row", {
|
||||
message: "last row should be in edit mode",
|
||||
});
|
||||
|
||||
// Click on the third status button to make the dropdown appear
|
||||
await click(".o_state_selection_cell .o_field_state_selection span.o_status:eq(2)");
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu").toHaveCount(1, "there should be a dropdown".msg);
|
||||
expect(".o-dropdown--menu .dropdown-item").toHaveCount(3, {
|
||||
message: "there should be three options in the dropdown",
|
||||
});
|
||||
|
||||
// Click on the last option, "Done"
|
||||
await click(".o-dropdown--menu .dropdown-item:last-child");
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu").toHaveCount(0, {
|
||||
message: "there should not be a dropdown anymore",
|
||||
});
|
||||
expect(".o_state_selection_cell .o_field_state_selection span.o_status").toHaveCount(5, {
|
||||
message: "should still have five status selection widgets",
|
||||
});
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red"
|
||||
).toHaveCount(0, { message: "should still have no red status" });
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green"
|
||||
).toHaveCount(2, { message: "should now have two green status" });
|
||||
expect(".o-dropdown--menu").toHaveCount(0, { message: "there should not be a dropdown" });
|
||||
|
||||
// save
|
||||
await click(".o_control_panel_main_buttons .o_list_button_save");
|
||||
await animationFrame();
|
||||
expect(".o_state_selection_cell .o_field_state_selection span.o_status").toHaveCount(5, {
|
||||
message: "should have five status selection widgets",
|
||||
});
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red"
|
||||
).toHaveCount(0, { message: "should have no red status" });
|
||||
expect(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green"
|
||||
).toHaveCount(2, { message: "should have two green status" });
|
||||
expect(".o-dropdown--menu").toHaveCount(0, { message: "there should not be a dropdown" });
|
||||
});
|
||||
|
||||
test('StateSelectionField edited by the smart actions "Set kanban state as <state name>"', async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="selection" widget="state_selection"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_status_red").toHaveCount(1);
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
expect(`.o_command:contains("Set kanban state as Normal\nALT + D")`).toHaveCount(1);
|
||||
const doneItem = `.o_command:contains("Set kanban state as Done\nALT + G")`;
|
||||
expect(doneItem).toHaveCount(1);
|
||||
|
||||
await click(doneItem);
|
||||
await animationFrame();
|
||||
expect(".o_status_green").toHaveCount(1);
|
||||
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
expect(`.o_command:contains("Set kanban state as Normal\nALT + D")`).toHaveCount(1);
|
||||
expect(`.o_command:contains("Set kanban state as Blocked\nALT + F")`).toHaveCount(1);
|
||||
expect(`.o_command:contains("Set kanban state as Done\nALT + G")`).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("StateSelectionField uses legend_* fields", async () => {
|
||||
Partner._fields.legend_normal = fields.Char();
|
||||
Partner._fields.legend_blocked = fields.Char();
|
||||
Partner._fields.legend_done = fields.Char();
|
||||
Partner._records[0].legend_normal = "Custom normal";
|
||||
Partner._records[0].legend_blocked = "Custom blocked";
|
||||
Partner._records[0].legend_done = "Custom done";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="legend_normal" invisible="1" />
|
||||
<field name="legend_blocked" invisible="1" />
|
||||
<field name="legend_done" invisible="1" />
|
||||
<field name="selection" widget="state_selection"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(".o_status");
|
||||
await animationFrame();
|
||||
expect(queryAllTexts(".o-dropdown--menu .dropdown-item")).toEqual([
|
||||
"Custom normal",
|
||||
"Custom blocked",
|
||||
"Custom done",
|
||||
]);
|
||||
|
||||
await click(".dropdown-item .o_status");
|
||||
await animationFrame();
|
||||
await click(".o_status");
|
||||
await animationFrame();
|
||||
expect(queryAllTexts(".o-dropdown--menu .dropdown-item")).toEqual([
|
||||
"Custom normal",
|
||||
"Custom blocked",
|
||||
"Custom done",
|
||||
]);
|
||||
});
|
||||
|
||||
test("works when required in a readonly view", async () => {
|
||||
Partner._records[0].selection = "normal";
|
||||
Partner._records = [Partner._records[0]];
|
||||
onRpc("web_save", ({ method }) => expect.step(method));
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="selection" widget="state_selection" required="1"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
expect(".o_status_label").toHaveCount(0);
|
||||
await click(".o_field_state_selection button");
|
||||
await animationFrame();
|
||||
await click(".dropdown-item:eq(2)");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["web_save"]);
|
||||
expect(".o_field_state_selection span").toHaveClass("o_status_green");
|
||||
});
|
||||
|
||||
test("StateSelectionField - auto save record when field toggled", async () => {
|
||||
onRpc("web_save", ({ method }) => expect.step(method));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="state_selection"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(".o_field_widget.o_field_state_selection .o_status");
|
||||
await animationFrame();
|
||||
await click(".dropdown-menu .dropdown-item:last-child");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("StateSelectionField - prevent auto save with autosave option", async () => {
|
||||
onRpc("write", ({ method }) => expect.step(method));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="state_selection" options="{'autosave': False}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(".o_field_widget.o_field_state_selection .o_status");
|
||||
await animationFrame();
|
||||
await click(".dropdown-menu .dropdown-item:last-child");
|
||||
await animationFrame();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("StateSelectionField - hotkey handling when there are more than 3 options available", async () => {
|
||||
Partner._fields.selection = fields.Selection({
|
||||
string: "Selection",
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
["martin", "Martin"],
|
||||
["martine", "Martine"],
|
||||
],
|
||||
});
|
||||
Partner._records[0].selection = null;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="state_selection" options="{'autosave': False}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(".o_field_widget.o_field_state_selection .o_status");
|
||||
await animationFrame();
|
||||
expect(".dropdown-menu .dropdown-item").toHaveCount(5, {
|
||||
message: "Five choices are displayed",
|
||||
});
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_command#o_command_2").toHaveText("Set kanban state as Done\nALT + G", {
|
||||
message: "hotkey and command are present",
|
||||
});
|
||||
expect(".o_command#o_command_4").toHaveText("Set kanban state as Martine", {
|
||||
message: "no hotkey is present, but the command exists",
|
||||
});
|
||||
|
||||
await click(".o_command#o_command_2");
|
||||
await animationFrame();
|
||||
expect(".o_field_state_selection .o_status").toHaveClass("o_status_green", {
|
||||
message: "green color and Done state have been set",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,810 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, press, queryAllTexts, queryAttribute, queryFirst } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
defineActions,
|
||||
defineModels,
|
||||
fields,
|
||||
getDropdownMenu,
|
||||
getService,
|
||||
models,
|
||||
mockService,
|
||||
mountView,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { EventBus } from "@odoo/owl";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
foo = fields.Char({ default: "My little Foo Value" });
|
||||
bar = fields.Boolean({ default: true });
|
||||
int_field = fields.Integer();
|
||||
qux = fields.Float({ digits: [16, 1] });
|
||||
p = fields.One2many({
|
||||
relation: "partner",
|
||||
relation_field: "trululu",
|
||||
});
|
||||
trululu = fields.Many2one({ relation: "partner" });
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
color = fields.Selection({
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
});
|
||||
user_id = fields.Many2one({ relation: "users" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "first record",
|
||||
bar: true,
|
||||
foo: "yop",
|
||||
int_field: 10,
|
||||
qux: 0.44,
|
||||
p: [],
|
||||
trululu: 4,
|
||||
user_id: 17,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "second record",
|
||||
bar: true,
|
||||
foo: "blip",
|
||||
int_field: 9,
|
||||
qux: 13,
|
||||
p: [],
|
||||
trululu: 1,
|
||||
product_id: 37,
|
||||
user_id: 17,
|
||||
},
|
||||
{ id: 4, name: "aaa", bar: false },
|
||||
];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 37, name: "xphone" },
|
||||
{ id: 41, name: "xpad" },
|
||||
];
|
||||
}
|
||||
|
||||
class Users extends models.Model {
|
||||
name = fields.Char();
|
||||
partner_ids = fields.One2many({
|
||||
relation: "partner",
|
||||
relation_field: "user_id",
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 17, name: "Aline", partner_ids: [1, 2] },
|
||||
{ id: 19, name: "Christine" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product, Users]);
|
||||
|
||||
test("static statusbar widget on many2one field", async () => {
|
||||
Partner._fields.trululu = fields.Many2one({
|
||||
relation: "partner",
|
||||
domain: "[('bar', '=', True)]",
|
||||
});
|
||||
Partner._records[1].bar = false;
|
||||
|
||||
onRpc("search_read", ({ kwargs }) => expect.step(kwargs.fields.toString()));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
// search_read should only fetch field display_name
|
||||
expect.verifySteps(["display_name"]);
|
||||
expect(".o_statusbar_status button:not(.dropdown-toggle)").toHaveCount(2);
|
||||
expect(".o_statusbar_status button:disabled").toHaveCount(5);
|
||||
expect('.o_statusbar_status button[data-value="4"]').toHaveClass("o_arrow_button_current");
|
||||
});
|
||||
|
||||
test("folded statusbar widget on selection field has selected value in the toggler", async () => {
|
||||
mockService("ui", (env) => {
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
value: true,
|
||||
});
|
||||
return {
|
||||
bus: new EventBus(),
|
||||
size: 0,
|
||||
isSmall: true,
|
||||
};
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="color" widget="statusbar" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_statusbar_status button.dropdown-toggle:contains(Red)").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("static statusbar widget on many2one field with domain", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
serverState.userId = 17;
|
||||
|
||||
onRpc("search_read", ({ kwargs }) => {
|
||||
expect(kwargs.domain).toEqual(["|", ["id", "=", 4], ["user_id", "=", 17]], {
|
||||
message: "search_read should sent the correct domain",
|
||||
});
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" domain="[('user_id', '=', uid)]" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
test("clickable statusbar widget on many2one field", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_statusbar_status button[data-value='4']").toHaveClass("o_arrow_button_current");
|
||||
expect(".o_statusbar_status button[data-value='4']").not.toBeEnabled();
|
||||
|
||||
expect(
|
||||
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current)"
|
||||
).toHaveCount(2);
|
||||
|
||||
await click(
|
||||
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current):eq(1)"
|
||||
);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_statusbar_status button[data-value='1']").toHaveClass("o_arrow_button_current");
|
||||
expect(".o_statusbar_status button[data-value='1']").not.toBeEnabled();
|
||||
});
|
||||
|
||||
test("statusbar with no status", async () => {
|
||||
Partner._records[1].product_id = false;
|
||||
Product._records = [];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_statusbar_status").not.toHaveClass("o_field_empty");
|
||||
expect(".o_statusbar_status > :not(.d-none)").toHaveCount(0, {
|
||||
message: "statusbar widget should be empty",
|
||||
});
|
||||
});
|
||||
|
||||
test("statusbar with tooltip for help text", async () => {
|
||||
Partner._fields.product_id = fields.Many2one({
|
||||
relation: "product",
|
||||
help: "some info about the field",
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_statusbar_status").not.toHaveClass("o_field_empty");
|
||||
expect(".o_field_statusbar").toHaveAttribute("data-tooltip-info");
|
||||
const tooltipInfo = JSON.parse(queryAttribute(".o_field_statusbar", "data-tooltip-info"));
|
||||
expect(tooltipInfo.field.help).toBe("some info about the field", {
|
||||
message: "tooltip text is present on the field",
|
||||
});
|
||||
});
|
||||
|
||||
test("statusbar with required modifier", async () => {
|
||||
mockService("notification", {
|
||||
add() {
|
||||
expect.step("Show error message");
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" required="1"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(".o_form_button_save");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_form_editable").toHaveCount(1, { message: "view should still be in edit" });
|
||||
// should display an 'invalid fields' notificationaveCount(1, { message: "view should still be in edit" });
|
||||
expect.verifySteps(["Show error message"]);
|
||||
});
|
||||
|
||||
test("statusbar with no value in readonly", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_statusbar_status").not.toHaveClass("o_field_empty");
|
||||
expect(".o_statusbar_status button:visible").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("statusbar with domain but no value (create mode)", async () => {
|
||||
Partner._fields.trululu = fields.Many2one({
|
||||
relation: "partner",
|
||||
domain: "[('bar', '=', True)]",
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_statusbar_status button:disabled").toHaveCount(5);
|
||||
});
|
||||
|
||||
test("clickable statusbar should change m2o fetching domain in edit mode", async () => {
|
||||
Partner._fields.trululu = fields.Many2one({
|
||||
relation: "partner",
|
||||
domain: "[('bar', '=', True)]",
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_statusbar_status button:not(.dropdown-toggle)").toHaveCount(3);
|
||||
await click(".o_statusbar_status button:not(.dropdown-toggle):eq(-1)");
|
||||
await animationFrame();
|
||||
expect(".o_statusbar_status button:not(.dropdown-toggle)").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("statusbar fold_field option and statusbar_visible attribute", async () => {
|
||||
Partner._records[0].bar = false;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'fold_field': 'bar'}" />
|
||||
<field name="color" widget="statusbar" statusbar_visible="red" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(".o_statusbar_status .dropdown-toggle:not(.d-none)");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_statusbar_status:first button:visible").toHaveCount(3);
|
||||
expect(".o_statusbar_status:last button:visible").toHaveCount(1);
|
||||
expect(".o_statusbar_status button").not.toBeEnabled({
|
||||
message: "no status bar buttons should be enabled",
|
||||
});
|
||||
});
|
||||
|
||||
test("statusbar: choose an item from the folded menu", async () => {
|
||||
Partner._records[0].bar = false;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': '1', 'fold_field': 'bar'}" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect("[aria-checked='true']").toHaveText("aaa", {
|
||||
message: "default status is 'aaa'",
|
||||
});
|
||||
|
||||
expect(".o_statusbar_status .dropdown-toggle.o_arrow_button").toHaveText("...", {
|
||||
message: "button has the correct text",
|
||||
});
|
||||
|
||||
await click(".o_statusbar_status .dropdown-toggle:not(.d-none)");
|
||||
await animationFrame();
|
||||
await click(".o-dropdown--menu .dropdown-item");
|
||||
await animationFrame();
|
||||
|
||||
expect("[aria-checked='true']").toHaveText("second record", {
|
||||
message: "status has changed to the selected dropdown item",
|
||||
});
|
||||
});
|
||||
|
||||
test("statusbar with dynamic domain", async () => {
|
||||
Partner._fields.trululu = fields.Many2one({
|
||||
relation: "partner",
|
||||
domain: "[('int_field', '>', qux)]",
|
||||
});
|
||||
Partner._records[2].int_field = 0;
|
||||
onRpc("search_read", () => {
|
||||
rpcCount++;
|
||||
});
|
||||
let rpcCount = 0;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" />
|
||||
</header>
|
||||
<field name="qux" />
|
||||
<field name="foo" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_statusbar_status button:disabled").toHaveCount(6);
|
||||
expect(rpcCount).toBe(1, { message: "should have done 1 search_read rpc" });
|
||||
await click(".o_field_widget[name='qux'] input");
|
||||
await edit(9.5, { confirm: "enter" });
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_statusbar_status button:disabled").toHaveCount(5);
|
||||
expect(rpcCount).toBe(2, { message: "should have done 1 more search_read rpc" });
|
||||
await edit("hey", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect(rpcCount).toBe(2, { message: "should not have done 1 more search_read rpc" });
|
||||
});
|
||||
|
||||
test(`statusbar edited by the smart action "Move to stage..."`, async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': '1'}"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget").toHaveCount(1);
|
||||
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
await click(`.o_command:contains("Move to Trululu")`);
|
||||
await animationFrame();
|
||||
expect(queryAllTexts(".o_command")).toEqual(["first record", "second record", "aaa"]);
|
||||
await click("#o_command_2");
|
||||
await animationFrame();
|
||||
});
|
||||
|
||||
test("smart actions are unavailable if readonly", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" readonly="1"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget").toHaveCount(1);
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
const moveStages = queryAllTexts(".o_command");
|
||||
expect(moveStages).not.toInclude("Move to Trululu\nALT + SHIFT + X");
|
||||
expect(moveStages).not.toInclude("Move to next\nALT + X");
|
||||
});
|
||||
|
||||
test("hotkeys are unavailable if readonly", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" readonly="1"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget").toHaveCount(1);
|
||||
await press(["alt", "shift", "x"]); // Move to stage...
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(0, { message: "command palette should not open" });
|
||||
|
||||
await press(["alt", "x"]); // Move to next
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(0, { message: "command palette should not open" });
|
||||
});
|
||||
|
||||
test("auto save record when field toggled", async () => {
|
||||
onRpc("web_save", () => expect.step("web_save"));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(
|
||||
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current):eq(-1)"
|
||||
);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("For the same record, a single rpc is done to recover the specialData", async () => {
|
||||
Partner._views = {
|
||||
"list,3": '<list><field name="display_name"/></list>',
|
||||
"search,9": `<search></search>`,
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" readonly="1"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
]);
|
||||
onRpc("has_group", () => true);
|
||||
onRpc("search_read", () => expect.step("search_read"));
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
|
||||
await click(".o_data_row .o_data_cell");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["search_read"]);
|
||||
|
||||
await click(".o_back_button");
|
||||
await animationFrame();
|
||||
await click(".o_data_row .o_data_cell");
|
||||
await animationFrame();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("open form with statusbar, leave and come back to another one with other domain", async () => {
|
||||
Partner._views = {
|
||||
"list,3": '<list><field name="display_name"/></list>',
|
||||
"search,9": `<search/>`,
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" domain="[['id', '>', id]]" readonly="1"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
]);
|
||||
onRpc("has_group", () => true);
|
||||
onRpc("search_read", () => expect.step("search_read"));
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
|
||||
// open first record
|
||||
await click(".o_data_row .o_data_cell");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["search_read"]);
|
||||
|
||||
// go back and open second record
|
||||
await click(".o_back_button");
|
||||
await animationFrame();
|
||||
await click(".o_data_row:eq(1) .o_data_cell");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["search_read"]);
|
||||
});
|
||||
|
||||
test("clickable statusbar with readonly modifier set to false is editable", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" options="{'clickable': true}" readonly="False"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(".o_statusbar_status button:visible").toHaveCount(2);
|
||||
expect(".o_statusbar_status button[disabled][aria-checked='false']:visible").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("clickable statusbar with readonly modifier set to true is not editable", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" options="{'clickable': true}" readonly="True"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(".o_statusbar_status button[disabled]:visible").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("non-clickable statusbar with readonly modifier set to false is not editable", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" options="{'clickable': false}" readonly="False"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(".o_statusbar_status button[disabled]:visible").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("last status bar button have a border radius (no arrow shape) on the right side when a prior folded stage gets selected", async () => {
|
||||
class Stage extends models.Model {
|
||||
name = fields.Char();
|
||||
folded = fields.Boolean({ default: false });
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "New" },
|
||||
{ id: 2, name: "In Progress", folded: true },
|
||||
{ id: 3, name: "Done" },
|
||||
];
|
||||
}
|
||||
|
||||
class Task extends models.Model {
|
||||
status = fields.Many2one({ relation: "stage" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, status: 1 },
|
||||
{ id: 2, status: 2 },
|
||||
{ id: 3, status: 3 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Stage, Task]);
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "task",
|
||||
resId: 3,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="status" widget="statusbar" options="{'clickable': true, 'fold_field': 'folded'}" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
await click(".o_statusbar_status .dropdown-toggle:not(.d-none)");
|
||||
await animationFrame();
|
||||
await click(
|
||||
queryFirst(".dropdown-item", {
|
||||
root: getDropdownMenu(".o_statusbar_status .dropdown-toggle:not(.d-none)"),
|
||||
})
|
||||
);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_statusbar_status button[data-value='3']").not.toHaveStyle({
|
||||
borderTopRightRadius: "0px",
|
||||
});
|
||||
expect(".o_statusbar_status button[data-value='3']").toHaveClass("o_first");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("correctly load statusbar when dynamic domain changes", async () => {
|
||||
class Stage extends models.Model {
|
||||
name = fields.Char();
|
||||
folded = fields.Boolean({ default: false });
|
||||
project_ids = fields.Many2many({ relation: "project" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "Stage Project 1", project_ids: [1] },
|
||||
{ id: 2, name: "Stage Project 2", project_ids: [2] },
|
||||
];
|
||||
}
|
||||
|
||||
class Project extends models.Model {
|
||||
display_name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "Project 1" },
|
||||
{ id: 2, display_name: "Project 2" },
|
||||
];
|
||||
}
|
||||
|
||||
class Task extends models.Model {
|
||||
status = fields.Many2one({ relation: "stage" });
|
||||
project_id = fields.Many2one({ relation: "project" });
|
||||
|
||||
_records = [{ id: 1, project_id: 1, status: 1 }];
|
||||
}
|
||||
Task._onChanges.project_id = (obj) => {
|
||||
obj.status = obj.project_id === 1 ? 1 : 2;
|
||||
};
|
||||
|
||||
defineModels([Stage, Project, Task]);
|
||||
|
||||
onRpc("search_read", ({ kwargs }) => expect.step(JSON.stringify(kwargs.domain)));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "task",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="status" widget="statusbar" domain="[('project_ids', 'in', project_id)]" />
|
||||
</header>
|
||||
<field name="project_id"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(queryAllTexts(".o_statusbar_status button:not(.d-none)")).toEqual(["Stage Project 1"]);
|
||||
expect.verifySteps(['["|",["id","=",1],["project_ids","in",1]]']);
|
||||
await click(`[name="project_id"] .dropdown input`);
|
||||
await animationFrame();
|
||||
await click(`[name="project_id"] .dropdown .dropdown-menu .ui-menu-item:contains("Project 2")`);
|
||||
await animationFrame();
|
||||
|
||||
expect(queryAllTexts(".o_statusbar_status button:not(.d-none)")).toEqual(["Stage Project 2"]);
|
||||
expect.verifySteps(['["|",["id","=",2],["project_ids","in",2]]']);
|
||||
await clickSave();
|
||||
expect(queryAllTexts(".o_statusbar_status button:not(.d-none)")).toEqual(["Stage Project 2"]);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test('"status" with no stages does not crash command palette', async () => {
|
||||
class Stage extends models.Model {
|
||||
name = fields.Char();
|
||||
_records = []; // no stages
|
||||
}
|
||||
|
||||
class Task extends models.Model {
|
||||
status = fields.Many2one({ relation: "stage" });
|
||||
_records = [{ id: 1, status: false }];
|
||||
}
|
||||
|
||||
defineModels([Stage, Task]);
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "task",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="status" widget="statusbar" options="{'withCommand': true, 'clickable': true}"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// Open the command palette (Ctrl+K)
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
|
||||
const commands = queryAllTexts(".o_command");
|
||||
|
||||
expect(commands).not.toInclude("Move to next Stage");
|
||||
});
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { press, queryAll, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
function fieldTextArea(name) {
|
||||
return contains(`.o_field_widget[name='${name}'] textarea`);
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
description = fields.Text();
|
||||
}
|
||||
|
||||
defineModels([Product]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("basic rendering", async () => {
|
||||
Product._records = [{ id: 1, description: "Description as text" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="description"/></form>',
|
||||
});
|
||||
expect(".o_field_text textarea").toHaveCount(1);
|
||||
expect(".o_field_text textarea").toHaveValue("Description as text");
|
||||
});
|
||||
|
||||
test("doesn't have a scrollbar with long content", async () => {
|
||||
Product._records = [{ id: 1, description: "L\no\nn\ng\nD\ne\ns\nc\nr\ni\np\nt\ni\no\nn\n" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="description"/></form>',
|
||||
});
|
||||
const textarea = queryOne(".o_field_text textarea");
|
||||
expect(textarea.clientHeight).toBe(textarea.scrollHeight);
|
||||
});
|
||||
|
||||
test("render following an onchange", async () => {
|
||||
Product._fields.name = fields.Char({
|
||||
onChange: (record) => {
|
||||
expect.step("onchange");
|
||||
record.description = "Content ".repeat(100); // long text
|
||||
},
|
||||
});
|
||||
Product._records = [{ id: 1, description: "Description as text" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="description"/><field name="name"/></form>`,
|
||||
});
|
||||
const textarea = queryOne(".o_field_text textarea");
|
||||
const initialHeight = textarea.offsetHeight;
|
||||
await fieldInput("name").edit("Let's trigger the onchange");
|
||||
await animationFrame();
|
||||
expect(textarea.offsetHeight).toBeGreaterThan(initialHeight);
|
||||
await fieldTextArea("description").edit("Description as text");
|
||||
expect(textarea.offsetHeight).toBe(initialHeight);
|
||||
expect(textarea.clientHeight).toBe(textarea.scrollHeight);
|
||||
expect.verifySteps(["onchange"]);
|
||||
});
|
||||
|
||||
test("no scroll bar in editable list", async () => {
|
||||
Product._records = [{ id: 1, description: "L\no\nn\ng\nD\ne\ns\nc\nr\ni\np\nt\ni\no\nn\n" }];
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "product",
|
||||
arch: '<list editable="top"><field name="description"/></list>',
|
||||
});
|
||||
await contains(".o_data_row .o_data_cell").click();
|
||||
const textarea = queryOne(".o_field_text textarea");
|
||||
expect(textarea.clientHeight).toBe(textarea.scrollHeight);
|
||||
await contains("tr:not(.o_data_row)").click();
|
||||
const cell = queryOne(".o_data_row .o_data_cell");
|
||||
expect(cell.clientHeight).toBe(cell.scrollHeight);
|
||||
});
|
||||
|
||||
test("set row on text fields", async () => {
|
||||
Product._records = [{ id: 1, description: "Description as text" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="description" rows="40"/><field name="description"/></form>`,
|
||||
});
|
||||
const textareas = queryAll(".o_field_text textarea");
|
||||
expect(textareas[0].rows).toBe(40);
|
||||
expect(textareas[0].clientHeight).toBeGreaterThan(textareas[1].clientHeight);
|
||||
});
|
||||
|
||||
test("is translatable", async () => {
|
||||
Product._fields.description.translate = true;
|
||||
Product._records = [{ id: 1, description: "Description as text" }];
|
||||
|
||||
serverState.multiLang = true;
|
||||
|
||||
onRpc("get_installed", () => [
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
]);
|
||||
onRpc("get_field_translations", () => [
|
||||
[
|
||||
{ lang: "en_US", source: "Description as text", value: "Description as text" },
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "Description as text",
|
||||
value: "Description sous forme de texte",
|
||||
},
|
||||
],
|
||||
{ translation_type: "text", translation_show_source: false },
|
||||
]);
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><sheet><group><field name="description"/></group></sheet></form>`,
|
||||
});
|
||||
expect(".o_field_text textarea").toHaveClass("o_field_translate");
|
||||
await contains(".o_field_text textarea").click();
|
||||
expect(".o_field_text .btn.o_field_translate").toHaveCount(1);
|
||||
await contains(".o_field_text .btn.o_field_translate").click();
|
||||
expect(".modal").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("is translatable on new record", async () => {
|
||||
Product._fields.description.translate = true;
|
||||
Product._records = [{ id: 1, description: "Description as text" }];
|
||||
|
||||
serverState.multiLang = true;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: `<form><sheet><group><field name="description"/></group></sheet></form>`,
|
||||
});
|
||||
expect(".o_field_text .btn.o_field_translate").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("press enter inside editable list", async () => {
|
||||
Product._records = [{ id: 1, description: "Description as text" }];
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "product",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="description" />
|
||||
</list>`,
|
||||
});
|
||||
await contains(".o_data_row .o_data_cell").click();
|
||||
expect("textarea.o_input").toHaveCount(1);
|
||||
expect("textarea.o_input").toHaveValue("Description as text");
|
||||
expect("textarea.o_input").toBeFocused();
|
||||
expect("textarea.o_input").toHaveValue("Description as text");
|
||||
// clear selection before enter
|
||||
await fieldTextArea("description").press(["right", "Enter"]);
|
||||
expect("textarea.o_input").toHaveValue("Description as text\n");
|
||||
expect("textarea.o_input").toBeFocused();
|
||||
expect("tr.o_data_row").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("in editable list view", async () => {
|
||||
Product._records = [{ id: 1, description: "Description as text" }];
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "product",
|
||||
arch: '<list editable="top"><field name="description"/></list>',
|
||||
});
|
||||
await contains(".o_list_button_add").click();
|
||||
expect("textarea").toBeFocused();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("with dynamic placeholder", async () => {
|
||||
onRpc("mail_allowed_qweb_expressions", () => []);
|
||||
|
||||
Product._fields.placeholder = fields.Char({ default: "product" });
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="placeholder" invisible="1"/>
|
||||
<sheet>
|
||||
<group>
|
||||
<field
|
||||
name="description"
|
||||
options="{
|
||||
'dynamic_placeholder': true,
|
||||
'dynamic_placeholder_model_reference_field': 'placeholder'
|
||||
}"
|
||||
/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_popover .o_model_field_selector_popover").toHaveCount(0);
|
||||
await press(["alt", "#"]);
|
||||
await animationFrame();
|
||||
expect(".o_popover .o_model_field_selector_popover").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("with dynamic placeholder in mobile", async () => {
|
||||
onRpc("mail_allowed_qweb_expressions", () => []);
|
||||
|
||||
Product._fields.placeholder = fields.Char({ default: "product" });
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="placeholder" invisible="1"/>
|
||||
<sheet>
|
||||
<group>
|
||||
<field
|
||||
name="description"
|
||||
options="{
|
||||
'dynamic_placeholder': true,
|
||||
'dynamic_placeholder_model_reference_field': 'placeholder'
|
||||
}"
|
||||
/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_popover .o_model_field_selector_popover").toHaveCount(0);
|
||||
await fieldTextArea("description").focus();
|
||||
await press(["alt", "#"]);
|
||||
await animationFrame();
|
||||
expect(".o_popover .o_model_field_selector_popover").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("text field without line breaks", async () => {
|
||||
Product._records = [{ id: 1, description: "Description as text" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="description" options="{'line_breaks': False}"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_text textarea").toHaveCount(1);
|
||||
expect(".o_field_text textarea").toHaveValue("Description as text");
|
||||
await contains(".o_field_text textarea").click();
|
||||
await press("Enter");
|
||||
expect(".o_field_text textarea").toHaveValue("Description as text");
|
||||
|
||||
await contains(".o_field_text textarea").clear({ confirm: false });
|
||||
await navigator.clipboard.writeText("text\nwith\nline\nbreaks\n"); // copy
|
||||
await press(["ctrl", "v"]); // paste
|
||||
expect(".o_field_text textarea").toHaveValue("text with line breaks ", {
|
||||
message: "no line break should appear",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryText } from "@odoo/hoot-dom";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Localization extends models.Model {
|
||||
country = fields.Selection({
|
||||
selection: [
|
||||
["belgium", "Belgium"],
|
||||
["usa", "United States"],
|
||||
],
|
||||
onChange: (record) => {
|
||||
record.tz_offset = "+4800";
|
||||
},
|
||||
});
|
||||
tz_offset = fields.Char();
|
||||
_records = [{ id: 1, country: "belgium" }];
|
||||
}
|
||||
|
||||
defineModels([Localization]);
|
||||
|
||||
test("in a list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "localization",
|
||||
resId: 1,
|
||||
arch: /*xml*/ `
|
||||
<list string="Localizations" editable="top">
|
||||
<field name="tz_offset" column_invisible="True"/>
|
||||
<field name="country" widget="timezone_mismatch" />
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
expect("td:contains(Belgium)").toHaveCount(1);
|
||||
await contains(".o_data_cell").click();
|
||||
expect(".o_field_widget[name=country] select").toHaveCount(1);
|
||||
await contains(".o_field_widget[name=country] select").select(`"usa"`);
|
||||
expect(".o_data_cell:first").toHaveText(
|
||||
/United States\s+\([0-9]+\/[0-9]+\/[0-9]+ [0-9]+:[0-9]+:[0-9]+\)/
|
||||
);
|
||||
expect(".o_tz_warning").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("in a form view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "localization",
|
||||
resId: 1,
|
||||
arch: /*xml*/ `
|
||||
<form>
|
||||
<field name="tz_offset" invisible="True"/>
|
||||
<field name="country" widget="timezone_mismatch" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(`.o_field_widget[name="country"]:contains(Belgium)`).toHaveCount(1);
|
||||
expect(".o_field_widget[name=country] select").toHaveCount(1);
|
||||
await contains(".o_field_widget[name=country] select").select(`"usa"`);
|
||||
expect(queryText(`.o_field_widget[name="country"]:first`)).toMatch(
|
||||
/United States\s+\([0-9]+\/[0-9]+\/[0-9]+ [0-9]+:[0-9]+:[0-9]+\)/
|
||||
);
|
||||
expect(".o_tz_warning").toHaveCount(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { queryAllAttributes, queryAllTexts, queryFirst } from "@odoo/hoot-dom";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "../../web_test_helpers";
|
||||
|
||||
class Product extends models.Model {
|
||||
url = fields.Char();
|
||||
}
|
||||
|
||||
defineModels([Product]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("UrlField in form view", async () => {
|
||||
Product._records = [{ id: 1, url: "https://www.example.com" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="url" widget="url"/></form>`,
|
||||
});
|
||||
expect(`.o_field_widget input[type="text"]`).toHaveCount(1);
|
||||
expect(`.o_field_widget input[type="text"]`).toHaveValue("https://www.example.com");
|
||||
expect(`.o_field_url a`).toHaveAttribute("href", "https://www.example.com");
|
||||
await fieldInput("url").edit("https://www.odoo.com");
|
||||
expect(`.o_field_widget input[type="text"]`).toHaveValue("https://www.odoo.com");
|
||||
});
|
||||
|
||||
test("in form view (readonly)", async () => {
|
||||
Product._records = [{ id: 1, url: "https://www.example.com" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="url" widget="url" readonly="1"/></form>`,
|
||||
});
|
||||
expect("a.o_field_widget.o_form_uri").toHaveCount(1);
|
||||
expect("a.o_field_widget.o_form_uri").toHaveAttribute("href", "https://www.example.com");
|
||||
expect("a.o_field_widget.o_form_uri").toHaveAttribute("target", "_blank");
|
||||
expect("a.o_field_widget.o_form_uri").toHaveText("https://www.example.com");
|
||||
});
|
||||
|
||||
test("it takes its text content from the text attribute", async () => {
|
||||
Product._records = [{ id: 1, url: "https://www.example.com" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="url" widget="url" text="https://another.com" readonly="1"/></form>',
|
||||
});
|
||||
expect(`.o_field_url a`).toHaveText("https://another.com");
|
||||
});
|
||||
|
||||
test("href attribute and website_path option", async () => {
|
||||
Product._fields.url1 = fields.Char();
|
||||
Product._fields.url2 = fields.Char();
|
||||
Product._fields.url3 = fields.Char();
|
||||
Product._fields.url4 = fields.Char();
|
||||
Product._records = [
|
||||
{
|
||||
id: 1,
|
||||
url1: "http://www.url1.com",
|
||||
url2: "www.url2.com",
|
||||
url3: "http://www.url3.com",
|
||||
url4: "https://url4.com",
|
||||
},
|
||||
];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="url1" widget="url" readonly="1"/>
|
||||
<field name="url2" widget="url" readonly="1" options="{'website_path': True}"/>
|
||||
<field name="url3" widget="url" readonly="1"/>
|
||||
<field name="url4" widget="url" readonly="1"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(`.o_field_widget[name="url1"] a`).toHaveAttribute("href", "http://www.url1.com");
|
||||
expect(`.o_field_widget[name="url2"] a`).toHaveAttribute("href", "www.url2.com");
|
||||
expect(`.o_field_widget[name="url3"] a`).toHaveAttribute("href", "http://www.url3.com");
|
||||
expect(`.o_field_widget[name="url4"] a`).toHaveAttribute("href", "https://url4.com");
|
||||
});
|
||||
|
||||
test("in editable list view", async () => {
|
||||
Product._records = [
|
||||
{ id: 1, url: "example.com" },
|
||||
{ id: 2, url: "odoo.com" },
|
||||
];
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "product",
|
||||
arch: '<list editable="bottom"><field name="url" widget="url"/></list>',
|
||||
});
|
||||
expect("tbody td:not(.o_list_record_selector) a").toHaveCount(2);
|
||||
expect(".o_field_url.o_field_widget[name='url'] a").toHaveCount(2);
|
||||
expect(queryAllAttributes(".o_field_url.o_field_widget[name='url'] a", "href")).toEqual([
|
||||
"http://example.com",
|
||||
"http://odoo.com",
|
||||
]);
|
||||
expect(queryAllTexts(".o_field_url.o_field_widget[name='url'] a")).toEqual([
|
||||
"example.com",
|
||||
"odoo.com",
|
||||
]);
|
||||
let cell = queryFirst("tbody td:not(.o_list_record_selector)");
|
||||
await contains(cell).click();
|
||||
expect(cell.parentElement).toHaveClass("o_selected_row");
|
||||
expect(cell.querySelector("input")).toHaveValue("example.com");
|
||||
await fieldInput("url").edit("test");
|
||||
await contains(getFixture()).click(); // click out
|
||||
cell = queryFirst("tbody td:not(.o_list_record_selector)");
|
||||
expect(cell.parentElement).not.toHaveClass("o_selected_row");
|
||||
expect("tbody td:not(.o_list_record_selector) a").toHaveCount(2);
|
||||
expect(".o_field_url.o_field_widget[name='url'] a").toHaveCount(2);
|
||||
expect(queryAllAttributes(".o_field_url.o_field_widget[name='url'] a", "href")).toEqual([
|
||||
"http://test",
|
||||
"http://odoo.com",
|
||||
]);
|
||||
expect(queryAllTexts(".o_field_url.o_field_widget[name='url'] a")).toEqual([
|
||||
"test",
|
||||
"odoo.com",
|
||||
]);
|
||||
});
|
||||
|
||||
test("with falsy value", async () => {
|
||||
Product._records = [{ id: 1, url: false }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="url" widget="url"/></form>',
|
||||
});
|
||||
expect(`[name=url] input`).toHaveCount(1);
|
||||
expect(`[name=url] input`).toHaveValue("");
|
||||
});
|
||||
|
||||
test("onchange scenario", async () => {
|
||||
Product._fields.url_source = fields.Char({
|
||||
onChange: (record) => (record.url = record.url_source),
|
||||
});
|
||||
Product._records = [{ id: 1, url: "odoo.com", url_source: "another.com" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="url" widget="url" readonly="True"/><field name="url_source"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget[name=url]").toHaveText("odoo.com");
|
||||
expect(".o_field_widget[name=url_source] input").toHaveValue("another.com");
|
||||
await fieldInput("url_source").edit("example.com");
|
||||
expect(".o_field_widget[name=url]").toHaveText("example.com");
|
||||
});
|
||||
|
||||
test("with placeholder", async () => {
|
||||
Product._records = [{ id: 1 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: `<form><field name="url" widget="url" placeholder="Placeholder"/></form>`,
|
||||
});
|
||||
expect(`.o_field_widget input`).toHaveAttribute("placeholder", "Placeholder");
|
||||
});
|
||||
|
||||
test("with non falsy, but non url value", async () => {
|
||||
Product._fields.url.default = "odoo://hello";
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: `<form><field name="url" widget="url"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget[name=url] a").toHaveAttribute("href", "http://odoo://hello");
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,455 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { createElement } from "@web/core/utils/xml";
|
||||
import { FormCompiler } from "@web/views/form/form_compiler";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
function compileTemplate(arch) {
|
||||
const parser = new DOMParser();
|
||||
const xml = parser.parseFromString(arch, "text/xml");
|
||||
const compiler = new FormCompiler({ form: xml.documentElement });
|
||||
return compiler.compile("form");
|
||||
}
|
||||
|
||||
test("properly compile simple div", () => {
|
||||
const arch = /*xml*/ `<form><div>lol</div></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div>lol</div>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("label with empty string compiles to FormLabel with empty string", () => {
|
||||
const arch = /*xml*/ `<form><field field_id="test" name="test"/><label for="test" string=""/></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<Field id="'test'" name="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
|
||||
<FormLabel id="'test'" fieldName="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" className="""" string="\`\`" />
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile simple div with field", () => {
|
||||
const arch = /*xml*/ `<form><div class="someClass">lol<field field_id="display_name" name="display_name"/></div></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div class="someClass">
|
||||
lol
|
||||
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile inner groups", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<group>
|
||||
<group><field field_id="display_name" name="display_name"/></group>
|
||||
<group><field field_id="charfield" name="charfield"/></group>
|
||||
</group>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<OuterGroup>
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
|
||||
<InnerGroup class="scope && scope.className">
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'display_name',fieldName:'display_name',record:__comp__.props.record,string:__comp__.props.record.fields.display_name.string,fieldInfo:__comp__.props.archInfo.fieldNodes['display_name']}" Component="__comp__.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
|
||||
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew" class="scope && scope.className"/>
|
||||
</t>
|
||||
</InnerGroup>
|
||||
</t>
|
||||
<t t-set-slot="item_1" type="'item'" sequence="1" t-slot-scope="scope" isVisible="true" itemSpan="1">
|
||||
<InnerGroup class="scope && scope.className">
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'charfield',fieldName:'charfield',record:__comp__.props.record,string:__comp__.props.record.fields.charfield.string,fieldInfo:__comp__.props.archInfo.fieldNodes['charfield']}" Component="__comp__.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
|
||||
<Field id="'charfield'" name="'charfield'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['charfield']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew" class="scope && scope.className"/>
|
||||
</t>
|
||||
</InnerGroup>
|
||||
</t>
|
||||
</OuterGroup>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile attributes with nested forms", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<group>
|
||||
<group>
|
||||
<form>
|
||||
<div>
|
||||
<field field_id="test" name="test"/>
|
||||
</div>
|
||||
</form>
|
||||
</group>
|
||||
</group>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<OuterGroup>
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
|
||||
<InnerGroup class="scope && scope.className">
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }} {{scope && scope.className || "" }}">
|
||||
<div><Field id="'test'" name="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/></div>
|
||||
</div>
|
||||
</t>
|
||||
</InnerGroup>
|
||||
</t>
|
||||
</OuterGroup>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile notebook", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<notebook>
|
||||
<page name="p1" string="Page1"><field field_id="charfield" name="charfield"/></page>
|
||||
<page name="p2" string="Page2"><field field_id="display_name" name="display_name"/></page>
|
||||
</notebook>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<Notebook defaultPage="__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[0]" onPageUpdate="(page) => __comp__.props.onNotebookPageChange(0, page)">
|
||||
<t t-set-slot="page_1" title="\`Page1\`" name="\`p1\`" isVisible="true">
|
||||
<Field id="'charfield'" name="'charfield'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['charfield']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
|
||||
</t>
|
||||
<t t-set-slot="page_2" title="\`Page2\`" name="\`p2\`" isVisible="true">
|
||||
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
|
||||
</t>
|
||||
</Notebook>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile field without placeholder", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<field field_id="display_name" name="display_name" placeholder="e.g. Contact's Name or //someinfo..."/>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile no sheet", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<header>someHeader</header>
|
||||
<div>someDiv</div>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div class="o_form_statusbar position-relative d-flex justify-content-between mb-0 mb-md-2 pb-2 pb-md-0"><StatusBarButtons t-if="!__comp__.env.isSmall or __comp__.env.inDialog"/></div>
|
||||
<div>someDiv</div>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile sheet", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<header>someHeader</header>
|
||||
<div>someDiv</div>
|
||||
<sheet>
|
||||
<div>inside sheet</div>
|
||||
</sheet>
|
||||
<div>after sheet</div>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex d-print-block {{ __comp__.uiService.size < 6 ? "flex-column" : "flex-nowrap h-100" }} {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div class="o_form_sheet_bg">
|
||||
<div class="o_form_statusbar position-relative d-flex justify-content-between mb-0 mb-md-2 pb-2 pb-md-0"><StatusBarButtons t-if="!__comp__.env.isSmall or __comp__.env.inDialog"/></div>
|
||||
<div>someDiv</div>
|
||||
<div class="o_form_sheet position-relative">
|
||||
<div>inside sheet</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>after sheet</div>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile buttonBox invisible in sheet", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box" invisible="'display_name' == 'plop'">
|
||||
<div>Hello</div>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer"
|
||||
t-att-class="__comp__.props.class"
|
||||
t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex d-print-block {{ __comp__.uiService.size < 6 ? "flex-column" : "flex-nowrap h-100" }} {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}"
|
||||
t-ref="compiled_view_root">
|
||||
<div class="o_form_sheet_bg">
|
||||
<div class="o_form_sheet position-relative">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile invisible", () => {
|
||||
// cf python side: def transfer_node_to_modifiers
|
||||
// modifiers' string are evaluated to their boolean or array form
|
||||
// So the following arch may actually be written as:
|
||||
// ```<form>
|
||||
// <field name="display_name" invisible="1" />
|
||||
// <div class="visible3" invisible="0"/>
|
||||
// <div invisible="display_name == 'take'"/>
|
||||
// </form>````
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<field field_id="display_name" name="display_name" invisible="True" />
|
||||
<div class="visible3" invisible="False"/>
|
||||
<div invisible="display_name == "take""/>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div class="visible3"/>
|
||||
<div t-if="!__comp__.evaluateBooleanExpr("display_name == \\"take\\"",__comp__.props.record.evalContextWithVirtualIds)"/>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("compile invisible containing string as domain", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<field name="display_name" invisible="True"/>
|
||||
<div class="visible3" invisible="False"/>
|
||||
<div invisible="display_name == 'take'"/>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div class="visible3" />
|
||||
<div t-if="!__comp__.evaluateBooleanExpr("display_name == 'take'",__comp__.props.record.evalContextWithVirtualIds)"/>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile status bar with content", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<header><div>someDiv</div></header>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div class="o_form_statusbar position-relative d-flex justify-content-between mb-0 mb-md-2 pb-2 pb-md-0">
|
||||
<StatusBarButtons t-if="!__comp__.env.isSmall or __comp__.env.inDialog">
|
||||
<t t-set-slot="button_0" isVisible="true">
|
||||
<div>someDiv</div>
|
||||
</t>
|
||||
</StatusBarButtons>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile status bar without content", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<header></header>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div class="o_form_statusbar position-relative d-flex justify-content-between mb-0 mb-md-2 pb-2 pb-md-0">
|
||||
<StatusBarButtons t-if="!__comp__.env.isSmall or __comp__.env.inDialog"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile settings", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<setting help="this is bar"
|
||||
documentation="/applications/technical/web/settings/this_is_a_test.html"
|
||||
company_dependent="1">
|
||||
<field field_id="bar" name="bar"/>
|
||||
<label>label with content</label>
|
||||
</setting>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<Setting title="\`\`"
|
||||
help="\`this is bar\`"
|
||||
companyDependent="true"
|
||||
documentation="\`/applications/technical/web/settings/this_is_a_test.html\`"
|
||||
record="__comp__.props.record"
|
||||
fieldInfo="__comp__.props.archInfo.fieldNodes['bar']"
|
||||
fieldName="\`bar\`"
|
||||
fieldId="\`bar\`"
|
||||
string="\`\`"
|
||||
addLabel="true">
|
||||
<t t-set-slot="fieldSlot">
|
||||
<Field id="'bar'"
|
||||
name="'bar'"
|
||||
record="__comp__.props.record"
|
||||
fieldInfo="__comp__.props.archInfo.fieldNodes['bar']"
|
||||
readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
|
||||
</t>
|
||||
<label>label with content</label>
|
||||
</Setting>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("properly compile empty ButtonBox", () => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex d-print-block {{ __comp__.uiService.size < 6 ? "flex-column" : "flex-nowrap h-100" }} {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div class="o_form_sheet_bg">
|
||||
<div class="o_form_sheet position-relative">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("invisible is correctly computed with another t-if", () => {
|
||||
patchWithCleanup(FormCompiler.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.compilers.push({
|
||||
selector: "myNode",
|
||||
fn: () => {
|
||||
const div = createElement("div");
|
||||
div.className = "myNode";
|
||||
div.setAttribute("t-if", "myCondition or myOtherCondition");
|
||||
return div;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const arch = `<myNode invisible="field == 'value'"/>`;
|
||||
const expected = `<t t-translation="off"><div class="myNode" t-if="( myCondition or myOtherCondition ) and !__comp__.evaluateBooleanExpr("field == 'value'",__comp__.props.record.evalContextWithVirtualIds)" t-ref="compiled_view_root"/></t>`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("keep nosheet style if a sheet is part of a nested form", () => {
|
||||
const arch = `
|
||||
<form>
|
||||
<field name="move_line_ids" field_id="move_line_ids">
|
||||
<form>
|
||||
<sheet/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`;
|
||||
|
||||
const expected = `<t t-translation="off">
|
||||
<div
|
||||
class="o_form_renderer o_form_nosheet"
|
||||
t-att-class="__comp__.props.class"
|
||||
t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}"
|
||||
t-ref="compiled_view_root"
|
||||
>
|
||||
<Field
|
||||
id="'move_line_ids'"
|
||||
name="'move_line_ids'"
|
||||
record="__comp__.props.record"
|
||||
fieldInfo="__comp__.props.archInfo.fieldNodes['move_line_ids']"
|
||||
readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"
|
||||
/>
|
||||
</div>
|
||||
</t>`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
|
||||
test("form with t-translation directive", () => {
|
||||
patchWithCleanup(console, { warn: (message) => expect.step(message) });
|
||||
const arch = `
|
||||
<form>
|
||||
<div t-translation="off">Hello</div>
|
||||
</form>`;
|
||||
|
||||
const expected = `<t t-translation="off">
|
||||
<div
|
||||
class="o_form_renderer o_form_nosheet"
|
||||
t-att-class="__comp__.props.class"
|
||||
t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}"
|
||||
t-ref="compiled_view_root">
|
||||
<div> Hello </div>
|
||||
</div>
|
||||
</t>`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
expect.verifySteps([]); // should no log any warning
|
||||
});
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { defineModels, fields, models, mountView, contains } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
charfield = fields.Char();
|
||||
|
||||
_records = [{ id: 1, name: "firstRecord", charfield: "content of charfield" }];
|
||||
}
|
||||
defineModels([Partner]);
|
||||
|
||||
test("compile form with modifiers", async () => {
|
||||
Partner._views = {
|
||||
form: /*xml*/ `
|
||||
<form>
|
||||
<div invisible="display_name == uid">
|
||||
<field name="charfield"/>
|
||||
</div>
|
||||
<field name="display_name" readonly="display_name == uid"/>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
expect(`.o_form_editable input`).toHaveCount(2);
|
||||
});
|
||||
|
||||
test("compile notebook with modifiers", async () => {
|
||||
Partner._views = {
|
||||
form: /*xml*/ `
|
||||
<form>
|
||||
<sheet>
|
||||
<notebook>
|
||||
<page name="p1" invisible="display_name == 'lol'"><field name="charfield"/></page>
|
||||
<page name="p2"><field name="display_name"/></page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
expect(queryAllTexts`.o_notebook_headers .nav-item`).toEqual(["p1", "p2"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("compile header and buttons on desktop", async () => {
|
||||
Partner._views = {
|
||||
form: /*xml*/ `
|
||||
<form>
|
||||
<header>
|
||||
<button string="ActionButton" class="oe_highlight" name="action_button" type="object"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
expect(`.o_statusbar_buttons button[name=action_button]:contains(ActionButton)`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("compile header and buttons on mobile", async () => {
|
||||
Partner._views = {
|
||||
form: /*xml*/ `
|
||||
<form>
|
||||
<header>
|
||||
<button string="ActionButton" class="oe_highlight" name="action_button" type="object"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
|
||||
expect(
|
||||
`.o-dropdown-item-unstyled-button button[name=action_button]:contains(ActionButton)`
|
||||
).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("render field with placeholder", async () => {
|
||||
registry.category("fields").add(
|
||||
"char",
|
||||
{
|
||||
component: class CharField extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div/>`;
|
||||
setup() {
|
||||
expect.step("setup field component");
|
||||
expect(this.props.placeholder).toBe("e.g. Contact's Name or //someinfo...");
|
||||
}
|
||||
},
|
||||
extractProps: ({ attrs }) => ({ placeholder: attrs.placeholder }),
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
Partner._views = {
|
||||
form: /*xml*/ `
|
||||
<form>
|
||||
<field name="display_name" placeholder="e.g. Contact's Name or //someinfo..." />
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
expect.verifySteps(["setup field component"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("compile a button with id on desktop", async () => {
|
||||
Partner._views = {
|
||||
form: /*xml*/ `
|
||||
<form>
|
||||
<header>
|
||||
<button id="action_button" string="ActionButton"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
expect(`button[id=action_button]`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("compile a button with id on mobile", async () => {
|
||||
Partner._views = {
|
||||
form: /*xml*/ `
|
||||
<form>
|
||||
<header>
|
||||
<button id="action_button" string="ActionButton"/>
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
|
||||
expect(`button[id=action_button]`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("compile a button with disabled", async () => {
|
||||
Partner._views = {
|
||||
form: /*xml*/ `
|
||||
<form>
|
||||
<button id="action_button" string="ActionButton" name="action_button" type="object" disabled="disabled"/>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
expect(`button[id=action_button]`).toHaveAttribute("disabled")
|
||||
});
|
||||
12716
odoo-bringout-oca-ocb-web/web/static/tests/views/form/form_view.test.js
Normal file
12716
odoo-bringout-oca-ocb-web/web/static/tests/views/form/form_view.test.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,212 @@
|
|||
import { before, expect } from "@odoo/hoot";
|
||||
import { queryAllTexts, queryOne } from "@odoo/hoot-dom";
|
||||
import { contains, findComponent, preloadBundle } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { ensureArray } from "@web/core/utils/arrays";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { GraphController } from "@web/views/graph/graph_controller";
|
||||
import { GraphRenderer } from "@web/views/graph/graph_renderer";
|
||||
|
||||
/**
|
||||
* @typedef {"bar" | "line" | "pie"} GraphMode
|
||||
*
|
||||
* @typedef {import("@web/views/view").View} GraphView
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
* @param {string | Iterable<string>} keys
|
||||
* @param {Record<string, any> | Iterable<Record<string, any>>} expectedDatasets
|
||||
*/
|
||||
export const checkDatasets = (view, keys, expectedDatasets) => {
|
||||
keys = ensureArray(keys);
|
||||
|
||||
const datasets = getChart(view).data.datasets;
|
||||
const actualValues = [];
|
||||
for (const dataset of datasets) {
|
||||
const partialDataset = {};
|
||||
for (const key of keys) {
|
||||
partialDataset[key] = dataset[key];
|
||||
}
|
||||
actualValues.push(partialDataset);
|
||||
}
|
||||
expect(actualValues).toEqual(ensureArray(expectedDatasets));
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
* @param {GraphMode} mode
|
||||
*/
|
||||
export const checkModeIs = (view, mode) => {
|
||||
expect(getGraphModelMetaData(view).mode).toBe(mode);
|
||||
expect(getChart(view).config.type).toBe(mode);
|
||||
expect(getModeButton(mode)).toHaveClass("active");
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
* @param {{ lines: { label: string, value: string }[], title?: string }} expectedTooltip
|
||||
* @param {number} index
|
||||
* @param {number} datasetIndex
|
||||
*/
|
||||
export const checkTooltip = (view, { title, lines }, index, datasetIndex = null) => {
|
||||
// If the tooltip options are changed, this helper should change: we construct the dataPoints
|
||||
// similarly to Chart.js according to the values set for the tooltips options 'mode' and 'intersect'.
|
||||
const chart = getChart(view);
|
||||
const { datasets } = chart.data;
|
||||
const dataPoints = [];
|
||||
for (let i = 0; i < datasets.length; i++) {
|
||||
const dataset = datasets[i];
|
||||
const raw = dataset.data[index];
|
||||
if (raw !== undefined && (datasetIndex === null || datasetIndex === i)) {
|
||||
dataPoints.push({
|
||||
datasetIndex: i,
|
||||
dataIndex: index,
|
||||
raw,
|
||||
});
|
||||
}
|
||||
}
|
||||
chart.config.options.plugins.tooltip.external({
|
||||
tooltip: { opacity: 1, x: 1, y: 1, dataPoints },
|
||||
});
|
||||
const lineLabels = [];
|
||||
const lineValues = [];
|
||||
for (const line of lines) {
|
||||
lineLabels.push(line.label);
|
||||
lineValues.push(String(line.value));
|
||||
}
|
||||
|
||||
expect(`.o_graph_custom_tooltip`).toHaveCount(1);
|
||||
expect(`table thead tr th.o_measure`).toHaveText(title || "Count");
|
||||
expect(queryAllTexts(`table tbody tr td small.o_label`)).toEqual(lineLabels);
|
||||
expect(queryAllTexts(`table tbody tr td.o_value`)).toEqual(lineValues);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {"asc" | "desc"} direction
|
||||
*/
|
||||
export const clickSort = (direction) => contains(`.fa-sort-amount-${direction}`).click();
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
*/
|
||||
export const getChart = (view) => getGraphRenderer(view).chart;
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
*/
|
||||
export const getGraphModelMetaData = (view) => getGraphModel(view).metaData;
|
||||
|
||||
/**
|
||||
* @param {GraphMode} mode
|
||||
*/
|
||||
export const getModeButton = (mode) => queryOne`.o_graph_button[data-mode=${mode}]`;
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
*/
|
||||
export const getScaleY = (view) => getChart(view).config.options.scales.y;
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
*/
|
||||
export const getYAxisLabel = (view) => getChart(view).config.options.scales.y.title.text;
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
* @param {string | Iterable<string>} expectedLabels
|
||||
*/
|
||||
export function checkLabels(view, expectedLabels) {
|
||||
expect(getChart(view).data.labels.map(String)).toEqual(ensureArray(expectedLabels));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
* @param {string | Iterable<string>} expectedLabels
|
||||
*/
|
||||
export function checkYTicks(view, expectedLabels) {
|
||||
const labels = getChart(view).scales.y.ticks.map((l) => l.label);
|
||||
expect(labels).toEqual(expectedLabels);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
* @param {string | Iterable<string>} expectedLabels
|
||||
*/
|
||||
export function checkLegend(view, expectedLabels) {
|
||||
const chart = getChart(view);
|
||||
const labels = chart.config.options.plugins.legend.labels
|
||||
.generateLabels(chart)
|
||||
.map((o) => o.text);
|
||||
const expectedLabelsList = ensureArray(expectedLabels);
|
||||
expect(labels).toEqual(expectedLabelsList, {
|
||||
message: `Legend should be matching: ${expectedLabelsList
|
||||
.map((label) => `"${label}"`)
|
||||
.join(", ")}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
*/
|
||||
export async function clickOnDataset(view) {
|
||||
const chart = getChart(view);
|
||||
const point = chart.getDatasetMeta(0).data[0].getCenterPoint();
|
||||
return contains(chart.canvas).click({ position: point, relative: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
*/
|
||||
export function getGraphController(view) {
|
||||
return findComponent(view, (c) => c instanceof GraphController);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
*/
|
||||
export function getGraphModel(view) {
|
||||
return getGraphController(view).model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
* @returns {GraphRenderer}
|
||||
*/
|
||||
export function getGraphRenderer(view) {
|
||||
return findComponent(view, (c) => c instanceof GraphRenderer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GraphMode} mode
|
||||
*/
|
||||
export function selectMode(mode) {
|
||||
return contains(getModeButton(mode)).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GraphView} view
|
||||
* @param {string} text
|
||||
*/
|
||||
export async function clickOnLegend(view, text) {
|
||||
const chart = getChart(view);
|
||||
const index = chart.legend.legendItems.findIndex((e) => e.text === text);
|
||||
const { left, top, width, height } = chart.legend.legendHitBoxes[index];
|
||||
const point = {
|
||||
x: left + width / 2,
|
||||
y: top + height / 2,
|
||||
};
|
||||
return contains(chart.canvas).click({ position: point, relative: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to call at the start of a test suite using the Chart.js lib.
|
||||
*
|
||||
* It will:
|
||||
* - pre-load the Chart.js lib before tests are run;
|
||||
* - disable all animations in the lib.
|
||||
*/
|
||||
export function setupChartJsForTests() {
|
||||
preloadBundle("web.chartjs_lib");
|
||||
before(() => patch(Chart.defaults, { animation: false }));
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,186 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { KanbanArchParser } from "@web/views/kanban/kanban_arch_parser";
|
||||
import { parseXML } from "@web/core/utils/xml";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
getDropdownMenu,
|
||||
getKanbanRecord,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
toggleKanbanRecordDropdown,
|
||||
} from "../../web_test_helpers";
|
||||
import { queryAll } from "@odoo/hoot-dom";
|
||||
|
||||
function parseArch(arch) {
|
||||
const parser = new KanbanArchParser();
|
||||
const xmlDoc = parseXML(arch);
|
||||
return parser.parse(xmlDoc, { fake: { name: { string: "Name", type: "char" } } }, "fake");
|
||||
}
|
||||
|
||||
class Category extends models.Model {
|
||||
_name = "category";
|
||||
|
||||
name = fields.Char();
|
||||
color = fields.Integer();
|
||||
|
||||
_records = [
|
||||
{ id: 6, name: "gold", color: 2 },
|
||||
{ id: 7, name: "silver", color: 5 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Category]);
|
||||
|
||||
// avoid "kanban-box" deprecation warnings in this suite, which defines legacy kanban on purpose
|
||||
beforeEach(() => {
|
||||
const originalConsoleWarn = console.warn;
|
||||
patchWithCleanup(console, {
|
||||
warn: (msg) => {
|
||||
if (msg !== "'kanban-box' is deprecated, define a 'card' template instead") {
|
||||
originalConsoleWarn(msg);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("oe_kanban_colorpicker in kanban-menu and kanban-box", async () => {
|
||||
const archInfo = parseArch(`
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-menu">
|
||||
<ul class="oe_kanban_colorpicker" data-field="kanban_menu_colorpicker" role="menu"/>
|
||||
</t>
|
||||
<t t-name="kanban-box"/>
|
||||
</templates>
|
||||
</kanban>
|
||||
`);
|
||||
expect(archInfo.colorField).toBe("kanban_menu_colorpicker", {
|
||||
message: "colorField should be 'kanban_menu_colorpicker'",
|
||||
});
|
||||
|
||||
const archInfo_1 = parseArch(`
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-menu"/>
|
||||
<t t-name="kanban-box">
|
||||
<ul class="oe_kanban_colorpicker" data-field="kanban_box_color" role="menu"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`);
|
||||
expect(archInfo_1.colorField).toBe("kanban_box_color", {
|
||||
message: "colorField should be 'kanban_box_color'",
|
||||
});
|
||||
});
|
||||
|
||||
test("kanban with colorpicker and node with color attribute", async () => {
|
||||
Category._fields.colorpickerField = fields.Integer();
|
||||
Category._records[0].colorpickerField = 3;
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(`write-color-${args[1].colorpickerField}`);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "category",
|
||||
arch: `
|
||||
<kanban>
|
||||
<field name="colorpickerField"/>
|
||||
<templates>
|
||||
<t t-name="kanban-menu">
|
||||
<div class="oe_kanban_colorpicker" data-field="colorpickerField"/>
|
||||
</t>
|
||||
<t t-name="kanban-box">
|
||||
<div color="colorpickerField">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
expect(getKanbanRecord({ index: 0 })).toHaveClass("o_kanban_color_3");
|
||||
await toggleKanbanRecordDropdown(0);
|
||||
await contains(`.oe_kanban_colorpicker li[title="Raspberry"] a.oe_kanban_color_9`).click();
|
||||
// should write on the color field
|
||||
expect.verifySteps(["write-color-9"]);
|
||||
expect(getKanbanRecord({ index: 0 })).toHaveClass("o_kanban_color_9");
|
||||
});
|
||||
|
||||
test("edit the kanban color with the colorpicker", async () => {
|
||||
Category._records[0].color = 12;
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(`write-color-${args[1].color}`);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "category",
|
||||
arch: `
|
||||
<kanban>
|
||||
<field name="color"/>
|
||||
<templates>
|
||||
<t t-name="kanban-menu">
|
||||
<div class="oe_kanban_colorpicker"/>
|
||||
</t>
|
||||
<t t-name="kanban-box">
|
||||
<div color="color">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
await toggleKanbanRecordDropdown(0);
|
||||
|
||||
expect(".o_kanban_record.oe_kanban_color_12").toHaveCount(0, {
|
||||
message: "no record should have the color 12",
|
||||
});
|
||||
expect(
|
||||
queryAll(".oe_kanban_colorpicker", { root: getDropdownMenu(getKanbanRecord({ index: 0 })) })
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
queryAll(".oe_kanban_colorpicker > *", {
|
||||
root: getDropdownMenu(getKanbanRecord({ index: 0 })),
|
||||
})
|
||||
).toHaveCount(12, { message: "the color picker should have 12 children (the colors)" });
|
||||
|
||||
await contains(".oe_kanban_colorpicker a.oe_kanban_color_9").click();
|
||||
|
||||
// should write on the color field
|
||||
expect.verifySteps(["write-color-9"]);
|
||||
expect(getKanbanRecord({ index: 0 })).toHaveClass("o_kanban_color_9");
|
||||
});
|
||||
|
||||
test("colorpicker doesn't appear when missing access rights", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "category",
|
||||
arch: `
|
||||
<kanban edit="0">
|
||||
<field name="color"/>
|
||||
<templates>
|
||||
<t t-name="kanban-menu">
|
||||
<div class="oe_kanban_colorpicker"/>
|
||||
</t>
|
||||
<t t-name="kanban-box">
|
||||
<div color="color">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
await toggleKanbanRecordDropdown(0);
|
||||
|
||||
expect(".oe_kanban_colorpicker").toHaveCount(0, {
|
||||
message: "there shouldn't be a color picker",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
|
||||
import { KanbanCompiler } from "@web/views/kanban/kanban_compiler";
|
||||
|
||||
function compileTemplate(arch) {
|
||||
const parser = new DOMParser();
|
||||
const xml = parser.parseFromString(arch, "text/xml");
|
||||
const compiler = new KanbanCompiler({ kanban: xml.documentElement });
|
||||
return compiler.compile("kanban");
|
||||
}
|
||||
|
||||
test("bootstrap dropdowns with kanban_ignore_dropdown class should be left as is", async () => {
|
||||
const arch = `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<button name="dropdown" class="kanban_ignore_dropdown" type="button" data-bs-toggle="dropdown">Boostrap dropdown</button>
|
||||
<div class="dropdown-menu kanban_ignore_dropdown" role="menu">
|
||||
<span>Dropdown content</span>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`;
|
||||
const expected = `
|
||||
<t t-translation="off">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<button name="dropdown" class="kanban_ignore_dropdown" type="button" data-bs-toggle="dropdown">Boostrap dropdown</button>
|
||||
<div class="dropdown-menu kanban_ignore_dropdown" role="menu">
|
||||
<span>Dropdown content</span>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</t>`;
|
||||
expect(compileTemplate(arch)).toHaveOuterHTML(expected);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
304
odoo-bringout-oca-ocb-web/web/static/tests/views/layout.test.js
Normal file
304
odoo-bringout-oca-ocb-web/web/static/tests/views/layout.test.js
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
Component,
|
||||
onWillStart,
|
||||
reactive,
|
||||
useChildSubEnv,
|
||||
useState,
|
||||
useSubEnv,
|
||||
xml,
|
||||
} from "@odoo/owl";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
makeMockEnv,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
mountWithSearch,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Layout } from "@web/search/layout";
|
||||
import { SearchModel } from "@web/search/search_model";
|
||||
import { getDefaultConfig } from "@web/views/view";
|
||||
|
||||
class Foo extends models.Model {
|
||||
aaa = fields.Selection({
|
||||
selection: [
|
||||
["a", "A"],
|
||||
["b", "B"],
|
||||
],
|
||||
});
|
||||
|
||||
_views = {
|
||||
search: `
|
||||
<search>
|
||||
<searchpanel>
|
||||
<field name="aaa"/>
|
||||
</searchpanel>
|
||||
</search>
|
||||
`,
|
||||
};
|
||||
}
|
||||
defineModels([Foo]);
|
||||
|
||||
test(`Simple rendering`, async () => {
|
||||
class ToyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<Layout className="'o_view_sample_data'" display="props.display">
|
||||
<div class="toy_content"/>
|
||||
</Layout>
|
||||
`;
|
||||
static components = { Layout };
|
||||
}
|
||||
|
||||
await mountWithCleanup(ToyComponent, {
|
||||
env: await makeMockEnv({ config: {} }),
|
||||
});
|
||||
expect(`.o_view_sample_data`).toHaveCount(1);
|
||||
expect(`.o_control_panel`).toHaveCount(0);
|
||||
expect(`.o_component_with_search_panel`).toHaveCount(0);
|
||||
expect(`.o_search_panel`).toHaveCount(0);
|
||||
expect(`.o_content > .toy_content`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(`Simple rendering: with search`, async () => {
|
||||
class ToyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<Layout display="props.display">
|
||||
<t t-set-slot="layout-actions">
|
||||
<div class="toy_search_bar"/>
|
||||
</t>
|
||||
<div class="toy_content"/>
|
||||
</Layout>
|
||||
`;
|
||||
static components = { Layout };
|
||||
}
|
||||
|
||||
await mountWithSearch(ToyComponent, {
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
});
|
||||
expect(`.o_control_panel .o_control_panel_actions .toy_search_bar`).toHaveCount(1);
|
||||
expect(`.o_component_with_search_panel .o_search_panel`).toHaveCount(1);
|
||||
expect(`.o_cp_searchview`).toHaveCount(0);
|
||||
expect(`.o_content > .toy_content`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(`Rendering with default ControlPanel and SearchPanel`, async () => {
|
||||
class ToyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<Layout className="'o_view_sample_data'" display="{ controlPanel: {}, searchPanel: true }">
|
||||
<div class="toy_content"/>
|
||||
</Layout>
|
||||
`;
|
||||
static components = { Layout };
|
||||
|
||||
setup() {
|
||||
this.searchModel = new SearchModel(this.env, {
|
||||
orm: useService("orm"),
|
||||
view: useService("view"),
|
||||
});
|
||||
useSubEnv({ searchModel: this.searchModel });
|
||||
onWillStart(async () => {
|
||||
await this.searchModel.load({ resModel: "foo" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(ToyComponent, {
|
||||
env: await makeMockEnv({
|
||||
config: {
|
||||
breadcrumbs: getDefaultConfig().breadcrumbs,
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(`.o_search_panel`).toHaveCount(1);
|
||||
expect(`.o_control_panel`).toHaveCount(1);
|
||||
expect(`.o_breadcrumb`).toHaveCount(1);
|
||||
expect(`.o_component_with_search_panel`).toHaveCount(1);
|
||||
expect(`.o_content > .toy_content`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(`Nested layouts`, async () => {
|
||||
// Component C: bottom (no control panel)
|
||||
class ToyC extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<Layout className="'toy_c'" display="display">
|
||||
<div class="toy_c_content"/>
|
||||
</Layout>
|
||||
`;
|
||||
static components = { Layout };
|
||||
|
||||
get display() {
|
||||
return {
|
||||
controlPanel: false,
|
||||
searchPanel: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Component B: center (with custom search panel)
|
||||
class SearchPanel extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div class="o_toy_search_panel"/>`;
|
||||
}
|
||||
|
||||
class ToyB extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<Layout className="'toy_b'" display="props.display">
|
||||
<t t-set-slot="layout-actions">
|
||||
<div class="toy_b_breadcrumbs"/>
|
||||
</t>
|
||||
<ToyC/>
|
||||
</Layout>
|
||||
`;
|
||||
static components = { Layout, ToyC };
|
||||
setup() {
|
||||
useChildSubEnv({
|
||||
config: {
|
||||
...getDefaultConfig(),
|
||||
SearchPanel,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Component A: top
|
||||
class ToyA extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<Layout className="'toy_a'" display="props.display">
|
||||
<t t-set-slot="layout-actions">
|
||||
<div class="toy_a_search"/>
|
||||
</t>
|
||||
<ToyB display="props.display"/>
|
||||
</Layout>
|
||||
`;
|
||||
static components = { Layout, ToyB };
|
||||
}
|
||||
|
||||
await mountWithSearch(ToyA, {
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
});
|
||||
expect(`.o_content.toy_a .o_content.toy_b .o_content.toy_c`).toHaveCount(1);
|
||||
expect(".o_control_panel").toHaveCount(2);
|
||||
expect(".o_content.o_component_with_search_panel").toHaveCount(3);
|
||||
expect(`.o_search_panel`).toHaveCount(1);
|
||||
expect(".o_toy_search_panel").toHaveCount(2);
|
||||
expect(`.toy_a_search`).toHaveCount(1);
|
||||
expect(`.toy_b_breadcrumbs`).toHaveCount(1);
|
||||
expect(`.toy_c_content`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(`Custom control panel`, async () => {
|
||||
class ToyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<Layout display="props.display">
|
||||
<div class="o_toy_content"/>
|
||||
</Layout>
|
||||
`;
|
||||
static components = { Layout };
|
||||
}
|
||||
|
||||
class ControlPanel extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div class="o_toy_search_panel"/>`;
|
||||
}
|
||||
|
||||
await mountWithSearch(
|
||||
ToyComponent,
|
||||
{
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
},
|
||||
{ ControlPanel }
|
||||
);
|
||||
expect(`.o_toy_content`).toHaveCount(1);
|
||||
expect(`.o_toy_search_panel`).toHaveCount(1);
|
||||
expect(`.o_control_panel`).toHaveCount(0);
|
||||
});
|
||||
|
||||
test(`Custom search panel`, async () => {
|
||||
class ToyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<Layout display="props.display">
|
||||
<div class="o_toy_content"/>
|
||||
</Layout>
|
||||
`;
|
||||
static components = { Layout };
|
||||
}
|
||||
|
||||
class SearchPanel extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div class="o_toy_search_panel"/>`;
|
||||
}
|
||||
|
||||
await mountWithSearch(
|
||||
ToyComponent,
|
||||
{
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
},
|
||||
{ SearchPanel }
|
||||
);
|
||||
expect(`.o_toy_content`).toHaveCount(1);
|
||||
expect(`.o_toy_search_panel`).toHaveCount(1);
|
||||
expect(`.o_search_panel`).toHaveCount(0);
|
||||
});
|
||||
|
||||
test(`Simple rendering: with dynamically displayed search`, async () => {
|
||||
const state = reactive({ displayLayoutActions: true });
|
||||
|
||||
class ToyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<Layout display="display">
|
||||
<t t-set-slot="layout-actions">
|
||||
<div class="toy_search_bar"/>
|
||||
</t>
|
||||
<div class="toy_content"/>
|
||||
</Layout>
|
||||
`;
|
||||
static components = { Layout };
|
||||
|
||||
setup() {
|
||||
this.state = useState(state);
|
||||
}
|
||||
|
||||
get display() {
|
||||
return {
|
||||
...this.props.display,
|
||||
controlPanel: {
|
||||
...this.props.display.controlPanel,
|
||||
layoutActions: this.state.displayLayoutActions,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithSearch(ToyComponent, {
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
});
|
||||
expect(`.o_control_panel .o_control_panel_actions .toy_search_bar`).toHaveCount(1);
|
||||
expect(`.o_component_with_search_panel .o_search_panel`).toHaveCount(1);
|
||||
expect(`.o_cp_searchview`).toHaveCount(0);
|
||||
expect(`.o_content > .toy_content`).toHaveCount(1);
|
||||
|
||||
state.displayLayoutActions = false;
|
||||
await animationFrame();
|
||||
expect(`.o_control_panel .o_control_panel_actions .toy_search_bar`).toHaveCount(0);
|
||||
expect(`.o_component_with_search_panel .o_search_panel`).toHaveCount(1);
|
||||
expect(`.o_cp_searchview`).toHaveCount(0);
|
||||
expect(`.o_content > .toy_content`).toHaveCount(1);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
16705
odoo-bringout-oca-ocb-web/web/static/tests/views/list/list_view.test.js
Normal file
16705
odoo-bringout-oca-ocb-web/web/static/tests/views/list/list_view.test.js
Normal file
File diff suppressed because it is too large
Load diff
4552
odoo-bringout-oca-ocb-web/web/static/tests/views/pivot_view.test.js
Normal file
4552
odoo-bringout-oca-ocb-web/web/static/tests/views/pivot_view.test.js
Normal file
File diff suppressed because it is too large
Load diff
1150
odoo-bringout-oca-ocb-web/web/static/tests/views/view.test.js
Normal file
1150
odoo-bringout-oca-ocb-web/web/static/tests/views/view.test.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,115 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Component, useRef, xml } from "@odoo/owl";
|
||||
|
||||
import { ViewButton } from "@web/views/view_button/view_button";
|
||||
import { useViewButtons } from "@web/views/view_button/view_button_hook";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
test("action can be prevented", async () => {
|
||||
registry.category("services").add(
|
||||
"action",
|
||||
{
|
||||
start() {
|
||||
return {
|
||||
doActionButton() {
|
||||
expect.step("doActionButton");
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
let executeInHook;
|
||||
let executeInHandler;
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div t-ref="root" t-on-click="onClick" class="myComponent">Some text</div>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
const rootRef = useRef("root");
|
||||
useViewButtons(rootRef, {
|
||||
beforeExecuteAction: () => {
|
||||
expect.step("beforeExecuteAction in hook");
|
||||
return executeInHook;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onClick() {
|
||||
const getResParams = () => ({
|
||||
resIds: [3],
|
||||
resId: 3,
|
||||
});
|
||||
const clickParams = {};
|
||||
const beforeExecute = () => {
|
||||
expect.step("beforeExecuteAction on handler");
|
||||
return executeInHandler;
|
||||
};
|
||||
this.env.onClickViewButton({ beforeExecute, getResParams, clickParams });
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
await contains(".myComponent").click();
|
||||
expect.verifySteps([
|
||||
"beforeExecuteAction on handler",
|
||||
"beforeExecuteAction in hook",
|
||||
"doActionButton",
|
||||
]);
|
||||
|
||||
executeInHook = false;
|
||||
await contains(".myComponent").click();
|
||||
expect.verifySteps(["beforeExecuteAction on handler", "beforeExecuteAction in hook"]);
|
||||
|
||||
executeInHandler = false;
|
||||
await contains(".myComponent").click();
|
||||
expect.verifySteps(["beforeExecuteAction on handler"]);
|
||||
});
|
||||
|
||||
test("ViewButton clicked in Dropdown close the Dropdown", async () => {
|
||||
registry.category("services").add(
|
||||
"action",
|
||||
{
|
||||
start() {
|
||||
return {
|
||||
doActionButton() {
|
||||
expect.step("doActionButton");
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
class MyComponent extends Component {
|
||||
static components = { Dropdown, DropdownItem, ViewButton };
|
||||
static template = xml`
|
||||
<div t-ref="root" class="myComponent">
|
||||
<Dropdown>
|
||||
<button>dropdown</button>
|
||||
<DropdownItem>
|
||||
<ViewButton tag="'a'" clickParams="{ type:'action' }" string="'coucou'" record="{ resId: 1 }" />
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
const rootRef = useRef("root");
|
||||
useViewButtons(rootRef);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
await contains(".dropdown-toggle").click();
|
||||
expect(".dropdown-menu").toHaveCount(1);
|
||||
|
||||
await contains("a[type=action]").click();
|
||||
expect.verifySteps(["doActionButton"]);
|
||||
expect(".dropdown-menu").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { ViewScaleSelector } from "@web/views/view_components/view_scale_selector";
|
||||
|
||||
test("basic ViewScaleSelector component usage", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { ViewScaleSelector };
|
||||
static template = xml`<ViewScaleSelector t-props="compProps" />`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({
|
||||
scale: "week",
|
||||
});
|
||||
}
|
||||
get compProps() {
|
||||
return {
|
||||
scales: {
|
||||
day: {
|
||||
description: "Daily",
|
||||
},
|
||||
week: {
|
||||
description: "Weekly",
|
||||
hotkey: "o",
|
||||
},
|
||||
year: {
|
||||
description: "Yearly",
|
||||
},
|
||||
},
|
||||
isWeekendVisible: true,
|
||||
setScale: (scale) => {
|
||||
this.state.scale = scale;
|
||||
expect.step(scale);
|
||||
},
|
||||
toggleWeekendVisibility: () => {
|
||||
expect.step("toggleWeekendVisibility");
|
||||
},
|
||||
currentScale: this.state.scale,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_view_scale_selector").toHaveCount(1);
|
||||
expect.verifySteps([]);
|
||||
expect(".o_view_scale_selector").toHaveText("Weekly");
|
||||
expect(".scale_button_selection").toHaveAttribute("data-hotkey", "v");
|
||||
await click(".scale_button_selection");
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu").toHaveCount(1);
|
||||
expect(".o-dropdown--menu .active:first").toHaveText("Weekly", {
|
||||
message: "the active option is selected",
|
||||
});
|
||||
expect(".o-dropdown--menu span:nth-child(2)").toHaveAttribute("data-hotkey", "o", {
|
||||
message: "'week' scale has the right hotkey",
|
||||
});
|
||||
await click(".o_scale_button_day");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["day"]);
|
||||
expect(".o_view_scale_selector").toHaveText("Daily");
|
||||
await click(".scale_button_selection");
|
||||
expect(".dropdown-item:last:interactive").not.toHaveCount();
|
||||
await contains(".dropdown-item:contains(Yearly)").click();
|
||||
await click(".scale_button_selection");
|
||||
await contains(".dropdown-item:last").click();
|
||||
expect.verifySteps(["year", "toggleWeekendVisibility"]);
|
||||
});
|
||||
|
||||
test("ViewScaleSelector with only one scale available", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { ViewScaleSelector };
|
||||
static template = xml`<ViewScaleSelector t-props="compProps" />`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({
|
||||
scale: "day",
|
||||
});
|
||||
}
|
||||
get compProps() {
|
||||
return {
|
||||
scales: {
|
||||
day: {
|
||||
description: "Daily",
|
||||
},
|
||||
},
|
||||
setScale: () => {},
|
||||
isWeekendVisible: false,
|
||||
toggleWeekendVisibility: () => {},
|
||||
currentScale: this.state.scale,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_view_scale_selector").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("ViewScaleSelector show weekends button is disabled when scale is day", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { ViewScaleSelector };
|
||||
static template = xml`<ViewScaleSelector t-props="compProps"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({
|
||||
scale: "day",
|
||||
});
|
||||
}
|
||||
get compProps() {
|
||||
return {
|
||||
scales: {
|
||||
day: {
|
||||
description: "Daily",
|
||||
},
|
||||
week: {
|
||||
description: "Weekly",
|
||||
hotkey: "o",
|
||||
},
|
||||
year: {
|
||||
description: "Yearly",
|
||||
},
|
||||
},
|
||||
setScale: (key) => (this.state.scale = key),
|
||||
isWeekendVisible: false,
|
||||
toggleWeekendVisibility: () => {},
|
||||
currentScale: this.state.scale,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_view_scale_selector").toHaveCount(1);
|
||||
await click(".scale_button_selection");
|
||||
await animationFrame();
|
||||
expect(".o_show_weekends").toHaveClass("disabled");
|
||||
await click(".dropdown-item:nth-child(2)");
|
||||
await animationFrame();
|
||||
await click(".scale_button_selection");
|
||||
await animationFrame();
|
||||
expect(".o_show_weekends").not.toHaveClass("disabled");
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,570 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, press, queryAllTexts, runAllTimers, waitFor } from "@odoo/hoot-dom";
|
||||
import { animationFrame, Deferred } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
getService,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
mountViewInDialog,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char({ string: "Displayed name" });
|
||||
foo = fields.Char({ string: "Foo" });
|
||||
bar = fields.Boolean({ string: "Bar" });
|
||||
instrument = fields.Many2one({
|
||||
string: "Instruments",
|
||||
relation: "instrument",
|
||||
});
|
||||
method1() {}
|
||||
method2() {}
|
||||
_records = [
|
||||
{ id: 1, foo: "blip", name: "blipblip", bar: true },
|
||||
{ id: 2, foo: "ta tata ta ta", name: "macgyver", bar: false },
|
||||
{ id: 3, foo: "piou piou", name: "Jack O'Neill", bar: true },
|
||||
];
|
||||
}
|
||||
|
||||
class Instrument extends models.Model {
|
||||
name = fields.Char({ string: "name" });
|
||||
badassery = fields.Many2many({
|
||||
string: "level",
|
||||
relation: "badassery",
|
||||
domain: [["level", "=", "Awsome"]],
|
||||
});
|
||||
}
|
||||
|
||||
class Badassery extends models.Model {
|
||||
level = fields.Char({ string: "level" });
|
||||
|
||||
_records = [{ id: 1, level: "Awsome" }];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "name" });
|
||||
partner = fields.One2many({ string: "Doors", relation: "partner" });
|
||||
|
||||
_records = [{ id: 1, name: "The end" }];
|
||||
}
|
||||
|
||||
defineModels([Partner, Instrument, Badassery, Product]);
|
||||
|
||||
test("formviewdialog buttons in footer are positioned properly", async () => {
|
||||
Partner._views["form"] = /* xml */ `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<group><field name="foo"/></group >
|
||||
<footer><button string="Custom Button" type="object" class="btn-primary"/></footer>
|
||||
</sheet>
|
||||
</form>
|
||||
`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal-body button").toHaveCount(0, { message: "should not have any button in body" });
|
||||
expect(".modal-footer button:not(.d-none)").toHaveCount(1, {
|
||||
message: "should have only one button in footer",
|
||||
});
|
||||
});
|
||||
|
||||
test("modifiers are considered on multiple <footer/> tags", async () => {
|
||||
Partner._views["form"] = /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<footer invisible="not bar">
|
||||
<button>Hello</button>
|
||||
<button>World</button>
|
||||
</footer>
|
||||
<footer invisible="bar">
|
||||
<button>Foo</button>
|
||||
</footer>
|
||||
</form>
|
||||
`;
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await animationFrame();
|
||||
|
||||
expect(queryAllTexts(".modal-footer button:not(.d-none)")).toEqual(["Hello", "World"], {
|
||||
message: "only the first button section should be visible",
|
||||
});
|
||||
|
||||
await click(".o_field_boolean input");
|
||||
await animationFrame();
|
||||
expect(queryAllTexts(".modal-footer button:not(.d-none)")).toEqual(["Foo"], {
|
||||
message: "only the second button section should be visible",
|
||||
});
|
||||
});
|
||||
|
||||
test("formviewdialog buttons in footer are not duplicated", async () => {
|
||||
Partner._fields.poney_ids = fields.One2many({
|
||||
string: "Poneys",
|
||||
relation: "partner",
|
||||
});
|
||||
Partner._records[0].poney_ids = [];
|
||||
Partner._views["form"] = /* xml */ `
|
||||
<form string="Partner">
|
||||
<field name="poney_ids"><list editable="top"><field name="name"/></list></field>
|
||||
<footer><button string="Custom Button" type="object" class="my_button"/></footer>
|
||||
</form>
|
||||
`;
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(".modal button.my_button").toHaveCount(1, { message: "should have 1 buttons in modal" });
|
||||
|
||||
await click(".o_field_x2many_list_row_add a");
|
||||
await animationFrame();
|
||||
await press("escape");
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(".modal button.btn-primary").toHaveCount(1, {
|
||||
message: "should still have 1 buttons in modal",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Form dialog and subview with _view_ref contexts", async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
Instrument._records = [{ id: 1, name: "Tromblon", badassery: [1] }];
|
||||
Partner._records[0].instrument = 1;
|
||||
// This is an old test, written before "get_views" (formerly "load_views") automatically
|
||||
// inlines x2many subviews. As the purpose of this test is to assert that the js fetches
|
||||
// the correct sub view when it is not inline (which can still happen in nested form views),
|
||||
// we bypass the inline mecanism of "get_views" by setting widget="many2many" on the field.
|
||||
Instrument._views["form"] = /* xml */ `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="badassery" widget="many2many" context="{'list_view_ref': 'some_other_tree_view'}"/>
|
||||
</form>
|
||||
`;
|
||||
Badassery._views["list"] = /* xml */ `<list><field name="level"/></list>`;
|
||||
|
||||
onRpc(({ kwargs, method, model }) => {
|
||||
if (method === "get_formview_id") {
|
||||
return false;
|
||||
}
|
||||
if (method === "get_views" && model === "instrument") {
|
||||
expect(kwargs.context).toEqual(
|
||||
{
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
list_view_ref: "some_tree_view",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"1 The correct _view_ref should have been sent to the server, first time",
|
||||
}
|
||||
);
|
||||
}
|
||||
if (method === "get_views" && model === "badassery") {
|
||||
expect(kwargs.context).toEqual(
|
||||
{
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
list_view_ref: "some_other_tree_view",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"2 The correct _view_ref should have been sent to the server for the subview",
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await mountViewInDialog({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="instrument" context="{'list_view_ref': 'some_tree_view'}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
await click('.o_field_widget[name="instrument"] button.o_external_button');
|
||||
await animationFrame();
|
||||
});
|
||||
|
||||
test("click on view buttons in a FormViewDialog", async () => {
|
||||
Partner._views["form"] = /* xml */ `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<button name="method1" type="object" string="Button 1" class="btn1"/>
|
||||
<button name="method2" type="object" string="Button 2" class="btn2" close="1"/>
|
||||
</form>
|
||||
`;
|
||||
|
||||
onRpc(({ method }) => expect.step(method));
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_form_view").toHaveCount(1);
|
||||
expect(".o_dialog .o_form_view button").toHaveCount(2);
|
||||
expect.verifySteps(["get_views", "web_read"]);
|
||||
await click(".o_dialog .o_form_view .btn1");
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_form_view").toHaveCount(1);
|
||||
expect.verifySteps(["method1", "web_read"]); // should re-read the record
|
||||
await click(".o_dialog .o_form_view .btn2");
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_form_view").toHaveCount(0);
|
||||
expect.verifySteps(["method2"]); // should not read as we closed
|
||||
});
|
||||
|
||||
test("formviewdialog is not closed when button handlers return a rejected promise", async () => {
|
||||
Partner._views["form"] = /* xml */ `
|
||||
<form string="Partner">
|
||||
<sheet><group><field name="foo"/></group></sheet>
|
||||
</form>
|
||||
`;
|
||||
let reject;
|
||||
onRpc("web_save", () => {
|
||||
if (reject) {
|
||||
return Promise.reject("rejected");
|
||||
}
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
context: { answer: 42 },
|
||||
});
|
||||
|
||||
await animationFrame();
|
||||
expect(".modal-body button").not.toHaveCount();
|
||||
expect(".modal-footer button:visible").toHaveCount(2);
|
||||
|
||||
// Click "save" inside the dialog (with rejection)
|
||||
expect.errors(1);
|
||||
reject = true;
|
||||
await clickSave();
|
||||
|
||||
expect.verifyErrors(["rejected"]);
|
||||
|
||||
// Close error modal
|
||||
await click(waitFor(".o_error_dialog .btn:contains(Close)"));
|
||||
|
||||
// Click "save" inside the dialog (without rejection)
|
||||
reject = false;
|
||||
await clickSave();
|
||||
|
||||
expect(".modal").not.toHaveCount();
|
||||
});
|
||||
|
||||
test("FormViewDialog with remove button", async () => {
|
||||
Partner._views["form"] = /* xml */ `<form><field name="foo"/></form>`;
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
removeRecord: () => expect.step("remove"),
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dialog .o_form_view").toHaveCount(1);
|
||||
expect(".o_dialog .modal-footer .o_form_button_remove").toHaveCount(1);
|
||||
await click(".o_dialog .modal-footer .o_form_button_remove");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["remove"]);
|
||||
expect(".o_dialog .o_form_view").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Buttons are set as disabled on click", async () => {
|
||||
Partner._views["form"] = /* xml */ `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const def = new Deferred();
|
||||
onRpc("web_save", async () => await def);
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await animationFrame();
|
||||
|
||||
await click(".o_dialog .o_content .o_field_char .o_input");
|
||||
await edit("test");
|
||||
await animationFrame();
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect(".o_dialog .modal-footer .o_form_button_save").toHaveAttribute("disabled", "1");
|
||||
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_form_view").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("FormViewDialog with discard button", async () => {
|
||||
Partner._views["form"] = /* xml */ `<form><field name="foo"/></form>`;
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
onRecordDiscarded: () => expect.step("discard"),
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dialog .o_form_view").toHaveCount(1);
|
||||
expect(".o_dialog .modal-footer .o_form_button_cancel").toHaveCount(1);
|
||||
await click(".o_dialog .modal-footer .o_form_button_cancel");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["discard"]);
|
||||
expect(".o_dialog .o_form_view").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Save a FormViewDialog when a required field is empty don't close the dialog", async () => {
|
||||
Partner._views["form"] = /* xml */ `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<group><field name="foo" required="1"/></group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="save" special="save" class="btn-primary"/>
|
||||
</footer>
|
||||
</form>
|
||||
`;
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
context: { answer: 42 },
|
||||
});
|
||||
|
||||
await animationFrame();
|
||||
|
||||
await click('.modal button[name="save"]');
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1, { message: "modal should still be opened" });
|
||||
await click("[name='foo'] input");
|
||||
await edit("new");
|
||||
await click('.modal button[name="save"]');
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(0, { message: "modal should be closed" });
|
||||
});
|
||||
|
||||
test("new record has an expand button", async () => {
|
||||
Partner._views["form"] = /* xml */ `<form><field name="foo"/></form>`;
|
||||
Partner._records = [];
|
||||
onRpc("web_save", () => {
|
||||
expect.step("save");
|
||||
});
|
||||
mockService("action", {
|
||||
doAction(actionRequest) {
|
||||
expect.step([
|
||||
actionRequest.res_id,
|
||||
actionRequest.res_model,
|
||||
actionRequest.type,
|
||||
actionRequest.views,
|
||||
]);
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_form_view").toHaveCount(1);
|
||||
expect(".o_dialog .modal-header .o_expand_button").toHaveCount(1);
|
||||
await fieldInput("foo").edit("new");
|
||||
await click(".o_dialog .modal-header .o_expand_button");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["save", [1, "partner", "ir.actions.act_window", [[false, "form"]]]]);
|
||||
});
|
||||
|
||||
test("existing record has an expand button", async () => {
|
||||
Partner._views["form"] = /* xml */ `<form><field name="foo"/></form>`;
|
||||
onRpc("web_save", () => {
|
||||
expect.step("save");
|
||||
});
|
||||
mockService("action", {
|
||||
doAction(actionRequest) {
|
||||
expect.step([
|
||||
actionRequest.res_id,
|
||||
actionRequest.res_model,
|
||||
actionRequest.type,
|
||||
actionRequest.views,
|
||||
actionRequest.context,
|
||||
]);
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
context: {key: "val"}
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_form_view").toHaveCount(1);
|
||||
expect(".o_dialog .modal-header .o_expand_button").toHaveCount(1);
|
||||
await fieldInput("foo").edit("hola");
|
||||
await click(".o_dialog .modal-header .o_expand_button");
|
||||
await animationFrame();
|
||||
expect.verifySteps([
|
||||
"save",
|
||||
[
|
||||
1,
|
||||
"partner",
|
||||
"ir.actions.act_window",
|
||||
[[false, "form"]],
|
||||
{
|
||||
key: "val",
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test("expand button with save and new", async () => {
|
||||
Instrument._views["form"] = /* xml */ `<form><field name="name"/></form>`;
|
||||
Instrument._records = [{ id: 1, name: "Violon" }];
|
||||
onRpc("web_save", () => {
|
||||
expect.step("save");
|
||||
});
|
||||
mockService("action", {
|
||||
doAction(actionRequest) {
|
||||
expect.step([
|
||||
actionRequest.res_id,
|
||||
actionRequest.res_model,
|
||||
actionRequest.type,
|
||||
actionRequest.views,
|
||||
]);
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "instrument",
|
||||
resId: 1,
|
||||
isToMany: true,
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_form_view").toHaveCount(1);
|
||||
expect(".o_dialog .modal-header .o_expand_button").toHaveCount(1);
|
||||
await fieldInput("name").edit("Violoncelle");
|
||||
await click(".o_dialog .modal-footer .o_form_button_save_new");
|
||||
await animationFrame();
|
||||
await fieldInput("name").edit("Flute");
|
||||
await click(".o_dialog .modal-header .o_expand_button");
|
||||
await animationFrame();
|
||||
expect.verifySteps([
|
||||
"save",
|
||||
"save",
|
||||
[2, "instrument", "ir.actions.act_window", [[false, "form"]]],
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("close dialog with escape after modifying a field with onchange (no blur)", async () => {
|
||||
Partner._views["form"] = `<form><field name="foo"/></form>`;
|
||||
Partner._onChanges.foo = () => {};
|
||||
onRpc("web_save", () => {
|
||||
throw new Error("should not save");
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
// must focus something else than body before opening the form view dialog, such that the ui
|
||||
// service has something to focus on dialog close, which will then blur the input and fire the
|
||||
// change event
|
||||
await contains(".o_navbar_apps_menu button").focus();
|
||||
expect(".o_navbar_apps_menu button").toBeFocused();
|
||||
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
|
||||
await contains(".o_field_widget[name=foo] input").edit("new value", { confirm: false });
|
||||
await press("escape");
|
||||
await animationFrame();
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
expect(".o_navbar_apps_menu button").toBeFocused();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("display a dialog if onchange result is a warning from within a dialog", async function () {
|
||||
Instrument._views = {
|
||||
form: `<form><field name="name" /></form>`,
|
||||
};
|
||||
|
||||
onRpc("instrument", "onchange", () => {
|
||||
expect.step("onchange warning");
|
||||
return Promise.resolve({
|
||||
value: {
|
||||
name: false,
|
||||
},
|
||||
warning: {
|
||||
title: "Warning",
|
||||
message: "You must first select a partner",
|
||||
type: "dialog",
|
||||
},
|
||||
});
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `<form><field name="instrument"/></form>`,
|
||||
resId: 2,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=instrument] input").edit("tralala", { confirm: false });
|
||||
await runAllTimers();
|
||||
await contains(".o_field_widget[name=instrument] .o_m2o_dropdown_option_create_edit").click();
|
||||
|
||||
await waitFor(".modal.o_inactive_modal");
|
||||
expect(".modal").toHaveCount(2);
|
||||
expect(".modal:not(.o_inactive_modal) .modal-body").toHaveText(
|
||||
"You must first select a partner"
|
||||
);
|
||||
|
||||
await contains(".modal:not(.o_inactive_modal) button").click();
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(".modal:not(.o_inactive_modal) .modal-title").toHaveText("Create Instruments");
|
||||
|
||||
expect.verifySteps(["onchange warning"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,569 @@
|
|||
import { renderToMarkup } from "@web/core/utils/render";
|
||||
import { useSetupAction } from "@web/search/action_hook";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
import { xml } from "@odoo/owl";
|
||||
|
||||
import {
|
||||
clickModalButton,
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
editFavoriteName,
|
||||
fields,
|
||||
getService,
|
||||
models,
|
||||
mockService,
|
||||
mountView,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
removeFacet,
|
||||
saveFavorite,
|
||||
toggleMenuItem,
|
||||
toggleSaveFavorite,
|
||||
toggleSearchBarMenu,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { click, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char({ string: "Displayed name" });
|
||||
foo = fields.Char({ string: "Foo" });
|
||||
bar = fields.Boolean({ string: "Bar" });
|
||||
instrument = fields.Many2one({ string: "Instrument", relation: "instrument" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, foo: "blip", name: "blipblip", bar: true },
|
||||
{ id: 2, foo: "ta tata ta ta", name: "macgyver", bar: false },
|
||||
{ id: 3, foo: "piou piou", name: "Jack O'Neill", bar: true },
|
||||
];
|
||||
}
|
||||
|
||||
class Instrument extends models.Model {
|
||||
name = fields.Char({ string: "name" });
|
||||
badassery = fields.Many2many({
|
||||
string: "level",
|
||||
relation: "badassery",
|
||||
domain: [["level", "=", "Awsome"]],
|
||||
});
|
||||
}
|
||||
|
||||
class Badassery extends models.Model {
|
||||
level = fields.Char({ string: "level" });
|
||||
|
||||
_records = [{ id: 1, level: "Awsome" }];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "name" });
|
||||
partner = fields.One2many({ string: "Doors", relation: "partner" });
|
||||
|
||||
_records = [{ id: 1, name: "The end" }];
|
||||
}
|
||||
|
||||
defineModels([Partner, Instrument, Badassery, Product]);
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
beforeEach(() => onRpc("has_group", () => true));
|
||||
|
||||
test("SelectCreateDialog use domain, group_by and search default", async () => {
|
||||
expect.assertions(3);
|
||||
Partner._views["list"] = /* xml */ `
|
||||
<list string="Partner">
|
||||
<field name="name"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`;
|
||||
Partner._views["search"] = /* xml */ `
|
||||
<search>
|
||||
<field name="foo" filter_domain="[('name','ilike',self), ('foo','ilike',self)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="groupby_bar" context="{'group_by' : 'bar'}"/>
|
||||
</group>
|
||||
</search>
|
||||
`;
|
||||
let search = 0;
|
||||
onRpc("web_read_group", ({ kwargs }) => {
|
||||
expect(kwargs).toMatchObject(
|
||||
{
|
||||
domain: [
|
||||
"&",
|
||||
["name", "like", "a"],
|
||||
"&",
|
||||
["name", "ilike", "piou"],
|
||||
["foo", "ilike", "piou"],
|
||||
],
|
||||
fields: [],
|
||||
groupby: ["bar"],
|
||||
orderby: "",
|
||||
lazy: true,
|
||||
limit: 80,
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"should search with the complete domain (domain + search), and group by 'bar'",
|
||||
}
|
||||
);
|
||||
});
|
||||
onRpc("web_search_read", ({ kwargs }) => {
|
||||
if (search === 0) {
|
||||
expect(kwargs).toMatchObject(
|
||||
{
|
||||
domain: [
|
||||
"&",
|
||||
["name", "like", "a"],
|
||||
"&",
|
||||
["name", "ilike", "piou"],
|
||||
["foo", "ilike", "piou"],
|
||||
],
|
||||
specification: { name: {}, foo: {} },
|
||||
limit: 80,
|
||||
offset: 0,
|
||||
order: "",
|
||||
count_limit: 10001,
|
||||
},
|
||||
{
|
||||
message: "should search with the complete domain (domain + search)",
|
||||
}
|
||||
);
|
||||
} else if (search === 1) {
|
||||
expect(kwargs).toMatchObject(
|
||||
{
|
||||
domain: [["name", "like", "a"]],
|
||||
specification: { name: {}, foo: {} },
|
||||
limit: 80,
|
||||
offset: 0,
|
||||
order: "",
|
||||
count_limit: 10001,
|
||||
},
|
||||
{
|
||||
message: "should search with the domain",
|
||||
}
|
||||
);
|
||||
}
|
||||
search++;
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(SelectCreateDialog, {
|
||||
noCreate: true,
|
||||
resModel: "partner",
|
||||
domain: [["name", "like", "a"]],
|
||||
context: {
|
||||
search_default_groupby_bar: true,
|
||||
search_default_foo: "piou",
|
||||
},
|
||||
});
|
||||
await animationFrame();
|
||||
await removeFacet("Bar");
|
||||
await removeFacet("Foo piou");
|
||||
});
|
||||
|
||||
test("SelectCreateDialog correctly evaluates domains", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
Partner._views["list"] = /* xml */ `
|
||||
<list string="Partner">
|
||||
<field name="name"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`;
|
||||
Partner._views["search"] = /* xml */ `<search><field name="foo"/></search>`;
|
||||
onRpc("web_search_read", ({ kwargs }) => {
|
||||
expect(kwargs.domain).toEqual([["id", "=", 2]], {
|
||||
message: "should have correctly evaluated the domain",
|
||||
});
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(SelectCreateDialog, {
|
||||
noCreate: true,
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", 2]],
|
||||
});
|
||||
await animationFrame();
|
||||
});
|
||||
|
||||
test("SelectCreateDialog list view in readonly", async () => {
|
||||
Partner._views["list"] = /* xml */ `
|
||||
<list string="Partner" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
getService("dialog").add(SelectCreateDialog, {
|
||||
resModel: "partner",
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
// click on the first row to see if the list is editable
|
||||
await contains(".o_list_view tbody tr td:first").click();
|
||||
|
||||
expect(".o_list_view tbody tr td .o_field_char input").toHaveCount(0, {
|
||||
message: "list view should not be editable in a SelectCreateDialog",
|
||||
});
|
||||
});
|
||||
|
||||
test("SelectCreateDialog cascade x2many in create mode", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
Partner._views["form"] = /* xml */ `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="instrument" widget="one2many" mode="list"/>
|
||||
</form>
|
||||
`;
|
||||
Instrument._views["form"] = /* xml */ `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="badassery">
|
||||
<list>
|
||||
<field name="level"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`;
|
||||
Badassery._views["list"] = /* xml */ `<list><field name="level"/></list>`;
|
||||
Badassery._views["search"] = /* xml */ `<search><field name="level"/></search>`;
|
||||
|
||||
onRpc(async ({ route, args }) => {
|
||||
if (route === "/web/dataset/call_kw/partner/get_formview_id") {
|
||||
return false;
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/instrument/get_formview_id") {
|
||||
return false;
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/instrument/web_save") {
|
||||
expect(args[1]).toEqual(
|
||||
{ badassery: [[4, 1]], name: "ABC" },
|
||||
{
|
||||
message: "The method create should have been called with the right arguments",
|
||||
}
|
||||
);
|
||||
return [{ id: 90 }];
|
||||
}
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="partner" widget="one2many" >
|
||||
<list editable="top">
|
||||
<field name="name"/>
|
||||
<field name="instrument"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
await contains(".o_field_widget[name=instrument] input").edit("ABC", { confirm: false });
|
||||
await runAllTimers();
|
||||
await contains(
|
||||
`[name="instrument"] .dropdown .dropdown-menu li:contains("Create and edit...")`
|
||||
).click();
|
||||
|
||||
expect(".modal .modal-lg").toHaveCount(1);
|
||||
|
||||
await contains(".modal .o_field_x2many_list_row_add a").click();
|
||||
|
||||
expect(".modal .modal-lg").toHaveCount(2);
|
||||
|
||||
await contains(".modal .o_data_row input[type=checkbox]").check();
|
||||
await clickModalButton({ text: "Select" });
|
||||
|
||||
expect(".modal .modal-lg").toHaveCount(1);
|
||||
expect(".modal .o_data_cell").toHaveText("Awsome");
|
||||
|
||||
// click on modal save button
|
||||
await clickSave({ index: 1 });
|
||||
});
|
||||
|
||||
test("SelectCreateDialog: save current search", async () => {
|
||||
expect.assertions(5);
|
||||
Partner._views["list"] = /* xml */ `<list><field name="name"/> </list>`;
|
||||
Partner._views[
|
||||
"search"
|
||||
] = /* xml */ `<search><filter name="bar" help="Bar" domain="[('bar', '=', True)]"/></search>`;
|
||||
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
useSetupAction({
|
||||
getContext: () => ({ shouldBeInFilterContext: true }),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("get_views", ({ kwargs }) => {
|
||||
expect(kwargs.options.load_filters).toBe(true, { message: "Missing load_filters option" });
|
||||
});
|
||||
onRpc("create_or_replace", ({ model, args }) => {
|
||||
if (model === "ir.filters") {
|
||||
const irFilter = args[0];
|
||||
expect(irFilter.domain).toBe(`[("bar", "=", True)]`, {
|
||||
message: "should save the correct domain",
|
||||
});
|
||||
const expectedContext = {
|
||||
group_by: [], // default groupby is an empty list
|
||||
shouldBeInFilterContext: true,
|
||||
};
|
||||
expect(irFilter.context).toEqual(expectedContext, {
|
||||
message: "should save the correct context",
|
||||
});
|
||||
return 7; // fake serverSideId
|
||||
}
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
getService("dialog").add(SelectCreateDialog, {
|
||||
context: { shouldNotBeInFilterContext: false },
|
||||
resModel: "partner",
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row").toHaveCount(3, { message: "should contain 3 records" });
|
||||
|
||||
// filter on bar
|
||||
await toggleSearchBarMenu();
|
||||
await toggleMenuItem("Bar");
|
||||
|
||||
expect(".o_data_row").toHaveCount(2, { message: "should contain 2 records" });
|
||||
|
||||
// save filter
|
||||
await toggleSaveFavorite();
|
||||
await editFavoriteName("some name");
|
||||
await saveFavorite();
|
||||
});
|
||||
|
||||
test("SelectCreateDialog calls on_selected with every record matching the domain", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
Partner._views["list"] = /* xml */ `
|
||||
<list limit="2" string="Partner">
|
||||
<field name="name"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`;
|
||||
Partner._views["search"] = /* xml */ `<search><field name="foo"/></search>`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(SelectCreateDialog, {
|
||||
resModel: "partner",
|
||||
onSelected: (records) => expect(records.join(",")).toBe("1,2,3"),
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
await contains("thead .o_list_record_selector input").click();
|
||||
await contains(".o_list_selection_box .o_list_select_domain").click();
|
||||
await clickModalButton({ text: "Select" });
|
||||
});
|
||||
|
||||
test("SelectCreateDialog calls on_selected with every record matching without selecting a domain", async () => {
|
||||
expect.assertions(1);
|
||||
Partner._views["list"] = /* xml */ `
|
||||
<list limit="2" string="Partner">
|
||||
<field name="name"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`;
|
||||
Partner._views["search"] = /* xml */ `<search><field name="foo"/></search>`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
getService("dialog").add(SelectCreateDialog, {
|
||||
resModel: "partner",
|
||||
onSelected: (records) => expect(records.join(",")).toBe("1,2"),
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
await contains("thead .o_list_record_selector input").click();
|
||||
await contains(".o_list_selection_box").click();
|
||||
await clickModalButton({ text: "Select", index: 1 });
|
||||
});
|
||||
|
||||
test("SelectCreateDialog: multiple clicks on record", async () => {
|
||||
Partner._views["list"] = /* xml */ `<list><field name="name"/></list>`;
|
||||
Partner._views["search"] = /* xml */ `<search><field name="foo"/></search>`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(SelectCreateDialog, {
|
||||
resModel: "partner",
|
||||
onSelected: async function (records) {
|
||||
expect.step(`select record ${records[0]}`);
|
||||
},
|
||||
});
|
||||
await animationFrame();
|
||||
await click(".modal .o_data_row .o_data_cell");
|
||||
await click(".modal .o_data_row .o_data_cell");
|
||||
await click(".modal .o_data_row .o_data_cell");
|
||||
await animationFrame();
|
||||
// should have called onSelected only once
|
||||
expect.verifySteps(["select record 1"]);
|
||||
});
|
||||
|
||||
test("SelectCreateDialog: default props, create a record", async () => {
|
||||
Partner._views["list"] = /* xml */ `<list><field name="name"/></list>`;
|
||||
Partner._views["search"] = /* xml */ `
|
||||
<search>
|
||||
<filter name="bar" help="Bar" domain="[('bar', '=', True)]"/>
|
||||
</search>
|
||||
`;
|
||||
Partner._views["form"] = /* xml */ `<form><field name="name"/></form>`;
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
getService("dialog").add(SelectCreateDialog, {
|
||||
onSelected: (resIds) => expect.step(`onSelected ${resIds}`),
|
||||
resModel: "partner",
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect(".o_dialog .o_list_view .o_data_row").toHaveCount(3);
|
||||
expect(".o_dialog footer button").toHaveCount(3);
|
||||
expect(".o_dialog footer button.o_select_button").toHaveCount(1);
|
||||
expect(".o_dialog footer button.o_create_button").toHaveCount(1);
|
||||
expect(".o_dialog footer button.o_form_button_cancel").toHaveCount(1);
|
||||
expect(".o_dialog .o_control_panel_main_buttons .o_list_button_add").toHaveCount(0);
|
||||
|
||||
await contains(".o_dialog footer button.o_create_button").click();
|
||||
|
||||
expect(".o_dialog").toHaveCount(2);
|
||||
expect(".o_dialog .o_form_view").toHaveCount(1);
|
||||
|
||||
await contains(".o_dialog .o_form_view .o_field_widget input").edit("hello");
|
||||
await clickSave();
|
||||
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
expect.verifySteps(["onSelected 4"]);
|
||||
});
|
||||
|
||||
test("SelectCreateDialog empty list, default no content helper", async () => {
|
||||
Partner._records = [];
|
||||
Partner._views["list"] = /* xml */ `
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`;
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(SelectCreateDialog, { resModel: "partner" });
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_list_view").toHaveCount(1);
|
||||
expect(".o_dialog .o_list_view .o_data_row").toHaveCount(0);
|
||||
expect(".o_dialog .o_list_view .o_view_nocontent").toHaveCount(1);
|
||||
expect(queryOne(".o_dialog .o_list_view .o_view_nocontent")).toHaveInnerHTML(
|
||||
`<div class="o_nocontent_help"><p>No record found</p><p>Adjust your filters or create a new record.</p></div>`
|
||||
);
|
||||
});
|
||||
|
||||
test("SelectCreateDialog empty list, noContentHelp props", async () => {
|
||||
Partner._records = [];
|
||||
Partner._views["list"] = /* xml */ `
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
const template = xml`
|
||||
<p class="custom_classname">Hello</p>
|
||||
<p>I'm an helper</p>
|
||||
`;
|
||||
getService("dialog").add(SelectCreateDialog, {
|
||||
resModel: "partner",
|
||||
noContentHelp: renderToMarkup(template),
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dialog .o_list_view").toHaveCount(1);
|
||||
expect(".o_dialog .o_list_view .o_data_row").toHaveCount(0);
|
||||
expect(".o_dialog .o_list_view .o_view_nocontent").toHaveCount(1);
|
||||
expect(queryOne(".o_dialog .o_list_view .o_view_nocontent")).toHaveInnerHTML(
|
||||
`<div class="o_nocontent_help"><p class="custom_classname">Hello</p><p>I'm an helper</p></div>`
|
||||
);
|
||||
});
|
||||
|
||||
test("SelectCreateDialog with open action", async () => {
|
||||
Instrument._records = [];
|
||||
for (let i = 0; i < 25; i++) {
|
||||
Instrument._records.push({
|
||||
id: i + 1,
|
||||
name: "Instrument " + i,
|
||||
});
|
||||
}
|
||||
mockService("action", {
|
||||
doActionButton(params) {
|
||||
const { name } = params;
|
||||
expect.step(`execute_action: ${name}`, params);
|
||||
},
|
||||
});
|
||||
Instrument._views["list"] = /* xml */ `
|
||||
<list action="test_action" type="object">
|
||||
<field name="name"/>
|
||||
</list>
|
||||
`;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="instrument"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
await contains(`.o_field_widget[name="instrument"] .dropdown input`).click();
|
||||
await contains(`.o_field_widget[name="instrument"] .o_m2o_dropdown_option_search_more`).click();
|
||||
await contains(
|
||||
`.o_list_renderer .o_data_row .o_field_cell.o_list_char[data-tooltip="Instrument 10"]`
|
||||
).click();
|
||||
expect("input").toHaveValue("Instrument 10");
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("SelectCreateDialog: enable select when grouped with domain selection", async () => {
|
||||
Partner._views["list"] = `
|
||||
<list string="Partner">
|
||||
<field name="name"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`;
|
||||
Partner._views["search"] = `
|
||||
<search>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="groupby_bar" context="{'group_by' : 'bar'}"/>
|
||||
</group>
|
||||
</search>
|
||||
`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("dialog").add(SelectCreateDialog, {
|
||||
noCreate: true,
|
||||
resModel: "partner",
|
||||
domain: [["name", "like", "a"]],
|
||||
context: {
|
||||
search_default_groupby_bar: true,
|
||||
},
|
||||
});
|
||||
await animationFrame();
|
||||
await contains("thead .o_list_record_selector input").click();
|
||||
|
||||
await animationFrame();
|
||||
expect(".o_select_button:not([disabled])").toHaveCount(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
defineModels,
|
||||
getService,
|
||||
makeMockEnv,
|
||||
models,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
class TakeFive extends models.Model {
|
||||
_name = "take.five";
|
||||
_views = {
|
||||
"list,99": /* xml */ `<list><field name="display_name" /></list>`,
|
||||
};
|
||||
}
|
||||
|
||||
class IrUiView extends models.Model {
|
||||
_name = "ir.ui.view";
|
||||
}
|
||||
|
||||
defineModels([TakeFive, IrUiView]);
|
||||
|
||||
test("stores calls in cache in success", async () => {
|
||||
expect.assertions(1);
|
||||
onRpc("get_views", () => {
|
||||
expect.step("get_views");
|
||||
});
|
||||
await makeMockEnv();
|
||||
await getService("view").loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
context: { default_field_value: 1 },
|
||||
},
|
||||
{}
|
||||
);
|
||||
await getService("view").loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
context: { default_field_value: 2 },
|
||||
},
|
||||
{}
|
||||
);
|
||||
expect.verifySteps(["get_views"]);
|
||||
});
|
||||
|
||||
test("stores calls in cache when failed", async () => {
|
||||
expect.assertions(3);
|
||||
onRpc("get_views", () => {
|
||||
expect.step("get_views");
|
||||
throw new Error("my little error");
|
||||
});
|
||||
await makeMockEnv();
|
||||
await expect(
|
||||
getService("view").loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
},
|
||||
{}
|
||||
)
|
||||
).rejects.toThrow(/my little error/);
|
||||
await expect(
|
||||
getService("view").loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
},
|
||||
{}
|
||||
)
|
||||
).rejects.toThrow(/my little error/);
|
||||
expect.verifySteps(["get_views", "get_views"]);
|
||||
});
|
||||
|
||||
test("clear cache when updating ir.ui.view", async () => {
|
||||
expect.assertions(4);
|
||||
onRpc("get_views", () => {
|
||||
expect.step("get_views");
|
||||
});
|
||||
await makeMockEnv();
|
||||
const loadView = () =>
|
||||
getService("view").loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
context: { default_field_value: 1 },
|
||||
},
|
||||
{}
|
||||
);
|
||||
await loadView();
|
||||
expect.verifySteps(["get_views"]);
|
||||
await loadView();
|
||||
expect.verifySteps([]); // cache works => no actual rpc
|
||||
await getService("orm").unlink("ir.ui.view", [3]);
|
||||
await loadView();
|
||||
expect.verifySteps(["get_views"]); // cache was invalidated
|
||||
await getService("orm").unlink("take.five", [3]);
|
||||
await loadView();
|
||||
expect.verifySteps([]); // cache was not invalidated
|
||||
});
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, manuallyDispatchProgrammaticEvent } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { AttachDocumentWidget } from "@web/views/widgets/attach_document/attach_document";
|
||||
|
||||
class Partner extends models.Model {
|
||||
display_name = fields.Char({ string: "Displayed name" });
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("attach document widget calls action with attachment ids", async () => {
|
||||
// FIXME: This ugly hack is needed because the input is not attached in the DOM
|
||||
// The input should be attached to the component and hidden in some way to make
|
||||
// the interaction easier and more natural.
|
||||
let fileInput;
|
||||
patchWithCleanup(AttachDocumentWidget.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
fileInput = this.fileInput;
|
||||
},
|
||||
});
|
||||
|
||||
mockService("http", {
|
||||
post(route, params) {
|
||||
expect.step("post");
|
||||
expect(route).toBe("/web/binary/upload_attachment");
|
||||
expect(params.model).toBe("partner");
|
||||
expect(params.id).toBe(1);
|
||||
return '[{ "id": 5 }, { "id": 2 }]';
|
||||
},
|
||||
});
|
||||
|
||||
onRpc(async ({ args, kwargs, method, model }) => {
|
||||
expect.step(method);
|
||||
if (method === "my_action") {
|
||||
expect(model).toBe("partner");
|
||||
expect(args).toEqual([1]);
|
||||
expect(kwargs.attachment_ids).toEqual([5, 2]);
|
||||
return true;
|
||||
}
|
||||
if (method === "web_save") {
|
||||
expect(args[1]).toEqual({ display_name: "yop" });
|
||||
}
|
||||
if (method === "web_read") {
|
||||
expect(args[0]).toEqual([1]);
|
||||
}
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<widget name="attach_document" action="my_action" string="Attach document"/>
|
||||
<field name="display_name" required="1"/>
|
||||
</form>`,
|
||||
});
|
||||
expect.verifySteps(["get_views", "web_read"]);
|
||||
|
||||
await contains("[name='display_name'] input").edit("yop");
|
||||
await animationFrame();
|
||||
await click(".o_attach_document");
|
||||
await animationFrame();
|
||||
await manuallyDispatchProgrammaticEvent(fileInput, "change");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["web_save", "post", "my_action", "web_read"]);
|
||||
});
|
||||
|
||||
test("attach document widget calls action with attachment ids on a new record", async () => {
|
||||
// FIXME: This ugly hack is needed because the input is not attached in the DOM
|
||||
// The input should be attached to the component and hidden in some way to make
|
||||
// the interaction easier and more natural.
|
||||
let fileInput;
|
||||
patchWithCleanup(AttachDocumentWidget.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
fileInput = this.fileInput;
|
||||
},
|
||||
});
|
||||
|
||||
mockService("http", {
|
||||
post(route, params) {
|
||||
expect.step("post");
|
||||
expect(route).toBe("/web/binary/upload_attachment");
|
||||
expect(params.model).toBe("partner");
|
||||
expect(params.id).toBe(2);
|
||||
return '[{ "id": 5 }, { "id": 2 }]';
|
||||
},
|
||||
});
|
||||
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.method);
|
||||
if (params.method === "my_action") {
|
||||
expect(params.model).toBe("partner");
|
||||
expect(params.args).toEqual([2]);
|
||||
expect(params.kwargs.attachment_ids).toEqual([5, 2]);
|
||||
return true;
|
||||
}
|
||||
if (params.method === "web_save") {
|
||||
expect(params.args[1]).toEqual({ display_name: "yop" });
|
||||
}
|
||||
if (params.method === "web_read") {
|
||||
expect(params.args[0]).toEqual([2]);
|
||||
}
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<widget name="attach_document" action="my_action" string="Attach document"/>
|
||||
<field name="display_name" required="1"/>
|
||||
</form>`,
|
||||
});
|
||||
expect.verifySteps(["get_views", "onchange"]);
|
||||
await contains("[name='display_name'] input").edit("yop");
|
||||
await click(".o_attach_document");
|
||||
await animationFrame();
|
||||
await manuallyDispatchProgrammaticEvent(fileInput, "change");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["web_save", "post", "my_action", "web_read"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
mountWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { DocumentationLink } from "@web/views/widgets/documentation_link/documentation_link";
|
||||
|
||||
class Partner extends models.Model {
|
||||
bar = fields.Boolean();
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("documentation_link: default label and icon", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<widget name="documentation_link" path="/this_is_a_test.html"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_doc_link").toHaveText("View Documentation");
|
||||
expect("a.alert-link").toHaveCount(0);
|
||||
expect(".o_doc_link .fa-external-link").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("documentationLink: alert-link", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<widget name="documentation_link" path="/this_is_a_test.html" alert_link="true"/>
|
||||
</form>`,
|
||||
});
|
||||
expect("a.alert-link").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("DocumentationLink Component: alert-link", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { DocumentationLink };
|
||||
static template = xml`
|
||||
<DocumentationLink path="'/this_is_a_test.html'" alertLink="true"/>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
expect("a.alert-link").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("documentation_link: given label", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<widget name="documentation_link" path="/this_is_a_test.html" label="docdoc"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_doc_link").toHaveText("docdoc");
|
||||
expect(".o_doc_link .fa").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("documentation_link: given icon", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<widget name="documentation_link" path="/this_is_a_test.html" icon="fa-question-circle"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_doc_link").toHaveText("");
|
||||
expect(".o_doc_link .fa-question-circle").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("documentation_link: given label and icon", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<widget name="documentation_link" path="/this_is_a_test.html" label="docdoc" icon="fa-question-circle"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_doc_link").toHaveText("docdoc");
|
||||
expect(".o_doc_link .fa-question-circle").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("documentation_link: relative path", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<widget name="documentation_link" path="/applications/technical/web/settings/this_is_a_test.html"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_doc_link").toHaveAttribute(
|
||||
"href",
|
||||
"https://www.odoo.com/documentation/1.0/applications/technical/web/settings/this_is_a_test.html"
|
||||
);
|
||||
});
|
||||
|
||||
test("documentation_link: absolute path (http)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<widget name="documentation_link" path="http://www.odoo.com/"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_doc_link").toHaveAttribute("href", "http://www.odoo.com/");
|
||||
});
|
||||
|
||||
test("documentation_link: absolute path (https)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<widget name="documentation_link" path="https://www.odoo.com/"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_doc_link").toHaveAttribute("href", "https://www.odoo.com/");
|
||||
});
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { mockPermission } from "@odoo/hoot-mock";
|
||||
import { defineModels, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_records = [{ id: 1 }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
const viewData = {
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><widget name="notification_alert"/></form>`,
|
||||
};
|
||||
|
||||
test("notification alert should be displayed when notification denied", async () => {
|
||||
mockPermission("notifications", "denied");
|
||||
await mountView(viewData);
|
||||
expect(".o_widget_notification_alert .alert").toHaveCount(1, {
|
||||
message: "notification alert should be displayed when notification denied",
|
||||
});
|
||||
});
|
||||
|
||||
test("notification alert should not be displayed when notification granted", async () => {
|
||||
mockPermission("notifications", "granted");
|
||||
await mountView(viewData);
|
||||
expect(".o_widget_notification_alert .alert").toHaveCount(0, {
|
||||
message: "notification alert should not be displayed when notification granted",
|
||||
});
|
||||
});
|
||||
|
||||
test("notification alert should not be displayed when notification default", async () => {
|
||||
mockPermission("notifications", "default");
|
||||
await mountView(viewData);
|
||||
expect(".o_widget_notification_alert .alert").toHaveCount(0, {
|
||||
message: "notification alert should not be displayed when notification default",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
import { NameAndSignature } from "@web/core/signature/name_and_signature";
|
||||
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
contains,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { beforeEach, test, expect } from "@odoo/hoot";
|
||||
import { click, waitFor } from "@odoo/hoot-dom";
|
||||
|
||||
class Partner extends models.Model {
|
||||
display_name = fields.Char();
|
||||
product_id = fields.Many2one({ string: "Product Name", relation: "product" });
|
||||
sign = fields.Binary({ string: "Signature" });
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "Pop's Chock'lit",
|
||||
product_id: 7,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product Name" });
|
||||
_records = [
|
||||
{
|
||||
id: 7,
|
||||
name: "Veggie Burger",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
beforeEach(async () => {
|
||||
onRpc("/web/sign/get_fonts/", () => {
|
||||
return {};
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Signature widget renders a Sign button on desktop", async () => {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
expect(this.props.signature.name).toBe("");
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<widget name="signature" string="Sign"/>
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("button.o_sign_button").toHaveClass("btn-secondary", {
|
||||
message: `The button must have the 'btn-secondary' class as "highlight=0"`,
|
||||
});
|
||||
expect(".o_widget_signature button.o_sign_button").toHaveCount(1, {
|
||||
message: "Should have a signature widget button",
|
||||
});
|
||||
expect(".modal-dialog").toHaveCount(0, {
|
||||
message: "Should not have any modal",
|
||||
});
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await click(".o_widget_signature button.o_sign_button");
|
||||
await waitFor(".modal .modal-body");
|
||||
expect(".modal-dialog").toHaveCount(1, {
|
||||
message: "Should have one modal opened",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("Signature widget renders a Sign button on mobile", async () => {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
expect(this.props.signature.name).toBe("");
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<widget name="signature" string="Sign"/>
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
|
||||
expect("button.o_sign_button").toHaveClass("btn-secondary", {
|
||||
message: `The button must have the 'btn-secondary' class as "highlight=0"`,
|
||||
});
|
||||
expect(".o_widget_signature button.o_sign_button").toHaveCount(1, {
|
||||
message: "Should have a signature widget button",
|
||||
});
|
||||
expect(".modal-dialog").toHaveCount(0, {
|
||||
message: "Should not have any modal",
|
||||
});
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await click(".o_widget_signature button.o_sign_button");
|
||||
await waitFor(".modal .modal-body");
|
||||
expect(".modal-dialog").toHaveCount(1, {
|
||||
message: "Should have one modal opened",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Signature widget: full_name option on desktop", async () => {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
expect.step(this.props.signature.name);
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<widget name="signature" string="Sign" full_name="display_name"/>
|
||||
</header>
|
||||
<field name="display_name"/>
|
||||
</form>`,
|
||||
});
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await click("span.o_sign_label");
|
||||
await waitFor(".modal .modal-body");
|
||||
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(1);
|
||||
expect.verifySteps(["Pop's Chock'lit"]);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("Signature widget: full_name option on mobile", async () => {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
expect.step(this.props.signature.name);
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<widget name="signature" string="Sign" full_name="display_name"/>
|
||||
</header>
|
||||
<field name="display_name"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await click("span.o_sign_label");
|
||||
await waitFor(".modal .modal-body");
|
||||
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(1);
|
||||
expect.verifySteps(["Pop's Chock'lit"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Signature widget: highlight option on desktop", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<widget name="signature" string="Sign" highlight="1"/>
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("button.o_sign_button").toHaveClass("btn-primary", {
|
||||
message: `The button must have the 'btn-primary' class as "highlight=1"`,
|
||||
});
|
||||
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await click(".o_widget_signature button.o_sign_button");
|
||||
await waitFor(".modal .modal-body");
|
||||
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("Signature widget: highlight option on mobile", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<widget name="signature" string="Sign" highlight="1"/>
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
|
||||
expect("button.o_sign_button").toHaveClass("btn-primary", {
|
||||
message: `The button must have the 'btn-primary' class as "highlight=1"`,
|
||||
});
|
||||
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await click(".o_widget_signature button.o_sign_button");
|
||||
await waitFor(".modal .modal-body");
|
||||
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
defineParams,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
sun = fields.Boolean();
|
||||
mon = fields.Boolean();
|
||||
tue = fields.Boolean();
|
||||
wed = fields.Boolean();
|
||||
thu = fields.Boolean();
|
||||
fri = fields.Boolean();
|
||||
sat = fields.Boolean();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
sun: false,
|
||||
mon: false,
|
||||
tue: false,
|
||||
wed: false,
|
||||
thu: false,
|
||||
fri: false,
|
||||
sat: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("simple week recurrence widget", async () => {
|
||||
expect.assertions(13);
|
||||
defineParams({ lang_parameters: { week_start: 1 } });
|
||||
let writeCall = 0;
|
||||
onRpc("web_save", ({ args }) => {
|
||||
writeCall++;
|
||||
if (writeCall === 1) {
|
||||
expect(args[1].sun).toBe(true);
|
||||
}
|
||||
if (writeCall === 2) {
|
||||
expect(args[1].sun).not.toBe(true);
|
||||
expect(args[1].mon).toBe(true);
|
||||
expect(args[1].tue).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><sheet><group><widget name="week_days" /></group></sheet></form>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_recurrent_weekday_label")).toEqual(
|
||||
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||
{ message: "labels should be short week names" }
|
||||
);
|
||||
expect(".form-check input:disabled").toHaveCount(0, {
|
||||
message: "all inputs should be enabled in edit mode",
|
||||
});
|
||||
|
||||
await click("td:nth-child(7) input");
|
||||
expect("td:nth-child(7) input").toBeChecked({
|
||||
message: "sunday checkbox should be checked",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
|
||||
await click("td:nth-child(1) input");
|
||||
expect("td:nth-child(1) input").toBeChecked({
|
||||
message: "monday checkbox should be checked",
|
||||
});
|
||||
|
||||
await click("td:nth-child(2) input");
|
||||
expect("td:nth-child(2) input").toBeChecked({
|
||||
message: "tuesday checkbox should be checked",
|
||||
});
|
||||
|
||||
// uncheck Sunday checkbox and check write call
|
||||
await click("td:nth-child(7) input");
|
||||
expect("td:nth-child(7) input").not.toBeChecked({
|
||||
message: "sunday checkbox should be unchecked",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect("td:nth-child(7) input").not.toBeChecked({
|
||||
message: "sunday checkbox should be unchecked",
|
||||
});
|
||||
expect("td:nth-child(1) input").toBeChecked({ message: "monday checkbox should be checked" });
|
||||
expect("td:nth-child(2) input").toBeChecked({
|
||||
message: "tuesday checkbox should be checked",
|
||||
});
|
||||
});
|
||||
|
||||
test("week recurrence widget readonly modifiers", async () => {
|
||||
defineParams({ lang_parameters: { week_start: 1 } });
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><sheet><group><widget name="week_days" readonly="1"/></group></sheet></form>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_recurrent_weekday_label")).toEqual(
|
||||
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||
{ message: "labels should be short week names" }
|
||||
);
|
||||
expect(".form-check input:disabled").toHaveCount(7, {
|
||||
message: "all inputs should be disabled in readonly mode",
|
||||
});
|
||||
});
|
||||
|
||||
test("week recurrence widget show week start as per language configuration", async () => {
|
||||
defineParams({ lang_parameters: { week_start: 5 } });
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><sheet><group><widget name="week_days"/></group></sheet></form>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_recurrent_weekday_label")).toEqual(
|
||||
["Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu"],
|
||||
{ message: "labels should be short week names" }
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue