mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-18 22:12:08 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
|
|
@ -0,0 +1,170 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarArchParser } from "@web/views/calendar/calendar_arch_parser";
|
||||
import { FAKE_FIELDS } from "./helpers";
|
||||
|
||||
function parseArch(arch, options = {}) {
|
||||
const parser = new CalendarArchParser();
|
||||
return parser.parse(arch, { fake: "fields" in options ? options.fields : FAKE_FIELDS }, "fake");
|
||||
}
|
||||
|
||||
function check(assert, paramName, paramValue, expectedName, expectedValue) {
|
||||
const arch = `<calendar date_start="start_date" ${paramName}="${paramValue}" />`;
|
||||
const data = parseArch(arch);
|
||||
assert.strictEqual(data[expectedName], expectedValue);
|
||||
}
|
||||
|
||||
QUnit.module("CalendarView - ArchParser");
|
||||
|
||||
QUnit.test("throw if date_start is not set", (assert) => {
|
||||
assert.throws(() => {
|
||||
parseArch(`<calendar />`);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("defaults", (assert) => {
|
||||
assert.deepEqual(parseArch(`<calendar date_start="start_date" />`), {
|
||||
canCreate: true,
|
||||
canDelete: true,
|
||||
eventLimit: 5,
|
||||
fieldMapping: {
|
||||
date_start: "start_date",
|
||||
},
|
||||
fieldNames: ["start_date"],
|
||||
filtersInfo: {},
|
||||
formViewId: false,
|
||||
hasEditDialog: false,
|
||||
hasQuickCreate: true,
|
||||
isDateHidden: false,
|
||||
isTimeHidden: false,
|
||||
popoverFields: {},
|
||||
scale: "week",
|
||||
scales: ["day", "week", "month", "year"],
|
||||
showUnusualDays: false,
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("canCreate", (assert) => {
|
||||
check(assert, "create", "", "canCreate", true);
|
||||
check(assert, "create", "true", "canCreate", true);
|
||||
check(assert, "create", "True", "canCreate", true);
|
||||
check(assert, "create", "1", "canCreate", true);
|
||||
check(assert, "create", "false", "canCreate", false);
|
||||
check(assert, "create", "False", "canCreate", false);
|
||||
check(assert, "create", "0", "canCreate", false);
|
||||
});
|
||||
|
||||
QUnit.test("canDelete", (assert) => {
|
||||
check(assert, "delete", "", "canDelete", true);
|
||||
check(assert, "delete", "true", "canDelete", true);
|
||||
check(assert, "delete", "True", "canDelete", true);
|
||||
check(assert, "delete", "1", "canDelete", true);
|
||||
check(assert, "delete", "false", "canDelete", false);
|
||||
check(assert, "delete", "False", "canDelete", false);
|
||||
check(assert, "delete", "0", "canDelete", false);
|
||||
});
|
||||
|
||||
QUnit.test("eventLimit", (assert) => {
|
||||
check(assert, "event_limit", "2", "eventLimit", 2);
|
||||
check(assert, "event_limit", "5", "eventLimit", 5);
|
||||
|
||||
assert.throws(() => {
|
||||
parseArch(`<calendar date_start="start_date" event_limit="five" />`);
|
||||
});
|
||||
|
||||
assert.throws(() => {
|
||||
parseArch(`<calendar date_start="start_date" event_limit="" />`);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("hasEditDialog", (assert) => {
|
||||
check(assert, "event_open_popup", "", "hasEditDialog", false);
|
||||
check(assert, "event_open_popup", "true", "hasEditDialog", true);
|
||||
check(assert, "event_open_popup", "True", "hasEditDialog", true);
|
||||
check(assert, "event_open_popup", "1", "hasEditDialog", true);
|
||||
check(assert, "event_open_popup", "false", "hasEditDialog", false);
|
||||
check(assert, "event_open_popup", "False", "hasEditDialog", false);
|
||||
check(assert, "event_open_popup", "0", "hasEditDialog", false);
|
||||
});
|
||||
|
||||
QUnit.test("hasQuickCreate", (assert) => {
|
||||
check(assert, "quick_add", "", "hasQuickCreate", true);
|
||||
check(assert, "quick_add", "true", "hasQuickCreate", true);
|
||||
check(assert, "quick_add", "True", "hasQuickCreate", true);
|
||||
check(assert, "quick_add", "1", "hasQuickCreate", true);
|
||||
check(assert, "quick_add", "false", "hasQuickCreate", false);
|
||||
check(assert, "quick_add", "False", "hasQuickCreate", false);
|
||||
check(assert, "quick_add", "0", "hasQuickCreate", false);
|
||||
check(assert, "quick_add", "390", "hasQuickCreate", true);
|
||||
});
|
||||
|
||||
QUnit.test("isDateHidden", (assert) => {
|
||||
check(assert, "hide_date", "", "isDateHidden", false);
|
||||
check(assert, "hide_date", "true", "isDateHidden", true);
|
||||
check(assert, "hide_date", "True", "isDateHidden", true);
|
||||
check(assert, "hide_date", "1", "isDateHidden", true);
|
||||
check(assert, "hide_date", "false", "isDateHidden", false);
|
||||
check(assert, "hide_date", "False", "isDateHidden", false);
|
||||
check(assert, "hide_date", "0", "isDateHidden", false);
|
||||
});
|
||||
|
||||
QUnit.test("isTimeHidden", (assert) => {
|
||||
check(assert, "hide_time", "", "isTimeHidden", false);
|
||||
check(assert, "hide_time", "true", "isTimeHidden", true);
|
||||
check(assert, "hide_time", "True", "isTimeHidden", true);
|
||||
check(assert, "hide_time", "1", "isTimeHidden", true);
|
||||
check(assert, "hide_time", "false", "isTimeHidden", false);
|
||||
check(assert, "hide_time", "False", "isTimeHidden", false);
|
||||
check(assert, "hide_time", "0", "isTimeHidden", false);
|
||||
});
|
||||
|
||||
QUnit.test("scale", (assert) => {
|
||||
check(assert, "mode", "day", "scale", "day");
|
||||
check(assert, "mode", "week", "scale", "week");
|
||||
check(assert, "mode", "month", "scale", "month");
|
||||
check(assert, "mode", "year", "scale", "year");
|
||||
assert.throws(() => {
|
||||
parseArch(`<calendar date_start="start_date" mode="other" />`);
|
||||
});
|
||||
|
||||
assert.throws(() => {
|
||||
parseArch(`<calendar date_start="start_date" mode="" />`);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("scales", (assert) => {
|
||||
function check(scales, expectedScales) {
|
||||
const arch = `<calendar date_start="start_date" scales="${scales}" />`;
|
||||
const data = parseArch(arch);
|
||||
assert.deepEqual(data.scales, expectedScales);
|
||||
}
|
||||
|
||||
check("", []);
|
||||
|
||||
check("day", ["day"]);
|
||||
check("day,week", ["day", "week"]);
|
||||
check("day,week,month", ["day", "week", "month"]);
|
||||
check("day,week,month,year", ["day", "week", "month", "year"]);
|
||||
check("week", ["week"]);
|
||||
check("week,month", ["week", "month"]);
|
||||
check("week,month,year", ["week", "month", "year"]);
|
||||
check("month", ["month"]);
|
||||
check("month,year", ["month", "year"]);
|
||||
check("year", ["year"]);
|
||||
|
||||
check("year,day,month,week", ["year", "day", "month", "week"]);
|
||||
|
||||
assert.throws(() => {
|
||||
parseArch(`<calendar date_start="start_date" scales="month" mode="day" />`);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("showUnusualDays", (assert) => {
|
||||
check(assert, "show_unusual_days", "", "showUnusualDays", false);
|
||||
check(assert, "show_unusual_days", "true", "showUnusualDays", true);
|
||||
check(assert, "show_unusual_days", "True", "showUnusualDays", true);
|
||||
check(assert, "show_unusual_days", "1", "showUnusualDays", true);
|
||||
check(assert, "show_unusual_days", "false", "showUnusualDays", false);
|
||||
check(assert, "show_unusual_days", "False", "showUnusualDays", false);
|
||||
check(assert, "show_unusual_days", "0", "showUnusualDays", false);
|
||||
});
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarCommonPopover } from "@web/views/calendar/calendar_common/calendar_common_popover";
|
||||
import { click, getFixture } from "../../helpers/utils";
|
||||
import { makeEnv, makeFakeDate, makeFakeModel, mountComponent } from "./helpers";
|
||||
|
||||
let target;
|
||||
|
||||
function makeFakeRecord(data = {}) {
|
||||
return {
|
||||
id: 5,
|
||||
title: "Meeting",
|
||||
isAllDay: false,
|
||||
start: makeFakeDate(),
|
||||
end: makeFakeDate().plus({ hours: 3, minutes: 15 }),
|
||||
colorIndex: 0,
|
||||
isTimeHidden: false,
|
||||
rawRecord: {
|
||||
name: "Meeting",
|
||||
},
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
async function start(params = {}) {
|
||||
const { services, props, model: modelParams } = params;
|
||||
const env = await makeEnv(services);
|
||||
const model = makeFakeModel(modelParams);
|
||||
return await mountComponent(CalendarCommonPopover, env, {
|
||||
model,
|
||||
record: makeFakeRecord(),
|
||||
createRecord() {},
|
||||
deleteRecord() {},
|
||||
editRecord() {},
|
||||
close() {},
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
/** @todo Add tests for fields **/
|
||||
|
||||
QUnit.module("CalendarView - CommonPopover", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test("mount a CalendarCommonPopover", async (assert) => {
|
||||
await start({});
|
||||
assert.containsOnce(target, ".popover-header");
|
||||
assert.strictEqual(target.querySelector(".popover-header").textContent, "Meeting");
|
||||
assert.containsN(target, ".list-group", 2);
|
||||
assert.containsOnce(target, ".list-group.o_cw_popover_fields_secondary");
|
||||
assert.containsOnce(target, ".card-footer .o_cw_popover_edit");
|
||||
assert.containsOnce(target, ".card-footer .o_cw_popover_delete");
|
||||
});
|
||||
|
||||
QUnit.test("date duration: is all day and is same day", async (assert) => {
|
||||
await start({
|
||||
props: {
|
||||
record: makeFakeRecord({ isAllDay: true, isTimeHidden: true }),
|
||||
},
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(dateTimeLabels, "July 16, 2021 (All day)");
|
||||
});
|
||||
|
||||
QUnit.test("date duration: is all day and two days duration", async (assert) => {
|
||||
await start({
|
||||
props: {
|
||||
record: makeFakeRecord({
|
||||
end: makeFakeDate().plus({ days: 1 }),
|
||||
isAllDay: true,
|
||||
isTimeHidden: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(dateTimeLabels, "July 16-17, 2021 (2 days)");
|
||||
});
|
||||
|
||||
QUnit.test("time duration: 1 hour diff", async (assert) => {
|
||||
await start({
|
||||
props: {
|
||||
record: makeFakeRecord({ end: makeFakeDate().plus({ hours: 1 }) }),
|
||||
},
|
||||
model: { isDateHidden: true },
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(dateTimeLabels, "08:00 - 09:00 (1 hour)");
|
||||
});
|
||||
|
||||
QUnit.test("time duration: 2 hours diff", async (assert) => {
|
||||
await start({
|
||||
props: {
|
||||
record: makeFakeRecord({ end: makeFakeDate().plus({ hours: 2 }) }),
|
||||
},
|
||||
model: { isDateHidden: true },
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(dateTimeLabels, "08:00 - 10:00 (2 hours)");
|
||||
});
|
||||
|
||||
QUnit.test("time duration: 1 minute diff", async (assert) => {
|
||||
await start({
|
||||
props: {
|
||||
record: makeFakeRecord({ end: makeFakeDate().plus({ minutes: 1 }) }),
|
||||
},
|
||||
model: { isDateHidden: true },
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(dateTimeLabels, "08:00 - 08:01 (1 minute)");
|
||||
});
|
||||
|
||||
QUnit.test("time duration: 2 minutes diff", async (assert) => {
|
||||
await start({
|
||||
props: {
|
||||
record: makeFakeRecord({ end: makeFakeDate().plus({ minutes: 2 }) }),
|
||||
},
|
||||
model: { isDateHidden: true },
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(dateTimeLabels, "08:00 - 08:02 (2 minutes)");
|
||||
});
|
||||
|
||||
QUnit.test("time duration: 3 hours and 15 minutes diff", async (assert) => {
|
||||
await start({
|
||||
model: { isDateHidden: true },
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(dateTimeLabels, "08:00 - 11:15 (3 hours, 15 minutes)");
|
||||
});
|
||||
|
||||
QUnit.test("isDateHidden is true", async (assert) => {
|
||||
await start({
|
||||
model: { isDateHidden: true },
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(dateTimeLabels, "08:00 - 11:15 (3 hours, 15 minutes)");
|
||||
});
|
||||
|
||||
QUnit.test("isDateHidden is false", async (assert) => {
|
||||
await start({
|
||||
model: { isDateHidden: false },
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(
|
||||
dateTimeLabels,
|
||||
"July 16, 2021 08:00 - 11:15 (3 hours, 15 minutes)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("isTimeHidden is true", async (assert) => {
|
||||
await start({
|
||||
props: {
|
||||
record: makeFakeRecord({ isTimeHidden: true }),
|
||||
},
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(dateTimeLabels, "July 16, 2021");
|
||||
});
|
||||
|
||||
QUnit.test("isTimeHidden is false", async (assert) => {
|
||||
await start({
|
||||
props: {
|
||||
record: makeFakeRecord({ isTimeHidden: false }),
|
||||
},
|
||||
});
|
||||
const dateTimeGroup = target.querySelector(`.list-group`);
|
||||
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
|
||||
assert.strictEqual(
|
||||
dateTimeLabels,
|
||||
"July 16, 2021 08:00 - 11:15 (3 hours, 15 minutes)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("canDelete is true", async (assert) => {
|
||||
await start({
|
||||
model: { canDelete: true },
|
||||
});
|
||||
assert.containsOnce(target, ".o_cw_popover_delete");
|
||||
});
|
||||
|
||||
QUnit.test("canDelete is false", async (assert) => {
|
||||
await start({
|
||||
model: { canDelete: false },
|
||||
});
|
||||
assert.containsNone(target, ".o_cw_popover_delete");
|
||||
});
|
||||
|
||||
QUnit.test("click on delete button", async (assert) => {
|
||||
assert.expect(2);
|
||||
await start({
|
||||
model: { canDelete: true },
|
||||
props: {
|
||||
deleteRecord: () => assert.step("delete"),
|
||||
},
|
||||
});
|
||||
await click(target, ".o_cw_popover_delete");
|
||||
assert.verifySteps(["delete"]);
|
||||
});
|
||||
|
||||
QUnit.test("click on edit button", async (assert) => {
|
||||
assert.expect(2);
|
||||
await start({
|
||||
props: {
|
||||
editRecord: () => assert.step("edit"),
|
||||
},
|
||||
});
|
||||
await click(target, ".o_cw_popover_edit");
|
||||
assert.verifySteps(["edit"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { CalendarCommonRenderer } from "@web/views/calendar/calendar_common/calendar_common_renderer";
|
||||
import { getFixture, patchWithCleanup } from "../../helpers/utils";
|
||||
import {
|
||||
makeEnv,
|
||||
makeFakeModel,
|
||||
mountComponent,
|
||||
clickAllDaySlot,
|
||||
selectTimeRange,
|
||||
clickEvent,
|
||||
makeFakeDate,
|
||||
} from "./helpers";
|
||||
|
||||
let target;
|
||||
|
||||
function makeFakePopoverService(add) {
|
||||
return { start: () => ({ add }) };
|
||||
}
|
||||
|
||||
async function start(params = {}) {
|
||||
const { services, props, model: modelParams } = params;
|
||||
const env = await makeEnv(services);
|
||||
const model = makeFakeModel(modelParams);
|
||||
return await mountComponent(CalendarCommonRenderer, env, {
|
||||
model,
|
||||
createRecord() {},
|
||||
deleteRecord() {},
|
||||
editRecord() {},
|
||||
displayName: "Plop",
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module("CalendarView - CommonRenderer", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test("mount a CalendarCommonRenderer", async (assert) => {
|
||||
await start({});
|
||||
assert.containsOnce(target, ".o_calendar_widget.fc");
|
||||
});
|
||||
|
||||
QUnit.test("Day: mount a CalendarCommonRenderer", async (assert) => {
|
||||
await start({ model: { scale: "day" } });
|
||||
assert.containsOnce(target, ".o_calendar_widget.fc .fc-timeGridDay-view");
|
||||
});
|
||||
|
||||
QUnit.test("Week: mount a CalendarCommonRenderer", async (assert) => {
|
||||
await start({ model: { scale: "week" } });
|
||||
assert.containsOnce(target, ".o_calendar_widget.fc .fc-timeGridWeek-view");
|
||||
});
|
||||
|
||||
QUnit.test("Month: mount a CalendarCommonRenderer", async (assert) => {
|
||||
await start({ model: { scale: "month" } });
|
||||
assert.containsOnce(target, ".o_calendar_widget.fc .fc-dayGridMonth-view");
|
||||
});
|
||||
|
||||
QUnit.test("Day: check week number", async (assert) => {
|
||||
await start({ model: { scale: "day" } });
|
||||
assert.containsOnce(target, ".fc-week-number");
|
||||
assert.strictEqual(target.querySelector(".fc-week-number").textContent, "Week 28");
|
||||
});
|
||||
|
||||
QUnit.test("Day: check date", async (assert) => {
|
||||
await start({ model: { scale: "day" } });
|
||||
assert.containsOnce(target, ".fc-day-header");
|
||||
assert.strictEqual(target.querySelector(".fc-day-header").textContent, "July 16, 2021");
|
||||
});
|
||||
|
||||
QUnit.test("Day: click all day slot", async (assert) => {
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout() {},
|
||||
});
|
||||
|
||||
await start({
|
||||
model: { scale: "day" },
|
||||
props: {
|
||||
createRecord(record) {
|
||||
const date = makeFakeDate().startOf("day");
|
||||
assert.ok(record.isAllDay);
|
||||
assert.strictEqual(record.start.valueOf(), date.valueOf());
|
||||
assert.step("create");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await clickAllDaySlot(target, "2021-07-16");
|
||||
assert.verifySteps(["create"]);
|
||||
});
|
||||
|
||||
QUnit.test("Day: select range", async (assert) => {
|
||||
await start({
|
||||
model: { scale: "day" },
|
||||
props: {
|
||||
createRecord(record) {
|
||||
assert.notOk(record.isAllDay);
|
||||
assert.strictEqual(
|
||||
record.start.valueOf(),
|
||||
luxon.DateTime.local(2021, 7, 16, 8, 0).valueOf()
|
||||
);
|
||||
assert.strictEqual(
|
||||
record.end.valueOf(),
|
||||
luxon.DateTime.local(2021, 7, 16, 10, 0).valueOf()
|
||||
);
|
||||
assert.step("create");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await selectTimeRange(target, "2021-07-16 08:00:00", "2021-07-16 10:00:00");
|
||||
assert.verifySteps(["create"]);
|
||||
});
|
||||
|
||||
QUnit.test("Day: check event", async (assert) => {
|
||||
await start({ model: { scale: "day" } });
|
||||
assert.containsOnce(target, ".o_event");
|
||||
assert.hasAttrValue(target.querySelector(".o_event"), "data-event-id", "1");
|
||||
});
|
||||
|
||||
QUnit.test("Day: click on event", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout() {},
|
||||
});
|
||||
|
||||
await start({
|
||||
model: { scale: "day" },
|
||||
services: {
|
||||
popover: makeFakePopoverService((target, _, props) => {
|
||||
assert.strictEqual(props.record.id, 1);
|
||||
return () => {};
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await clickEvent(target, 1);
|
||||
});
|
||||
|
||||
QUnit.test("Week: check week number", async (assert) => {
|
||||
await start({ model: { scale: "week" } });
|
||||
assert.containsOnce(target, ".fc-week-number");
|
||||
assert.strictEqual(target.querySelector(".fc-week-number").textContent, "Week 28");
|
||||
});
|
||||
|
||||
QUnit.test("Week: check dates", async (assert) => {
|
||||
await start({ model: { scale: "week" } });
|
||||
assert.containsN(target, ".fc-day-header", 7);
|
||||
|
||||
const dates = ["Sun 11", "Mon 12", "Tue 13", "Wed 14", "Thu 15", "Fri 16", "Sat 17"];
|
||||
|
||||
const els = target.querySelectorAll(".fc-day-header");
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
assert.strictEqual(els[i].textContent, dates[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarDatePicker } from "@web/views/calendar/date_picker/calendar_date_picker";
|
||||
import { click, getFixture, patchDate } from "../../helpers/utils";
|
||||
import { makeEnv, makeFakeModel, mountComponent } from "./helpers";
|
||||
|
||||
let target;
|
||||
|
||||
async function start(params = {}) {
|
||||
const { services, props, model: modelParams } = params;
|
||||
const env = await makeEnv(services);
|
||||
const model = makeFakeModel(modelParams);
|
||||
return await mountComponent(CalendarDatePicker, env, {
|
||||
model,
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module("CalendarView - DatePicker", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
target = getFixture();
|
||||
patchDate(2021, 7, 14, 8, 0, 0);
|
||||
});
|
||||
|
||||
QUnit.test("Mount a CalendarDatePicker", async (assert) => {
|
||||
await start({ model: { scale: "day" } });
|
||||
assert.containsOnce(target, ".o_calendar_mini.hasDatepicker");
|
||||
assert.strictEqual(target.querySelector(".o_selected_range").textContent, "16");
|
||||
assert.containsOnce(target, `[data-month="6"][data-year="2021"] .o_selected_range`);
|
||||
assert.strictEqual(target.querySelector(".ui-datepicker-month").textContent, "Jul");
|
||||
assert.strictEqual(target.querySelector(".ui-datepicker-year").textContent, "2021");
|
||||
assert.strictEqual(target.querySelector("thead").textContent, "SMTWTFS");
|
||||
});
|
||||
|
||||
QUnit.test("Scale: init with day", async (assert) => {
|
||||
await start({ model: { scale: "day" } });
|
||||
assert.containsOnce(target, ".o_selected_range");
|
||||
assert.containsOnce(target, "a.o_selected_range");
|
||||
assert.strictEqual(target.querySelector(".o_selected_range").textContent, "16");
|
||||
});
|
||||
|
||||
QUnit.test("Scale: init with week", async (assert) => {
|
||||
await start({ model: { scale: "week" } });
|
||||
assert.containsOnce(target, ".o_selected_range");
|
||||
assert.containsOnce(target, "tr.o_selected_range");
|
||||
assert.hasClass(target.querySelector("tr.o_selected_range"), "o_color");
|
||||
assert.strictEqual(target.querySelector(".o_selected_range").textContent, "11121314151617");
|
||||
});
|
||||
|
||||
QUnit.test("Scale: init with month", async (assert) => {
|
||||
await start({ model: { scale: "month" } });
|
||||
assert.containsN(target, "td.o_selected_range", 35);
|
||||
});
|
||||
|
||||
QUnit.test("Scale: init with year", async (assert) => {
|
||||
await start({ model: { scale: "year" } });
|
||||
assert.containsN(target, "td.o_selected_range", 35);
|
||||
});
|
||||
|
||||
QUnit.test("First day: 0 = Sunday", async (assert) => {
|
||||
await start({ model: { scale: "day", firstDayOfWeek: 0 } });
|
||||
assert.strictEqual(target.querySelector("thead").textContent, "SMTWTFS");
|
||||
});
|
||||
|
||||
QUnit.test("First day: 1 = Monday", async (assert) => {
|
||||
await start({ model: { scale: "day", firstDayOfWeek: 1 } });
|
||||
assert.strictEqual(target.querySelector("thead").textContent, "MTWTFSS");
|
||||
});
|
||||
|
||||
QUnit.test("Click on active day should change scale : day -> month", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
await start({
|
||||
model: {
|
||||
scale: "day",
|
||||
load(params) {
|
||||
assert.strictEqual(params.scale, "month");
|
||||
assert.ok(params.date.equals(luxon.DateTime.local(2021, 7, 16)));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".ui-state-active");
|
||||
});
|
||||
|
||||
QUnit.test("Click on active day should change scale : month -> week", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
await start({
|
||||
model: {
|
||||
scale: "month",
|
||||
load(params) {
|
||||
assert.strictEqual(params.scale, "week");
|
||||
assert.ok(params.date.equals(luxon.DateTime.local(2021, 7, 16)));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".ui-state-active");
|
||||
});
|
||||
|
||||
QUnit.test("Click on active day should change scale : week -> day", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
await start({
|
||||
model: {
|
||||
scale: "week",
|
||||
load(params) {
|
||||
assert.strictEqual(params.scale, "day");
|
||||
assert.ok(params.date.equals(luxon.DateTime.local(2021, 7, 16)));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".ui-state-active");
|
||||
});
|
||||
|
||||
QUnit.test("Scale: today is correctly highlighted", async (assert) => {
|
||||
patchDate(2021, 6, 4, 8, 0, 0);
|
||||
await start({ model: { scale: "month" } });
|
||||
assert.containsOnce(target, ".ui-datepicker-today");
|
||||
assert.strictEqual(target.querySelector(".ui-datepicker-today").textContent, "4");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarFilterPanel } from "@web/views/calendar/filter_panel/calendar_filter_panel";
|
||||
import { click, getFixture, triggerEvent } from "../../helpers/utils";
|
||||
import { makeEnv, makeFakeModel, mountComponent } from "./helpers";
|
||||
|
||||
let target;
|
||||
|
||||
async function start(params = {}) {
|
||||
const { services, props, model: modelParams } = params;
|
||||
const env = await makeEnv(services);
|
||||
const model = makeFakeModel(modelParams);
|
||||
return await mountComponent(CalendarFilterPanel, env, {
|
||||
model,
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module("CalendarView - FilterPanel", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test("render filter panel", async (assert) => {
|
||||
await start({});
|
||||
|
||||
assert.containsN(target, ".o_calendar_filter", 2);
|
||||
const sections = target.querySelectorAll(".o_calendar_filter");
|
||||
|
||||
let header = sections[0].querySelector(".o_cw_filter_label");
|
||||
assert.strictEqual(header.textContent, "Attendees");
|
||||
assert.containsN(sections[0], ".o_calendar_filter_item", 4);
|
||||
|
||||
header = sections[1].querySelector(".o_cw_filter_label");
|
||||
assert.strictEqual(header.textContent, "Users");
|
||||
assert.containsN(sections[1], ".o_calendar_filter_item", 2);
|
||||
});
|
||||
|
||||
QUnit.test("filters are correctly sorted", async (assert) => {
|
||||
await start({});
|
||||
|
||||
assert.containsN(target, ".o_calendar_filter", 2);
|
||||
const sections = target.querySelectorAll(".o_calendar_filter");
|
||||
|
||||
let header = sections[0].querySelector(".o_cw_filter_label");
|
||||
assert.strictEqual(header.textContent, "Attendees");
|
||||
assert.containsN(sections[0], ".o_calendar_filter_item", 4);
|
||||
assert.strictEqual(
|
||||
sections[0].textContent.trim(),
|
||||
"AttendeesMitchell AdminMarc DemoBrandon FreemanEverybody's calendar"
|
||||
);
|
||||
|
||||
header = sections[1].querySelector(".o_cw_filter_label");
|
||||
assert.strictEqual(header.textContent, "Users");
|
||||
assert.containsN(sections[1], ".o_calendar_filter_item", 2);
|
||||
assert.strictEqual(sections[1].textContent.trim(), "UsersMarc DemoBrandon Freeman");
|
||||
});
|
||||
|
||||
QUnit.test("section can collapse", async (assert) => {
|
||||
await start({});
|
||||
|
||||
const section = target.querySelectorAll(".o_calendar_filter")[0];
|
||||
assert.containsOnce(section, ".o_cw_filter_collapse_icon");
|
||||
assert.containsN(section, ".o_calendar_filter_item", 4);
|
||||
|
||||
await click(section, ".o_cw_filter_label");
|
||||
assert.containsNone(section, ".o_calendar_filter_item");
|
||||
|
||||
await click(section, ".o_cw_filter_label");
|
||||
assert.containsN(section, ".o_calendar_filter_item", 4);
|
||||
});
|
||||
|
||||
QUnit.test("section cannot collapse", async (assert) => {
|
||||
await start({});
|
||||
|
||||
const section = target.querySelectorAll(".o_calendar_filter")[1];
|
||||
assert.containsNone(section, ".o_cw_filter_label > i");
|
||||
assert.doesNotHaveClass(section, "o_calendar_filter-collapsed");
|
||||
assert.containsN(section, ".o_calendar_filter_item", 2);
|
||||
|
||||
await click(section, ".o_cw_filter_label");
|
||||
assert.doesNotHaveClass(section, "o_calendar_filter-collapsed");
|
||||
assert.containsN(section, ".o_calendar_filter_item", 2);
|
||||
});
|
||||
|
||||
QUnit.test("filters can have avatar", async (assert) => {
|
||||
await start({});
|
||||
|
||||
const section = target.querySelectorAll(".o_calendar_filter")[0];
|
||||
const filters = section.querySelectorAll(".o_calendar_filter_item");
|
||||
|
||||
assert.containsN(section, ".o_cw_filter_avatar", 4);
|
||||
assert.containsN(section, "img.o_cw_filter_avatar", 3);
|
||||
assert.containsOnce(section, "i.o_cw_filter_avatar");
|
||||
|
||||
assert.hasAttrValue(
|
||||
filters[0].querySelector(".o_cw_filter_avatar"),
|
||||
"data-src",
|
||||
"/web/image/res.partner/3/avatar_128"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
filters[1].querySelector(".o_cw_filter_avatar"),
|
||||
"data-src",
|
||||
"/web/image/res.partner/6/avatar_128"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
filters[2].querySelector(".o_cw_filter_avatar"),
|
||||
"data-src",
|
||||
"/web/image/res.partner/4/avatar_128"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("filters cannot have avatar", async (assert) => {
|
||||
await start({});
|
||||
|
||||
const section = target.querySelectorAll(".o_calendar_filter")[1];
|
||||
assert.containsN(section, ".o_calendar_filter_item", 2);
|
||||
assert.containsNone(section, ".o_cw_filter_avatar");
|
||||
});
|
||||
|
||||
QUnit.test("filter can have remove button", async (assert) => {
|
||||
await start({});
|
||||
|
||||
const section = target.querySelectorAll(".o_calendar_filter")[0];
|
||||
const filters = section.querySelectorAll(".o_calendar_filter_item");
|
||||
|
||||
assert.containsN(section, ".o_calendar_filter_item", 4);
|
||||
assert.containsN(section, ".o_calendar_filter_item .o_remove", 2);
|
||||
assert.containsNone(filters[0], ".o_remove");
|
||||
assert.containsOnce(filters[1], ".o_remove");
|
||||
assert.containsOnce(filters[2], ".o_remove");
|
||||
assert.containsNone(filters[3], ".o_remove");
|
||||
});
|
||||
|
||||
QUnit.test("click on remove button", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
await start({
|
||||
model: {
|
||||
unlinkFilter(fieldName, recordId) {
|
||||
assert.step(`${fieldName} ${recordId}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const section = target.querySelectorAll(".o_calendar_filter")[0];
|
||||
const filters = section.querySelectorAll(".o_calendar_filter_item");
|
||||
|
||||
await click(filters[1], ".o_calendar_filter_item .o_remove");
|
||||
await click(filters[2], ".o_calendar_filter_item .o_remove");
|
||||
assert.verifySteps(["partner_ids 2", "partner_ids 1"]);
|
||||
});
|
||||
|
||||
QUnit.test("click on filter", async (assert) => {
|
||||
assert.expect(6);
|
||||
|
||||
await start({
|
||||
model: {
|
||||
updateFilters(fieldName, args) {
|
||||
assert.step(`${fieldName} ${Object.keys(args)[0]} ${Object.values(args)[0]}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const section = target.querySelectorAll(".o_calendar_filter")[0];
|
||||
const filters = section.querySelectorAll(".o_calendar_filter_item");
|
||||
|
||||
await click(filters[0], "input");
|
||||
await click(filters[1], "input");
|
||||
await click(filters[2], "input");
|
||||
await click(filters[3], "input");
|
||||
await click(filters[3], "input");
|
||||
assert.verifySteps([
|
||||
"partner_ids 3 false",
|
||||
"partner_ids 6 true",
|
||||
"partner_ids 4 false",
|
||||
"partner_ids all true",
|
||||
"partner_ids all false",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("hover filter opens tooltip", async (assert) => {
|
||||
await start({
|
||||
services: {
|
||||
popover: {
|
||||
start: () => ({
|
||||
add: (target, _, props) => {
|
||||
assert.step(props.filter.label);
|
||||
assert.step("" + props.filter.hasAvatar);
|
||||
assert.step("" + props.filter.value);
|
||||
return () => {
|
||||
assert.step("popOver Closed");
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const section = target.querySelectorAll(".o_calendar_filter")[0];
|
||||
const filters = section.querySelectorAll(".o_calendar_filter_item");
|
||||
|
||||
await triggerEvent(filters[0], null, "mouseenter");
|
||||
assert.verifySteps(["Mitchell Admin", "true", "3"]);
|
||||
await triggerEvent(filters[0], null, "mouseleave");
|
||||
assert.verifySteps(["popOver Closed"]);
|
||||
|
||||
await triggerEvent(filters[3], null, "mouseenter");
|
||||
assert.verifySteps([]);
|
||||
await triggerEvent(filters[3], null, "mouseleave");
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarQuickCreate } from "@web/views/calendar/quick_create/calendar_quick_create";
|
||||
import { click, getFixture } from "../../helpers/utils";
|
||||
import { makeEnv, makeFakeModel, mountComponent } from "./helpers";
|
||||
|
||||
let target;
|
||||
|
||||
async function start(params = {}) {
|
||||
const { services, props, model: modelParams } = params;
|
||||
const env = await makeEnv(services);
|
||||
env.dialogData = {
|
||||
isActive: true,
|
||||
close() {},
|
||||
};
|
||||
const model = makeFakeModel(modelParams);
|
||||
return await mountComponent(CalendarQuickCreate, env, {
|
||||
model,
|
||||
record: {},
|
||||
close() {},
|
||||
editRecord() {},
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module("CalendarView - QuickCreate", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test("mount a CalendarQuickCreate", async (assert) => {
|
||||
await start({});
|
||||
assert.containsOnce(target, ".o-calendar-quick-create");
|
||||
assert.containsOnce(target, ".o_dialog .modal-sm");
|
||||
assert.strictEqual(target.querySelector(".modal-title").textContent, "New Event");
|
||||
assert.strictEqual(target.querySelector(`input[name="title"]`), document.activeElement);
|
||||
assert.containsOnce(target, ".o-calendar-quick-create--create-btn");
|
||||
assert.containsOnce(target, ".o-calendar-quick-create--edit-btn");
|
||||
assert.containsOnce(target, ".o-calendar-quick-create--cancel-btn");
|
||||
});
|
||||
|
||||
QUnit.test("click on create button", async (assert) => {
|
||||
assert.expect(2);
|
||||
await start({
|
||||
props: {
|
||||
close: () => assert.step("close"),
|
||||
},
|
||||
model: {
|
||||
createRecord: () => assert.step("create"),
|
||||
},
|
||||
});
|
||||
await click(target, ".o-calendar-quick-create--create-btn");
|
||||
assert.verifySteps([]);
|
||||
assert.hasClass(target.querySelector("input[name=title]"), "o_field_invalid");
|
||||
});
|
||||
|
||||
QUnit.test("click on create button (with name)", async (assert) => {
|
||||
assert.expect(4);
|
||||
await start({
|
||||
props: {
|
||||
close: () => assert.step("close"),
|
||||
},
|
||||
model: {
|
||||
createRecord(record) {
|
||||
assert.step("create");
|
||||
assert.strictEqual(record.title, "TEST");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o-calendar-quick-create--input");
|
||||
input.value = "TEST";
|
||||
|
||||
await click(target, ".o-calendar-quick-create--create-btn");
|
||||
assert.verifySteps(["create", "close"]);
|
||||
});
|
||||
|
||||
QUnit.test("click on edit button", async (assert) => {
|
||||
assert.expect(3);
|
||||
await start({
|
||||
props: {
|
||||
close: () => assert.step("close"),
|
||||
editRecord: () => assert.step("edit"),
|
||||
},
|
||||
});
|
||||
await click(target, ".o-calendar-quick-create--edit-btn");
|
||||
assert.verifySteps(["edit", "close"]);
|
||||
});
|
||||
|
||||
QUnit.test("click on edit button (with name)", async (assert) => {
|
||||
assert.expect(4);
|
||||
await start({
|
||||
props: {
|
||||
close: () => assert.step("close"),
|
||||
editRecord(record) {
|
||||
assert.step("edit");
|
||||
assert.strictEqual(record.title, "TEST");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o-calendar-quick-create--input");
|
||||
input.value = "TEST";
|
||||
|
||||
await click(target, ".o-calendar-quick-create--edit-btn");
|
||||
assert.verifySteps(["edit", "close"]);
|
||||
});
|
||||
|
||||
QUnit.test("click on cancel button", async (assert) => {
|
||||
assert.expect(2);
|
||||
await start({
|
||||
props: {
|
||||
close: () => assert.step("close"),
|
||||
},
|
||||
});
|
||||
await click(target, ".o-calendar-quick-create--cancel-btn");
|
||||
assert.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
QUnit.test("check default title", async (assert) => {
|
||||
assert.expect(1);
|
||||
await start({
|
||||
props: {
|
||||
title: "Example Title",
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o-calendar-quick-create--input");
|
||||
assert.strictEqual(input.value, "Example Title");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,120 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarYearPopover } from "@web/views/calendar/calendar_year/calendar_year_popover";
|
||||
import { click, getFixture } from "../../helpers/utils";
|
||||
import { mountComponent, makeEnv, makeFakeModel, makeFakeRecords, makeFakeDate } from "./helpers";
|
||||
|
||||
let target, fakePopoverRecords;
|
||||
|
||||
async function start(params = {}) {
|
||||
const { services, props, model: modelParams } = params;
|
||||
const env = await makeEnv(services);
|
||||
const model = makeFakeModel(modelParams);
|
||||
return await mountComponent(CalendarYearPopover, env, {
|
||||
model,
|
||||
date: makeFakeDate(),
|
||||
records: fakePopoverRecords,
|
||||
createRecord() {},
|
||||
deleteRecord() {},
|
||||
editRecord() {},
|
||||
close() {},
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module("CalendarView - YearPopover", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
target = getFixture();
|
||||
fakePopoverRecords = [
|
||||
{
|
||||
id: 1,
|
||||
start: makeFakeDate(),
|
||||
end: makeFakeDate(),
|
||||
isAllDay: true,
|
||||
title: "R1",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
start: makeFakeDate().set({ hours: 14 }),
|
||||
end: makeFakeDate().set({ hours: 16 }),
|
||||
isAllDay: false,
|
||||
title: "R2",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
start: makeFakeDate().minus({ days: 1 }),
|
||||
end: makeFakeDate().plus({ days: 1 }),
|
||||
isAllDay: true,
|
||||
title: "R3",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
start: makeFakeDate().minus({ days: 3 }),
|
||||
end: makeFakeDate().plus({ days: 1 }),
|
||||
isAllDay: true,
|
||||
title: "R4",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
start: makeFakeDate().minus({ days: 1 }),
|
||||
end: makeFakeDate().plus({ days: 3 }),
|
||||
isAllDay: true,
|
||||
title: "R5",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
QUnit.test("canCreate is true", async (assert) => {
|
||||
await start({ model: { canCreate: true } });
|
||||
assert.containsOnce(target, ".o_cw_popover_create");
|
||||
});
|
||||
|
||||
QUnit.test("canCreate is false", async (assert) => {
|
||||
await start({ model: { canCreate: false } });
|
||||
assert.containsNone(target, ".o_cw_popover_create");
|
||||
});
|
||||
|
||||
QUnit.test("click on create button", async (assert) => {
|
||||
assert.expect(3);
|
||||
await start({
|
||||
props: {
|
||||
createRecord: () => assert.step("create"),
|
||||
},
|
||||
model: { canCreate: true },
|
||||
});
|
||||
assert.containsOnce(target, ".o_cw_popover_create");
|
||||
await click(target, ".o_cw_popover_create");
|
||||
assert.verifySteps(["create"]);
|
||||
});
|
||||
|
||||
QUnit.test("group records", async (assert) => {
|
||||
await start({});
|
||||
|
||||
assert.containsN(target, ".o_cw_body > div", 5);
|
||||
assert.containsN(target, ".o_cw_body > a", 5);
|
||||
|
||||
const sectionTitles = target.querySelectorAll(".o_cw_body > div");
|
||||
assert.strictEqual(sectionTitles[0].textContent.trim(), "July 16, 2021");
|
||||
assert.strictEqual(sectionTitles[1].textContent.trim(), "July 13-17, 2021");
|
||||
assert.strictEqual(sectionTitles[2].textContent.trim(), "July 15-17, 2021");
|
||||
assert.strictEqual(sectionTitles[3].textContent.trim(), "July 15-19, 2021");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_cw_body").textContent.trim(),
|
||||
"July 16, 2021R114:00 R2July 13-17, 2021R4July 15-17, 2021R3July 15-19, 2021R5 Create"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("click on record", async (assert) => {
|
||||
assert.expect(3);
|
||||
await start({
|
||||
props: {
|
||||
records: [makeFakeRecords()[3]],
|
||||
editRecord: () => assert.step("edit"),
|
||||
},
|
||||
});
|
||||
assert.containsOnce(target, ".o_cw_body > a");
|
||||
await click(target, ".o_cw_body > a");
|
||||
assert.verifySteps(["edit"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { CalendarYearRenderer } from "@web/views/calendar/calendar_year/calendar_year_renderer";
|
||||
import { getFixture, patchTimeZone, patchWithCleanup } from "../../helpers/utils";
|
||||
import { clickDate, mountComponent, selectDateRange, makeEnv, makeFakeModel } from "./helpers";
|
||||
|
||||
let target;
|
||||
|
||||
async function start(params = {}) {
|
||||
const { services, props, model: modelParams } = params;
|
||||
const env = await makeEnv(services);
|
||||
const model = makeFakeModel(modelParams);
|
||||
return await mountComponent(CalendarYearRenderer, env, {
|
||||
model,
|
||||
createRecord() {},
|
||||
deleteRecord() {},
|
||||
editRecord() {},
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module("CalendarView - YearRenderer", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test("mount a CalendarYearRenderer", async (assert) => {
|
||||
await start({});
|
||||
|
||||
assert.containsN(target, ".fc-month-container", 12);
|
||||
const monthHeaders = target.querySelectorAll(".fc-header-toolbar .fc-center");
|
||||
|
||||
// check "title format"
|
||||
assert.strictEqual(monthHeaders.length, 12);
|
||||
const monthTitles = [
|
||||
"Jan 2021",
|
||||
"Feb 2021",
|
||||
"Mar 2021",
|
||||
"Apr 2021",
|
||||
"May 2021",
|
||||
"Jun 2021",
|
||||
"Jul 2021",
|
||||
"Aug 2021",
|
||||
"Sep 2021",
|
||||
"Oct 2021",
|
||||
"Nov 2021",
|
||||
"Dec 2021",
|
||||
];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
assert.strictEqual(monthHeaders[i].textContent, monthTitles[i]);
|
||||
}
|
||||
const dayHeaders = target
|
||||
.querySelector(".fc-month-container")
|
||||
.querySelectorAll(".fc-day-header");
|
||||
|
||||
// check day header format
|
||||
assert.strictEqual(dayHeaders.length, 7);
|
||||
const dayTitles = ["S", "M", "T", "W", "T", "F", "S"];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
assert.strictEqual(dayHeaders[i].textContent, dayTitles[i]);
|
||||
}
|
||||
|
||||
// check showNonCurrentDates
|
||||
assert.containsN(target, ".fc-day-number", 365);
|
||||
});
|
||||
|
||||
QUnit.test("display events", async (assert) => {
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
|
||||
await start({
|
||||
props: {
|
||||
createRecord(record) {
|
||||
assert.step(`${record.start.toISODate()} allDay:${record.isAllDay} no event`);
|
||||
},
|
||||
},
|
||||
services: {
|
||||
popover: {
|
||||
start: () => ({
|
||||
add: (target, _, props) => {
|
||||
assert.step(`${props.date.toISODate()} ${props.records[0].title}`);
|
||||
return () => {};
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await clickDate(target, "2021-07-15");
|
||||
assert.verifySteps(["2021-07-15 allDay:true no event"]);
|
||||
await clickDate(target, "2021-07-16");
|
||||
assert.verifySteps(["2021-07-16 1 day, all day in July"]);
|
||||
await clickDate(target, "2021-07-17");
|
||||
assert.verifySteps(["2021-07-17 allDay:true no event"]);
|
||||
|
||||
await clickDate(target, "2021-07-18");
|
||||
assert.verifySteps(["2021-07-18 3 days, all day in July"]);
|
||||
await clickDate(target, "2021-07-19");
|
||||
assert.verifySteps(["2021-07-19 3 days, all day in July"]);
|
||||
await clickDate(target, "2021-07-20");
|
||||
assert.verifySteps(["2021-07-20 3 days, all day in July"]);
|
||||
await clickDate(target, "2021-07-21");
|
||||
assert.verifySteps(["2021-07-21 allDay:true no event"]);
|
||||
|
||||
await clickDate(target, "2021-06-28");
|
||||
assert.verifySteps(["2021-06-28 allDay:true no event"]);
|
||||
await clickDate(target, "2021-06-29");
|
||||
assert.verifySteps(["2021-06-29 Over June and July"]);
|
||||
await clickDate(target, "2021-06-30");
|
||||
assert.verifySteps(["2021-06-30 Over June and July"]);
|
||||
await clickDate(target, "2021-07-01");
|
||||
assert.verifySteps(["2021-07-01 Over June and July"]);
|
||||
await clickDate(target, "2021-07-02");
|
||||
assert.verifySteps(["2021-07-02 Over June and July"]);
|
||||
await clickDate(target, "2021-07-03");
|
||||
assert.verifySteps(["2021-07-03 Over June and July"]);
|
||||
await clickDate(target, "2021-07-04");
|
||||
assert.verifySteps(["2021-07-04 allDay:true no event"]);
|
||||
});
|
||||
|
||||
QUnit.test("select a range of date", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
await start({
|
||||
props: {
|
||||
createRecord(record) {
|
||||
assert.ok(record.isAllDay);
|
||||
assert.ok(record.start.equals(luxon.DateTime.local(2021, 7, 2, 0, 0, 0, 0)));
|
||||
assert.ok(record.end.equals(luxon.DateTime.local(2021, 7, 5, 0, 0, 0, 0)));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await selectDateRange(target, "2021-07-02", "2021-07-05");
|
||||
});
|
||||
|
||||
QUnit.test("display correct column header for days, independent of the timezone", async (assert) => {
|
||||
// Regression test: when the system tz is somewhere in a negative GMT (in our example Alaska)
|
||||
// the day headers of a months were incorrectly set. (S S M T W T F) instead of (S M T W T F S)
|
||||
// if the first day of the week is Sunday.
|
||||
patchTimeZone(-540); // UTC-9 = Alaska
|
||||
|
||||
await start({});
|
||||
|
||||
const dayHeaders = target
|
||||
.querySelector(".fc-month-container")
|
||||
.querySelectorAll(".fc-day-header");
|
||||
|
||||
assert.deepEqual([...dayHeaders].map((el) => el.textContent), ["S", "M", "T", "W", "T", "F", "S"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,536 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import { click, getFixture, mount, nextTick, triggerEvent } from "../../helpers/utils";
|
||||
import { setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
export function makeEnv(services = {}) {
|
||||
clearRegistryWithCleanup(registry.category("main_components"));
|
||||
setupViewRegistries();
|
||||
services = Object.assign(
|
||||
{
|
||||
ui: uiService,
|
||||
},
|
||||
services
|
||||
);
|
||||
|
||||
for (const [key, service] of Object.entries(services)) {
|
||||
registry.category("services").add(key, service, { force: true });
|
||||
}
|
||||
|
||||
return makeTestEnv({
|
||||
config: {
|
||||
setDisplayName: () => {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export async function mountComponent(C, env, props) {
|
||||
const target = getFixture();
|
||||
return await mount(C, target, { env, props });
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export function makeFakeDate() {
|
||||
return luxon.DateTime.local(2021, 7, 16, 8, 0, 0, 0);
|
||||
}
|
||||
|
||||
export function makeFakeRecords() {
|
||||
return {
|
||||
1: {
|
||||
id: 1,
|
||||
title: "1 day, all day in July",
|
||||
start: makeFakeDate(),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate(),
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
title: "3 days, all day in July",
|
||||
start: makeFakeDate().plus({ days: 2 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().plus({ days: 4 }),
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
title: "1 day, all day in June",
|
||||
start: makeFakeDate().plus({ months: -1 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().plus({ months: -1 }),
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
title: "3 days, all day in June",
|
||||
start: makeFakeDate().plus({ months: -1, days: 2 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().plus({ months: -1, days: 4 }),
|
||||
},
|
||||
5: {
|
||||
id: 5,
|
||||
title: "Over June and July",
|
||||
start: makeFakeDate().startOf("month").plus({ days: -2 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().startOf("month").plus({ days: 2 }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const FAKE_FILTER_SECTIONS = [
|
||||
{
|
||||
label: "Attendees",
|
||||
fieldName: "partner_ids",
|
||||
avatar: {
|
||||
model: "res.partner",
|
||||
field: "avatar_128",
|
||||
},
|
||||
hasAvatar: true,
|
||||
write: {
|
||||
model: "filter_partner",
|
||||
field: "partner_id",
|
||||
},
|
||||
canCollapse: true,
|
||||
canAddFilter: true,
|
||||
filters: [
|
||||
{
|
||||
type: "user",
|
||||
label: "Mitchell Admin",
|
||||
active: true,
|
||||
value: 3,
|
||||
colorIndex: 3,
|
||||
recordId: null,
|
||||
canRemove: false,
|
||||
hasAvatar: true,
|
||||
},
|
||||
{
|
||||
type: "all",
|
||||
label: "Everybody's calendar",
|
||||
active: false,
|
||||
value: "all",
|
||||
colorIndex: null,
|
||||
recordId: null,
|
||||
canRemove: false,
|
||||
hasAvatar: false,
|
||||
},
|
||||
{
|
||||
type: "record",
|
||||
label: "Brandon Freeman",
|
||||
active: true,
|
||||
value: 4,
|
||||
colorIndex: 4,
|
||||
recordId: 1,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
{
|
||||
type: "record",
|
||||
label: "Marc Demo",
|
||||
active: false,
|
||||
value: 6,
|
||||
colorIndex: 6,
|
||||
recordId: 2,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Users",
|
||||
fieldName: "user_id",
|
||||
avatar: {
|
||||
model: null,
|
||||
field: null,
|
||||
},
|
||||
hasAvatar: false,
|
||||
write: {
|
||||
model: null,
|
||||
field: null,
|
||||
},
|
||||
canCollapse: false,
|
||||
canAddFilter: false,
|
||||
filters: [
|
||||
{
|
||||
type: "record",
|
||||
label: "Brandon Freeman",
|
||||
active: false,
|
||||
value: 1,
|
||||
colorIndex: false,
|
||||
recordId: null,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
{
|
||||
type: "record",
|
||||
label: "Marc Demo",
|
||||
active: false,
|
||||
value: 2,
|
||||
colorIndex: false,
|
||||
recordId: null,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const FAKE_FIELDS = {
|
||||
id: { string: "Id", type: "integer" },
|
||||
user_id: { string: "User", type: "many2one", relation: "user", default: -1 },
|
||||
partner_id: {
|
||||
string: "Partner",
|
||||
type: "many2one",
|
||||
relation: "partner",
|
||||
related: "user_id.partner_id",
|
||||
default: 1,
|
||||
},
|
||||
name: { string: "Name", type: "char" },
|
||||
start_date: { string: "Start Date", type: "date" },
|
||||
stop_date: { string: "Stop Date", type: "date" },
|
||||
start: { string: "Start Datetime", type: "datetime" },
|
||||
stop: { string: "Stop Datetime", type: "datetime" },
|
||||
delay: { string: "Delay", type: "float" },
|
||||
allday: { string: "Is All Day", type: "boolean" },
|
||||
partner_ids: {
|
||||
string: "Attendees",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
default: [[6, 0, [1]]],
|
||||
},
|
||||
type: { string: "Type", type: "integer" },
|
||||
event_type_id: { string: "Event Type", type: "many2one", relation: "event_type" },
|
||||
color: { string: "Color", type: "integer", related: "event_type_id.color" },
|
||||
};
|
||||
|
||||
function makeFakeModelState() {
|
||||
return {
|
||||
canCreate: true,
|
||||
canDelete: true,
|
||||
canEdit: true,
|
||||
date: makeFakeDate(),
|
||||
fieldMapping: {
|
||||
date_start: "start_date",
|
||||
date_stop: "stop_date",
|
||||
date_delay: "delay",
|
||||
all_day: "allday",
|
||||
color: "color",
|
||||
},
|
||||
fieldNames: ["start_date", "stop_date", "color", "delay", "allday", "user_id"],
|
||||
fields: FAKE_FIELDS,
|
||||
filterSections: FAKE_FILTER_SECTIONS,
|
||||
firstDayOfWeek: 0,
|
||||
isDateHidden: false,
|
||||
isTimeHidden: false,
|
||||
hasAllDaySlot: true,
|
||||
hasEditDialog: false,
|
||||
hasQuickCreate: false,
|
||||
popoverFields: {
|
||||
name: { rawAttrs: {}, options: {} },
|
||||
},
|
||||
rangeEnd: makeFakeDate().endOf("month"),
|
||||
rangeStart: makeFakeDate().startOf("month"),
|
||||
records: makeFakeRecords(),
|
||||
resModel: "event",
|
||||
scale: "month",
|
||||
scales: ["day", "week", "month", "year"],
|
||||
unusualDays: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function makeFakeModel(state = {}) {
|
||||
return {
|
||||
...makeFakeModelState(),
|
||||
load() {},
|
||||
createFilter() {},
|
||||
createRecord() {},
|
||||
unlinkFilter() {},
|
||||
unlinkRecord() {},
|
||||
updateFilter() {},
|
||||
updateRecord() {},
|
||||
...state,
|
||||
};
|
||||
}
|
||||
|
||||
// DOM Utils
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
async function scrollTo(el, scrollParam) {
|
||||
el.scrollIntoView(scrollParam);
|
||||
await new Promise(window.requestAnimationFrame);
|
||||
}
|
||||
|
||||
export function findPickedDate(target) {
|
||||
return target.querySelector(".ui-datepicker-current-day");
|
||||
}
|
||||
|
||||
export async function pickDate(target, date) {
|
||||
const [year, month, day] = date.split("-");
|
||||
const iMonth = parseInt(month, 10) - 1;
|
||||
const iDay = parseInt(day, 10) - 1;
|
||||
const el = target.querySelectorAll(
|
||||
`.ui-datepicker-calendar td[data-year="${year}"][data-month="${iMonth}"]`
|
||||
)[iDay];
|
||||
el.scrollIntoView();
|
||||
await click(el);
|
||||
}
|
||||
|
||||
function findAllDaySlot(target, date) {
|
||||
return target.querySelector(`.fc-day-grid .fc-day[data-date="${date}"]`);
|
||||
}
|
||||
|
||||
export function findDateCell(target, date) {
|
||||
return target.querySelector(`.fc-day-top[data-date="${date}"]`);
|
||||
}
|
||||
|
||||
export function findEvent(target, eventId) {
|
||||
return target.querySelector(`.o_event[data-event-id="${eventId}"]`);
|
||||
}
|
||||
|
||||
function findDateCol(target, date) {
|
||||
return target.querySelector(`.fc-day-header[data-date="${date}"]`);
|
||||
}
|
||||
|
||||
export function findTimeRow(target, time) {
|
||||
return target.querySelector(`.fc-slats [data-time="${time}"] .fc-widget-content`);
|
||||
}
|
||||
|
||||
async function triggerEventForCalendar(el, type, position = {}) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = position.x || rect.x + rect.width / 2;
|
||||
const y = position.y || rect.y + rect.height / 2;
|
||||
const attrs = {
|
||||
which: 1,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
};
|
||||
await triggerEvent(el, null, type, attrs);
|
||||
}
|
||||
|
||||
export async function clickAllDaySlot(target, date) {
|
||||
const el = findAllDaySlot(target, date);
|
||||
await scrollTo(el);
|
||||
await triggerEventForCalendar(el, "mousedown");
|
||||
await triggerEventForCalendar(el, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function clickDate(target, date) {
|
||||
const el = findDateCell(target, date);
|
||||
await scrollTo(el);
|
||||
await triggerEventForCalendar(el, "mousedown");
|
||||
await triggerEventForCalendar(el, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function clickEvent(target, eventId) {
|
||||
const el = findEvent(target, eventId);
|
||||
await scrollTo(el);
|
||||
await click(el);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function selectTimeRange(target, startDateTime, endDateTime) {
|
||||
const [startDate, startTime] = startDateTime.split(" ");
|
||||
const [endDate, endTime] = endDateTime.split(" ");
|
||||
|
||||
const startCol = findDateCol(target, startDate);
|
||||
const endCol = findDateCol(target, endDate);
|
||||
const startRow = findTimeRow(target, startTime);
|
||||
const endRow = findTimeRow(target, endTime);
|
||||
|
||||
await scrollTo(startRow);
|
||||
const startColRect = startCol.getBoundingClientRect();
|
||||
const startRowRect = startRow.getBoundingClientRect();
|
||||
|
||||
await triggerEventForCalendar(startRow, "mousedown", {
|
||||
x: startColRect.x + startColRect.width / 2,
|
||||
y: startRowRect.y + 1,
|
||||
});
|
||||
|
||||
await scrollTo(endRow, false);
|
||||
const endColRect = endCol.getBoundingClientRect();
|
||||
const endRowRect = endRow.getBoundingClientRect();
|
||||
|
||||
await triggerEventForCalendar(endRow, "mousemove", {
|
||||
x: endColRect.x + endColRect.width / 2,
|
||||
y: endRowRect.y - 1,
|
||||
});
|
||||
await triggerEventForCalendar(endRow, "mouseup", {
|
||||
x: endColRect.x + endColRect.width / 2,
|
||||
y: endRowRect.y - 1,
|
||||
});
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function selectDateRange(target, startDate, endDate) {
|
||||
const start = findDateCell(target, startDate);
|
||||
const end = findDateCell(target, endDate);
|
||||
await scrollTo(start);
|
||||
await triggerEventForCalendar(start, "mousedown");
|
||||
await scrollTo(end);
|
||||
await triggerEventForCalendar(end, "mousemove");
|
||||
await triggerEventForCalendar(end, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function selectAllDayRange(target, startDate, endDate) {
|
||||
const start = findAllDaySlot(target, startDate);
|
||||
const end = findAllDaySlot(target, endDate);
|
||||
await scrollTo(start);
|
||||
await triggerEventForCalendar(start, "mousedown");
|
||||
await scrollTo(end);
|
||||
await triggerEventForCalendar(end, "mousemove");
|
||||
await triggerEventForCalendar(end, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function moveEventToDate(target, eventId, date, options = {}) {
|
||||
const event = findEvent(target, eventId);
|
||||
const cell = findDateCell(target, date);
|
||||
|
||||
await scrollTo(event);
|
||||
await triggerEventForCalendar(event, "mousedown");
|
||||
|
||||
await scrollTo(cell);
|
||||
await triggerEventForCalendar(cell, "mousemove");
|
||||
|
||||
if (!options.disableDrop) {
|
||||
await triggerEventForCalendar(cell, "mouseup");
|
||||
}
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function moveEventToTime(target, eventId, dateTime) {
|
||||
const event = findEvent(target, eventId);
|
||||
const [date, time] = dateTime.split(" ");
|
||||
|
||||
const col = findDateCol(target, date);
|
||||
const row = findTimeRow(target, time);
|
||||
|
||||
// Find event position
|
||||
await scrollTo(event);
|
||||
const eventRect = event.getBoundingClientRect();
|
||||
const eventPos = {
|
||||
x: eventRect.x + eventRect.width / 2,
|
||||
y: eventRect.y,
|
||||
};
|
||||
|
||||
await triggerEventForCalendar(event, "mousedown", eventPos);
|
||||
|
||||
// Find target position
|
||||
await scrollTo(row, false);
|
||||
const colRect = col.getBoundingClientRect();
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const toPos = {
|
||||
x: colRect.x + colRect.width / 2,
|
||||
y: rowRect.y - 1,
|
||||
};
|
||||
|
||||
await triggerEventForCalendar(row, "mousemove", toPos);
|
||||
await triggerEventForCalendar(row, "mouseup", toPos);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function moveEventToAllDaySlot(target, eventId, date) {
|
||||
const event = findEvent(target, eventId);
|
||||
const slot = findAllDaySlot(target, date);
|
||||
|
||||
// Find event position
|
||||
await scrollTo(event);
|
||||
const eventRect = event.getBoundingClientRect();
|
||||
const eventPos = {
|
||||
x: eventRect.x + eventRect.width / 2,
|
||||
y: eventRect.y,
|
||||
};
|
||||
await triggerEventForCalendar(event, "mousedown", eventPos);
|
||||
|
||||
// Find target position
|
||||
await scrollTo(slot);
|
||||
const slotRect = slot.getBoundingClientRect();
|
||||
const toPos = {
|
||||
x: slotRect.x + slotRect.width / 2,
|
||||
y: slotRect.y - 1,
|
||||
};
|
||||
await triggerEventForCalendar(slot, "mousemove", toPos);
|
||||
await triggerEventForCalendar(slot, "mouseup", toPos);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function resizeEventToTime(target, eventId, dateTime) {
|
||||
const event = findEvent(target, eventId);
|
||||
const [date, time] = dateTime.split(" ");
|
||||
|
||||
const col = findDateCol(target, date);
|
||||
const row = findTimeRow(target, time);
|
||||
|
||||
// Find event position
|
||||
await scrollTo(event);
|
||||
await triggerEventForCalendar(event, "mouseenter");
|
||||
|
||||
// Find event resizer
|
||||
const resizer = event.querySelector(".fc-end-resizer");
|
||||
resizer.style.display = "block";
|
||||
resizer.style.width = "100%";
|
||||
resizer.style.height = "1em";
|
||||
resizer.style.bottom = "0";
|
||||
const resizerRect = resizer.getBoundingClientRect();
|
||||
const resizerPos = {
|
||||
x: resizerRect.x + resizerRect.width / 2,
|
||||
y: resizerRect.y + resizerRect.height / 2,
|
||||
};
|
||||
await triggerEventForCalendar(resizer, "mousedown", resizerPos);
|
||||
|
||||
// Find target position
|
||||
await scrollTo(row, false);
|
||||
const colRect = col.getBoundingClientRect();
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const toPos = {
|
||||
x: colRect.x + colRect.width / 2,
|
||||
y: rowRect.y - 1,
|
||||
};
|
||||
|
||||
await triggerEventForCalendar(row, "mousemove", toPos);
|
||||
await triggerEventForCalendar(row, "mouseup", toPos);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function changeScale(target, scale) {
|
||||
await click(target, `.o_calendar_scale_buttons .scale_button_selection`);
|
||||
await click(target, `.o_calendar_scale_buttons .o_calendar_button_${scale}`);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function navigate(target, direction) {
|
||||
await click(target, `.o_calendar_navigation_buttons .o_calendar_button_${direction}`);
|
||||
}
|
||||
|
||||
export function findFilterPanelSection(target, sectionName) {
|
||||
return target.querySelector(`.o_calendar_filter[data-name="${sectionName}"]`);
|
||||
}
|
||||
|
||||
export function findFilterPanelFilter(target, sectionName, filterValue) {
|
||||
return findFilterPanelSection(target, sectionName).querySelector(
|
||||
`.o_calendar_filter_item[data-value="${filterValue}"]`
|
||||
);
|
||||
}
|
||||
|
||||
export function findFilterPanelSectionFilter(target, sectionName) {
|
||||
return findFilterPanelSection(target, sectionName).querySelector(
|
||||
`.o_calendar_filter_items_checkall`
|
||||
);
|
||||
}
|
||||
|
||||
export async function toggleFilter(target, sectionName, filterValue) {
|
||||
const el = findFilterPanelFilter(target, sectionName, filterValue).querySelector(`input`);
|
||||
await scrollTo(el);
|
||||
await click(el);
|
||||
}
|
||||
|
||||
export async function toggleSectionFilter(target, sectionName) {
|
||||
const el = findFilterPanelSectionFilter(target, sectionName).querySelector(`input`);
|
||||
await scrollTo(el);
|
||||
await click(el);
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { getFixture, triggerEvents } from "@web/../tests/helpers/utils";
|
||||
import { pagerNext } from "@web/../tests/search/helpers";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { fakeCookieService } from "@web/../tests/helpers/mock_services";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "text",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, foo: `yop` },
|
||||
{ id: 2, foo: `blip` },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
registry.category("services").add("cookie", fakeCookieService);
|
||||
});
|
||||
|
||||
QUnit.module("AceEditorField");
|
||||
|
||||
QUnit.test("AceEditorField on text fields works", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" widget="ace" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.ok("ace" in window, "the ace library should be loaded");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.ace_content",
|
||||
"should have rendered something with ace editor"
|
||||
);
|
||||
|
||||
assert.ok(target.querySelector(".o_field_ace").textContent.includes("yop"));
|
||||
});
|
||||
|
||||
QUnit.test("AceEditorField doesn't crash when editing", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="display_name" />
|
||||
<field name="foo" widget="ace" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await triggerEvents(target, ".ace-view-editor textarea", ["focus", "click"]);
|
||||
assert.hasClass(target.querySelector(".ace-view-editor"), "ace_focus");
|
||||
});
|
||||
|
||||
QUnit.test("AceEditorField is updated on value change", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="ace" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.ok(target.querySelector(".o_field_ace").textContent.includes("yop"));
|
||||
|
||||
await pagerNext(target);
|
||||
|
||||
assert.ok(target.querySelector(".o_field_ace").textContent.includes("blip"));
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"leaving an untouched record with an unset ace field should not write",
|
||||
async (assert) => {
|
||||
serverData.models.partner.records.forEach((rec) => {
|
||||
rec.foo = false;
|
||||
});
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="ace" />
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method) {
|
||||
assert.step(`${args.method}: ${JSON.stringify(args.args)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(["get_views: []", 'read: [[1],["foo","display_name"]]']);
|
||||
await pagerNext(target);
|
||||
assert.verifySteps(['read: [[2],["foo","display_name"]]']);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: {
|
||||
string: "Char Field",
|
||||
type: "char",
|
||||
default: "Default char value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
many2one_field: {
|
||||
string: "Many2one Field",
|
||||
type: "many2one",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
},
|
||||
selection_field: {
|
||||
string: "Selection",
|
||||
type: "selection",
|
||||
searchable: true,
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("BadgeField");
|
||||
|
||||
QUnit.test("BadgeField component on a char field in list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<list><field name="display_name" widget="badge"/></list>',
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.o_field_badge[name="display_name"]:contains(first record)');
|
||||
assert.containsOnce(target, '.o_field_badge[name="display_name"]:contains(second record)');
|
||||
assert.containsOnce(target, '.o_field_badge[name="display_name"]:contains(fourth record)');
|
||||
});
|
||||
|
||||
QUnit.test("BadgeField component on a selection field in list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<list><field name="selection_field" widget="badge"/></list>',
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.o_field_badge[name="selection_field"]:contains(Blocked)');
|
||||
assert.containsOnce(target, '.o_field_badge[name="selection_field"]:contains(Normal)');
|
||||
assert.containsN(target, '.o_field_badge[name="selection_field"]:contains(Done)', 2);
|
||||
});
|
||||
|
||||
QUnit.test("BadgeField component on a many2one field in list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<list><field name="many2one_field" widget="badge"/></list>',
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.o_field_badge[name="many2one_field"]:contains(first record)');
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_field_badge[name="many2one_field"]:contains(fourth record)'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("BadgeField component with decoration-xxx attributes", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
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>`,
|
||||
});
|
||||
|
||||
assert.containsN(target, '.o_field_badge[name="display_name"]', 4);
|
||||
assert.containsOnce(target, '.o_field_badge[name="display_name"] .text-bg-danger');
|
||||
assert.containsOnce(target, '.o_field_badge[name="display_name"] .text-bg-warning');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
product_id: { string: "Product", type: "many2one", relation: "product" },
|
||||
color: {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
string: "Color",
|
||||
},
|
||||
reference: {
|
||||
string: "Reference Field",
|
||||
type: "reference",
|
||||
selection: [
|
||||
["product", "Product"],
|
||||
["partner_type", "Partner Type"],
|
||||
["partner", "Partner"],
|
||||
],
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
reference: "product,37",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
product_id: 37,
|
||||
},
|
||||
],
|
||||
},
|
||||
product: {
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BadgeSelectionField");
|
||||
|
||||
QUnit.test("BadgeSelectionField widget on a many2one in a new record", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="product_id" widget="selection_badge"/></form>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_selection_badge",
|
||||
"should have rendered outer div"
|
||||
);
|
||||
assert.containsN(target, "span.o_selection_badge", 2, "should have 2 possible choices");
|
||||
assert.strictEqual(
|
||||
target.querySelector("span.o_selection_badge").textContent,
|
||||
"xphone",
|
||||
"one of them should be xphone"
|
||||
);
|
||||
assert.containsNone(target, "span.active", "none of the input should be checked");
|
||||
|
||||
await click(target.querySelector("span.o_selection_badge"));
|
||||
|
||||
assert.containsOnce(target, "span.active", "one of the input should be checked");
|
||||
|
||||
await click(target, ".o_form_button_save");
|
||||
|
||||
var newRecord = _.last(serverData.models.partner.records);
|
||||
assert.strictEqual(newRecord.product_id, 37, "should have saved record with correct value");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"BadgeSelectionField widget on a selection in a new record",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="color" widget="selection_badge"/></form>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_selection_badge",
|
||||
"should have rendered outer div"
|
||||
);
|
||||
assert.containsN(target, "span.o_selection_badge", 2, "should have 2 possible choices");
|
||||
assert.strictEqual(
|
||||
target.querySelector("span.o_selection_badge").textContent,
|
||||
"Red",
|
||||
"one of them should be Red"
|
||||
);
|
||||
|
||||
// click on 2nd option
|
||||
await click(target.querySelector("span.o_selection_badge:last-child"));
|
||||
|
||||
await click(target.querySelector(".o_form_button_save"));
|
||||
|
||||
var newRecord = _.last(serverData.models.partner.records);
|
||||
assert.strictEqual(
|
||||
newRecord.color,
|
||||
"black",
|
||||
"should have saved record with correct value"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"BadgeSelectionField widget on a selection in a readonly mode",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="color" widget="selection_badge" readonly="1"/></form>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_readonly_modifier span",
|
||||
"should have 1 possible value in readonly mode"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"BadgeSelectionField widget on a selection unchecking selected value",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="color" widget="selection_badge"/></form>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_selection_badge",
|
||||
"should have rendered outer div"
|
||||
);
|
||||
assert.containsN(target, "span.o_selection_badge", 2, "should have 2 possible choices");
|
||||
assert.strictEqual(
|
||||
target.querySelector("span.o_selection_badge").textContent,
|
||||
"Red",
|
||||
"one of them should be Red"
|
||||
);
|
||||
|
||||
// click again on red option
|
||||
await click(target.querySelector("span.o_selection_badge.active"));
|
||||
|
||||
await click(target.querySelector(".o_form_button_save"));
|
||||
|
||||
var newRecord = _.last(serverData.models.partner.records);
|
||||
assert.strictEqual(
|
||||
newRecord.color,
|
||||
false,
|
||||
"the new value should be false as we have selected same value as default"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,581 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { makeMockXHR } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { RPCError } from "@web/core/network/rpc_service";
|
||||
import { MAX_FILENAME_SIZE_BYTES } from "@web/views/fields/binary/binary_field";
|
||||
import { toBase64Length } from "@web/core/utils/binary";
|
||||
|
||||
const BINARY_FILE =
|
||||
"R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
trim: true,
|
||||
},
|
||||
document: { string: "Binary", type: "binary" },
|
||||
product_id: {
|
||||
string: "Product",
|
||||
type: "many2one",
|
||||
relation: "product",
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
foo: "coucou.txt",
|
||||
document: "coucou==\n",
|
||||
},
|
||||
],
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BinaryField");
|
||||
|
||||
QUnit.test("BinaryField is correctly rendered (readonly)", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
async function send(data) {
|
||||
assert.ok(data instanceof FormData);
|
||||
assert.strictEqual(
|
||||
data.get("field"),
|
||||
"document",
|
||||
"we should download the field document"
|
||||
);
|
||||
assert.strictEqual(
|
||||
data.get("data"),
|
||||
"coucou==\n",
|
||||
"we should download the correct data"
|
||||
);
|
||||
|
||||
this.status = 200;
|
||||
this.response = new Blob([data.get("data")], { type: "text/plain" });
|
||||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form edit="0">
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_field_widget[name="document"] a > .fa-download',
|
||||
"the binary field should be rendered as a downloadable link in readonly"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="document"]').textContent,
|
||||
"coucou.txt",
|
||||
"the binary field should display the name of the file in the link"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_char").textContent,
|
||||
"coucou.txt",
|
||||
"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 prom = makeDeferred();
|
||||
const downloadOnClick = (ev) => {
|
||||
const target = ev.target;
|
||||
if (target.tagName === "A" && "download" in target.attributes) {
|
||||
ev.preventDefault();
|
||||
document.removeEventListener("click", downloadOnClick);
|
||||
prom.resolve();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", downloadOnClick);
|
||||
registerCleanup(() => document.removeEventListener("click", downloadOnClick));
|
||||
await click(target.querySelector('.o_field_widget[name="document"] a'));
|
||||
await prom;
|
||||
});
|
||||
|
||||
QUnit.test("BinaryField is correctly rendered", async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
async function send(data) {
|
||||
assert.ok(data instanceof FormData);
|
||||
assert.strictEqual(
|
||||
data.get("field"),
|
||||
"document",
|
||||
"we should download the field document"
|
||||
);
|
||||
assert.strictEqual(
|
||||
data.get("data"),
|
||||
"coucou==\n",
|
||||
"we should download the correct data"
|
||||
);
|
||||
|
||||
this.status = 200;
|
||||
this.response = new Blob([data.get("data")], { type: "text/plain" });
|
||||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_widget[name="document"] a > .fa-download',
|
||||
"the binary field should not be rendered as a downloadable link in edit"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="document"].o_field_binary .o_input').value,
|
||||
"coucou.txt",
|
||||
"the binary field should display the file name in the input edit mode"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_binary .o_clear_file_button",
|
||||
"there shoud be a button to clear the file"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_char input").value,
|
||||
"coucou.txt",
|
||||
"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 prom = makeDeferred();
|
||||
const downloadOnClick = (ev) => {
|
||||
const target = ev.target;
|
||||
if (target.tagName === "A" && "download" in target.attributes) {
|
||||
ev.preventDefault();
|
||||
document.removeEventListener("click", downloadOnClick);
|
||||
prom.resolve();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", downloadOnClick);
|
||||
registerCleanup(() => document.removeEventListener("click", downloadOnClick));
|
||||
await click(target.querySelector(".fa-download"));
|
||||
await prom;
|
||||
|
||||
await click(target.querySelector(".o_field_binary .o_clear_file_button"));
|
||||
|
||||
assert.isNotVisible(
|
||||
target.querySelector(".o_field_binary input"),
|
||||
"the input should be hidden"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_binary .o_select_file_button",
|
||||
"there should be a button to upload the file"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_char input").value,
|
||||
"",
|
||||
"the filename field should be empty since we removed the file"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_widget[name="document"] a > .fa-download',
|
||||
"the binary field should not render as a downloadable link since we removed the file"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
"o_field_widget span",
|
||||
"the binary field should not display a filename in the link since we removed the file"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("BinaryField is correctly rendered (isDirty)", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
// Simulate a file upload
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await editInput(target, ".o_field_binary .o_input_file", file);
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_widget[name="document"] .fa-download',
|
||||
"the binary field should not be rendered as a downloadable since the record is dirty"
|
||||
);
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_field_widget[name="document"] .fa-download',
|
||||
"the binary field should render as a downloadable link since the record is not dirty"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("file name field is not defined", async (assert) => {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary").textContent,
|
||||
"",
|
||||
"there should be no text since the name field is not in the view"
|
||||
);
|
||||
assert.isVisible(
|
||||
target,
|
||||
".o_field_binary .o_form_uri fa-download",
|
||||
"download icon should be visible"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"binary fields input value is empty when clearing after uploading",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await editInput(target, ".o_field_binary .o_input_file", file);
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_binary input[type=text]").hasAttribute("readonly")
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary input[type=text]").value,
|
||||
"fake_file.txt",
|
||||
'displayed value should be changed to "fake_file.txt"'
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_char input[type=text]").value,
|
||||
"fake_file.txt",
|
||||
'related value should be changed to "fake_file.txt"'
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_clear_file_button"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary .o_input_file").value,
|
||||
"",
|
||||
"file input value should be empty"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_char input").value,
|
||||
"",
|
||||
"related value should be empty"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("BinaryField: option accepted_file_extensions", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="binary" options="{'accepted_file_extensions': '.dat,.bin'}"/>
|
||||
</form>`,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_input_file").getAttribute("accept"),
|
||||
".dat,.bin",
|
||||
"the input should have the correct ``accept`` attribute"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"BinaryField that is readonly in create mode does not download",
|
||||
async function (assert) {
|
||||
async function download() {
|
||||
assert.step("We shouldn't be getting the file.");
|
||||
}
|
||||
const MockXHR = makeMockXHR("", download);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
product_id: function (obj) {
|
||||
obj.document = "onchange==\n";
|
||||
},
|
||||
};
|
||||
|
||||
serverData.models.partner.fields.document.readonly = true;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id"/>
|
||||
<field name="document" filename="yooo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(target, ".o_form_button_create");
|
||||
await click(target, ".o_field_many2one[name='product_id'] input");
|
||||
await click(
|
||||
target.querySelector(".o_field_many2one[name='product_id'] .dropdown-item")
|
||||
);
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_widget[name="document"] a',
|
||||
"The link to download the binary should not be present"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_widget[name="document"] a > .fa-download',
|
||||
"The download icon should not be present"
|
||||
);
|
||||
|
||||
assert.verifySteps([], "We shouldn't have passed through steps");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Binary field in list view", async function (assert) {
|
||||
serverData.models.partner.records[0].document = BINARY_FILE;
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="document" filename="yooo"/>
|
||||
</tree>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row .o_data_cell").textContent,
|
||||
"93.43 Bytes"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Binary field for new record has no download button", async function (assert) {
|
||||
serverData.models.partner.fields.document.default = BINARY_FILE;
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
assert.containsNone(target, "button.fa-download");
|
||||
});
|
||||
|
||||
QUnit.test("Binary filename doesn't exceed 255 bytes", async function (assert) {
|
||||
const LARGE_BINARY_FILE = BINARY_FILE.repeat(5);
|
||||
assert.ok((LARGE_BINARY_FILE.length / 4 * 3) > MAX_FILENAME_SIZE_BYTES,
|
||||
"The initial binary file should be larger than max bytes that can represent the filename");
|
||||
serverData.models.partner.fields.document.default = LARGE_BINARY_FILE;
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary input[type=text]").value.length,
|
||||
toBase64Length(MAX_FILENAME_SIZE_BYTES),
|
||||
"The filename shouldn't exceed the maximum size in bytes in base64"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("BinaryField filename is updated when using the pager", async function (assert) {
|
||||
serverData.models.partner.records.push(
|
||||
{
|
||||
id: 1,
|
||||
document: "abc",
|
||||
foo: "abc.txt",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
document: "def",
|
||||
foo: "def.txt",
|
||||
}
|
||||
);
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary input[type=text]").value,
|
||||
"abc.txt",
|
||||
'displayed value should be "abc.txt"'
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_pager_next"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary input[type=text]").value,
|
||||
"def.txt",
|
||||
'displayed value should be changed to "def.txt"'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('isUploading state should be set to false after upload', async function(assert) {
|
||||
assert.expect(1);
|
||||
serverData.models.partner.onchanges = {
|
||||
document: function (obj) {
|
||||
if (obj.document) {
|
||||
const error = new RPCError();
|
||||
error.exceptionName = "odoo.exceptions.ValidationError";
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document"/>
|
||||
</form>`,
|
||||
});
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await editInput(target, ".o_field_binary .o_input_file", file);
|
||||
assert.equal(
|
||||
target.querySelector(".o_select_file_button").innerText,
|
||||
"UPLOAD YOUR FILE",
|
||||
"displayed value should be upload your file"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("doesn't crash if value is not a string", async (assert) => {
|
||||
serverData.models.partner.records = [{
|
||||
id: 1,
|
||||
document: {},
|
||||
}]
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document"/>
|
||||
</form>`,
|
||||
});
|
||||
assert.equal(
|
||||
target.querySelector(".o_field_binary input").value,
|
||||
""
|
||||
);
|
||||
})
|
||||
});
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
bar: { string: "Bar", type: "boolean", default: true, searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, bar: true },
|
||||
{ id: 2, bar: true },
|
||||
{ id: 4, bar: true },
|
||||
{ id: 3, bar: true },
|
||||
{ id: 5, bar: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BooleanFavoriteField");
|
||||
|
||||
QUnit.test("FavoriteField in kanban view", async function (assert) {
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="bar" widget="boolean_favorite" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "=", 1]],
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_kanban_record .o_field_widget .o_favorite > a").textContent,
|
||||
" Remove from Favorites",
|
||||
'the label should say "Remove from Favorites"'
|
||||
);
|
||||
|
||||
// click on favorite
|
||||
await click(target, ".o_field_widget .o_favorite");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should not be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_kanban_record .o_field_widget .o_favorite > a").textContent,
|
||||
" Add to Favorites",
|
||||
'the label should say "Add to Favorites"'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("FavoriteField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="bar" widget="boolean_favorite" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .o_favorite > a").textContent,
|
||||
" Remove from Favorites",
|
||||
'the label should say "Remove from Favorites"'
|
||||
);
|
||||
|
||||
// click on favorite
|
||||
await click(target, ".o_field_widget .o_favorite");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should not be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .o_favorite > a").textContent,
|
||||
" Add to Favorites",
|
||||
'the label should say "Add to Favorites"'
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_favorite > a i.fa.fa-star-o",
|
||||
"should not be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .o_favorite > a").textContent,
|
||||
" Add to Favorites",
|
||||
'the label should say "Add to Favorites"'
|
||||
);
|
||||
|
||||
// click on favorite
|
||||
await click(target, ".o_field_widget .o_favorite");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .o_favorite > a").textContent,
|
||||
" Remove from Favorites",
|
||||
'the label should say "Remove from Favorites"'
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .o_favorite > a").textContent,
|
||||
" Remove from Favorites",
|
||||
'the label should say "Remove from Favorites"'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("FavoriteField in editable list view without label", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="bar" widget="boolean_favorite" nolabel="1" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
|
||||
// switch to edit mode
|
||||
await click(target.querySelector("tbody td:not(.o_list_record_selector)"));
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
|
||||
// click on favorite
|
||||
await click(target.querySelector(".o_data_row .o_field_widget .o_favorite"));
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should not be favorite"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star-o",
|
||||
"should not be favorite"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,276 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
getFixture,
|
||||
nextTick,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
bar: { string: "Bar", type: "boolean", default: true, searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, bar: true },
|
||||
{ id: 2, bar: true },
|
||||
{ id: 3, bar: true },
|
||||
{ id: 4, bar: true },
|
||||
{ id: 5, bar: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BooleanField");
|
||||
|
||||
QUnit.test("boolean field in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<label for="bar" string="Awesome checkbox" />
|
||||
<field name="bar" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be checked"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:disabled",
|
||||
"checkbox should not be disabled"
|
||||
);
|
||||
|
||||
// uncheck the checkbox
|
||||
await click(target, ".o_field_boolean input:checked");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should no longer be checked"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still no longer be checked"
|
||||
);
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be unchecked"
|
||||
);
|
||||
|
||||
// check the checkbox
|
||||
await click(target, ".o_field_boolean input");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should now be checked"
|
||||
);
|
||||
|
||||
// uncheck it back
|
||||
await click(target, ".o_field_boolean input");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should now be unchecked"
|
||||
);
|
||||
|
||||
// check the checkbox by clicking on label
|
||||
await click(target, ".o_form_view label:not(.form-check-label)");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should now be checked"
|
||||
);
|
||||
|
||||
// uncheck it back
|
||||
await click(target, ".o_form_view label:not(.form-check-label)");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should now be unchecked"
|
||||
);
|
||||
|
||||
// check the checkbox by hitting the "enter" key after focusing it
|
||||
await triggerEvents(target, ".o_field_boolean input", [
|
||||
["focusin"],
|
||||
["keydown", { key: "Enter" }],
|
||||
["keyup", { key: "Enter" }],
|
||||
]);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should now be checked"
|
||||
);
|
||||
|
||||
// blindly press enter again, it should uncheck the checkbox
|
||||
await triggerEvent(document.activeElement, null, "keydown", { key: "Enter" });
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should not be checked"
|
||||
);
|
||||
await nextTick();
|
||||
// blindly press enter again, it should check the checkbox back
|
||||
await triggerEvent(document.activeElement, null, "keydown", { key: "Enter" });
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be checked"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be checked"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("boolean field in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="bar" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox input",
|
||||
5,
|
||||
"should have 5 checkboxes"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox input:checked",
|
||||
4,
|
||||
"should have 4 checked input"
|
||||
);
|
||||
|
||||
// Edit a line
|
||||
let cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
assert.ok(
|
||||
cell.querySelector(".o-checkbox input:checked").disabled,
|
||||
"input should be disabled in readonly mode"
|
||||
);
|
||||
await click(cell, ".o-checkbox");
|
||||
assert.hasClass(
|
||||
document.querySelector("tr.o_data_row:nth-child(1)"),
|
||||
"o_selected_row",
|
||||
"the row is now selected, in edition"
|
||||
);
|
||||
assert.ok(
|
||||
!cell.querySelector(".o-checkbox input:checked").disabled,
|
||||
"input should now be enabled"
|
||||
);
|
||||
await click(cell);
|
||||
assert.notOk(
|
||||
cell.querySelector(".o-checkbox input:checked").disabled,
|
||||
"input should not have the disabled property in edit mode"
|
||||
);
|
||||
await click(cell, ".o-checkbox");
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
assert.ok(
|
||||
cell.querySelector(".o-checkbox input:not(:checked)").disabled,
|
||||
"input should be disabled again"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox",
|
||||
5,
|
||||
"should still have 5 checkboxes"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox input:checked",
|
||||
3,
|
||||
"should now have only 3 checked input"
|
||||
);
|
||||
|
||||
// Re-Edit the line and fake-check the checkbox
|
||||
await click(cell);
|
||||
await click(cell, ".o-checkbox");
|
||||
await click(cell, ".o-checkbox");
|
||||
|
||||
// Save
|
||||
await clickSave(target);
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox",
|
||||
5,
|
||||
"should still have 5 checkboxes"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox input:checked",
|
||||
3,
|
||||
"should still have only 3 checked input"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("readonly boolean field", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<form><field name="bar" readonly="1"/></form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be checked"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:disabled",
|
||||
"checkbox should still be disabled"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_boolean .o-checkbox");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be checked"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:disabled",
|
||||
"checkbox should still be disabled"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
bar: { string: "Bar", type: "boolean", default: true, searchable: true },
|
||||
},
|
||||
records: [{ id: 1, bar: false }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BooleanToggleField");
|
||||
|
||||
QUnit.test("use BooleanToggleField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle",
|
||||
"Boolean toggle widget applied to boolean field"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle input:checked",
|
||||
"Boolean toggle should be checked"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle input:not(:checked)",
|
||||
"Boolean toggle shouldn't be checked"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("readonly BooleanToggleField is disabled in edit mode", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" readonly="1" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_form_editable");
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
});
|
||||
|
||||
QUnit.test("BooleanToggleField is not disabled in readonly mode", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="bar" widget="boolean_toggle"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_form_editable");
|
||||
assert.containsOnce(target, ".form-check.o_boolean_toggle");
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").checked);
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").checked);
|
||||
});
|
||||
|
||||
QUnit.test("BooleanToggleField is disabled with a readonly attribute", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="bar" widget="boolean_toggle" readonly="1"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".form-check.o_boolean_toggle");
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
});
|
||||
|
||||
QUnit.test("BooleanToggleField is enabled in edit mode", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="bar" widget="boolean_toggle"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".form-check.o_boolean_toggle");
|
||||
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").checked);
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").checked);
|
||||
});
|
||||
|
||||
QUnit.test("boolean toggle widget is not disabled in readonly mode", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle",
|
||||
"Boolean toggle widget applied to boolean field"
|
||||
);
|
||||
assert.containsNone(target, ".o_boolean_toggle input:checked");
|
||||
|
||||
await click(target, ".o_boolean_toggle");
|
||||
assert.containsOnce(target, ".o_boolean_toggle input:checked");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"boolean toggle widget is disabled with a readonly attribute",
|
||||
async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" readonly="1" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle",
|
||||
"Boolean toggle widget applied to boolean field"
|
||||
);
|
||||
assert.containsNone(target, ".o_boolean_toggle input:checked");
|
||||
|
||||
await click(target, ".o_boolean_toggle");
|
||||
assert.containsNone(target, ".o_boolean_toggle input:checked");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("boolean toggle widget is enabled in edit mode", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle",
|
||||
"Boolean toggle widget applied to boolean field"
|
||||
);
|
||||
assert.containsNone(target, ".o_boolean_toggle input:checked");
|
||||
|
||||
await click(target, ".o_boolean_toggle");
|
||||
assert.containsOnce(target, ".o_boolean_toggle input:checked");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"BooleanToggleField is disabled if readonly in editable list",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.bar.readonly = true;
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</tree>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_boolean_toggle input:disabled",
|
||||
"field should be readonly"
|
||||
);
|
||||
assert.containsOnce(target, ".o_boolean_toggle input:not(:checked)");
|
||||
|
||||
await click(target, ".o_boolean_toggle");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_boolean_toggle input:disabled",
|
||||
"field should still be readonly"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_boolean_toggle input:not(:checked)",
|
||||
"should keep unchecked on cell click"
|
||||
);
|
||||
|
||||
await click(target, ".o_boolean_toggle");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_boolean_toggle input:not(:checked)",
|
||||
"should keep unchecked on click"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("BooleanToggleField - auto save record when field toggled", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(_route, { method }) {
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
}
|
||||
},
|
||||
});
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.verifySteps(["write"]);
|
||||
});
|
||||
|
||||
QUnit.test("BooleanToggleField - autosave option set to false", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" options="{'autosave': false}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(_route, { method }) {
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
}
|
||||
},
|
||||
});
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,182 @@
|
|||
/** @odoo-module **/
|
||||
import { click, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
hex_color: { string: "hexadecimal color", type: "char" },
|
||||
foo: { type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
hex_color: "#ff4444",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("ColorField");
|
||||
|
||||
QUnit.test("field contains a color input", async function (assert) {
|
||||
serverData.models.partner.onchanges = {
|
||||
hex_color: () => {},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="hex_color" widget="color" />
|
||||
</group>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "onchange") {
|
||||
assert.step(`onchange ${JSON.stringify(args.args)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_color input[type='color']",
|
||||
"native color input is used by the field"
|
||||
);
|
||||
// style returns the value in the rgb format
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_color div").style.backgroundColor,
|
||||
"initial",
|
||||
"field has the transparent background if no color value has been selected"
|
||||
);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_color input").value, "#000000");
|
||||
await editInput(target, ".o_field_color input", "#fefefe");
|
||||
assert.verifySteps([
|
||||
'onchange [[1],{"id":1,"hex_color":"#fefefe"},"hex_color",{"hex_color":"1"}]',
|
||||
]);
|
||||
assert.strictEqual(target.querySelector(".o_field_color input").value, "#fefefe");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_color div").style.backgroundColor,
|
||||
"rgb(254, 254, 254)",
|
||||
"field has the new color set as background"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("color field in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="hex_color" widget="color" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_color input[type='color']",
|
||||
2,
|
||||
"native color input is used on each row"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_field_color input"));
|
||||
assert.doesNotHaveClass(target.querySelector(".o_data_row"), "o_selected_row");
|
||||
});
|
||||
|
||||
QUnit.test("read-only color field in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="hex_color" readonly="1" widget="color" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
'.o_field_color input:disabled',
|
||||
2,
|
||||
"the field should not be editable"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("color field read-only in model definition, in non-editable list", async function (assert) {
|
||||
serverData.models.partner.fields.hex_color.readonly = true;
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="hex_color" widget="color" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
'.o_field_color input:disabled',
|
||||
2,
|
||||
"the field should not be editable"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("color field change via another field's onchange", async (assert) => {
|
||||
serverData.models.partner.onchanges = {
|
||||
foo: (rec) => {
|
||||
rec.hex_color = "#fefefe";
|
||||
},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="hex_color" widget="color" />
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "onchange") {
|
||||
assert.step(`onchange ${JSON.stringify(args.args)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_color div").style.backgroundColor,
|
||||
"initial",
|
||||
"field has transparent background if no color value has been selected"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_field_color input").value, "#000000");
|
||||
await editInput(target, ".o_field_char[name='foo'] input", "someValue");
|
||||
assert.verifySteps([
|
||||
'onchange [[1],{"id":1,"foo":"someValue","hex_color":false},"foo",{"foo":"1","hex_color":""}]',
|
||||
]);
|
||||
assert.strictEqual(target.querySelector(".o_field_color input").value, "#fefefe");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_color div").style.backgroundColor,
|
||||
"rgb(254, 254, 254)",
|
||||
"field has the new color set as background"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
foo: "first",
|
||||
int_field: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("ColorPickerField");
|
||||
|
||||
QUnit.test(
|
||||
"No chosen color is a red line with a white background (color 0)",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_color_picker button"),
|
||||
"o_colorlist_item_color_0",
|
||||
"The default no color value does have the right class"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_color_picker button");
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_color_picker button"),
|
||||
"o_colorlist_item_color_0",
|
||||
"The no color item does have the right class in the list"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_color_picker .o_colorlist_item_color_3");
|
||||
await click(target, ".o_field_color_picker button");
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_color_picker button"),
|
||||
"o_colorlist_item_color_0",
|
||||
"The no color item still have the right class in the list"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("closes when color selected or outside click", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
<field name="foo"/>
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, ".o_field_color_picker button");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_color_picker button").length > 1,
|
||||
true,
|
||||
"there should be more color elements when the component is opened"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_color_picker .o_colorlist_item_color_3");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_color_picker button").length,
|
||||
1,
|
||||
"there should be one color element when the component is closed"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_color_picker button");
|
||||
|
||||
await click(target.querySelector('.o_field_widget[name="foo"] input'));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_color_picker button").length,
|
||||
1,
|
||||
"there should be one color element when the component is closed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("color picker on tree view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
<field name="display_name" />
|
||||
</tree>`,
|
||||
selectRecord() {
|
||||
assert.step("record selected to open");
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_color_picker button");
|
||||
assert.verifySteps(
|
||||
["record selected to open"],
|
||||
"the color is not editable and the record has been opened"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("color picker in editable list view", async function (assert) {
|
||||
serverData.models.partner.records.push({
|
||||
int_field: 1,
|
||||
});
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:nth-child(1) .o_field_color_picker button",
|
||||
"color picker list is not open by default"
|
||||
);
|
||||
|
||||
await click(target, ".o_data_row:nth-child(1) .o_field_color_picker button");
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_data_row:nth-child(1)"),
|
||||
"o_selected_row",
|
||||
"first row is selected"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(1) .o_field_color_picker button",
|
||||
12,
|
||||
"color picker list is open when the row is in edition"
|
||||
);
|
||||
|
||||
await click(
|
||||
target,
|
||||
".o_data_row:nth-child(1) .o_field_color_picker .o_colorlist_item_color_6"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(1) .o_field_color_picker button",
|
||||
12,
|
||||
"color picker list is still open after color has been selected"
|
||||
);
|
||||
|
||||
await click(target, ".o_data_row:nth-child(2) .o_data_cell");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:nth-child(1) .o_field_color_picker button",
|
||||
"color picker list is no longer open on the first row"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(2) .o_field_color_picker button",
|
||||
12,
|
||||
"color picker list is open when the row is in edition"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("column widths: dont overflow color picker in list", async function (assert) {
|
||||
serverData.models.partner.fields.date_field = {
|
||||
string: "Date field",
|
||||
type: "date",
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="date_field"/>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
</tree>`,
|
||||
domain: [["id", "<", 0]],
|
||||
});
|
||||
await click(target.querySelector(".o_list_button_add"));
|
||||
const date_column_width = target
|
||||
.querySelector('.o_list_table thead th[data-name="date_field"]')
|
||||
.style.width.replace("px", "");
|
||||
const int_field_column_width = target
|
||||
.querySelector('.o_list_table thead th[data-name="int_field"]')
|
||||
.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.
|
||||
assert.ok(
|
||||
parseFloat(date_column_width) < parseFloat(int_field_column_width),
|
||||
"colorpicker should display properly (Horizontly)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { click, getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach((assert) => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char", searchable: true },
|
||||
char_field: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
text_field: {
|
||||
string: "txt",
|
||||
type: "text",
|
||||
default: "My little txt Value\nHo-ho-hoooo Merry Christmas",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
char_field: "yop",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("CopyClipboardField");
|
||||
|
||||
QUnit.test("Char & Text Fields: Copy to clipboard button", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<div>
|
||||
<field name="text_field" widget="CopyClipboardText"/>
|
||||
<field name="char_field" widget="CopyClipboardChar"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_clipboard_button.o_btn_text_copy",
|
||||
"Should have copy button on text type field"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_clipboard_button.o_btn_char_copy",
|
||||
"Should have copy button on char type field"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("CopyClipboardField on unset field", async function (assert) {
|
||||
serverData.models.partner.records[0].char_field = false;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="char_field" widget="CopyClipboardChar" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_copy[name="char_field"] .o_clipboard_button',
|
||||
"char_field (unset) should not contain a button"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target.querySelector(".o_field_widget[name=char_field]"),
|
||||
"input",
|
||||
"char_field (unset) should contain an input field"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"CopyClipboardField on readonly unset fields in create mode",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.display_name.readonly = true;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="display_name" widget="CopyClipboardChar" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_copy[name="display_name"] .o_clipboard_button',
|
||||
"the readonly unset field should not contain a button"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("CopyClipboard fields: display a tooltip on click", async function (assert) {
|
||||
const fakePopoverService = {
|
||||
async start() {
|
||||
return {
|
||||
add(el, comp, params) {
|
||||
assert.strictEqual(el.textContent, "Copy", "button has the right text");
|
||||
assert.deepEqual(
|
||||
params,
|
||||
{ tooltip: "Copied" },
|
||||
"tooltip has the right parameters"
|
||||
);
|
||||
assert.step("copied tooltip");
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.remove("popover");
|
||||
serviceRegistry.add("popover", fakePopoverService);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
navigator: {
|
||||
clipboard: {
|
||||
writeText: (text) => {
|
||||
assert.strictEqual(
|
||||
text,
|
||||
"My little txt Value\nHo-ho-hoooo Merry Christmas",
|
||||
"copied text is equal to displayed text"
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<div>
|
||||
<field name="text_field" widget="CopyClipboardText"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_clipboard_button.o_btn_text_copy",
|
||||
"should have copy button on text type field"
|
||||
);
|
||||
|
||||
await click(target, ".o_clipboard_button");
|
||||
await nextTick();
|
||||
assert.verifySteps(["copied tooltip"]);
|
||||
});
|
||||
|
||||
QUnit.test("CopyClipboard fields with clipboard not available", async function (assert) {
|
||||
patchWithCleanup(browser, {
|
||||
console: {
|
||||
warn: (msg) => assert.step(msg),
|
||||
},
|
||||
navigator: {
|
||||
clipboard: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<div>
|
||||
<field name="text_field" widget="CopyClipboardText"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(target, ".o_clipboard_button");
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["This browser doesn't allow to copy to clipboard"],
|
||||
"console simply displays a warning on failure"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.module("CopyToClipboardButtonField");
|
||||
|
||||
QUnit.test("CopyToClipboardButtonField in form view", async function (assert) {
|
||||
patchWithCleanup(browser, {
|
||||
navigator: {
|
||||
clipboard: {
|
||||
writeText: (text) => {
|
||||
assert.step(text);
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<div>
|
||||
<field name="text_field" widget="CopyClipboardButton"/>
|
||||
<field name="char_field" widget="CopyClipboardButton"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsNone(target.querySelector(".o_field_widget[name=char_field]"), "input");
|
||||
assert.containsNone(target.querySelector(".o_field_widget[name=text_field]"), "input");
|
||||
assert.containsOnce(target, ".o_clipboard_button.o_btn_text_copy");
|
||||
assert.containsOnce(target, ".o_clipboard_button.o_btn_char_copy");
|
||||
|
||||
await click(target.querySelector(".o_clipboard_button.o_btn_text_copy"));
|
||||
await click(target.querySelector(".o_clipboard_button.o_btn_char_copy"));
|
||||
|
||||
assert.verifySteps([
|
||||
`My little txt Value
|
||||
Ho-ho-hoooo Merry Christmas`,
|
||||
"yop",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,774 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
clickCreate,
|
||||
clickDiscard,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
patchDate,
|
||||
patchTimeZone,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
triggerScroll,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { strftimeToLuxonFormat } from "@web/core/l10n/dates";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
date: { string: "A date", type: "date", searchable: true },
|
||||
datetime: { string: "A datetime", type: "datetime", searchable: true },
|
||||
display_name: { string: "Displayed name", type: "char", searchable: true },
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
date: "2017-02-03",
|
||||
datetime: "2017-02-08 10:00:00",
|
||||
display_name: "first record",
|
||||
foo: "yop",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
foo: "blip",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
foo: "abc",
|
||||
},
|
||||
{ id: 3, foo: "gnap" },
|
||||
{ id: 5, foo: "blop" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("DateField");
|
||||
|
||||
QUnit.test("DateField: toggle datepicker", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="date" />
|
||||
</form>`,
|
||||
});
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed initially"
|
||||
);
|
||||
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
|
||||
// focus another field
|
||||
await click(target, ".o_field_widget[name='foo'] input");
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should close itself when the user clicks outside"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField: toggle datepicker far in the future", async function (assert) {
|
||||
serverData.models.partner.records = [
|
||||
{
|
||||
id: 1,
|
||||
date: "9999-12-30",
|
||||
foo: "yop",
|
||||
},
|
||||
];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="date" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed initially"
|
||||
);
|
||||
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
|
||||
// focus another field
|
||||
await click(target, ".o_field_widget[name='foo'] input");
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should close itself when the user clicks outside"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("date field is empty if no date is set", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_datepicker_input",
|
||||
"should have one input in the form view"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"",
|
||||
"and it should be empty"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"DateField: set an invalid date when the field is already set",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='date'] input");
|
||||
assert.strictEqual(input.value, "02/03/2017");
|
||||
|
||||
input.value = "mmmh";
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(input.value, "02/03/2017", "should have reset the original value");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"DateField: set an invalid date when the field is not set yet",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='date'] input");
|
||||
assert.strictEqual(input.value, "");
|
||||
|
||||
input.value = "mmmh";
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(input.value, "", "The date field should be empty");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DateField value should not set on first click", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
// open datepicker and select a date
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='date'] input").value,
|
||||
"",
|
||||
"date field's input should be empty on first click"
|
||||
);
|
||||
await click(document.body, ".day[data-day*='/22/']");
|
||||
|
||||
// re-open datepicker
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".day.active").textContent,
|
||||
"22",
|
||||
"datepicker should be highlight with 22nd day of month"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField in form view (with positive time zone offset)", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
patchTimeZone(120); // Should be ignored by date fields
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
assert.strictEqual(
|
||||
args[1].date,
|
||||
"2017-02-22",
|
||||
"the correct value should be saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/03/2017",
|
||||
"the date should be correct in edit mode"
|
||||
);
|
||||
|
||||
// open datepicker and select another value
|
||||
await click(target, ".o_datepicker_input");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".day.active[data-day='02/03/2017']",
|
||||
"datepicker should be highlight February 3"
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[1]);
|
||||
await click(document.body.querySelector(".day[data-day*='/22/']"));
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/22/2017",
|
||||
"the selected date should be displayed in the input"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_date input").value,
|
||||
"02/22/2017",
|
||||
"the selected date should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField in form view (with negative time zone offset)", async function (assert) {
|
||||
patchTimeZone(-120); // Should be ignored by date fields
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/03/2017",
|
||||
"the date should be correct in edit mode"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField dropdown disappears on scroll", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<div class="scrollable" style="height: 2000px;">
|
||||
<field name="date" />
|
||||
</div>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
|
||||
await triggerScroll(target, { top: 50 });
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField with label opens datepicker on click", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<label for="date" string="What date is it" />
|
||||
<field name="date" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target.querySelector("label.o_form_label"));
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField with warn_future option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date" options="{ 'datepicker': { 'warn_future': true } }" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// open datepicker and select another value
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[11]);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[11]);
|
||||
await click(document.body, ".day[data-day*='/31/']");
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_datepicker_warning",
|
||||
"should have a warning in the form view"
|
||||
);
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='date'] input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "change"); // remove the value
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_datepicker_warning",
|
||||
"the warning in the form view should be hidden"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"DateField with warn_future option: do not overwrite datepicker option",
|
||||
async function (assert) {
|
||||
// Making sure we don't have a legit default value
|
||||
// or any onchange that would set the value
|
||||
serverData.models.partner.fields.date.default = undefined;
|
||||
serverData.models.partner.onchanges = {};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" /> <!-- Do not let the date field get the focus in the first place -->
|
||||
<field name="date" options="{ 'datepicker': { 'warn_future': true } }" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='date'] input").value,
|
||||
"02/03/2017",
|
||||
"The existing record should have a value for the date field"
|
||||
);
|
||||
|
||||
//Create a new record
|
||||
await clickCreate(target);
|
||||
assert.notOk(
|
||||
target.querySelector(".o_field_widget[name='date'] input").value,
|
||||
"The new record should not have a value that the framework would have set"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DateField in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<tree editable="bottom"><field name="date"/></tree>',
|
||||
});
|
||||
|
||||
const cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
assert.strictEqual(
|
||||
cell.textContent,
|
||||
"02/03/2017",
|
||||
"the date should be displayed correctly in readonly"
|
||||
);
|
||||
await click(cell);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"input.o_datepicker_input",
|
||||
"the view should have a date input for editable mode"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_datepicker_input"),
|
||||
document.activeElement,
|
||||
"date input should have the focus"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_datepicker_input").value,
|
||||
"02/03/2017",
|
||||
"the date should be correct in edit mode"
|
||||
);
|
||||
|
||||
// open datepicker and select another value
|
||||
await click(target, ".o_datepicker_input");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[1]);
|
||||
await click(document.body.querySelector(".day[data-day*='/22/']"));
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/22/2017",
|
||||
"the selected date should be displayed in the input"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tr.o_data_row td:not(.o_list_record_selector)").textContent,
|
||||
"02/22/2017",
|
||||
"the selected date should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"multi edition of DateField in list view: clear date in input",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[1].date = "2017-02-03";
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree multi_edit="1"><field name="date"/></tree>',
|
||||
});
|
||||
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
|
||||
// select two records and edit them
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[1], ".o_list_record_selector input");
|
||||
|
||||
await click(rows[0], ".o_data_cell");
|
||||
|
||||
assert.containsOnce(target, "input.o_datepicker_input");
|
||||
await editInput(target, ".o_datepicker_input", "");
|
||||
|
||||
assert.containsOnce(document.body, ".modal");
|
||||
await click(target, ".modal .modal-footer .btn-primary");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:first-child .o_data_cell").textContent,
|
||||
""
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:nth-child(2) .o_data_cell").textContent,
|
||||
""
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DateField remove value", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
assert.strictEqual(args[1].date, false, "the correct value should be saved");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/03/2017",
|
||||
"the date should be correct in edit mode"
|
||||
);
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
input.value = "";
|
||||
await triggerEvents(input, null, ["input", "change", "focusout"]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"",
|
||||
"should have correctly removed the value"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_date").textContent,
|
||||
"",
|
||||
"the selected date should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"do not trigger a field_changed for datetime field with date widget",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="datetime" widget="date"/></form>',
|
||||
mockRPC(route, { method }) {
|
||||
assert.step(method);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/08/2017",
|
||||
"the date should be correct"
|
||||
);
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='datetime'] input");
|
||||
input.value = "02/08/2017";
|
||||
await triggerEvents(input, null, ["input", "change", "focusout"]);
|
||||
|
||||
assert.containsOnce(target, ".o_form_saved");
|
||||
assert.verifySteps(["get_views", "read"]); // should not have save as nothing changed
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"field date should select its content onclick when there is one",
|
||||
async function (assert) {
|
||||
assert.expect(3);
|
||||
const done = assert.async();
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
$(target).on("show.datetimepicker", () => {
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"bootstrap-datetimepicker is visible"
|
||||
);
|
||||
const active = document.activeElement;
|
||||
assert.strictEqual(
|
||||
active.tagName,
|
||||
"INPUT",
|
||||
"The datepicker input should be focused"
|
||||
);
|
||||
assert.strictEqual(
|
||||
active.value.slice(active.selectionStart, active.selectionEnd),
|
||||
"02/03/2017",
|
||||
"The whole input of the date field should have been selected"
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DateField support internationalization", async function (assert) {
|
||||
// The DatePicker component needs the locale to be available since it
|
||||
// is still using Moment.js for the bootstrap datepicker
|
||||
const originalLocale = moment.locale();
|
||||
moment.defineLocale("no", {
|
||||
monthsShort: "jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),
|
||||
monthsParseExact: true,
|
||||
dayOfMonthOrdinalParse: /\d{1,2}\./,
|
||||
ordinal: "%d.",
|
||||
});
|
||||
|
||||
registry.category("services").remove("localization");
|
||||
registry
|
||||
.category("services")
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({ dateFormat: strftimeToLuxonFormat("%d-%m/%Y") })
|
||||
);
|
||||
patchWithCleanup(luxon.Settings, {
|
||||
defaultLocale: "no",
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
const dateViewForm = target.querySelector(".o_field_date input").value;
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
dateViewForm,
|
||||
"input date field should be the same as it was in the view form"
|
||||
);
|
||||
await click(document.body.querySelector(".day[data-day*='/22/']"));
|
||||
const dateEditForm = target.querySelector(".o_datepicker_input").value;
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_date input").value,
|
||||
dateEditForm,
|
||||
"date field should be the same as the one selected in the view form"
|
||||
);
|
||||
|
||||
moment.locale(originalLocale);
|
||||
moment.updateLocale("no", null);
|
||||
});
|
||||
|
||||
QUnit.test("DateField: hit enter should update value", async function (assert) {
|
||||
patchTimeZone(120);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
const input = target.querySelector(".o_field_widget[name='date'] input");
|
||||
|
||||
input.value = "01/08";
|
||||
await triggerEvent(input, null, "keydown", { key: "Enter" });
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='date'] input").value,
|
||||
`01/08/${year}`
|
||||
);
|
||||
|
||||
input.value = "08/01";
|
||||
await triggerEvent(input, null, "keydown", { key: "Enter" });
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='date'] input").value,
|
||||
`08/01/${year}`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField: allow to use compute dates (+5d for instance)", async function (assert) {
|
||||
patchDate(2021, 1, 15, 10, 0, 0); // current date : 15 Feb 2021 10:00:00
|
||||
serverData.models.partner.fields.date.default = "2019-09-15";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="date"></field></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "09/15/2019"); // default date
|
||||
|
||||
// Calculate a new date from current date + 5 days
|
||||
await editInput(target, ".o_field_widget[name=date] .o_datepicker_input", "+5d");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "02/20/2021");
|
||||
|
||||
// Discard and do it again
|
||||
await clickDiscard(target);
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "09/15/2019"); // default date
|
||||
await editInput(target, ".o_field_widget[name=date] .o_datepicker_input", "+5d");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "02/20/2021");
|
||||
|
||||
// Save and do it again
|
||||
await clickSave(target);
|
||||
// new computed date (current date + 5 days) is saved
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "02/20/2021");
|
||||
await editInput(target, ".o_field_widget[name=date] .o_datepicker_input", "+5d");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "02/20/2021");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,661 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
patchTimeZone,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
date: { string: "A date", type: "date", searchable: true },
|
||||
datetime: { string: "A datetime", type: "datetime", searchable: true },
|
||||
p: {
|
||||
string: "one2many field",
|
||||
type: "one2many",
|
||||
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,
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("DatetimeField");
|
||||
|
||||
QUnit.test("DatetimeField in form view", async function (assert) {
|
||||
patchTimeZone(120);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
const expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_datetime input").value,
|
||||
expectedDateString,
|
||||
"the datetime should be correctly displayed in readonly"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
expectedDateString,
|
||||
"the datetime should be correct in edit mode"
|
||||
);
|
||||
|
||||
// datepicker should not open on focus
|
||||
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
await click(target, ".o_datepicker_input");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
// select 22 February at 8:25:35
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[3]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .day[data-day*='/22/']")
|
||||
);
|
||||
await click(document.body.querySelector(".bootstrap-datetimepicker-widget .fa-clock-o"));
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-hour")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .hour")[8]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-minute")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .minute")[5]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-second")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .second")[7]);
|
||||
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed"
|
||||
);
|
||||
|
||||
const newExpectedDateString = "04/22/2017 08:25:35";
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
newExpectedDateString,
|
||||
"the selected date should be displayed in the input"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_datetime input").value,
|
||||
newExpectedDateString,
|
||||
"the selected date should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"DatetimeField does not trigger fieldChange before datetime completly picked",
|
||||
async function (assert) {
|
||||
patchTimeZone(120);
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
datetime() {},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
mockRPC(route, { method }) {
|
||||
if (method === "onchange") {
|
||||
assert.step("onchange");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_datepicker_input");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
// select a date and time
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[3]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelector(
|
||||
".bootstrap-datetimepicker-widget .day[data-day*='/22/']"
|
||||
)
|
||||
);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .fa-clock-o")
|
||||
);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-hour")
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .hour")[8]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-minute")
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .minute")[5]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-second")
|
||||
);
|
||||
assert.verifySteps([], "should not have done any onchange yet");
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .second")[7]
|
||||
);
|
||||
|
||||
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"04/22/2017 08:25:35"
|
||||
);
|
||||
assert.verifySteps(["onchange"], "should have done only one onchange");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DatetimeField with datetime formatted without second", async function (assert) {
|
||||
patchTimeZone(0);
|
||||
|
||||
serverData.models.partner.fields.datetime.default = "2017-08-02 12:00:05";
|
||||
serverData.models.partner.fields.datetime.required = true;
|
||||
|
||||
registry.category("services").add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({
|
||||
dateFormat: "MM/dd/yyyy",
|
||||
timeFormat: "HH:mm",
|
||||
dateTimeFormat: "MM/dd/yyyy HH:mm",
|
||||
}),
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
const expectedDateString = "08/02/2017 12:00";
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_datetime input").value,
|
||||
expectedDateString,
|
||||
"the datetime should be correctly displayed in readonly"
|
||||
);
|
||||
|
||||
await click(target, ".o_form_button_cancel");
|
||||
assert.containsNone(document.body, ".modal", "there should not be a Warning dialog");
|
||||
});
|
||||
|
||||
QUnit.test("DatetimeField in editable list view", async function (assert) {
|
||||
patchTimeZone(120);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `<tree editable="bottom"><field name="datetime"/></tree>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone
|
||||
const cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
assert.strictEqual(
|
||||
cell.textContent,
|
||||
expectedDateString,
|
||||
"the datetime should be correctly displayed in readonly"
|
||||
);
|
||||
|
||||
// switch to edit mode
|
||||
await click(target.querySelector(".o_data_row .o_data_cell"));
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"input.o_datepicker_input",
|
||||
"the view should have a date input for editable mode"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_datepicker_input"),
|
||||
document.activeElement,
|
||||
"date input should have the focus"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_datepicker_input").value,
|
||||
expectedDateString,
|
||||
"the date should be correct in edit mode"
|
||||
);
|
||||
|
||||
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
|
||||
await click(target, ".o_datepicker_input");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
// select 22 February at 8:25:35
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[3]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .day[data-day*='/22/']")
|
||||
);
|
||||
await click(document.body.querySelector(".bootstrap-datetimepicker-widget .fa-clock-o"));
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-hour")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .hour")[8]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-minute")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .minute")[5]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-second")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .second")[7]);
|
||||
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed"
|
||||
);
|
||||
|
||||
const newExpectedDateString = "04/22/2017 08:25:35";
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
newExpectedDateString,
|
||||
"the selected datetime should be displayed in the input"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tr.o_data_row td:not(.o_list_record_selector)").textContent,
|
||||
newExpectedDateString,
|
||||
"the selected datetime should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"multi edition of DatetimeField in list view: edit date in input",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree multi_edit="1"><field name="datetime"/></tree>',
|
||||
});
|
||||
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
|
||||
// select two records and edit them
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[1], ".o_list_record_selector input");
|
||||
|
||||
await click(rows[0], ".o_data_cell");
|
||||
|
||||
assert.containsOnce(target, "input.o_datepicker_input");
|
||||
await editInput(target, ".o_datepicker_input", "10/02/2019 09:00:00");
|
||||
|
||||
assert.containsOnce(document.body, ".modal");
|
||||
await click(target.querySelector(".modal .modal-footer .btn-primary"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:first-child .o_data_cell").textContent,
|
||||
"10/02/2019 09:00:00"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:nth-child(2) .o_data_cell").textContent,
|
||||
"10/02/2019 09:00:00"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"multi edition of DatetimeField in list view: clear date in input",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[1].datetime = "2017-02-08 10:00:00";
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree multi_edit="1"><field name="datetime"/></tree>',
|
||||
});
|
||||
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
|
||||
// select two records and edit them
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[1], ".o_list_record_selector input");
|
||||
|
||||
await click(rows[0], ".o_data_cell");
|
||||
|
||||
assert.containsOnce(target, "input.o_datepicker_input");
|
||||
await editInput(target, ".o_datepicker_input", "");
|
||||
|
||||
assert.containsOnce(document.body, ".modal");
|
||||
await click(target, ".modal .modal-footer .btn-primary");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:first-child .o_data_cell").textContent,
|
||||
""
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:nth-child(2) .o_data_cell").textContent,
|
||||
""
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DatetimeField remove value", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
patchTimeZone(120);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
assert.strictEqual(
|
||||
args[1].datetime,
|
||||
false,
|
||||
"the correct value should be saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/08/2017 12:00:00",
|
||||
"the date time should be correct in edit mode"
|
||||
);
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
input.value = "";
|
||||
await triggerEvents(input, null, ["input", "change", "focusout"]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"",
|
||||
"should have an empty input"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_datetime").textContent,
|
||||
"",
|
||||
"the selected date should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"DatetimeField with date/datetime widget (with day change)",
|
||||
async function (assert) {
|
||||
patchTimeZone(-240);
|
||||
|
||||
serverData.models.partner.records[0].p = [2];
|
||||
serverData.models.partner.records[1].datetime = "2017-02-08 02:00:00"; // UTC
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree>
|
||||
<field name="datetime" />
|
||||
</tree>
|
||||
<form>
|
||||
<field name="datetime" widget="date" />
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "02/07/2017 22:00:00"; // local time zone
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='p'] .o_data_cell").textContent,
|
||||
expectedDateString,
|
||||
"the datetime (datetime widget) should be correctly displayed in tree view"
|
||||
);
|
||||
|
||||
// switch to form view
|
||||
await click(target, ".o_field_widget[name='p'] .o_data_cell");
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".modal .o_field_date[name='datetime'] input").value,
|
||||
"02/07/2017",
|
||||
"the datetime (date widget) should be correctly displayed in form view"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"DatetimeField with date/datetime widget (without day change)",
|
||||
async function (assert) {
|
||||
patchTimeZone(-240);
|
||||
|
||||
serverData.models.partner.records[0].p = [2];
|
||||
serverData.models.partner.records[1].datetime = "2017-02-08 10:00:00"; // without timezone
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree>
|
||||
<field name="datetime" />
|
||||
</tree>
|
||||
<form>
|
||||
<field name="datetime" widget="date" />
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "02/08/2017 06:00:00"; // with timezone
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='p'] .o_data_cell").textContent,
|
||||
expectedDateString,
|
||||
"the datetime (datetime widget) should be correctly displayed in tree view"
|
||||
);
|
||||
|
||||
// switch to form view
|
||||
await click(target, ".o_field_widget[name='p'] .o_data_cell");
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".modal .o_field_date[name='datetime'] input").value,
|
||||
"02/08/2017",
|
||||
"the datetime (date widget) should be correctly displayed in form view"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("datepicker option: daysOfWeekDisabled", async function (assert) {
|
||||
serverData.models.partner.fields.datetime.default = "2017-08-02 12:00:05";
|
||||
serverData.models.partner.fields.datetime.required = true;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="datetime" options="{'datepicker': { 'daysOfWeekDisabled': [0, 6] }}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, ".o_datepicker_input");
|
||||
|
||||
for (const el of document.body.querySelectorAll(".day:nth-child(2), .day:last-child")) {
|
||||
assert.hasClass(el, "disabled", "first and last days must be disabled");
|
||||
}
|
||||
|
||||
// the assertions below could be replaced by a single hasClass classic on the jQuery set using the idea
|
||||
// All not <=> not Exists. But we want to be sure that the set is non empty. We don't have an helper
|
||||
// function for that.
|
||||
for (const el of document.body.querySelectorAll(
|
||||
".day:not(:nth-child(2)):not(:last-child)"
|
||||
)) {
|
||||
assert.doesNotHaveClass(el, "disabled", "other days must stay clickable");
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("datetime field: hit enter should update value", async function (assert) {
|
||||
// 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
|
||||
patchTimeZone(120);
|
||||
|
||||
registry.category("services").add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({
|
||||
dateFormat: "%m/%d/%Y",
|
||||
timeFormat: "%H:%M:%S",
|
||||
}),
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
const datetime = target.querySelector(".o_field_datetime input");
|
||||
|
||||
// Enter a beginning of date and press enter to validate
|
||||
await editInput(datetime, null, "01/08/22 14:30:40");
|
||||
await triggerEvent(datetime, null, "keydown", { key: "Enter" });
|
||||
|
||||
const datetimeValue = `01/08/2022 14:30:40`;
|
||||
|
||||
assert.strictEqual(datetime.value, datetimeValue);
|
||||
|
||||
// Click outside the field to check that the field is not changed
|
||||
await click(target);
|
||||
assert.strictEqual(datetime.value, datetimeValue);
|
||||
|
||||
// Save and check that it's still ok
|
||||
await clickSave(target);
|
||||
|
||||
const { value } = target.querySelector(".o_field_datetime input");
|
||||
assert.strictEqual(value, datetimeValue);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"datetime field with date widget: hit enter should update value",
|
||||
async function (assert) {
|
||||
/**
|
||||
* Don't think this test is usefull in the new system.
|
||||
*/
|
||||
patchTimeZone(120);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="datetime" widget="date"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await editInput(target, ".o_field_widget .o_datepicker_input", "01/08/22");
|
||||
await triggerEvent(target, ".o_field_widget .o_datepicker_input", "keydown", {
|
||||
key: "Enter",
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "01/08/2022");
|
||||
|
||||
// Click outside the field to check that the field is not changed
|
||||
await clickSave(target);
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "01/08/2022");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DateTimeField with label opens datepicker on click", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<label for="datetime" string="When is it" />
|
||||
<field name="datetime" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target.querySelector("label.o_form_label"));
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,259 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
trim: true,
|
||||
},
|
||||
empty_string: {
|
||||
string: "Empty string",
|
||||
type: "char",
|
||||
default: false,
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [{ foo: "yop" }, { foo: "blip" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("EmailField");
|
||||
|
||||
QUnit.test("EmailField in form view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="email"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// switch to edit mode and check the result
|
||||
const mailtoEdit = target.querySelector('.o_field_email input[type="email"]');
|
||||
assert.containsOnce(target, mailtoEdit, "should have an input for the email field");
|
||||
assert.strictEqual(
|
||||
mailtoEdit.value,
|
||||
"yop",
|
||||
"input should contain field value in edit mode"
|
||||
);
|
||||
|
||||
const emailBtn = target.querySelector(".o_field_email a");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
emailBtn,
|
||||
"should have rendered the email button as a link with correct classes"
|
||||
);
|
||||
assert.hasAttrValue(emailBtn, "href", "mailto:yop", "should have proper mailto prefix");
|
||||
|
||||
// change value in edit mode
|
||||
await editInput(target, ".o_field_email input[type='email']", "new");
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
const mailtoLink = target.querySelector(".o_field_email input[type='email']");
|
||||
assert.strictEqual(mailtoLink.value, "new", "new value should be displayed properly");
|
||||
});
|
||||
|
||||
QUnit.test("EmailField in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree editable="bottom"><field name="foo" widget="email"/></tree>',
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll("tbody td:not(.o_list_record_selector) a").length,
|
||||
2,
|
||||
"should have 2 cells with a link"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tbody td:not(.o_list_record_selector)").textContent,
|
||||
"yop",
|
||||
"value should be displayed properly as text"
|
||||
);
|
||||
|
||||
let mailtoLink = target.querySelectorAll(".o_field_email a");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_email a",
|
||||
2,
|
||||
"should have 2 anchors with correct classes"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
mailtoLink[0],
|
||||
"href",
|
||||
"mailto:yop",
|
||||
"should have proper mailto prefix"
|
||||
);
|
||||
// Edit a line and check the result
|
||||
let cell = target.querySelector("tbody td:not(.o_list_record_selector)");
|
||||
await click(cell);
|
||||
assert.hasClass(cell.parentElement, "o_selected_row", "should be set as edit mode");
|
||||
const mailField = cell.querySelector("input");
|
||||
assert.strictEqual(
|
||||
mailField.value,
|
||||
"yop",
|
||||
"should have the correct value in internal input"
|
||||
);
|
||||
await editInput(cell, "input", "new");
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
cell = target.querySelector("tbody td:not(.o_list_record_selector)");
|
||||
assert.doesNotHaveClass(
|
||||
cell.parentElement,
|
||||
"o_selected_row",
|
||||
"should not be in edit mode anymore"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tbody td:not(.o_list_record_selector)").textContent,
|
||||
"new",
|
||||
"value should be properly updated"
|
||||
);
|
||||
mailtoLink = target.querySelectorAll(".o_field_widget[name='foo'] a");
|
||||
assert.strictEqual(mailtoLink.length, 2, "should still have anchors with correct classes");
|
||||
assert.hasAttrValue(
|
||||
mailtoLink[0],
|
||||
"href",
|
||||
"mailto:new",
|
||||
"should still have proper mailto prefix"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("EmailField with empty value", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="empty_string" widget="email" placeholder="Placeholder"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
const input = target.querySelector(".o_field_email input");
|
||||
assert.strictEqual(input.placeholder, "Placeholder");
|
||||
assert.strictEqual(input.value, "", "the value should be displayed properly");
|
||||
});
|
||||
|
||||
QUnit.test("EmailField trim user value", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="foo" widget="email"/></form>',
|
||||
});
|
||||
await editInput(target, ".o_field_widget[name='foo'] input", " abc@abc.com ");
|
||||
const mailFieldInput = target.querySelector('.o_field_widget[name="foo"] input');
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
mailFieldInput.value,
|
||||
"abc@abc.com",
|
||||
"Foo value should have been trimmed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"readonly EmailField is properly rerendered after been changed by onchange",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[0].foo = "dolores.abernathy@delos";
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" on_change="1"/> <!-- onchange to update mobile in readonly mode directly -->
|
||||
<field name="foo" widget="email" readonly="1"/> <!-- readonly only, we don't want to go through write mode -->
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(route, { method }) {
|
||||
if (method === "onchange") {
|
||||
return Promise.resolve({
|
||||
value: {
|
||||
foo: "lara.espin@unknown", // onchange to update foo in readonly mode directly
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
// check initial rendering
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_email").textContent,
|
||||
"dolores.abernathy@delos",
|
||||
"Initial email text should be set"
|
||||
);
|
||||
|
||||
// edit the phone field, but with the mail in readonly mode
|
||||
await editInput(target, ".o_field_widget[name='int_field'] input", 3);
|
||||
|
||||
// check rendering after changes
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_email").textContent,
|
||||
"lara.espin@unknown",
|
||||
"email text should be updated"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("email field with placeholder", async function (assert) {
|
||||
serverData.models.partner.fields.foo.default = false;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" placeholder="New Placeholder" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='foo'] input").placeholder,
|
||||
"New Placeholder"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: { qux: { string: "Qux", type: "float", digits: [16, 1] } },
|
||||
records: [{ id: 1, qux: 9.1 }],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("FloatFactorField");
|
||||
|
||||
QUnit.test("FloatFactorField in form view", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="qux" widget="float_factor" options="{'factor': 0.5}" digits="[16,2]" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
// 2.3 / 0.5 = 4.6
|
||||
assert.strictEqual(args[1].qux, 4.6, "the correct float value should be saved");
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='qux'] input").value,
|
||||
"4.55",
|
||||
"The value should be rendered correctly in the input."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name='qux'] input", "2.3");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"2.30",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,479 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { click, clickSave, editInput, getFixture, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
float_field: { string: "Float field", type: "float" },
|
||||
int_field: { string: "Int field", type: "integer" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, float_field: 0.36 },
|
||||
{ id: 2, float_field: 0 },
|
||||
{ id: 3, float_field: -3.89859 },
|
||||
{ id: 4, float_field: false },
|
||||
{ id: 5, float_field: 9.1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("FloatField");
|
||||
|
||||
QUnit.test("unset field should be set to 0", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_field_widget"),
|
||||
"o_field_empty",
|
||||
"Non-set float field should be considered as 0.00"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"0.00",
|
||||
"Non-set float field should be considered as 0."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use correct digit precision from field definition", async function (assert) {
|
||||
serverData.models.partner.fields.float_field.digits = [0, 1];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float input").value,
|
||||
"0.4",
|
||||
"should contain a number rounded to 1 decimal"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use correct digit precision from options", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 1] }" /></form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float input").value,
|
||||
"0.4",
|
||||
"should contain a number rounded to 1 decimal"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use correct digit precision from field attrs", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="float_field" digits="[0, 1]" /></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float input").value,
|
||||
"0.4",
|
||||
"should contain a number rounded to 1 decimal"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("with 'step' option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="float_field" options="{'type': 'number', 'step': 0.3}"/></form>`,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget input").hasAttribute("step"),
|
||||
"Integer field with option type must have a step attribute."
|
||||
);
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"step",
|
||||
"0.3",
|
||||
'Integer field with option type must have a step attribute equals to "3".'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("basic flow in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_field_widget"),
|
||||
"o_field_empty",
|
||||
"Float field should be considered set for value 0."
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"0.000",
|
||||
"The value should be displayed properly."
|
||||
);
|
||||
|
||||
await editInput(target, 'div[name="float_field"] input', "108.2451938598598");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=float_field] input").value,
|
||||
"108.245",
|
||||
"The value should have been formatted on blur."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "18.8958938598598");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"18.896",
|
||||
"The new value should be rounded properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use a formula", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=20+3*2");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"26.000",
|
||||
"The new value should be calculated properly."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=2**3");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"8.000",
|
||||
"The new value should be calculated properly."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=2^3");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"8.000",
|
||||
"The new value should be calculated properly."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=100/3");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"33.333",
|
||||
"The new value should be calculated properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use incorrect formula", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=abc");
|
||||
await clickSave(target);
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name=float_field]"),
|
||||
"o_field_invalid",
|
||||
"fload field should be displayed as invalid"
|
||||
);
|
||||
assert.containsOnce(target, ".o_form_editable", "form view should still be editable");
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=3:2?+4");
|
||||
await clickSave(target);
|
||||
|
||||
assert.containsOnce(target, ".o_form_editable", "form view should still be editable");
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name=float_field]"),
|
||||
"o_field_invalid",
|
||||
"float field should be displayed as invalid"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float field in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="float_field" widget="float" digits="[5,3]" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
// switch to edit mode
|
||||
var cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
await click(cell);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'div[name="float_field"] input',
|
||||
"The view should have 1 input for editable float."
|
||||
);
|
||||
|
||||
await editInput(target, 'div[name="float_field"] input', "108.2458938598598");
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="float_field"] input').value,
|
||||
"108.246",
|
||||
"The value should have been formatted on blur."
|
||||
);
|
||||
|
||||
await editInput(target, 'div[name="float_field"] input', "18.8958938598598");
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget").textContent,
|
||||
"18.896",
|
||||
"The new value should be rounded properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float field with type number option", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" options="{'type': 'number'}"/>
|
||||
</form>`,
|
||||
resId: 4,
|
||||
});
|
||||
registry.category("services").remove("localization");
|
||||
registry
|
||||
.category("services")
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({ thousandsSep: ",", grouping: [3, 0] })
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget input").hasAttribute("type"),
|
||||
"Float field with option type must have a type attribute."
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"type",
|
||||
"number",
|
||||
'Float field with option type must have a type attribute equals to "number".'
|
||||
);
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "123456.7890");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"123456.789",
|
||||
"Float value must be not formatted if input type is number. (but the trailing 0 is gone)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"float field with type number option and comma decimal separator",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" options="{'type': 'number'}"/>
|
||||
</form>`,
|
||||
resId: 4,
|
||||
});
|
||||
registry.category("services").remove("localization");
|
||||
registry.category("services").add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({
|
||||
thousandsSep: ".",
|
||||
decimalPoint: ",",
|
||||
grouping: [3, 0],
|
||||
})
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget input").hasAttribute("type"),
|
||||
"Float field with option type must have a type attribute."
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"type",
|
||||
"number",
|
||||
'Float field with option type must have a type attribute equals to "number".'
|
||||
);
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "123456.789");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"123456.789",
|
||||
"Float value must be not formatted if input type is number."
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("float field without type number option", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
resId: 4,
|
||||
});
|
||||
registry.category("services").remove("localization");
|
||||
registry
|
||||
.category("services")
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({ thousandsSep: ",", grouping: [3, 0] })
|
||||
);
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"type",
|
||||
"text",
|
||||
"Float field with option type must have a text type (default type)."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "123456.7890");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"123,456.79",
|
||||
"Float value must be formatted if input type isn't number."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float_field field with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="float_field" placeholder="Placeholder"/></form>',
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='float_field'] input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='float_field'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float field can be updated by another field/widget", async function (assert) {
|
||||
class MyWidget extends owl.Component {
|
||||
onClick() {
|
||||
const val = this.props.record.data.float_field;
|
||||
this.props.record.update({ float_field: val + 1 });
|
||||
}
|
||||
}
|
||||
MyWidget.template = owl.xml`<button t-on-click="onClick">do it</button>`;
|
||||
registry.category("view_widgets").add("wi", MyWidget);
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field"/>
|
||||
<field name="float_field"/>
|
||||
<widget name="wi"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await editInput(
|
||||
target.querySelector(".o_field_widget[name=float_field] input"),
|
||||
null,
|
||||
"40"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
"40.00",
|
||||
target.querySelectorAll(".o_field_widget[name=float_field] input")[0].value
|
||||
);
|
||||
assert.strictEqual(
|
||||
"40.00",
|
||||
target.querySelectorAll(".o_field_widget[name=float_field] input")[1].value
|
||||
);
|
||||
|
||||
await click(target, ".o_widget button");
|
||||
|
||||
assert.strictEqual(
|
||||
"41.00",
|
||||
target.querySelectorAll(".o_field_widget[name=float_field] input")[0].value
|
||||
);
|
||||
assert.strictEqual(
|
||||
"41.00",
|
||||
target.querySelectorAll(".o_field_widget[name=float_field] input")[1].value
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float field with digits=0", async function (assert) {
|
||||
// This isn't in the orm documentation, so it shouldn't be supported, but
|
||||
// people do it and thus now we need to support it.
|
||||
// Historically, it behaves like "no digits attribute defined", so it
|
||||
// fallbacks on a precision of 2 digits.
|
||||
// We will change that in master s.t. we do not round anymore in that case.
|
||||
serverData.models.partner.fields.float_field.digits = 0;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float input").value,
|
||||
"0.36",
|
||||
"should contain a number rounded to 1 decimal"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { clickSave, editInput, getFixture, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
qux: { string: "Qux", type: "float", digits: [16, 1], searchable: true },
|
||||
},
|
||||
records: [{ id: 5, qux: 9.1 }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("FloatTimeField");
|
||||
|
||||
QUnit.test("FloatTimeField in form view", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
// 48 / 60 = 0.8
|
||||
assert.strictEqual(
|
||||
args.args[1].qux,
|
||||
-11.8,
|
||||
"the correct float value should be saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
resId: 5,
|
||||
});
|
||||
|
||||
// 9 + 0.1 * 60 = 9.06
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float_time[name=qux] input").value,
|
||||
"09:06",
|
||||
"The value should be rendered correctly in the input."
|
||||
);
|
||||
|
||||
await editInput(
|
||||
target.querySelector(".o_field_float_time[name=qux] input"),
|
||||
null,
|
||||
"-11:48"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float_time[name=qux] input").value,
|
||||
"-11:48",
|
||||
"The new value should be displayed properly in the input."
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"-11:48",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("FloatTimeField value formatted on blur", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
assert.strictEqual(
|
||||
args.args[1].qux,
|
||||
9.5,
|
||||
"the correct float value should be saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
resId: 5,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"09:06",
|
||||
"The formatted time value should be displayed properly."
|
||||
);
|
||||
|
||||
await editInput(target.querySelector(".o_field_float_time[name=qux] input"), null, "9.5");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float_time[name=qux] input").value,
|
||||
"09:30",
|
||||
"The new value should be displayed properly in the input."
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"09:30",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("FloatTimeField with invalid value", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await editInput(
|
||||
target.querySelector(".o_field_float_time[name=qux] input"),
|
||||
null,
|
||||
"blabla"
|
||||
);
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_title").textContent,
|
||||
"Invalid fields: "
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_content").innerHTML,
|
||||
"<ul><li>Qux</li></ul>"
|
||||
);
|
||||
assert.hasClass(target.querySelector(".o_notification"), "border-danger");
|
||||
assert.hasClass(target.querySelector(".o_field_float_time[name=qux]"), "o_field_invalid");
|
||||
|
||||
await editInput(target.querySelector(".o_field_float_time[name=qux] input"), null, "6.5");
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_field_float_time[name=qux] input"),
|
||||
"o_field_invalid",
|
||||
"date field should not be displayed as invalid now"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float_time field with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time" placeholder="Placeholder"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='qux'] input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='qux'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float_time field does not have an inputmode attribute", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time" placeholder="Placeholder"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='qux'] input");
|
||||
assert.notOk(input.attributes.inputMode);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
float_field: { string: "Float field", type: "float" },
|
||||
},
|
||||
records: [{ id: 1, float_field: 0.44444 }],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("FloatToggleField");
|
||||
|
||||
QUnit.test("basic flow in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
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>`,
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
// 1.000 / 0.125 = 8
|
||||
assert.step(args[1].float_field.toString());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget").textContent,
|
||||
"0.056", // 0.4444 * 0.125
|
||||
"The formatted time value should be displayed properly."
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("button.o_field_float_toggle").textContent,
|
||||
"0.056",
|
||||
"The value should be rendered correctly on the button."
|
||||
);
|
||||
|
||||
await click(target.querySelector("button.o_field_float_toggle"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("button.o_field_float_toggle").textContent,
|
||||
"0.000",
|
||||
"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 click(target.querySelector("button.o_field_float_toggle"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("button.o_field_float_toggle").textContent,
|
||||
"1.000",
|
||||
"The value should be rendered correctly on the button."
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget").textContent,
|
||||
"1.000",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
|
||||
assert.verifySteps(["8"]);
|
||||
});
|
||||
|
||||
QUnit.test("kanban view (readonly) with option force_button", async function (assert) {
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="float_field" widget="float_toggle" options="{'force_button': true}"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"button.o_field_float_toggle",
|
||||
"should have rendered toggle button"
|
||||
);
|
||||
|
||||
const value = target.querySelector("button.o_field_float_toggle").textContent;
|
||||
await click(target.querySelector("button.o_field_float_toggle"));
|
||||
assert.notEqual(
|
||||
target.querySelector("button.o_field_float_toggle").textContent,
|
||||
value,
|
||||
"float_field field value should be changed"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/** @odoo-module **/
|
||||
import { editSelect, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
fonts: {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["Lato", "Lato"],
|
||||
["Oswald", "Oswald"],
|
||||
],
|
||||
default: "Lato",
|
||||
string: "Fonts",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("FontSelectionField");
|
||||
|
||||
QUnit.test("FontSelectionField displays the correct fonts on options", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="fonts" widget="font" placeholder="Placeholder"/></form>',
|
||||
});
|
||||
const options = target.querySelectorAll('.o_field_widget[name="fonts"] option');
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="fonts"] > *').style.fontFamily,
|
||||
"Lato",
|
||||
"Widget font should be default (Lato)"
|
||||
);
|
||||
assert.strictEqual(options[0].value, "false", "Unselected option has no value");
|
||||
assert.strictEqual(
|
||||
options[0].textContent,
|
||||
"Placeholder",
|
||||
"Unselected option is the placeholder"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
options[1].style.fontFamily,
|
||||
"Lato",
|
||||
"Option 1 should have the correct font (Lato)"
|
||||
);
|
||||
assert.strictEqual(
|
||||
options[2].style.fontFamily,
|
||||
"Oswald",
|
||||
"Option 2 should have the correct font (Oswald)"
|
||||
);
|
||||
|
||||
await editSelect(target, ".o_input", options[2].value);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="fonts"] > *').style.fontFamily,
|
||||
"Oswald",
|
||||
"Widget font should be updated (Oswald)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"FontSelectionField displays one blank option (not required)",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.fonts.selection = [
|
||||
[false, ""],
|
||||
...serverData.models.partner.fields.fonts.selection,
|
||||
];
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="fonts" widget="font"/></form>',
|
||||
});
|
||||
assert.containsN(target.querySelector(".o_field_widget[name='fonts']"), "option", 3);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { session } from "@web/session";
|
||||
import {
|
||||
formatFloat,
|
||||
formatFloatFactor,
|
||||
formatFloatTime,
|
||||
formatJson,
|
||||
formatInteger,
|
||||
formatMany2one,
|
||||
formatMonetary,
|
||||
formatPercentage,
|
||||
formatReference,
|
||||
formatX2many,
|
||||
} from "@web/views/fields/formatters";
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
});
|
||||
|
||||
QUnit.module("Formatters");
|
||||
|
||||
QUnit.test("formatFloat", function (assert) {
|
||||
assert.strictEqual(formatFloat(false), "");
|
||||
assert.strictEqual(formatFloat(null), "0.00");
|
||||
assert.strictEqual(formatFloat(1000000), "1,000,000.00");
|
||||
|
||||
const options = { grouping: [3, 2, -1], decimalPoint: "?", thousandsSep: "€" };
|
||||
assert.strictEqual(formatFloat(106500, options), "1€06€500?00");
|
||||
|
||||
assert.strictEqual(formatFloat(1500, { thousandsSep: "" }), "1500.00");
|
||||
assert.strictEqual(formatFloat(-1.01), "-1.01");
|
||||
assert.strictEqual(formatFloat(-0.01), "-0.01");
|
||||
|
||||
assert.strictEqual(formatFloat(38.0001, { noTrailingZeros: true }), "38");
|
||||
assert.strictEqual(formatFloat(38.1, { noTrailingZeros: true }), "38.1");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 3, 3, 3] });
|
||||
assert.strictEqual(formatFloat(1000000), "1,000,000.00");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 2, -1] });
|
||||
assert.strictEqual(formatFloat(106500), "1,06,500.00");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [1, 2, -1] });
|
||||
assert.strictEqual(formatFloat(106500), "106,50,0.00");
|
||||
|
||||
patchWithCleanup(localization, {
|
||||
grouping: [2, 0],
|
||||
decimalPoint: "!",
|
||||
thousandsSep: "@",
|
||||
});
|
||||
assert.strictEqual(formatFloat(6000), "60@00!00");
|
||||
});
|
||||
|
||||
QUnit.test("formatFloat (humanReadable=true)", async (assert) => {
|
||||
assert.strictEqual(
|
||||
formatFloat(1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1020000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"1,020k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(10200000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"10.20M"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1002, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.00k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(101, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"101.00"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(64.2, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"64.20"
|
||||
);
|
||||
assert.strictEqual(formatFloat(1e18, { humanReadable: true }), "1E");
|
||||
assert.strictEqual(
|
||||
formatFloat(1e21, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1e+21"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.0045e22, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.0045e22, { humanReadable: true, decimals: 3, minDigits: 1 }),
|
||||
"1.005e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.012e43, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.01e+43"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.012e43, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"1.01e+43"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1020000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"-1,020k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-10200000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"-10.20M"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1002, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.00k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-101, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-101.00"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-64.2, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-64.20"
|
||||
);
|
||||
assert.strictEqual(formatFloat(-1e18, { humanReadable: true }), "-1E");
|
||||
assert.strictEqual(
|
||||
formatFloat(-1e21, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1e+21"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.0045e22, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.0045e22, { humanReadable: true, decimals: 3, minDigits: 1 }),
|
||||
"-1.004e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.012e43, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.01e+43"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.012e43, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"-1.01e+43"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("formatFloatFactor", function (assert) {
|
||||
assert.strictEqual(formatFloatFactor(false), "");
|
||||
assert.strictEqual(formatFloatFactor(6000), "6,000.00");
|
||||
assert.strictEqual(formatFloatFactor(6000, { factor: 3 }), "18,000.00");
|
||||
assert.strictEqual(formatFloatFactor(6000, { factor: 0.5 }), "3,000.00");
|
||||
});
|
||||
|
||||
QUnit.test("formatFloatTime", function (assert) {
|
||||
assert.strictEqual(formatFloatTime(2), "02:00");
|
||||
assert.strictEqual(formatFloatTime(3.5), "03:30");
|
||||
assert.strictEqual(formatFloatTime(0.25), "00:15");
|
||||
assert.strictEqual(formatFloatTime(0.58), "00:35");
|
||||
assert.strictEqual(formatFloatTime(2 / 60, { displaySeconds: true }), "00:02:00");
|
||||
assert.strictEqual(
|
||||
formatFloatTime(2 / 60 + 1 / 3600, { displaySeconds: true }),
|
||||
"00:02:01"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloatTime(2 / 60 + 2 / 3600, { displaySeconds: true }),
|
||||
"00:02:02"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloatTime(2 / 60 + 3 / 3600, { displaySeconds: true }),
|
||||
"00:02:03"
|
||||
);
|
||||
assert.strictEqual(formatFloatTime(0.25, { displaySeconds: true }), "00:15:00");
|
||||
assert.strictEqual(formatFloatTime(0.25 + 15 / 3600, { displaySeconds: true }), "00:15:15");
|
||||
assert.strictEqual(formatFloatTime(0.25 + 45 / 3600, { displaySeconds: true }), "00:15:45");
|
||||
assert.strictEqual(formatFloatTime(56 / 3600, { displaySeconds: true }), "00:00:56");
|
||||
assert.strictEqual(formatFloatTime(-0.5), "-00:30");
|
||||
|
||||
const options = { noLeadingZeroHour: true };
|
||||
assert.strictEqual(formatFloatTime(2, options), "2:00");
|
||||
assert.strictEqual(formatFloatTime(3.5, options), "3:30");
|
||||
assert.strictEqual(formatFloatTime(3.5, { ...options, displaySeconds: true }), "3:30:00");
|
||||
assert.strictEqual(
|
||||
formatFloatTime(3.5 + 15 / 3600, { ...options, displaySeconds: true }),
|
||||
"3:30:15"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloatTime(3.5 + 45 / 3600, { ...options, displaySeconds: true }),
|
||||
"3:30:45"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloatTime(56 / 3600, { ...options, displaySeconds: true }),
|
||||
"0:00:56"
|
||||
);
|
||||
assert.strictEqual(formatFloatTime(-0.5, options), "-0:30");
|
||||
});
|
||||
|
||||
QUnit.test("formatJson", function (assert) {
|
||||
assert.strictEqual(formatJson(false), "");
|
||||
assert.strictEqual(formatJson({}), "{}");
|
||||
assert.strictEqual(formatJson({ 1: 111 }), '{"1":111}');
|
||||
assert.strictEqual(formatJson({ 9: 11, 666: 42 }), '{"9":11,"666":42}');
|
||||
});
|
||||
|
||||
QUnit.test("formatInteger", function (assert) {
|
||||
assert.strictEqual(formatInteger(false), "");
|
||||
assert.strictEqual(formatInteger(0), "0");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 3, 3, 3] });
|
||||
assert.strictEqual(formatInteger(1000000), "1,000,000");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 2, -1] });
|
||||
assert.strictEqual(formatInteger(106500), "1,06,500");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [1, 2, -1] });
|
||||
assert.strictEqual(formatInteger(106500), "106,50,0");
|
||||
|
||||
const options = { grouping: [2, 0], thousandsSep: "€" };
|
||||
assert.strictEqual(formatInteger(6000, options), "60€00");
|
||||
});
|
||||
|
||||
QUnit.test("formatMany2one", function (assert) {
|
||||
assert.strictEqual(formatMany2one(false), "");
|
||||
assert.strictEqual(formatMany2one([false, "M2O value"]), "M2O value");
|
||||
assert.strictEqual(formatMany2one([1, "M2O value"]), "M2O value");
|
||||
assert.strictEqual(formatMany2one([1, "M2O value"], { escape: true }), "M2O%20value");
|
||||
});
|
||||
|
||||
QUnit.test("formatX2many", function (assert) {
|
||||
// Results are cast as strings since they're lazy translated.
|
||||
assert.strictEqual(String(formatX2many({ currentIds: [] })), "No records");
|
||||
assert.strictEqual(String(formatX2many({ currentIds: [1] })), "1 record");
|
||||
assert.strictEqual(String(formatX2many({ currentIds: [1, 3] })), "2 records");
|
||||
});
|
||||
|
||||
QUnit.test("formatMonetary", function (assert) {
|
||||
patchWithCleanup(session.currencies, {
|
||||
10: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "€",
|
||||
},
|
||||
11: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$",
|
||||
},
|
||||
12: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "&",
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(formatMonetary(false), "");
|
||||
assert.strictEqual(formatMonetary(200), "200.00");
|
||||
|
||||
assert.deepEqual(formatMonetary(1234567.654, { currencyId: 10 }), "1,234,567.65\u00a0€");
|
||||
assert.deepEqual(formatMonetary(1234567.654, { currencyId: 11 }), "$\u00a01,234,567.65");
|
||||
assert.deepEqual(formatMonetary(1234567.654, { currencyId: 44 }), "1,234,567.65");
|
||||
assert.deepEqual(
|
||||
formatMonetary(1234567.654, { currencyId: 10, noSymbol: true }),
|
||||
"1,234,567.65"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatMonetary(8.0, { currencyId: 10, humanReadable: true }),
|
||||
"8.00\u00a0€"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatMonetary(1234567.654, { currencyId: 10, humanReadable: true }),
|
||||
"1.23M\u00a0€"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatMonetary(1990000.001, { currencyId: 10, humanReadable: true }),
|
||||
"1.99M\u00a0€"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatMonetary(1234567.654, { currencyId: 44, digits: [69, 1] }),
|
||||
"1,234,567.7"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatMonetary(1234567.654, { currencyId: 11, digits: [69, 1] }),
|
||||
"$\u00a01,234,567.7",
|
||||
"options digits should take over currency digits when both are defined"
|
||||
);
|
||||
|
||||
// GES TODO do we keep below behavior ?
|
||||
// with field and data
|
||||
// const field = {
|
||||
// type: "monetary",
|
||||
// currency_field: "c_x",
|
||||
// };
|
||||
// let data = {
|
||||
// c_x: { res_id: 11 },
|
||||
// c_y: { res_id: 12 },
|
||||
// };
|
||||
// assert.strictEqual(formatMonetary(200, { field, currencyId: 10, data }), "200.00 €");
|
||||
// assert.strictEqual(formatMonetary(200, { field, data }), "$ 200.00");
|
||||
// assert.strictEqual(formatMonetary(200, { field, currencyField: "c_y", data }), "200.00 &");
|
||||
//
|
||||
// const floatField = { type: "float" };
|
||||
// data = {
|
||||
// currency_id: { res_id: 11 },
|
||||
// };
|
||||
// assert.strictEqual(formatMonetary(200, { field: floatField, data }), "$ 200.00");
|
||||
});
|
||||
|
||||
QUnit.test("formatMonetary without currency", function (assert) {
|
||||
patchWithCleanup(session, {
|
||||
currencies: {},
|
||||
});
|
||||
assert.deepEqual(
|
||||
formatMonetary(1234567.654, { currencyId: 10, humanReadable: true }),
|
||||
"1.23M"
|
||||
);
|
||||
assert.deepEqual(formatMonetary(1234567.654, { currencyId: 10 }), "1,234,567.65");
|
||||
});
|
||||
|
||||
QUnit.test("formatPercentage", function (assert) {
|
||||
assert.strictEqual(formatPercentage(false), "0%");
|
||||
assert.strictEqual(formatPercentage(0), "0%");
|
||||
assert.strictEqual(formatPercentage(0.5), "50%");
|
||||
|
||||
assert.strictEqual(formatPercentage(1), "100%");
|
||||
|
||||
assert.strictEqual(formatPercentage(-0.2), "-20%");
|
||||
assert.strictEqual(formatPercentage(2.5), "250%");
|
||||
|
||||
assert.strictEqual(formatPercentage(0.125), "12.5%");
|
||||
assert.strictEqual(formatPercentage(0.666666), "66.67%");
|
||||
assert.strictEqual(formatPercentage(125), "12500%");
|
||||
|
||||
assert.strictEqual(formatPercentage(50, { humanReadable: true }), "5k%");
|
||||
assert.strictEqual(formatPercentage(0.5, { noSymbol: true }), "50");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 0], decimalPoint: ",", thousandsSep: "." });
|
||||
assert.strictEqual(formatPercentage(0.125), "12,5%");
|
||||
assert.strictEqual(formatPercentage(0.666666), "66,67%");
|
||||
});
|
||||
|
||||
QUnit.test("formatReference", function (assert) {
|
||||
assert.strictEqual(formatReference(false), "");
|
||||
const value = { resModel: "product", resId: 2, displayName: "Chair" };
|
||||
assert.strictEqual(formatReference(value), "Chair");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char", searchable: true },
|
||||
p: {
|
||||
string: "one2many field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
},
|
||||
sequence: { type: "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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("HandleField");
|
||||
|
||||
QUnit.test("HandleField in x2m", async function (assert) {
|
||||
serverData.models.partner.records[0].p = [2, 4];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree editable="bottom">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("td span.o_row_handle").textContent,
|
||||
"",
|
||||
"handle should not have any content"
|
||||
);
|
||||
|
||||
assert.isVisible(
|
||||
target.querySelector("td span.o_row_handle"),
|
||||
"handle should be invisible"
|
||||
);
|
||||
|
||||
assert.containsN(target, "span.o_row_handle", 2, "should have 2 handles");
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector("td"),
|
||||
"o_handle_cell",
|
||||
"column widget should be displayed in css class"
|
||||
);
|
||||
|
||||
assert.notStrictEqual(
|
||||
getComputedStyle(target.querySelector("td span.o_row_handle")).display,
|
||||
"none",
|
||||
"handle should be visible in edit mode"
|
||||
);
|
||||
|
||||
await click(target.querySelectorAll("td")[1]);
|
||||
assert.containsOnce(
|
||||
target.querySelector("td"),
|
||||
"span.o_row_handle",
|
||||
"content of the cell should have been replaced"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("HandleField with falsy values", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
const visibleRowHandles = [...target.querySelectorAll(".o_row_handle")].filter(
|
||||
(el) => getComputedStyle(el).display !== "none"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
visibleRowHandles,
|
||||
serverData.models.partner.records.length,
|
||||
"there should be a visible handle for each record"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("HandleField in a readonly one2many", async function (assert) {
|
||||
serverData.models.partner.records[0].p = [1, 2, 4];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p" readonly="1">
|
||||
<tree editable="top">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsN(target, ".o_row_handle", 3, "there should be 3 handles, one for each row");
|
||||
assert.strictEqual(
|
||||
getComputedStyle(target.querySelector("td span.o_row_handle")).display,
|
||||
"none",
|
||||
"handle should be invisible"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { HtmlField } from "@web/views/fields/html/html_field";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { session } from "@web/session";
|
||||
|
||||
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>`;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("Fields", ({ beforeEach }) => {
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
txt: { string: "txt", type: "html", trim: true },
|
||||
},
|
||||
records: [{ id: 1, txt: RED_TEXT }],
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
|
||||
setupViewRegistries();
|
||||
|
||||
// Explicitly removed by web_editor, we need to add it back
|
||||
registry.category("fields").add("html", HtmlField, { force: true });
|
||||
});
|
||||
|
||||
QUnit.module("HtmlField");
|
||||
|
||||
QUnit.test("html fields are correctly rendered in form view (readonly)", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `<form><field name="txt" readonly="1" /></form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "div.kek");
|
||||
assert.strictEqual(target.querySelector(".o_field_html .kek").style.color, "red");
|
||||
assert.strictEqual(target.querySelector(".o_field_html").textContent, "some text");
|
||||
});
|
||||
|
||||
QUnit.test("html field with required attribute", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `<form><field name="txt" required="1"/></form>`,
|
||||
});
|
||||
|
||||
const textarea = target.querySelector(".o_field_html textarea");
|
||||
assert.ok(textarea, "should have a text area");
|
||||
|
||||
await editInput(textarea, null, "");
|
||||
assert.strictEqual(textarea.value, "");
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_title").textContent,
|
||||
"Invalid fields: "
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_content").innerHTML,
|
||||
"<ul><li>txt</li></ul>"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("html fields are correctly rendered (edit)", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `<form><field name="txt" /></form>`,
|
||||
});
|
||||
|
||||
const textarea = target.querySelector(".o_field_html textarea");
|
||||
assert.ok(textarea, "should have a text area");
|
||||
assert.strictEqual(textarea.value, RED_TEXT);
|
||||
|
||||
await editInput(textarea, null, GREEN_TEXT);
|
||||
assert.strictEqual(textarea.value, GREEN_TEXT);
|
||||
assert.containsNone(target.querySelector(".o_field_html"), ".kek");
|
||||
|
||||
await editInput(textarea, null, BLUE_TEXT);
|
||||
assert.strictEqual(textarea.value, BLUE_TEXT);
|
||||
});
|
||||
|
||||
QUnit.test("html fields are correctly rendered in list view", async (assert) => {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="txt"/>
|
||||
</tree>`,
|
||||
});
|
||||
const txt = target.querySelector(".o_data_row [name='txt']");
|
||||
assert.strictEqual(txt.textContent, "some text");
|
||||
assert.strictEqual(txt.querySelector(".kek").style.color, "red");
|
||||
|
||||
await click(target.querySelector(".o_data_row [name='txt']"));
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row [name='txt'] textarea").value,
|
||||
'<div class="kek" style="color:red">some text</div>'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"html field displays an empty string for the value false in list view",
|
||||
async (assert) => {
|
||||
serverData.models.partner.records[0].txt = false;
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="txt"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_data_row [name='txt']").textContent, "");
|
||||
|
||||
await click(target.querySelector(".o_data_row [name='txt']"));
|
||||
assert.strictEqual(target.querySelector(".o_data_row [name='txt'] textarea").value, "");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("html fields are correctly rendered in kanban view", async (assert) => {
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<kanban class="o_kanban_test">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="txt"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
const txt = target.querySelector(".kek");
|
||||
assert.strictEqual(txt.textContent, "some text");
|
||||
assert.strictEqual(txt.style.color, "red");
|
||||
});
|
||||
|
||||
QUnit.test("field html translatable", async (assert) => {
|
||||
assert.expect(10);
|
||||
|
||||
serverData.models.partner.fields.txt.translate = true;
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
|
||||
force: true,
|
||||
});
|
||||
patchWithCleanup(session.user_context, {
|
||||
lang: "en_US",
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="txt" widget="html"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(route, { args, method, model }) {
|
||||
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
|
||||
assert.deepEqual(
|
||||
args,
|
||||
[[1], "txt"],
|
||||
"should translate the txt field of the record"
|
||||
);
|
||||
return Promise.resolve([
|
||||
[
|
||||
{ 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 },
|
||||
]);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
|
||||
return Promise.resolve([
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
]);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/partner/update_field_translations") {
|
||||
assert.deepEqual(
|
||||
args,
|
||||
[
|
||||
[1],
|
||||
"txt",
|
||||
{
|
||||
en_US: { "first paragraph": "first paragraph modified" },
|
||||
fr_BE: {
|
||||
"first paragraph": "premier paragraphe modifié",
|
||||
"deuxième paragraphe": "deuxième paragraphe modifié",
|
||||
},
|
||||
},
|
||||
],
|
||||
"the new translation value should be written"
|
||||
);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelector("[name=txt] textarea"), "o_field_translate");
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_html .btn.o_field_translate",
|
||||
"should have a translate button"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_html .btn.o_field_translate").textContent,
|
||||
"EN",
|
||||
"the button should have as test the current language"
|
||||
);
|
||||
await click(target, ".o_field_html .btn.o_field_translate");
|
||||
|
||||
assert.containsOnce(target, ".modal", "a translate modal should be visible");
|
||||
assert.containsN(target, ".translation", 4, "four rows should be visible");
|
||||
|
||||
const translations = target.querySelectorAll(
|
||||
".modal .o_translation_dialog .translation input"
|
||||
);
|
||||
|
||||
const enField1 = translations[0];
|
||||
assert.strictEqual(
|
||||
enField1.value,
|
||||
"first paragraph",
|
||||
"first part of english translation should be filled"
|
||||
);
|
||||
await editInput(enField1, null, "first paragraph modified");
|
||||
|
||||
const frField1 = translations[2];
|
||||
assert.strictEqual(
|
||||
frField1.value,
|
||||
"",
|
||||
"first part of french translation should not be filled"
|
||||
);
|
||||
await editInput(frField1, null, "premier paragraphe modifié");
|
||||
|
||||
const frField2 = translations[3];
|
||||
assert.strictEqual(
|
||||
frField2.value,
|
||||
"deuxième paragraphe",
|
||||
"second part of french translation should be filled"
|
||||
);
|
||||
await editInput(frField2, null, "deuxième paragraphe modifié");
|
||||
|
||||
await click(target, ".modal button.btn-primary"); // save
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
report: {
|
||||
fields: {
|
||||
int_field: { string: "Int Field", type: "integer" },
|
||||
html_field: { string: "Content of report", type: "html" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
html_field: `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { color : rgb(255, 0, 0); }
|
||||
</style>
|
||||
<head>
|
||||
<body>
|
||||
<div class="nice_div"><p>Some content</p></div>
|
||||
</body>
|
||||
</html>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("IframeWrapperField");
|
||||
|
||||
QUnit.test("IframeWrapperField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "report",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="html_field" widget="iframe_wrapper"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const iframeDoc = target.querySelector("iframe").contentDocument;
|
||||
assert.strictEqual(iframeDoc.querySelector(".nice_div").innerHTML, "<p>Some content</p>");
|
||||
assert.strictEqual($(iframeDoc).find(".nice_div p").css("color"), "rgb(255, 0, 0)");
|
||||
});
|
||||
|
||||
QUnit.test("IframeWrapperField in form view with onchange", async function (assert) {
|
||||
serverData.models.report.onchanges = {
|
||||
int_field(record) {
|
||||
record.html_field = record.html_field.replace("Some content", "New content");
|
||||
},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "report",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field"/>
|
||||
<field name="html_field" widget="iframe_wrapper"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const iframeDoc = target.querySelector("iframe").contentDocument;
|
||||
assert.strictEqual(iframeDoc.querySelector(".nice_div").innerHTML, "<p>Some content</p>");
|
||||
assert.strictEqual($(iframeDoc).find(".nice_div p").css("color"), "rgb(255, 0, 0)");
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", 264);
|
||||
assert.strictEqual(iframeDoc.querySelector(".nice_div").innerHTML, "<p>New content</p>");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,963 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
nextTick,
|
||||
triggerEvent,
|
||||
clickSave,
|
||||
editInput,
|
||||
patchDate,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { pagerNext } from "@web/../tests/search/helpers";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
const MY_IMAGE =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
|
||||
const PRODUCT_IMAGE =
|
||||
"R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
function getUnique(target) {
|
||||
const src = target.dataset.src;
|
||||
return new URL(src).searchParams.get("unique");
|
||||
}
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: {
|
||||
string: "Displayed name",
|
||||
type: "char",
|
||||
searchable: true,
|
||||
},
|
||||
timmy: {
|
||||
string: "pokemon",
|
||||
type: "many2many",
|
||||
relation: "partner_type",
|
||||
searchable: true,
|
||||
},
|
||||
foo: { type: "char" },
|
||||
document: { string: "Binary", type: "binary" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
timmy: [],
|
||||
document: "coucou==",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
timmy: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
},
|
||||
],
|
||||
},
|
||||
partner_type: {
|
||||
fields: {
|
||||
name: { string: "Partner Type", type: "char", searchable: true },
|
||||
color: { string: "Color index", type: "integer", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 12, display_name: "gold", color: 2 },
|
||||
{ id: 14, display_name: "silver", color: 5 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("ImageField");
|
||||
|
||||
QUnit.test("ImageField is correctly rendered", async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
|
||||
serverData.models.partner.records[0].document = MY_IMAGE;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}" />
|
||||
</form>`,
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/read") {
|
||||
assert.deepEqual(
|
||||
args[1],
|
||||
["__last_update", "document", "display_name"],
|
||||
"The fields document, display_name and __last_update should be present when reading an image"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name='document']"),
|
||||
"o_field_image",
|
||||
"the widget should have the correct class"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget[name='document'] img",
|
||||
"the widget should contain an image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="document"] img').dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
"the image should have the correct src"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name='document'] img"),
|
||||
"img-fluid",
|
||||
"the image should have the correct class"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget[name='document'] img"),
|
||||
"width",
|
||||
"90",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='document'] img").style.maxWidth,
|
||||
"90px",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
|
||||
const computedStyle = window.getComputedStyle(
|
||||
target.querySelector(".o_field_widget[name='document'] img")
|
||||
);
|
||||
assert.strictEqual(
|
||||
computedStyle.width,
|
||||
"90px",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
assert.strictEqual(
|
||||
computedStyle.height,
|
||||
"90px",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_image .o_select_file_button",
|
||||
"the image can be edited"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_image .o_clear_file_button",
|
||||
"the image can be deleted"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_input_file").getAttribute("accept"),
|
||||
"image/*",
|
||||
'the default value for the attribute "accept" on the "image" widget must be "image/*"'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"ImageField is correctly replaced when given an incorrect value",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[0].document = "incorrect_base64_value";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="document"] img').dataset.src,
|
||||
"data:image/png;base64,incorrect_base64_value",
|
||||
"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
|
||||
await triggerEvent(target, 'div[name="document"] img', "error");
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector('.o_field_widget[name="document"]'),
|
||||
"o_field_image",
|
||||
"the widget should have the correct class"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget[name='document'] img",
|
||||
"the widget should contain an image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="document"] img').dataset.src,
|
||||
"/web/static/img/placeholder.png",
|
||||
"the image should have the correct src"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name='document'] img"),
|
||||
"img-fluid",
|
||||
"the image should have the correct class"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget[name='document'] img"),
|
||||
"width",
|
||||
"90",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='document'] img").style.maxWidth,
|
||||
"90px",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_image .o_select_file_button",
|
||||
"the image can be edited"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_image .o_clear_file_button",
|
||||
"the image cannot be deleted as it has not been uploaded"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("ImageField preview is updated when an image is uploaded", async function (assert) {
|
||||
const imageData = Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)));
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="document"] img').dataset.src,
|
||||
"data:image/png;base64,coucou==",
|
||||
"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.
|
||||
const fileInput = target.querySelector("input[type=file]");
|
||||
const fakeInput = {
|
||||
files: [new File([imageData], "fake_file.png", { type: "png" })],
|
||||
};
|
||||
fileInput.addEventListener(
|
||||
"change",
|
||||
(ev) => {
|
||||
Object.defineProperty(ev, "target", { value: fakeInput });
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
// It can take some time to encode the data as a base64 url
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
// Wait for a render
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=document] img").dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
"the image should have the new src"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"clicking save manually after uploading new image should change the unique of the image src",
|
||||
async function (assert) {
|
||||
serverData.models.partner.onchanges = { foo: () => {} };
|
||||
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.__last_update = "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;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="document" widget="image" />
|
||||
</form>`,
|
||||
mockRPC(_route, { method, args }) {
|
||||
if (method === "write") {
|
||||
args[1].__last_update = lastUpdates[index];
|
||||
args[1].document = "4 kb";
|
||||
index++;
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_image img")),
|
||||
"1659688620000"
|
||||
);
|
||||
|
||||
await editInput(
|
||||
target,
|
||||
"input[type=file]",
|
||||
new File(
|
||||
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file.png",
|
||||
{ type: "png" }
|
||||
)
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=document] img").dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=document] img").dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_image img")),
|
||||
"1659692220000"
|
||||
);
|
||||
|
||||
// Change the image again. After clicking save, it should have the correct new url.
|
||||
await editInput(
|
||||
target,
|
||||
"input[type=file]",
|
||||
new File(
|
||||
[Uint8Array.from([...atob(PRODUCT_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file2.gif",
|
||||
{ type: "png" }
|
||||
)
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=document] img").dataset.src,
|
||||
`data:image/gif;base64,${PRODUCT_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_image img")),
|
||||
"1659695820000"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("save record with image field modified by onchange", async function (assert) {
|
||||
serverData.models.partner.onchanges = {
|
||||
foo: (data) => {
|
||||
data.document = MY_IMAGE;
|
||||
},
|
||||
};
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.__last_update = "2022-08-05 08:37:00"; // 1659688620000
|
||||
|
||||
// 1659692220000
|
||||
const lastUpdates = ["2022-08-05 09:37:00"];
|
||||
let index = 0;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="document" widget="image" />
|
||||
</form>`,
|
||||
mockRPC(_route, { method, args }) {
|
||||
if (method === "write") {
|
||||
args[1].__last_update = lastUpdates[index];
|
||||
args[1].document = "3 kb";
|
||||
index++;
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
|
||||
await editInput(target, "[name='foo'] input", "grrr");
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=document] img").dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
|
||||
});
|
||||
|
||||
QUnit.test("ImageField: option accepted_file_extensions", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'accepted_file_extensions': '.png,.jpeg'}" />
|
||||
</form>`,
|
||||
});
|
||||
// The view must be in edit mode
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_input_file").getAttribute("accept"),
|
||||
".png,.jpeg",
|
||||
"the input should have the correct ``accept`` attribute"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField: set 0 width/height in the size option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<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 = target.querySelectorAll(".o_field_widget img");
|
||||
|
||||
assert.deepEqual(
|
||||
[imgs[0].attributes.width, imgs[0].attributes.height],
|
||||
[undefined, undefined],
|
||||
"if both size are set to 0, both attributes are undefined"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[imgs[1].attributes.width, imgs[1].attributes.height.value],
|
||||
[undefined, "50"],
|
||||
"if only the width is set to 0, the width attribute is not set on the img"
|
||||
);
|
||||
assert.deepEqual(
|
||||
[
|
||||
imgs[1].style.width,
|
||||
imgs[1].style.maxWidth,
|
||||
imgs[1].style.height,
|
||||
imgs[1].style.maxHeight,
|
||||
],
|
||||
["auto", "100%", "", "50px"],
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[imgs[2].attributes.width.value, imgs[2].attributes.height],
|
||||
["50", undefined],
|
||||
"if only the height is set to 0, the height attribute is not set on the img"
|
||||
);
|
||||
assert.deepEqual(
|
||||
[
|
||||
imgs[2].style.width,
|
||||
imgs[2].style.maxWidth,
|
||||
imgs[2].style.height,
|
||||
imgs[2].style.maxHeight,
|
||||
],
|
||||
["", "50px", "auto", "100%"],
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField: zoom and zoom_delay options (readonly)", async (assert) => {
|
||||
serverData.models.partner.records[0].document = MY_IMAGE;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'zoom_delay': 600}" readonly="1" />
|
||||
</form>`,
|
||||
});
|
||||
// data-tooltip attribute is used by the tooltip service
|
||||
assert.strictEqual(
|
||||
JSON.parse(target.querySelector(".o_field_image img").dataset["tooltipInfo"]).url,
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
"shows a tooltip on hover"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_image img").dataset["tooltipDelay"],
|
||||
"600",
|
||||
"tooltip has the right delay"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField: zoom and zoom_delay options (edit)", async function (assert) {
|
||||
serverData.models.partner.records[0].document = "3 kb";
|
||||
serverData.models.partner.records[0].__last_update = "2022-08-05 08:37:00";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'zoom_delay': 600}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
JSON.parse(
|
||||
target.querySelector(".o_field_image img").dataset["tooltipInfo"]
|
||||
).url.endsWith("/web/image?model=partner&id=1&field=document&unique=1659688620000"),
|
||||
"tooltip show the full image from the field value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_image img").dataset["tooltipDelay"],
|
||||
"600",
|
||||
"tooltip has the right delay"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"ImageField displays the right images with zoom and preview_image options (readonly)",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[0].document = "3 kb";
|
||||
serverData.models.partner.records[0].__last_update = "2022-08-05 08:37:00";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'preview_image': 'document_preview', 'zoom_delay': 600}" readonly="1" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
JSON.parse(
|
||||
target.querySelector(".o_field_image img").dataset["tooltipInfo"]
|
||||
).url.endsWith("/web/image?model=partner&id=1&field=document&unique=1659688620000"),
|
||||
"tooltip show the full image from the field value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_image img").dataset["tooltipDelay"],
|
||||
"600",
|
||||
"tooltip has the right delay"
|
||||
);
|
||||
assert.ok(
|
||||
target
|
||||
.querySelector(".o_field_image img")
|
||||
.dataset.src.endsWith(
|
||||
"/web/image?model=partner&id=1&field=document_preview&unique=1659688620000"
|
||||
),
|
||||
"image src is the preview image given in option"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("ImageField in subviews is loaded correctly", async function (assert) {
|
||||
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
|
||||
serverData.models.partner.records[0].document = MY_IMAGE;
|
||||
serverData.models.partner_type.fields.image = {
|
||||
name: "image",
|
||||
type: "binary",
|
||||
};
|
||||
serverData.models.partner_type.records[0].image = PRODUCT_IMAGE;
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}" />
|
||||
<field name="timmy" widget="many2many" mode="kanban">
|
||||
<kanban>
|
||||
<field name="display_name" />
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<span>
|
||||
<t t-esc="record.display_name.value" />
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<field name="image" widget="image" />
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, `img[data-src="data:image/png;base64,${MY_IMAGE}"]`);
|
||||
assert.containsOnce(target, ".o_kanban_record .oe_kanban_global_click");
|
||||
|
||||
// Actual flow: click on an element of the m2m to get its form view
|
||||
await click(target, ".oe_kanban_global_click");
|
||||
assert.containsOnce(target, ".modal", "The modal should have opened");
|
||||
|
||||
assert.containsOnce(target, `img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField in x2many list is loaded correctly", async function (assert) {
|
||||
serverData.models.partner_type.fields.image = {
|
||||
name: "image",
|
||||
type: "binary",
|
||||
};
|
||||
serverData.models.partner_type.records[0].image = PRODUCT_IMAGE;
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many">
|
||||
<tree>
|
||||
<field name="image" widget="image" />
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "tr.o_data_row", "There should be one record in the many2many");
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`),
|
||||
"The list's image is in the DOM"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField with required attribute", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>`,
|
||||
mockRPC(route, { method }) {
|
||||
if (method === "create") {
|
||||
throw new Error("Should not do a create RPC with unset required image field");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
assert.containsOnce(
|
||||
target.querySelector(".o_form_view"),
|
||||
".o_form_editable",
|
||||
"form view should still be editable"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget"),
|
||||
"o_field_invalid",
|
||||
"image field should be displayed as invalid"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField is reset when changing record", async function (assert) {
|
||||
const imageData = Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)));
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
async function setFiles() {
|
||||
const list = new DataTransfer();
|
||||
list.items.add(new File([imageData], "fake_file.png", { type: "png" }));
|
||||
const fileInput = target.querySelector("input[type=file]");
|
||||
fileInput.files = list.files;
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
// It can take some time to encode the data as a base64 url
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
// Wait for a render
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("img[data-alt='Binary file']").dataset.src,
|
||||
"/web/static/img/placeholder.png",
|
||||
"image field should not be set"
|
||||
);
|
||||
|
||||
await setFiles();
|
||||
assert.ok(
|
||||
target
|
||||
.querySelector("img[data-alt='Binary file']")
|
||||
.dataset.src.includes("data:image/png;base64"),
|
||||
"image field should be set"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
await click(target, ".o_form_button_create");
|
||||
assert.strictEqual(
|
||||
target.querySelector("img[data-alt='Binary file']").dataset.src,
|
||||
"/web/static/img/placeholder.png",
|
||||
"image field should be reset"
|
||||
);
|
||||
|
||||
await setFiles();
|
||||
assert.ok(
|
||||
target
|
||||
.querySelector("img[data-alt='Binary file']")
|
||||
.dataset.src.includes("data:image/png;base64"),
|
||||
"image field should be set"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("unique in url doesn't change on onchange", async (assert) => {
|
||||
serverData.models.partner.onchanges = {
|
||||
foo: () => {},
|
||||
};
|
||||
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.__last_update = "2022-08-05 08:37:00";
|
||||
|
||||
await makeView({
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>`,
|
||||
mockRPC(route, { method, args }) {
|
||||
assert.step(method);
|
||||
if (method === "write") {
|
||||
// 1659692220000
|
||||
args[1].__last_update = "2022-08-05 09:37:00";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(["get_views", "read"]);
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
|
||||
|
||||
assert.verifySteps([]);
|
||||
// same unique as before
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
|
||||
|
||||
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
|
||||
assert.verifySteps(["onchange"]);
|
||||
// also same unique
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
|
||||
|
||||
await clickSave(target);
|
||||
assert.verifySteps(["write", "read"]);
|
||||
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
|
||||
});
|
||||
|
||||
QUnit.test("unique in url change on record change", async (assert) => {
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.__last_update = "2022-08-05 08:37:00";
|
||||
|
||||
const rec2 = serverData.models.partner.records.find((rec) => rec.id === 2);
|
||||
rec2.document = "3 kb";
|
||||
rec2.__last_update = "2022-08-05 09:37:00";
|
||||
|
||||
await makeView({
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
function getUnique(target) {
|
||||
const src = target.dataset.src;
|
||||
return new URL(src).searchParams.get("unique");
|
||||
}
|
||||
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
|
||||
await pagerNext(target);
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"unique in url does not change on record change if no_reload option is set",
|
||||
async (assert) => {
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.__last_update = "2022-08-05 08:37:00";
|
||||
|
||||
await makeView({
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" options="{'no_reload': true}" />
|
||||
<field name="__last_update" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
function getUnique(target) {
|
||||
const src = target.dataset.src;
|
||||
return new URL(src).searchParams.get("unique");
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_image img")),
|
||||
"1659688620000"
|
||||
);
|
||||
await editInput(
|
||||
target.querySelector(
|
||||
"div[name='__last_update'] > div > input",
|
||||
"2022-08-05 08:39:00"
|
||||
)
|
||||
);
|
||||
await click(target, ".o_form_button_save");
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_image img")),
|
||||
"1659688620000"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"url should not use the record last updated date when the field is related",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.related = {
|
||||
name: "Binary",
|
||||
type: "binary",
|
||||
related: "user.image",
|
||||
};
|
||||
|
||||
serverData.models.partner.fields.user = {
|
||||
name: "User",
|
||||
type: "many2one",
|
||||
relation: "user",
|
||||
default: 1,
|
||||
};
|
||||
|
||||
serverData.models.user = {
|
||||
fields: {
|
||||
image: {
|
||||
name: "Image",
|
||||
type: "binary",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
image: "3 kb",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
|
||||
|
||||
patchDate(2017, 1, 6, 11, 0, 0);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" />
|
||||
<field name="user"/>
|
||||
<field name="related" widget="image"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
async mockRPC(route, { args }, performRpc) {
|
||||
if (route === "/web/dataset/call_kw/partner/read") {
|
||||
const res = await performRpc(...arguments);
|
||||
// The mockRPC doesn't implement related fields
|
||||
res[0].related = "3 kb";
|
||||
return res;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const initialUnique = Number(getUnique(target.querySelector(".o_field_image img")));
|
||||
assert.ok(
|
||||
DateTime.fromMillis(initialUnique).hasSame(DateTime.fromISO("2017-02-06"), "days")
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
|
||||
|
||||
// the unique should be the same
|
||||
assert.strictEqual(
|
||||
initialUnique,
|
||||
Number(getUnique(target.querySelector(".o_field_image img")))
|
||||
);
|
||||
|
||||
patchDate(2017, 1, 9, 11, 0, 0);
|
||||
await editInput(
|
||||
target,
|
||||
"input[type=file]",
|
||||
new File(
|
||||
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file.png",
|
||||
{ type: "png" }
|
||||
)
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_image img").dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
patchDate(2017, 1, 9, 12, 0, 0);
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
const unique = Number(getUnique(target.querySelector(".o_field_image img")));
|
||||
assert.ok(DateTime.fromMillis(unique).hasSame(DateTime.fromISO("2017-02-09"), "days"));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
const FR_FLAG_URL = "/base/static/img/country_flags/fr.png";
|
||||
const EN_FLAG_URL = "/base/static/img/country_flags/gb.png";
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char", searchable: true },
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
p: {
|
||||
string: "one2many field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
},
|
||||
timmy: {
|
||||
string: "pokemon",
|
||||
type: "many2many",
|
||||
relation: "partner_type",
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
foo: FR_FLAG_URL,
|
||||
timmy: [],
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
partner_type: {
|
||||
fields: {
|
||||
name: { string: "Partner Type", type: "char", searchable: true },
|
||||
color: { string: "Color index", type: "integer", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 12, display_name: "gold", color: 2 },
|
||||
{ id: 14, display_name: "silver", color: 5 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
/**
|
||||
* Same tests than for Image fields, but for Char fields with image_url widget.
|
||||
*/
|
||||
QUnit.module("ImageUrlField");
|
||||
|
||||
QUnit.test("image fields are correctly rendered", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector('div[name="foo"]'),
|
||||
"o_field_image_url",
|
||||
"the widget should have the correct class"
|
||||
);
|
||||
assert.containsOnce(target, 'div[name="foo"] > img', "the widget should contain an image");
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="foo"] > img').dataset.src,
|
||||
FR_FLAG_URL,
|
||||
"the image should have the correct src"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector('div[name="foo"] > img'),
|
||||
"img-fluid",
|
||||
"the image should have the correct class"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector('div[name="foo"] > img'),
|
||||
"width",
|
||||
"90",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="foo"] > img').style.maxWidth,
|
||||
"90px",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageUrlField in subviews are loaded correctly", async function (assert) {
|
||||
serverData.models.partner_type.fields.image = { name: "image", type: "char" };
|
||||
serverData.models.partner_type.records[0].image = EN_FLAG_URL;
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
<field name="timmy" widget="many2many" mode="kanban">
|
||||
<kanban>
|
||||
<field name="display_name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<span><t t-esc="record.display_name.value"/></span>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<field name="image" widget="image_url"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="${FR_FLAG_URL}"]`),
|
||||
"The view's image is in the DOM"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record .oe_kanban_global_click",
|
||||
"There should be one record in the many2many"
|
||||
);
|
||||
|
||||
// Actual flow: click on an element of the m2m to get its form view
|
||||
await click(target.querySelector(".oe_kanban_global_click"));
|
||||
assert.containsOnce(document.body, ".modal", "The modal should have opened");
|
||||
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="${EN_FLAG_URL}"]`),
|
||||
"The dialog's image is in the DOM"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("image fields in x2many list are loaded correctly", async function (assert) {
|
||||
serverData.models.partner_type.fields.image = { name: "image", type: "char" };
|
||||
serverData.models.partner_type.records[0].image = EN_FLAG_URL;
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many">
|
||||
<tree>
|
||||
<field name="image" widget="image_url"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "tr.o_data_row", "There should be one record in the many2many");
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="${EN_FLAG_URL}"]`),
|
||||
"The list's image is in the DOM"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("image url fields in kanban don't stop opening record", async function (assert) {
|
||||
patchWithCleanup(KanbanController.prototype, {
|
||||
openRecord() {
|
||||
assert.step("open record");
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="foo" widget="image_url"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
await click(target.querySelector(".o_kanban_record"));
|
||||
assert.verifySteps(["open record"]);
|
||||
});
|
||||
|
||||
QUnit.test("image fields with empty value", async function (assert) {
|
||||
serverData.models.partner.records[0].foo = false;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector('div[name="foo"]'),
|
||||
"o_field_image_url",
|
||||
"the widget should have the correct class"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
'div[name="foo"] > img',
|
||||
"the widget should not contain an image"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("onchange update image fields", async function (assert) {
|
||||
const srcTest = "/my/test/src";
|
||||
serverData.models.partner.onchanges = {
|
||||
display_name(record) {
|
||||
record.foo = srcTest;
|
||||
},
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="foo"] > img').dataset.src,
|
||||
FR_FLAG_URL,
|
||||
"the image should have the correct src"
|
||||
);
|
||||
|
||||
await editInput(target, '[name="display_name"] input', "test");
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="foo"] > img').dataset.src,
|
||||
srcTest,
|
||||
"the image should have the onchange src"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,386 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
findElement,
|
||||
getFixture,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, int_field: 10 },
|
||||
{ id: 2, int_field: false },
|
||||
{ id: 3, int_field: 8069 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("IntegerField");
|
||||
|
||||
QUnit.test("should be 0 when unset", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_field_widget"),
|
||||
"o_field_empty",
|
||||
"Non-set integer field should be recognized as 0."
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"0",
|
||||
"Non-set integer field should be recognized as 0."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("basic form view flow", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=int_field] input").value,
|
||||
"10",
|
||||
"The value should be rendered correctly in edit mode."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", "30");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=int_field] input").value,
|
||||
"30",
|
||||
"The value should be correctly displayed in the input."
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"30",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("rounded when using formula in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", "=100/3");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"33",
|
||||
"The new value should be calculated properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("with input type 'number' option", async function (assert) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="int_field" options="{'type': 'number'}"/></form>`,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget input").hasAttribute("type"),
|
||||
"Integer field with option type must have a type attribute."
|
||||
);
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"type",
|
||||
"number",
|
||||
'Integer field with option type must have a type attribute equals to "number".'
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", "1234567890");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"1234567890",
|
||||
"Integer value must be not formatted if input type is number."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("with 'step' option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="int_field" options="{'type': 'number', 'step': 3}"/></form>`,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget input").hasAttribute("step"),
|
||||
"Integer field with option type must have a step attribute."
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"step",
|
||||
"3",
|
||||
'Integer field with option type must have a step attribute equals to "3".'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("without input type option", async function (assert) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"type",
|
||||
"text",
|
||||
"Integer field without option type must have a text type (default type)."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", "1234567890");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"1,234,567,890",
|
||||
"Integer value must be formatted if input type isn't number."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("with disable formatting option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 3,
|
||||
arch: `<form><field name="int_field" options="{'format': 'false'}"/></form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"8069",
|
||||
"Integer value must not be formatted"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("IntegerField is formatted by default", async function (assert) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 3,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"8,069",
|
||||
"Integer value must be formatted by default"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("basic flow in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree editable="bottom"><field name="int_field"/></tree>',
|
||||
});
|
||||
var zeroValues = Array.from(target.querySelectorAll("td")).filter(
|
||||
(el) => el.textContent === "0"
|
||||
);
|
||||
assert.strictEqual(
|
||||
zeroValues.length,
|
||||
1,
|
||||
"Unset integer values should not be rendered as zeros."
|
||||
);
|
||||
|
||||
// switch to edit mode
|
||||
var cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
await click(cell);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_field_widget[name="int_field"] input',
|
||||
"The view should have 1 input for editable integer."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", "-28");
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="int_field"] input').value,
|
||||
"-28",
|
||||
"The value should be displayed properly in the input."
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
assert.strictEqual(
|
||||
target.querySelector("td:not(.o_list_record_selector)").textContent,
|
||||
"-28",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("IntegerField field with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form><field name="int_field" placeholder="Placeholder"/></form>`,
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='int_field'] input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='int_field'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"no need to focus out of the input to save the record after correcting an invalid input",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
const input = findElement(target, ".o_field_widget[name=int_field] input");
|
||||
assert.strictEqual(input.value, "10");
|
||||
|
||||
input.value = "a";
|
||||
triggerEvent(input, null, "input", {});
|
||||
assert.strictEqual(input.value, "a");
|
||||
await clickSave(target);
|
||||
assert.containsOnce(target, ".o_form_status_indicator span i.fa-warning");
|
||||
input.value = "1";
|
||||
triggerEvent(input, null, "input", {});
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_form_status_indicator span i.fa-warning");
|
||||
assert.containsOnce(target, ".o_form_button_save");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"make a valid integer field invalid, then reset the original value to make it valid again",
|
||||
async function (assert) {
|
||||
// This test is introduced to fix a bug:
|
||||
// Have a valid value, change it to an invalid value, blur, then change it back to the same valid value.
|
||||
// The field was considered not dirty, so the onChange code wasn't executed, and the model still thought the value was invalid.
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
const fieldSelector = ".o_field_widget[name=int_field]";
|
||||
const inputSelector = fieldSelector + " input";
|
||||
|
||||
assert.strictEqual(target.querySelector(inputSelector).value, "10");
|
||||
|
||||
await editInput(target.querySelector(inputSelector), null, "a");
|
||||
assert.strictEqual(target.querySelector(inputSelector).value, "a");
|
||||
assert.hasClass(target.querySelector(fieldSelector), "o_field_invalid");
|
||||
|
||||
await editInput(target.querySelector(inputSelector), null, "10");
|
||||
assert.strictEqual(target.querySelector(inputSelector).value, "10");
|
||||
assert.doesNotHaveClass(target.querySelector(fieldSelector), "o_field_invalid");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("value is formatted on Enter", async function (assert) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
target.querySelector(".o_field_widget input").value = 1000;
|
||||
await triggerEvent(target, ".o_field_widget input", "input");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "1000");
|
||||
|
||||
await triggerEvent(target, ".o_field_widget input", "keydown", { key: "Enter" });
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "1,000");
|
||||
});
|
||||
|
||||
QUnit.test("value is formatted on Enter (even if same value)", async function (assert) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 3,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "8,069");
|
||||
|
||||
target.querySelector(".o_field_widget input").value = 8069;
|
||||
await triggerEvent(target, ".o_field_widget input", "input");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "8069");
|
||||
|
||||
await triggerEvent(target, ".o_field_widget input", "keydown", { key: "Enter" });
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "8,069");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { fakeCookieService } from "@web/../tests/helpers/mock_services";
|
||||
import { click, getFixture, nextTick, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
const graph_values = [
|
||||
{ value: 300, label: "5-11 Dec" },
|
||||
{ value: 500, label: "12-18 Dec" },
|
||||
{ value: 100, label: "19-25 Dec" },
|
||||
];
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
selection: {
|
||||
string: "Selection",
|
||||
type: "selection",
|
||||
searchable: true,
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
},
|
||||
graph_data: { string: "Graph Data", type: "text" },
|
||||
graph_type: {
|
||||
string: "Graph Type",
|
||||
type: "selection",
|
||||
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,
|
||||
},
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
registry.category("services").add("cookie", fakeCookieService);
|
||||
});
|
||||
|
||||
async function reloadKanbanView(target) {
|
||||
await click(target, "input.o_searchview_input");
|
||||
await triggerEvent(target, "input.o_searchview_input", "keydown", { key: "Enter" });
|
||||
}
|
||||
// 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 nextTick();
|
||||
};
|
||||
|
||||
QUnit.module("JournalDashboardGraphField");
|
||||
|
||||
QUnit.test("JournalDashboardGraphField is rendered correctly", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<field name="graph_type"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "in", [1, 2]]],
|
||||
});
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_dashboard_graph canvas",
|
||||
2,
|
||||
"there should be two graphs rendered"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:nth-child(1) .o_graph_barchart",
|
||||
"graph of first record should be a barchart"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:nth-child(2) .o_graph_linechart",
|
||||
"graph of second record should be a linechart"
|
||||
);
|
||||
|
||||
await reloadKanbanView(target);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_dashboard_graph canvas",
|
||||
2,
|
||||
"there should be two graphs rendered"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"rendering of a JournalDashboardGraphField in an updated grouped kanban view",
|
||||
async function (assert) {
|
||||
const kanban = await makeView({
|
||||
serverData,
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<field name="graph_type"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "in", [1, 2]]],
|
||||
});
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_dashboard_graph canvas",
|
||||
2,
|
||||
"there should be two graph rendered"
|
||||
);
|
||||
|
||||
await reload(kanban, { groupBy: ["selection"], domain: [["int_field", "=", 10]] });
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_dashboard_graph canvas",
|
||||
"there should be one graph rendered"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
selection: {
|
||||
string: "Selection",
|
||||
type: "selection",
|
||||
searchable: true,
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
foo: "yop",
|
||||
selection: "blocked",
|
||||
},
|
||||
{
|
||||
foo: "blip",
|
||||
selection: "normal",
|
||||
},
|
||||
{
|
||||
foo: "abc",
|
||||
selection: "done",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("LabelSelectionField");
|
||||
|
||||
QUnit.test("LabelSelectionField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="label_selection"
|
||||
options="{'classes': {'normal': 'secondary', 'blocked': 'warning','done': 'success'}}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-warning",
|
||||
"should have a warning status label since selection is the second, blocked state"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-secondary",
|
||||
"should not have a default status since selection is the second, blocked state"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-success",
|
||||
"should not have a success status since selection is the second, blocked state"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-warning").textContent,
|
||||
"Blocked",
|
||||
"the label should say 'Blocked' since this is the label value for that state"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("LabelSelectionField in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="foo"/>
|
||||
<field name="selection" widget="label_selection"
|
||||
options="{'classes': {'normal': 'secondary', 'blocked': 'warning','done': 'success'}}"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_widget .badge:not(:empty)").length,
|
||||
3,
|
||||
"should have three visible status labels"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-warning",
|
||||
"should have one warning status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-warning").textContent,
|
||||
"Blocked",
|
||||
"the warning label should read 'Blocked'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-secondary",
|
||||
"should have one default status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-secondary").textContent,
|
||||
"Normal",
|
||||
"the default label should read 'Normal'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-success",
|
||||
"should have one success status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-success").textContent,
|
||||
"Done",
|
||||
"the success label should read 'Done'"
|
||||
);
|
||||
|
||||
// switch to edit mode and check the result
|
||||
await click(target.querySelector("tbody td:not(.o_list_record_selector)"));
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_widget .badge:not(:empty)").length,
|
||||
3,
|
||||
"should have three visible status labels"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-warning",
|
||||
"should have one warning status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-warning").textContent,
|
||||
"Blocked",
|
||||
"the warning label should read 'Blocked'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-secondary",
|
||||
"should have one default status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-secondary").textContent,
|
||||
"Normal",
|
||||
"the default label should read 'Normal'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-success",
|
||||
"should have one success status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-success").textContent,
|
||||
"Done",
|
||||
"the success label should read 'Done'"
|
||||
);
|
||||
|
||||
// save and check the result
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_widget .badge:not(:empty)").length,
|
||||
3,
|
||||
"should have three visible status labels"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-warning",
|
||||
"should have one warning status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-warning").textContent,
|
||||
"Blocked",
|
||||
"the warning label should read 'Blocked'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-secondary",
|
||||
"should have one default status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-secondary").textContent,
|
||||
"Normal",
|
||||
"the default label should read 'Normal'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-success",
|
||||
"should have one success status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-success").textContent,
|
||||
"Done",
|
||||
"the success label should read 'Done'"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, getFixture, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
turtle: {
|
||||
fields: {
|
||||
picture_ids: {
|
||||
string: "Pictures",
|
||||
type: "many2many",
|
||||
relation: "ir.attachment",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
picture_ids: [17],
|
||||
},
|
||||
],
|
||||
},
|
||||
"ir.attachment": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
mimetype: { string: "Mimetype", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 17,
|
||||
name: "Marley&Me.jpg",
|
||||
mimetype: "jpg",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Many2ManyBinaryField");
|
||||
|
||||
QUnit.test("widget many2many_binary", async function (assert) {
|
||||
assert.expect(24);
|
||||
|
||||
const fakeHTTPService = {
|
||||
start() {
|
||||
return {
|
||||
post: (route, params) => {
|
||||
assert.strictEqual(route, "/web/binary/upload_attachment");
|
||||
assert.strictEqual(
|
||||
params.ufile[0].name,
|
||||
"fake_file.tiff",
|
||||
"file is correctly uploaded to the server"
|
||||
);
|
||||
const file = {
|
||||
id: 10,
|
||||
name: params.ufile[0].name,
|
||||
mimetype: "text/plain",
|
||||
};
|
||||
serverData.models["ir.attachment"].records.push(file);
|
||||
return JSON.stringify([file]);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("http", fakeHTTPService);
|
||||
|
||||
serverData.views = {
|
||||
"ir.attachment,false,list": '<tree string="Pictures"><field name="name"/></tree>',
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="picture_ids" widget="many2many_binary" options="{'accepted_file_extensions': 'image/*'}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(route, args) {
|
||||
if (args.method !== "get_views") {
|
||||
assert.step(route);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/ir.attachment/read") {
|
||||
assert.deepEqual(args.args[1], ["name", "mimetype"]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload",
|
||||
"there should be the attachment widget"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachments",
|
||||
"there should be one attachment"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attach",
|
||||
"there should be an Add button (edit)"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete",
|
||||
"there should be a Delete button (edit)"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attach",
|
||||
"there should be an Add button"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("div.o_field_widget .oe_fileupload .o_attach").textContent.trim(),
|
||||
"Pictures",
|
||||
"the button should be correctly named"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_input_file").getAttribute("accept"),
|
||||
"image/*",
|
||||
'there should be an attribute "accept" on the input'
|
||||
);
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/turtle/read",
|
||||
"/web/dataset/call_kw/ir.attachment/read",
|
||||
]);
|
||||
|
||||
// Set and trigger the change of a file for the input
|
||||
const fileInput = target.querySelector('input[type="file"]');
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(new File(["fake_file"], "fake_file.tiff", { type: "text/plain" }));
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_attachment:nth-child(2) .caption a").textContent,
|
||||
"fake_file.tiff",
|
||||
'value of attachment should be "fake_file.tiff"'
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_attachment:nth-child(2) .caption.small a").textContent,
|
||||
"tiff",
|
||||
"file extension should be correct"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_attachment:nth-child(2) .o_image.o_hover").dataset.mimetype,
|
||||
"text/plain",
|
||||
"preview displays the right mimetype"
|
||||
);
|
||||
|
||||
// delete the attachment
|
||||
await click(
|
||||
target.querySelector(
|
||||
"div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete"
|
||||
)
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachments",
|
||||
"there should be only one attachment left"
|
||||
);
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/ir.attachment/read",
|
||||
"/web/dataset/call_kw/turtle/write",
|
||||
"/web/dataset/call_kw/turtle/read",
|
||||
"/web/dataset/call_kw/ir.attachment/read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("widget many2many_binary displays notification on error", async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
const fakeHTTPService = {
|
||||
start() {
|
||||
return {
|
||||
post: (route, params) => {
|
||||
assert.strictEqual(route, "/web/binary/upload_attachment");
|
||||
assert.deepEqual(
|
||||
[params.ufile[0].name, params.ufile[1].name],
|
||||
["good_file.txt", "bad_file.txt"],
|
||||
"files are correctly sent to the server"
|
||||
);
|
||||
const files = [
|
||||
{
|
||||
id: 10,
|
||||
name: params.ufile[0].name,
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: params.ufile[1].name,
|
||||
mimetype: "text/plain",
|
||||
error: `Error on file: ${params.ufile[1].name}`,
|
||||
},
|
||||
];
|
||||
serverData.models["ir.attachment"].records.push(files[0]);
|
||||
return JSON.stringify(files);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("http", fakeHTTPService);
|
||||
|
||||
serverData.views = {
|
||||
"ir.attachment,false,list": '<tree string="Pictures"><field name="name"/></tree>',
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="picture_ids" widget="many2many_binary" options="{'accepted_file_extensions': 'image/*'}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload",
|
||||
"there should be the attachment widget"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachments",
|
||||
"there should be one attachment"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attach",
|
||||
"there should be an Add button (edit)"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete",
|
||||
"there should be a Delete button (edit)"
|
||||
);
|
||||
|
||||
// Set and trigger the import of 2 files in the input
|
||||
const fileInput = target.querySelector('input[type="file"]');
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(new File(["good_file"], "good_file.txt", { type: "text/plain" }));
|
||||
dataTransfer.items.add(new File(["bad_file"], "bad_file.txt", { type: "text/plain" }));
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_attachment:nth-child(2) .caption a").textContent,
|
||||
"good_file.txt",
|
||||
'value of attachment should be "good_file.txt"'
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachments",
|
||||
"there should be only one attachment uploaded"
|
||||
);
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_title").textContent,
|
||||
"Uploading error"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_content").textContent,
|
||||
"Error on file: bad_file.txt"
|
||||
);
|
||||
assert.hasClass(target.querySelector(".o_notification"), "border-danger");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,411 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: { string: "int_field", type: "integer", sortable: true },
|
||||
timmy: { string: "pokemon", type: "many2many", relation: "partner_type" },
|
||||
p: {
|
||||
string: "one2many field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
relation_field: "trululu",
|
||||
},
|
||||
trululu: { string: "Trululu", type: "many2one", relation: "partner" },
|
||||
},
|
||||
records: [{ id: 1, int_field: 10, p: [1] }],
|
||||
onchanges: {},
|
||||
},
|
||||
partner_type: {
|
||||
records: [
|
||||
{ id: 12, display_name: "gold" },
|
||||
{ id: 14, display_name: "silver" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Many2ManyCheckBoxesField");
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField", async function (assert) {
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
const commands = [[[6, false, [12, 14]]], [[6, false, [14]]]];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "write") {
|
||||
assert.step("write");
|
||||
assert.deepEqual(args.args[1].timmy, commands.shift());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(target, "div.o_field_widget div.form-check", 2);
|
||||
|
||||
let checkboxes = target.querySelectorAll("div.o_field_widget div.form-check input");
|
||||
assert.ok(checkboxes[0].checked);
|
||||
assert.notOk(checkboxes[1].checked);
|
||||
|
||||
assert.containsNone(target, "div.o_field_widget div.form-check input:disabled");
|
||||
|
||||
// add a m2m value by clicking on input
|
||||
checkboxes = target.querySelectorAll("div.o_field_widget div.form-check input");
|
||||
await click(checkboxes[1]);
|
||||
await clickSave(target);
|
||||
assert.containsN(target, "div.o_field_widget div.form-check input:checked", 2);
|
||||
|
||||
// remove a m2m value by clinking on label
|
||||
await click(target.querySelector("div.o_field_widget div.form-check > label"));
|
||||
await clickSave(target);
|
||||
checkboxes = target.querySelectorAll("div.o_field_widget div.form-check input");
|
||||
assert.notOk(checkboxes[0].checked);
|
||||
assert.ok(checkboxes[1].checked);
|
||||
|
||||
assert.verifySteps(["write", "write"]);
|
||||
});
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField (readonly)", async function (assert) {
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" attrs="{'readonly': true}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
"div.o_field_widget div.form-check",
|
||||
2,
|
||||
"should have fetched and displayed the 2 values of the many2many"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"div.o_field_widget div.form-check input:disabled",
|
||||
2,
|
||||
"the checkboxes should be disabled"
|
||||
);
|
||||
|
||||
await click(target.querySelectorAll("div.o_field_widget div.form-check > label")[1]);
|
||||
|
||||
assert.ok(
|
||||
target.querySelector("div.o_field_widget div.form-check input").checked,
|
||||
"first checkbox should be checked"
|
||||
);
|
||||
assert.notOk(
|
||||
target.querySelectorAll("div.o_field_widget div.form-check input")[1].checked,
|
||||
"second checkbox should not be checked"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Many2ManyCheckBoxesField: start non empty, then remove twice",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[0].timmy = [12, 14];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target.querySelectorAll("div.o_field_widget div.form-check input")[0]);
|
||||
await click(target.querySelectorAll("div.o_field_widget div.form-check input")[1]);
|
||||
await clickSave(target);
|
||||
assert.notOk(
|
||||
target.querySelectorAll("div.o_field_widget div.form-check input")[0].checked,
|
||||
"first checkbox should not be checked"
|
||||
);
|
||||
assert.notOk(
|
||||
target.querySelectorAll("div.o_field_widget div.form-check input")[1].checked,
|
||||
"second checkbox should not be checked"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"Many2ManyCheckBoxesField: values are updated when domain changes",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field" />
|
||||
<field name="timmy" widget="many2many_checkboxes" domain="[['id', '>', int_field]]" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='int_field'] input").value,
|
||||
"10"
|
||||
);
|
||||
assert.containsN(target, ".o_field_widget[name='timmy'] .form-check", 2);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='timmy']").textContent,
|
||||
"goldsilver"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name='int_field'] input", 13);
|
||||
assert.containsOnce(target, ".o_field_widget[name='timmy'] .form-check");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='timmy']").textContent,
|
||||
"silver"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField with 40+ values", async function (assert) {
|
||||
// 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.
|
||||
assert.expect(3);
|
||||
|
||||
const records = [];
|
||||
for (let id = 1; id <= 90; id++) {
|
||||
records.push({
|
||||
id,
|
||||
display_name: `type ${id}`,
|
||||
});
|
||||
}
|
||||
serverData.models.partner_type.records = records;
|
||||
serverData.models.partner.records[0].timmy = records.map((r) => r.id);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</form>`,
|
||||
mockRPC(route, { args, method }) {
|
||||
if (method === "write") {
|
||||
const expectedIds = records.map((r) => r.id);
|
||||
expectedIds.pop();
|
||||
assert.deepEqual(args[1].timmy, [[6, false, expectedIds]]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget[name='timmy'] input[type='checkbox']:checked",
|
||||
90
|
||||
);
|
||||
|
||||
// toggle the last value
|
||||
let checkboxes = target.querySelectorAll(
|
||||
".o_field_widget[name='timmy'] input[type='checkbox']"
|
||||
);
|
||||
await click(checkboxes[checkboxes.length - 1]);
|
||||
|
||||
await clickSave(target);
|
||||
checkboxes = target.querySelectorAll(
|
||||
".o_field_widget[name='timmy'] input[type='checkbox']"
|
||||
);
|
||||
assert.notOk(checkboxes[checkboxes.length - 1].checked);
|
||||
});
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField with 100+ values", async function (assert) {
|
||||
// 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.
|
||||
assert.expect(7);
|
||||
|
||||
const records = [];
|
||||
for (let id = 1; id < 150; id++) {
|
||||
records.push({
|
||||
id,
|
||||
display_name: `type ${id}`,
|
||||
});
|
||||
}
|
||||
serverData.models.partner_type.records = records;
|
||||
serverData.models.partner.records[0].timmy = records.map((r) => r.id);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</form>`,
|
||||
async mockRPC(route, { args, method }) {
|
||||
if (method === "write") {
|
||||
const expectedIds = records.map((r) => r.id);
|
||||
expectedIds.shift();
|
||||
assert.deepEqual(args[1].timmy, [[6, false, expectedIds]]);
|
||||
assert.step("write");
|
||||
}
|
||||
if (method === "name_search") {
|
||||
assert.step("name_search");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget[name='timmy'] input[type='checkbox']",
|
||||
100,
|
||||
"should only display 100 checkboxes"
|
||||
);
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget[name='timmy'] input[type='checkbox']").checked
|
||||
);
|
||||
|
||||
// toggle the first value
|
||||
await click(target.querySelector(".o_field_widget[name='timmy'] input[type='checkbox']"));
|
||||
|
||||
await clickSave(target);
|
||||
assert.notOk(
|
||||
target.querySelector(".o_field_widget[name='timmy'] input[type='checkbox']").checked
|
||||
);
|
||||
assert.verifySteps(["name_search", "write"]);
|
||||
});
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField in a one2many", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
serverData.models.partner_type.records.push({ id: 15, display_name: "bronze" });
|
||||
serverData.models.partner.records[0].timmy = [14, 15];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree><field name="id"/></tree>
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "write") {
|
||||
assert.deepEqual(args.args[1], {
|
||||
p: [[1, 1, { timmy: [[6, false, [15, 12]]] }]],
|
||||
});
|
||||
}
|
||||
},
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(target.querySelector(".o_data_cell"));
|
||||
|
||||
// edit the timmy field by (un)checking boxes on the widget
|
||||
const firstCheckbox = target.querySelector(".modal .form-check-input");
|
||||
await click(firstCheckbox);
|
||||
assert.ok(firstCheckbox.checked, "the checkbox should be ticked");
|
||||
const secondCheckbox = target.querySelectorAll(".modal .form-check-input")[1];
|
||||
await click(secondCheckbox);
|
||||
assert.notOk(secondCheckbox.checked, "the checkbox should be unticked");
|
||||
|
||||
await click(target.querySelector(".modal .o_form_button_save"));
|
||||
await clickSave(target);
|
||||
});
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField with default values", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
serverData.models.partner.fields.timmy.default = [3];
|
||||
serverData.models.partner.fields.timmy.type = "many2many";
|
||||
serverData.models.partner_type.records.push({ id: 3, display_name: "bronze" });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes"/>
|
||||
</form>`,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
assert.deepEqual(
|
||||
args.args[0].timmy,
|
||||
[[6, false, [12]]],
|
||||
"correct values should have been sent to create"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.notOk(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[0].checked,
|
||||
"first checkbox should not be checked"
|
||||
);
|
||||
assert.notOk(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[1].checked,
|
||||
"second checkbox should not be checked"
|
||||
);
|
||||
assert.ok(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[2].checked,
|
||||
"third checkbox should be checked"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_form_view .form-check input:checked"));
|
||||
await click(target.querySelector(".o_form_view .form-check input"));
|
||||
await click(target.querySelector(".o_form_view .form-check input"));
|
||||
await click(target.querySelector(".o_form_view .form-check input"));
|
||||
|
||||
assert.ok(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[0].checked,
|
||||
"first checkbox should be checked"
|
||||
);
|
||||
assert.notOk(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[1].checked,
|
||||
"second checkbox should not be checked"
|
||||
);
|
||||
assert.notOk(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[2].checked,
|
||||
"third checkbox should not be checked"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,456 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, getFixture, selectDropdownItem } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { triggerHotkey } from "../../helpers/utils";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "first record" },
|
||||
{ id: 2, display_name: "second record" },
|
||||
{ id: 4, display_name: "aaa" },
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
turtle: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
partner_ids: { string: "Partner", type: "many2many", relation: "partner" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "leonardo", partner_ids: [] },
|
||||
{ id: 2, display_name: "donatello", partner_ids: [2, 4] },
|
||||
{ id: 3, display_name: "raphael" },
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Many2ManyTagsAvatarField");
|
||||
|
||||
QUnit.test("widget many2many_tags_avatar", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_many2many_tags_avatar.o_field_widget .badge",
|
||||
2,
|
||||
"should have 2 records"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .badge img").dataset
|
||||
.src,
|
||||
"/web/image/partner/2/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("widget many2many_tags_avatar in list view", async function (assert) {
|
||||
const records = [];
|
||||
for (let id = 5; id <= 15; id++) {
|
||||
records.push({
|
||||
id,
|
||||
display_name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
serverData.models.partner.records = serverData.models.partner.records.concat(records);
|
||||
|
||||
serverData.models.turtle.records.push({
|
||||
id: 4,
|
||||
display_name: "crime master gogo",
|
||||
partner_ids: [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
|
||||
});
|
||||
serverData.models.turtle.records[0].partner_ids = [1];
|
||||
serverData.models.turtle.records[1].partner_ids = [1, 2, 4, 5, 6, 7];
|
||||
serverData.models.turtle.records[2].partner_ids = [1, 2, 4, 5, 7];
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "turtle",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</tree>`,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row .o_field_many2many_tags_avatar img.o_m2m_avatar")
|
||||
.dataset.src,
|
||||
"/web/image/partner/1/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(
|
||||
".o_data_row .o_many2many_tags_avatar_cell .o_field_many2many_tags_avatar"
|
||||
)
|
||||
.textContent.trim(),
|
||||
"first record",
|
||||
"should display like many2one avatar if there is only one record"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:not(.o_m2m_avatar_empty)",
|
||||
4,
|
||||
"should have 4 records"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(3) .o_field_many2many_tags_avatar .o_tag:not(.o_m2m_avatar_empty)",
|
||||
5,
|
||||
"should have 5 records"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty",
|
||||
"should have o_m2m_avatar_empty span"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
)
|
||||
.textContent.trim(),
|
||||
"+2",
|
||||
"should have +2 in o_m2m_avatar_empty"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/1/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:nth-child(2) img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/2/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:nth-child(3) img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/4/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:nth-child(4) img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/5/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_data_row:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty",
|
||||
"should have o_m2m_avatar_empty span"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_tag:not(.o_m2m_avatar_empty)",
|
||||
4,
|
||||
"should have 4 records"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty",
|
||||
"should have o_m2m_avatar_empty span"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
)
|
||||
.textContent.trim(),
|
||||
"+9",
|
||||
"should have +9 in o_m2m_avatar_empty"
|
||||
);
|
||||
|
||||
// check data-tooltip attribute (used by the tooltip service)
|
||||
const tag = target.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
);
|
||||
assert.strictEqual(
|
||||
tag.dataset["tooltipTemplate"],
|
||||
"web.TagsList.Tooltip",
|
||||
"uses the proper tooltip template"
|
||||
);
|
||||
const tooltipInfo = JSON.parse(tag.dataset["tooltipInfo"]);
|
||||
assert.strictEqual(
|
||||
tooltipInfo.tags.map((tag) => tag.text).join(" "),
|
||||
"record 6 record 7",
|
||||
"shows a tooltip on hover"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_data_row .o_many2many_tags_avatar_cell"));
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row.o_selected_row .o_many2many_tags_avatar_cell .badge",
|
||||
1,
|
||||
"should have 1 many2many badges in edit mode"
|
||||
);
|
||||
|
||||
await selectDropdownItem(target, "partner_ids", "second record");
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:first-child .o_field_many2many_tags_avatar .o_tag",
|
||||
2,
|
||||
"should have 2 records"
|
||||
);
|
||||
|
||||
// Select the first row and enter edit mode on the x2many field.
|
||||
await click(target, ".o_data_row:nth-child(1) .o_list_record_selector input");
|
||||
await click(target, ".o_data_row:nth-child(1) .o_data_cell");
|
||||
|
||||
// Only the first row should have tags with delete buttons.
|
||||
assert.containsN(target, ".o_data_row:nth-child(1) .o_field_tags span .o_delete", 2);
|
||||
assert.containsNone(target, ".o_data_row:nth-child(2) .o_field_tags span .o_delete");
|
||||
assert.containsNone(target, ".o_data_row:nth-child(3) .o_field_tags span .o_delete");
|
||||
assert.containsNone(target, ".o_data_row:nth-child(4) .o_field_tags span .o_delete");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"widget many2many_tags_avatar list view - don't crash on keyboard navigation",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "turtle",
|
||||
serverData,
|
||||
arch: /*xml*/ `
|
||||
<tree editable="bottom">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</tree>
|
||||
`,
|
||||
});
|
||||
|
||||
// Select the 2nd row and enter edit mode on the x2many field.
|
||||
await click(target, ".o_data_row:nth-child(2) .o_list_record_selector input");
|
||||
await click(target, ".o_data_row:nth-child(2) .o_data_cell");
|
||||
|
||||
// Pressing left arrow should focus on the right-most (second) tag.
|
||||
await triggerHotkey("arrowleft");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:nth-child(2) .o_field_tags span:nth-child(2)"),
|
||||
document.activeElement
|
||||
);
|
||||
|
||||
// Pressing left arrow again should not crash and should focus on the first tag.
|
||||
await triggerHotkey("arrowleft");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:nth-child(2) .o_field_tags span:nth-child(1)"),
|
||||
document.activeElement
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("widget many2many_tags_avatar in kanban view", async function (assert) {
|
||||
assert.expect(13);
|
||||
|
||||
const records = [];
|
||||
for (let id = 5; id <= 15; id++) {
|
||||
records.push({
|
||||
id,
|
||||
display_name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
serverData.models.partner.records = serverData.models.partner.records.concat(records);
|
||||
|
||||
serverData.models.turtle.records.push({
|
||||
id: 4,
|
||||
display_name: "crime master gogo",
|
||||
partner_ids: [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
|
||||
});
|
||||
serverData.models.turtle.records[0].partner_ids = [1];
|
||||
serverData.models.turtle.records[1].partner_ids = [1, 2, 4];
|
||||
serverData.models.turtle.records[2].partner_ids = [1, 2, 4, 5];
|
||||
serverData.views = {
|
||||
"turtle,false,form": '<form><field name="display_name"/></form>',
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "turtle",
|
||||
serverData,
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="display_name"/>
|
||||
<div class="oe_kanban_footer">
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
selectRecord(recordId) {
|
||||
assert.strictEqual(
|
||||
recordId,
|
||||
1,
|
||||
"should call its selectRecord prop with the clicked record"
|
||||
);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_kanban_record:first-child .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/1/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag",
|
||||
3,
|
||||
"should have 3 records"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_tag",
|
||||
2,
|
||||
"should have 2 records"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/1/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
)[1].dataset.src,
|
||||
"/web/image/partner/2/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty",
|
||||
"should have o_m2m_avatar_empty span"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
)
|
||||
.textContent.trim(),
|
||||
"+2",
|
||||
"should have +2 in o_m2m_avatar_empty"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_tag",
|
||||
2,
|
||||
"should have 2 records"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty",
|
||||
"should have o_m2m_avatar_empty span"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
)
|
||||
.textContent.trim(),
|
||||
"9+",
|
||||
"should have 9+ in o_m2m_avatar_empty"
|
||||
);
|
||||
|
||||
// check data-tooltip attribute (used by the tooltip service)
|
||||
const tag = target.querySelector(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
);
|
||||
assert.strictEqual(
|
||||
tag.dataset["tooltipTemplate"],
|
||||
"web.TagsList.Tooltip",
|
||||
"uses the proper tooltip template"
|
||||
);
|
||||
const tooltipInfo = JSON.parse(tag.dataset["tooltipInfo"]);
|
||||
assert.strictEqual(
|
||||
tooltipInfo.tags.map((tag) => tag.text).join(" "),
|
||||
"aaa record 5",
|
||||
"shows a tooltip on hover"
|
||||
);
|
||||
|
||||
await click(
|
||||
target.querySelector(".o_kanban_record .o_field_many2many_tags_avatar img.o_m2m_avatar")
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("widget many2many_tags_avatar delete tag", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
resId: 2,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_many2many_tags_avatar.o_field_widget .badge",
|
||||
2,
|
||||
"should have 2 records"
|
||||
);
|
||||
|
||||
await click(
|
||||
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .badge .o_delete")
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_many2many_tags_avatar.o_field_widget .badge",
|
||||
"should have 1 record"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_many2many_tags_avatar.o_field_widget .badge",
|
||||
"should have 1 record"
|
||||
);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,342 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
getNodesTextContent,
|
||||
patchWithCleanup,
|
||||
selectDropdownItem,
|
||||
triggerEvent,
|
||||
clickDiscard,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: { string: "int_field", type: "integer" },
|
||||
user_id: { string: "User", type: "many2one", relation: "user" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, user_id: 17 },
|
||||
{ id: 2, user_id: 19 },
|
||||
{ id: 3, user_id: 17 },
|
||||
{ id: 4, user_id: false },
|
||||
],
|
||||
},
|
||||
user: {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
partner_ids: {
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
relation_field: "user_id",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 17,
|
||||
name: "Aline",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Christine",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("Many2OneAvatar");
|
||||
|
||||
QUnit.test("basic form view flow", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=user_id] input").value,
|
||||
"Aline"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/17/avatar_128"]'
|
||||
);
|
||||
assert.containsOnce(target, '.o_field_many2one_avatar > div[data-tooltip="Aline"]');
|
||||
|
||||
assert.containsOnce(target, ".o_input_dropdown");
|
||||
assert.strictEqual(target.querySelector(".o_input_dropdown input").value, "Aline");
|
||||
assert.containsOnce(target, ".o_external_button");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/17/avatar_128"]'
|
||||
);
|
||||
|
||||
await selectDropdownItem(target, "user_id", "Christine");
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/19/avatar_128"]'
|
||||
);
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=user_id] input").value,
|
||||
"Christine"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/19/avatar_128"]'
|
||||
);
|
||||
|
||||
await editInput(target, '.o_field_widget[name="user_id"] input', "");
|
||||
|
||||
assert.containsNone(target, ".o_m2o_avatar > img");
|
||||
assert.containsOnce(target, ".o_m2o_avatar > .o_m2o_avatar_empty");
|
||||
await clickSave(target);
|
||||
|
||||
assert.containsNone(target, ".o_m2o_avatar > img");
|
||||
assert.containsOnce(target, ".o_m2o_avatar > .o_m2o_avatar_empty");
|
||||
});
|
||||
|
||||
QUnit.test("onchange in form view flow", async function (assert) {
|
||||
serverData.models.partner.onchanges = {
|
||||
int_field: function (obj) {
|
||||
if (obj.int_field === 1) {
|
||||
obj.user_id = [19, "Christine"];
|
||||
} else if (obj.int_field === 2) {
|
||||
obj.user_id = false;
|
||||
} else {
|
||||
obj.user_id = [17, "Aline"]; // default value
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field"/>
|
||||
<field name="user_id" widget="many2one_avatar" readonly="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=user_id]").textContent.trim(),
|
||||
"Aline"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/17/avatar_128"]'
|
||||
);
|
||||
|
||||
await editInput(target, "div[name=int_field] input", 1);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=user_id]").textContent.trim(),
|
||||
"Christine"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/19/avatar_128"]'
|
||||
);
|
||||
|
||||
await editInput(target, "div[name=int_field] input", 2);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=user_id]").textContent.trim(),
|
||||
""
|
||||
);
|
||||
assert.containsNone(target, ".o_m2o_avatar > img");
|
||||
});
|
||||
|
||||
QUnit.test("basic list view flow", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<tree><field name="user_id" widget="many2one_avatar"/></tree>',
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id'] span span")),
|
||||
["Aline", "Christine", "Aline", ""]
|
||||
);
|
||||
const imgs = target.querySelectorAll(".o_m2o_avatar > img");
|
||||
assert.strictEqual(imgs[0].dataset.src, "/web/image/user/17/avatar_128");
|
||||
assert.strictEqual(imgs[1].dataset.src, "/web/image/user/19/avatar_128");
|
||||
assert.strictEqual(imgs[2].dataset.src, "/web/image/user/17/avatar_128");
|
||||
});
|
||||
|
||||
QUnit.test("basic flow in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<tree editable="top"><field name="user_id" widget="many2one_avatar"/></tree>',
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id'] span span")),
|
||||
["Aline", "Christine", "Aline", ""]
|
||||
);
|
||||
|
||||
const imgs = target.querySelectorAll(".o_m2o_avatar > img");
|
||||
assert.strictEqual(imgs[0].dataset.src, "/web/image/user/17/avatar_128");
|
||||
assert.strictEqual(imgs[1].dataset.src, "/web/image/user/19/avatar_128");
|
||||
assert.strictEqual(imgs[2].dataset.src, "/web/image/user/17/avatar_128");
|
||||
|
||||
await click(target.querySelectorAll(".o_data_row .o_data_cell")[0]);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_m2o_avatar > img").dataset.src,
|
||||
"/web/image/user/17/avatar_128"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Many2OneAvatar with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch:
|
||||
'<form><field name="user_id" widget="many2one_avatar" placeholder="Placeholder"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='user_id'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("click on many2one_avatar in a list view (multi_edit='1')", async function (assert) {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
assert.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree multi_edit="1">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
await click(target.querySelectorAll(".o_data_row")[0], ".o_list_record_selector input");
|
||||
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id'] span span"));
|
||||
assert.hasClass(target.querySelector(".o_data_row"), "o_selected_row");
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("click on many2one_avatar in an editable list view", async function (assert) {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
assert.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
await click(target.querySelectorAll(".o_data_row")[0], ".o_list_record_selector input");
|
||||
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id'] span span"));
|
||||
assert.hasClass(target.querySelector(".o_data_row"), "o_selected_row");
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("click on many2one_avatar in an editable list view", async function (assert) {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
assert.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id'] span span"));
|
||||
assert.containsNone(target, ".o_selected_row");
|
||||
|
||||
assert.verifySteps(["openRecord"]);
|
||||
});
|
||||
|
||||
QUnit.test("cancelling create dialog should clear value in the field", async function (assert) {
|
||||
serverData.views = {
|
||||
"user,false,form": `
|
||||
<form>
|
||||
<field name="name" />
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
await click(target.querySelectorAll(".o_data_cell")[0]);
|
||||
const input = target.querySelector(".o_field_widget[name=user_id] input");
|
||||
input.value = "yy";
|
||||
await triggerEvent(input, null, "input");
|
||||
await click(target, ".o_field_widget[name=user_id] input");
|
||||
await selectDropdownItem(target, "user_id", "Create and edit...");
|
||||
|
||||
await clickDiscard(target.querySelector(".modal"));
|
||||
assert.strictEqual(target.querySelector(".o_field_widget[name=user_id] input").value, "");
|
||||
assert.containsOnce(target, ".o_field_widget[name=user_id] span.o_m2o_avatar_empty");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { click, clickSave, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
import * as BarcodeScanner from "@web/webclient/barcode/barcode_scanner";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
const CREATE = "create";
|
||||
const NAME_SEARCH = "name_search";
|
||||
const PRODUCT_PRODUCT = "product.product";
|
||||
const SALE_ORDER_LINE = "sale_order_line";
|
||||
const PRODUCT_FIELD_NAME = "product_id";
|
||||
|
||||
// MockRPC to allow the search in barcode too
|
||||
async function barcodeMockRPC(route, args, performRPC) {
|
||||
if (args.method === NAME_SEARCH && args.model === PRODUCT_PRODUCT) {
|
||||
const result = await performRPC(route, args);
|
||||
const records = serverData.models[PRODUCT_PRODUCT].records
|
||||
.filter((record) => record.barcode === args.kwargs.name)
|
||||
.map((record) => [record.id, record.name]);
|
||||
return records.concat(result);
|
||||
}
|
||||
}
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
[PRODUCT_PRODUCT]: {
|
||||
fields: {
|
||||
id: { type: "integer" },
|
||||
name: {},
|
||||
barcode: {},
|
||||
},
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
[SALE_ORDER_LINE]: {
|
||||
fields: {
|
||||
id: { type: "integer" },
|
||||
[PRODUCT_FIELD_NAME]: {
|
||||
string: PRODUCT_FIELD_NAME,
|
||||
type: "many2one",
|
||||
relation: PRODUCT_PRODUCT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
|
||||
patchWithCleanup(AutoComplete, {
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
// simulate a environment with a camera/webcam
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
Object.assign({}, browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
navigator: {
|
||||
userAgent: "Chrome/0.0.0 (Linux; Android 13; Odoo TestSuite)",
|
||||
mediaDevices: {
|
||||
getUserMedia: () => [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.module("Many2OneField Barcode (Desktop)");
|
||||
|
||||
QUnit.test(
|
||||
"Many2OneBarcode component should display the barcode icon",
|
||||
async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: SALE_ORDER_LINE,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="${PRODUCT_FIELD_NAME}" widget="many2one_barcode"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
const scanButton = target.querySelector(".o_barcode");
|
||||
assert.containsOnce(target, scanButton, "has scanner barcode button");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("barcode button with single results", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = serverData.models[PRODUCT_PRODUCT].records[0];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => selectedRecordTest.barcode,
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: SALE_ORDER_LINE,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="${PRODUCT_FIELD_NAME}" options="{'can_scan_barcode': True}"/>
|
||||
</form>
|
||||
`,
|
||||
async mockRPC(route, args, performRPC) {
|
||||
if (args.method === CREATE && args.model === SALE_ORDER_LINE) {
|
||||
const selectedId = args.args[0][PRODUCT_FIELD_NAME];
|
||||
assert.equal(
|
||||
selectedId,
|
||||
selectedRecordTest.id,
|
||||
`product id selected ${selectedId}, should be ${selectedRecordTest.id} (${selectedRecordTest.barcode})`
|
||||
);
|
||||
return performRPC(route, args, performRPC);
|
||||
}
|
||||
return barcodeMockRPC(route, args, performRPC);
|
||||
},
|
||||
});
|
||||
|
||||
const scanButton = target.querySelector(".o_barcode");
|
||||
assert.containsOnce(target, scanButton, "has scanner barcode button");
|
||||
|
||||
await click(target, ".o_barcode");
|
||||
await clickSave(target);
|
||||
});
|
||||
|
||||
QUnit.test("barcode button with multiple results", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = serverData.models[PRODUCT_PRODUCT].records[1];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => "mask",
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: SALE_ORDER_LINE,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="${PRODUCT_FIELD_NAME}" options="{'can_scan_barcode': True}"/>
|
||||
</form>`,
|
||||
async mockRPC(route, args, performRPC) {
|
||||
if (args.method === CREATE && args.model === SALE_ORDER_LINE) {
|
||||
const selectedId = args.args[0][PRODUCT_FIELD_NAME];
|
||||
assert.equal(
|
||||
selectedId,
|
||||
selectedRecordTest.id,
|
||||
`product id selected ${selectedId}, should be ${selectedRecordTest.id} (${selectedRecordTest.barcode})`
|
||||
);
|
||||
return performRPC(route, args, performRPC);
|
||||
}
|
||||
return barcodeMockRPC(route, args, performRPC);
|
||||
},
|
||||
});
|
||||
|
||||
const scanButton = target.querySelector(".o_barcode");
|
||||
assert.containsOnce(target, scanButton, "has scanner barcode button");
|
||||
|
||||
await click(target, ".o_barcode");
|
||||
|
||||
const autocompleteDropdown = target.querySelector(".o-autocomplete--dropdown-menu");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
autocompleteDropdown,
|
||||
"there should be one autocomplete dropdown opened"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
autocompleteDropdown,
|
||||
".o-autocomplete--dropdown-item.ui-menu-item:not(.o_m2o_dropdown_option)",
|
||||
2,
|
||||
"there should be 2 records displayed"
|
||||
);
|
||||
|
||||
await click(autocompleteDropdown, ".o-autocomplete--dropdown-item:nth-child(1)");
|
||||
await clickSave(target);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,69 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture, getNodesTextContent } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
res_id: {
|
||||
string: "Ressource Id",
|
||||
type: "many2one_reference",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, res_id: 10 },
|
||||
{ id: 2, res_id: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Many2OneReferenceField");
|
||||
|
||||
QUnit.test("Many2OneReferenceField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="res_id"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "10");
|
||||
});
|
||||
|
||||
QUnit.test("Many2OneReferenceField in list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<list><field name="res_id"/></list>',
|
||||
});
|
||||
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_data_cell")), ["10", ""]);
|
||||
});
|
||||
|
||||
QUnit.test("should be 0 when unset", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: '<form><field name="res_id"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,361 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { useNumpadDecimal } from "@web/views/fields/numpad_decimal_hook";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
|
||||
const { Component, mount, useState, xml } = owl;
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
qux: { string: "Qux", type: "float", digits: [16, 1], searchable: true },
|
||||
currency_id: {
|
||||
string: "Currency",
|
||||
type: "many2one",
|
||||
relation: "currency",
|
||||
searchable: true,
|
||||
},
|
||||
float_factor_field: {
|
||||
string: "Float Factor",
|
||||
type: "float_factor",
|
||||
},
|
||||
percentage: {
|
||||
string: "Percentage",
|
||||
type: "percentage",
|
||||
},
|
||||
monetary: { string: "Monetary", type: "monetary" },
|
||||
progressbar: {
|
||||
type: "integer",
|
||||
},
|
||||
progressmax: {
|
||||
type: "float",
|
||||
},
|
||||
},
|
||||
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,
|
||||
progressmax: 5.41,
|
||||
},
|
||||
],
|
||||
},
|
||||
currency: {
|
||||
fields: {
|
||||
digits: { string: "Digits" },
|
||||
symbol: { string: "Currency Sumbol", type: "char", searchable: true },
|
||||
position: { string: "Currency Position", type: "char", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "$",
|
||||
symbol: "$",
|
||||
position: "before",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
});
|
||||
|
||||
QUnit.module("Numeric fields");
|
||||
|
||||
QUnit.test(
|
||||
"Numeric fields: fields with keydown on numpad decimal key",
|
||||
async function (assert) {
|
||||
registry.category("services").remove("localization");
|
||||
registry
|
||||
.category("services")
|
||||
.add("localization", makeFakeLocalizationService({ decimalPoint: "🇧🇪" }));
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_factor_field" options="{'factor': 0.5}"/>
|
||||
<field name="qux"/>
|
||||
<field name="int_field"/>
|
||||
<field name="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="percentage"/>
|
||||
<field name="progressbar" widget="progressbar" options="{'editable': true, 'max_value': 'qux', 'edit_max_value': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Get all inputs
|
||||
const floatFactorField = target.querySelector(".o_field_float_factor input");
|
||||
const floatInput = target.querySelector(".o_field_float input");
|
||||
const integerInput = target.querySelector(".o_field_integer input");
|
||||
const monetaryInput = target.querySelector(".o_field_monetary input");
|
||||
const percentageInput = target.querySelector(".o_field_percentage input");
|
||||
const progressbarInput = target.querySelector(".o_field_progressbar input");
|
||||
|
||||
// Dispatch numpad "dot" and numpad "comma" keydown events to all inputs and check
|
||||
// Numpad "comma" is specific to some countries (Brazil...)
|
||||
floatFactorField.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
floatFactorField.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(floatFactorField.value, "5🇧🇪00🇧🇪🇧🇪");
|
||||
|
||||
floatInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
floatInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(floatInput.value, "0🇧🇪4🇧🇪🇧🇪");
|
||||
|
||||
integerInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
integerInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(integerInput.value, "10🇧🇪🇧🇪");
|
||||
|
||||
monetaryInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
monetaryInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(monetaryInput.value, "9🇧🇪99🇧🇪🇧🇪");
|
||||
|
||||
percentageInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
percentageInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(percentageInput.value, "99🇧🇪🇧🇪");
|
||||
|
||||
progressbarInput.focus();
|
||||
await nextTick();
|
||||
|
||||
// When the input is focused, we get the length of the input value to be
|
||||
// able to set the cursor position at the end of the value.
|
||||
const length = progressbarInput.value.length;
|
||||
|
||||
// Make sure that the cursor position is at the end of the value.
|
||||
progressbarInput.setSelectionRange(length, length);
|
||||
progressbarInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
progressbarInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(progressbarInput.value, "0🇧🇪44🇧🇪🇧🇪");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"Numeric fields: NumpadDecimal key is different from the decimalPoint",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /*xml*/ `
|
||||
<form>
|
||||
<field name="float_factor_field" options="{'factor': 0.5}"/>
|
||||
<field name="qux"/>
|
||||
<field name="int_field"/>
|
||||
<field name="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="percentage"/>
|
||||
<field name="progressbar" widget="progressbar" options="{'editable': true, 'max_value': 'qux', 'edit_max_value': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Get all inputs
|
||||
const floatFactorField = target.querySelector(".o_field_float_factor input");
|
||||
const floatInput = target.querySelector(".o_field_float input");
|
||||
const integerInput = target.querySelector(".o_field_integer input");
|
||||
const monetaryInput = target.querySelector(".o_field_monetary input");
|
||||
const percentageInput = target.querySelector(".o_field_percentage input");
|
||||
const progressbarInput = target.querySelector(".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;
|
||||
|
||||
el.focus();
|
||||
await nextTick();
|
||||
|
||||
el.setSelectionRange(...selectionRange);
|
||||
const numpadDecimalEvent = new KeyboardEvent("keydown", {
|
||||
code: "NumpadDecimal",
|
||||
key: ".",
|
||||
});
|
||||
numpadDecimalEvent.preventDefault = () => assert.step("preventDefault");
|
||||
el.dispatchEvent(numpadDecimalEvent);
|
||||
await nextTick();
|
||||
|
||||
// dispatch an extra keydown event and assert that it's not default prevented
|
||||
const extraEvent = new KeyboardEvent("keydown", { code: "Digit1", key: "1" });
|
||||
extraEvent.preventDefault = () => {
|
||||
throw new Error("should not be default prevented");
|
||||
};
|
||||
el.dispatchEvent(extraEvent);
|
||||
await nextTick();
|
||||
|
||||
// Selection range should be at 1 + the specified selection start.
|
||||
assert.strictEqual(el.selectionStart, selectionRange[0] + 1);
|
||||
assert.strictEqual(el.selectionEnd, selectionRange[0] + 1);
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["preventDefault"],
|
||||
"NumpadDecimal event should be default prevented"
|
||||
);
|
||||
assert.strictEqual(el.value, expectedValue, msg);
|
||||
}
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: floatFactorField,
|
||||
selectionRange: [1, 3],
|
||||
expectedValue: "5,0",
|
||||
msg: "Float factor field from 5,00 to 5,0",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: floatInput,
|
||||
selectionRange: [0, 2],
|
||||
expectedValue: ",4",
|
||||
msg: "Float field from 0,4 to ,4",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: integerInput,
|
||||
selectionRange: [1, 2],
|
||||
expectedValue: "1,",
|
||||
msg: "Integer field from 10 to 1,",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: monetaryInput,
|
||||
selectionRange: [0, 3],
|
||||
expectedValue: ",9",
|
||||
msg: "Monetary field from 9,99 to ,9",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: percentageInput,
|
||||
selectionRange: [1, 1],
|
||||
expectedValue: "9,9",
|
||||
msg: "Percentage field from 99 to 9,9",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: progressbarInput,
|
||||
selectionRange: [1, 3],
|
||||
expectedValue: "0,4",
|
||||
msg: "Progressbar field 2 from 0,44 to 0,4",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"useNumpadDecimal should synchronize handlers on input elements",
|
||||
async function (assert) {
|
||||
/**
|
||||
* 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) {
|
||||
inputEl.focus();
|
||||
const numpadDecimalEvent = new KeyboardEvent("keydown", {
|
||||
code: "NumpadDecimal",
|
||||
key: ".",
|
||||
});
|
||||
numpadDecimalEvent.preventDefault = () => assert.step("preventDefault");
|
||||
inputEl.dispatchEvent(numpadDecimalEvent);
|
||||
await nextTick();
|
||||
|
||||
// dispatch an extra keydown event and assert that it's not default prevented
|
||||
const extraEvent = new KeyboardEvent("keydown", { code: "Digit1", key: "1" });
|
||||
extraEvent.preventDefault = () => {
|
||||
throw new Error("should not be default prevented");
|
||||
};
|
||||
inputEl.dispatchEvent(extraEvent);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["preventDefault"]);
|
||||
}
|
||||
}
|
||||
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useNumpadDecimal();
|
||||
this.state = useState({ showOtherInput: false });
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<main t-ref="numpadDecimal">
|
||||
<input type="text" placeholder="input 1" />
|
||||
<input t-if="state.showOtherInput" type="text" placeholder="input 2" />
|
||||
</main>
|
||||
`;
|
||||
const comp = await mount(MyComponent, target, { env: await makeTestEnv() });
|
||||
|
||||
// Initially, only one input should be rendered.
|
||||
assert.containsOnce(target, "main > input");
|
||||
await testInputElements(target.querySelectorAll("main > input"));
|
||||
|
||||
// We show the second input by manually updating the state.
|
||||
comp.state.showOtherInput = true;
|
||||
await nextTick();
|
||||
|
||||
// The second input should also be able to handle numpad decimal.
|
||||
assert.containsN(target, "main > input", 2);
|
||||
await testInputElements(target.querySelectorAll("main > input"));
|
||||
}
|
||||
);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,208 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import {
|
||||
parseFloat,
|
||||
parseFloatTime,
|
||||
parseInteger,
|
||||
parsePercentage,
|
||||
parseMonetary,
|
||||
} from "@web/views/fields/parsers";
|
||||
import { session } from "@web/session";
|
||||
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { nbsp } from "@web/core/utils/strings";
|
||||
|
||||
function expectInvalidNumberError(assert, func, value, options) {
|
||||
let message = `${func.name} fails on value: "${value}"`;
|
||||
if (options) {
|
||||
message += ` with options: ${JSON.stringify(options)}`;
|
||||
}
|
||||
assert.throws(() => func(value, options), message);
|
||||
}
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
patchWithCleanup(localization, defaultLocalization);
|
||||
});
|
||||
|
||||
QUnit.module("Parsers");
|
||||
|
||||
QUnit.test("parseFloat", async (assert) => {
|
||||
assert.strictEqual(parseFloat(""), 0);
|
||||
assert.strictEqual(parseFloat("0"), 0);
|
||||
assert.strictEqual(parseFloat("100.00"), 100);
|
||||
assert.strictEqual(parseFloat("-100.00"), -100);
|
||||
assert.strictEqual(parseFloat("1,000.00"), 1000);
|
||||
assert.strictEqual(parseFloat("1,000,000.00"), 1000000);
|
||||
assert.strictEqual(parseFloat("1,234.567"), 1234.567);
|
||||
expectInvalidNumberError(assert, parseFloat, "1.000.000");
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
assert.strictEqual(parseFloat("1.234,567"), 1234.567);
|
||||
|
||||
// Can evaluate expression from locale with decimal point different from ".".
|
||||
assert.strictEqual(parseFloat("=1.000,1 + 2.000,2"), 3000.3);
|
||||
assert.strictEqual(parseFloat("=1.000,00 + 11.121,00"), 12121);
|
||||
assert.strictEqual(parseFloat("=1000,00 + 11122,00"), 12122);
|
||||
assert.strictEqual(parseFloat("=1000 + 11123"), 12123);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: false });
|
||||
assert.strictEqual(parseFloat("1234,567"), 1234.567);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: nbsp });
|
||||
assert.strictEqual(parseFloat("9 876,543"), 9876.543);
|
||||
assert.strictEqual(parseFloat("1 234 567,89"), 1234567.89);
|
||||
assert.strictEqual(parseFloat(`98${nbsp}765 432,1`), 98765432.1);
|
||||
});
|
||||
|
||||
QUnit.test("parseFloatTime", function (assert) {
|
||||
assert.strictEqual(parseFloatTime("0"), 0);
|
||||
assert.strictEqual(parseFloatTime("100"), 100);
|
||||
assert.strictEqual(parseFloatTime("100.00"), 100);
|
||||
assert.strictEqual(parseFloatTime("7:15"), 7.25);
|
||||
assert.strictEqual(parseFloatTime("-4:30"), -4.5);
|
||||
assert.strictEqual(parseFloatTime(":"), 0);
|
||||
assert.strictEqual(parseFloatTime("1:"), 1);
|
||||
assert.strictEqual(parseFloatTime(":12"), 0.2);
|
||||
|
||||
expectInvalidNumberError(assert, parseFloatTime, "a:1");
|
||||
expectInvalidNumberError(assert, parseFloatTime, "1:a");
|
||||
expectInvalidNumberError(assert, parseFloatTime, "1:1:");
|
||||
expectInvalidNumberError(assert, parseFloatTime, ":1:1");
|
||||
});
|
||||
|
||||
QUnit.test("parseInteger", function (assert) {
|
||||
assert.strictEqual(parseInteger(""), 0);
|
||||
assert.strictEqual(parseInteger("0"), 0);
|
||||
assert.strictEqual(parseInteger("100"), 100);
|
||||
assert.strictEqual(parseInteger("-100"), -100);
|
||||
assert.strictEqual(parseInteger("1,000"), 1000);
|
||||
assert.strictEqual(parseInteger("1,000,000"), 1000000);
|
||||
assert.strictEqual(parseInteger("-2,147,483,648"), -2147483648);
|
||||
assert.strictEqual(parseInteger("2,147,483,647"), 2147483647);
|
||||
expectInvalidNumberError(assert, parseInteger, "1.000.000");
|
||||
expectInvalidNumberError(assert, parseInteger, "1,234.567");
|
||||
expectInvalidNumberError(assert, parseInteger, "-2,147,483,649");
|
||||
expectInvalidNumberError(assert, parseInteger, "2,147,483,648");
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
|
||||
assert.strictEqual(parseInteger("1.000.000"), 1000000);
|
||||
expectInvalidNumberError(assert, parseInteger, "1.234,567");
|
||||
// fallback to en localization
|
||||
assert.strictEqual(parseInteger("1,000,000"), 1000000);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: false });
|
||||
assert.strictEqual(parseInteger("1000000"), 1000000);
|
||||
});
|
||||
|
||||
QUnit.test("parsePercentage", function (assert) {
|
||||
assert.strictEqual(parsePercentage(""), 0);
|
||||
assert.strictEqual(parsePercentage("0"), 0);
|
||||
assert.strictEqual(parsePercentage("0.5"), 0.005);
|
||||
assert.strictEqual(parsePercentage("1"), 0.01);
|
||||
assert.strictEqual(parsePercentage("100"), 1);
|
||||
assert.strictEqual(parsePercentage("50%"), 0.5);
|
||||
expectInvalidNumberError(assert, parsePercentage, "50%40");
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
|
||||
assert.strictEqual(parsePercentage("1.234,56"), 12.3456);
|
||||
assert.strictEqual(parsePercentage("6,02"), 0.0602);
|
||||
});
|
||||
|
||||
QUnit.test("parseMonetary", function (assert) {
|
||||
patchWithCleanup(session, {
|
||||
currencies: {
|
||||
1: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "€",
|
||||
},
|
||||
3: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(parseMonetary(""), 0);
|
||||
assert.strictEqual(parseMonetary("0"), 0);
|
||||
assert.strictEqual(parseMonetary("100.00\u00a0€"), 100);
|
||||
assert.strictEqual(parseMonetary("-100.00"), -100);
|
||||
assert.strictEqual(parseMonetary("1,000.00"), 1000);
|
||||
assert.strictEqual(parseMonetary(".1"), 0.1);
|
||||
assert.strictEqual(parseMonetary("1,000,000.00"), 1000000);
|
||||
assert.strictEqual(parseMonetary("$\u00a0125.00"), 125);
|
||||
assert.strictEqual(parseMonetary("1,000.00\u00a0€"), 1000);
|
||||
|
||||
assert.strictEqual(parseMonetary("\u00a0"), 0);
|
||||
assert.strictEqual(parseMonetary("1\u00a0"), 1);
|
||||
assert.strictEqual(parseMonetary("\u00a01"), 1);
|
||||
|
||||
assert.strictEqual(parseMonetary("12.00 €"), 12);
|
||||
assert.strictEqual(parseMonetary("$ 12.00"), 12);
|
||||
assert.strictEqual(parseMonetary("1\u00a0$"), 1);
|
||||
assert.strictEqual(parseMonetary("$\u00a01"), 1);
|
||||
|
||||
assert.throws(() => parseMonetary("1$\u00a01"));
|
||||
assert.throws(() => parseMonetary("$\u00a012.00\u00a034"));
|
||||
|
||||
// nbsp as thousands separator
|
||||
patchWithCleanup(localization, { thousandsSep: "\u00a0", decimalPoint: "," });
|
||||
assert.strictEqual(parseMonetary("1\u00a0000,06\u00a0€"), 1000.06);
|
||||
assert.strictEqual(parseMonetary("$\u00a01\u00a0000,07"), 1000.07);
|
||||
assert.strictEqual(parseMonetary("1000000,08"), 1000000.08);
|
||||
assert.strictEqual(parseMonetary("$ -1\u00a0000,09"), -1000.09);
|
||||
|
||||
// symbol not separated from the value
|
||||
assert.strictEqual(parseMonetary("1\u00a0000,08€"), 1000.08);
|
||||
assert.strictEqual(parseMonetary("€1\u00a0000,09"), 1000.09);
|
||||
assert.strictEqual(parseMonetary("$1\u00a0000,10"), 1000.1);
|
||||
assert.strictEqual(parseMonetary("$-1\u00a0000,11"), -1000.11);
|
||||
|
||||
// any symbol
|
||||
assert.strictEqual(parseMonetary("1\u00a0000,11EUROS"), 1000.11);
|
||||
assert.strictEqual(parseMonetary("EUR1\u00a0000,12"), 1000.12);
|
||||
assert.strictEqual(parseMonetary("DOL1\u00a0000,13"), 1000.13);
|
||||
assert.strictEqual(parseMonetary("1\u00a0000,14DOLLARS"), 1000.14);
|
||||
assert.strictEqual(parseMonetary("DOLLARS+1\u00a0000,15"), 1000.15);
|
||||
assert.strictEqual(parseMonetary("EURO-1\u00a0000,16DOGE"), -1000.16);
|
||||
|
||||
// comma as decimal point and dot as thousands separator
|
||||
patchWithCleanup(localization, { thousandsSep: ".", decimalPoint: "," });
|
||||
assert.strictEqual(parseMonetary("10,08"), 10.08);
|
||||
assert.strictEqual(parseMonetary(""), 0);
|
||||
assert.strictEqual(parseMonetary("0"), 0);
|
||||
assert.strictEqual(parseMonetary("100,12\u00a0€"), 100.12);
|
||||
assert.strictEqual(parseMonetary("-100,12"), -100.12);
|
||||
assert.strictEqual(parseMonetary("1.000,12"), 1000.12);
|
||||
assert.strictEqual(parseMonetary(",1"), 0.1);
|
||||
assert.strictEqual(parseMonetary("1.000.000,12"), 1000000.12);
|
||||
assert.strictEqual(parseMonetary("$\u00a0125,12"), 125.12);
|
||||
assert.strictEqual(parseMonetary("1.000,00\u00a0€"), 1000);
|
||||
assert.strictEqual(parseMonetary(","), 0);
|
||||
assert.strictEqual(parseMonetary("1\u00a0"), 1);
|
||||
assert.strictEqual(parseMonetary("\u00a01"), 1);
|
||||
assert.strictEqual(parseMonetary("12,34 €"), 12.34);
|
||||
assert.strictEqual(parseMonetary("$ 12,34"), 12.34);
|
||||
|
||||
// Can evaluate expression
|
||||
assert.strictEqual(parseMonetary("=1.000,1 + 2.000,2"), 3000.3);
|
||||
assert.strictEqual(parseMonetary("=1.000,00 + 11.121,00"), 12121);
|
||||
assert.strictEqual(parseMonetary("=1000,00 + 11122,00"), 12122);
|
||||
assert.strictEqual(parseMonetary("=1000 + 11123"), 12123);
|
||||
});
|
||||
|
||||
QUnit.test("parsers fallback on english localisation", function (assert) {
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: ",",
|
||||
thousandsSep: ".",
|
||||
});
|
||||
|
||||
assert.strictEqual(parseInteger("1,000,000"), 1000000);
|
||||
assert.strictEqual(parseFloat("1,000,000.50"), 1000000.5);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
const getIframe = () => target.querySelector(".o_field_widget iframe.o_pdfview_iframe");
|
||||
|
||||
const getIframeProtocol = () => getIframe().dataset.src.match(/\?file=(\w+)%3A/)[1];
|
||||
|
||||
const getIframeViewerParams = () =>
|
||||
decodeURIComponent(getIframe().dataset.src.match(/%2Fweb%2Fcontent%3F(.*)#page/)[1]);
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
document: { string: "Binary", type: "binary" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
document: "coucou==\n",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("PdfViewerField");
|
||||
|
||||
QUnit.test("PdfViewerField without data", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelector(".o_field_widget"), "o_field_pdf_viewer");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_select_file_button:not(.o_hidden)",
|
||||
"there should be a visible 'Upload' button"
|
||||
);
|
||||
assert.containsNone(target, ".o_pdfview_iframe", "there should be no iframe");
|
||||
assert.containsOnce(target, 'input[type="file"]', "there should be one input");
|
||||
});
|
||||
|
||||
QUnit.test("PdfViewerField: basic rendering", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelector(".o_field_widget"), "o_field_pdf_viewer");
|
||||
assert.containsOnce(target, ".o_select_file_button", "there should be an 'Upload' button");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget iframe.o_pdfview_iframe",
|
||||
"there should be an iframe"
|
||||
);
|
||||
assert.strictEqual(getIframeProtocol(), "http");
|
||||
assert.strictEqual(getIframeViewerParams(), "model=partner&field=document&id=1");
|
||||
});
|
||||
|
||||
QUnit.test("PdfViewerField: upload rendering", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
async mockRPC(_route, { method, args }) {
|
||||
if (method === "create") {
|
||||
assert.deepEqual(args[0], { document: btoa("test") });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsNone(target, ".o_pdfview_iframe", "there is no PDF Viewer");
|
||||
|
||||
const file = new File(["test"], "test.pdf", { type: "application/pdf" });
|
||||
await editInput(target, ".o_field_pdf_viewer input[type=file]", file);
|
||||
|
||||
assert.containsOnce(target, ".o_pdfview_iframe", "there is a PDF Viewer");
|
||||
assert.strictEqual(getIframeProtocol(), "blob");
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(getIframeProtocol(), "blob");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
float_field: {
|
||||
string: "Float_field",
|
||||
type: "float",
|
||||
digits: [0, 1],
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, foo: "yop", int_field: 10 },
|
||||
{ id: 2, foo: "gnap", int_field: 80 },
|
||||
{ id: 3, foo: "dop", float_field: 65.6},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("PercentPieField");
|
||||
|
||||
QUnit.test("PercentPieField in form view with value < 50%", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"10%",
|
||||
"should have 10% as pie value since int_field=10"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(180deg)",
|
||||
"left mask should be covering the whole left side of the pie"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1].style
|
||||
.transform,
|
||||
"rotate(36deg)",
|
||||
"right mask should be rotated from 360*(10/100) = 36 degrees"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"10%",
|
||||
"should have 10% as pie value since int_field=10"
|
||||
);
|
||||
assert.ok(
|
||||
_.str.include(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(180deg)"
|
||||
),
|
||||
"left mask should be covering the whole left side of the pie"
|
||||
);
|
||||
assert.ok(
|
||||
_.str.include(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1]
|
||||
.style.transform,
|
||||
"rotate(36deg)"
|
||||
),
|
||||
"right mask should be rotated from 360*(10/100) = 36 degrees"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"10%",
|
||||
"should have 10% as pie value since int_field=10"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(180deg)",
|
||||
"left mask should be covering the whole left side of the pie"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1].style
|
||||
.transform,
|
||||
"rotate(36deg)",
|
||||
"right mask should be rotated from 360*(10/100) = 36 degrees"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PercentPieField in form view with value > 50%", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"80%",
|
||||
"should have 80% as pie value since int_field=80"
|
||||
);
|
||||
assert.ok(
|
||||
_.str.include(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(288deg)"
|
||||
),
|
||||
"left mask should be rotated from 360*(80/100) = 288 degrees"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1],
|
||||
"o_full",
|
||||
"right mask should be hidden since the value > 50%"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"80%",
|
||||
"should have 80% as pie value since int_field=80"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(288deg)",
|
||||
"left mask should be rotated from 360*(80/100) = 288 degrees"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1],
|
||||
"o_full",
|
||||
"right mask should be hidden since the value > 50%"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"80%",
|
||||
"should have 80% as pie value since int_field=80"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(288deg)",
|
||||
"left mask should be rotated from 360*(80/100) = 288 degrees"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1],
|
||||
"o_full",
|
||||
"right mask should be hidden since the value > 50%"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PercentPieField in form view with float value", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="float_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 3,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"66%",
|
||||
"should have 66% as pie value since float_field=65.6"
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: This test would pass without any issue since all the classes and
|
||||
// custom style attributes are correctly set on the widget in list
|
||||
// view, but since the scss itself for this widget currently only
|
||||
// applies inside the form view, the widget is unusable. This test can
|
||||
// be uncommented when we refactor the scss files so that this widget
|
||||
// stylesheet applies in both form and list view.
|
||||
// QUnit.test('percentpie widget in editable list view', async function(assert) {
|
||||
// assert.expect(10);
|
||||
//
|
||||
// var list = await createView({
|
||||
// View: ListView,
|
||||
// model: 'partner',
|
||||
// data: this.data,
|
||||
// arch: '<tree editable="bottom">' +
|
||||
// '<field name="foo"/>' +
|
||||
// '<field name="int_field" widget="percentpie"/>' +
|
||||
// '</tree>',
|
||||
// });
|
||||
//
|
||||
// assert.containsN(list, '.o_field_percent_pie .o_pie', 5,
|
||||
// "should have five pie charts");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_pie_value').textContent,
|
||||
// '10%', "should have 10% as pie value since int_field=10");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').attr('style'),
|
||||
// 'rotate(180deg)', "left mask should be covering the whole left side of the pie");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
|
||||
// 'rotate(36deg)', "right mask should be rotated from 360*(10/100) = 36 degrees");
|
||||
//
|
||||
// // switch to edit mode and check the result
|
||||
// testUtils.dom.click( target.querySelector('tbody td:not(.o_list_record_selector)'));
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_pie_value').textContent,
|
||||
// '10%', "should have 10% as pie value since int_field=10");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').attr('style'),
|
||||
// 'rotate(180deg)', "left mask should be covering the whole right side of the pie");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
|
||||
// 'rotate(36deg)', "right mask should be rotated from 360*(10/100) = 36 degrees");
|
||||
//
|
||||
// // save
|
||||
// testUtils.dom.click( list.$buttons.find('.o_list_button_save'));
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_pie_value').textContent,
|
||||
// '10%', "should have 10% as pie value since int_field=10");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').attr('style'),
|
||||
// 'rotate(180deg)', "left mask should be covering the whole right side of the pie");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
|
||||
// 'rotate(36deg)', "right mask should be rotated from 360*(10/100) = 36 degrees");
|
||||
//
|
||||
// list.destroy();
|
||||
// });
|
||||
});
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { clickSave, editInput, getFixture, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
float_field: {
|
||||
string: "Float_field",
|
||||
type: "float",
|
||||
digits: [0, 1],
|
||||
},
|
||||
},
|
||||
records: [{ float_field: 0.44444 }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("PercentageField");
|
||||
|
||||
QUnit.test("PercentageField in form view", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="percentage"/>
|
||||
</form>`,
|
||||
mockRPC(route, { args, method }) {
|
||||
if (method === "write") {
|
||||
assert.strictEqual(
|
||||
args[1].float_field,
|
||||
0.24,
|
||||
"the correct float value should be saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
resId: 1,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=float_field] input").value,
|
||||
"44.4",
|
||||
"The input should be rendered without the percentage symbol."
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=float_field] span").textContent,
|
||||
"%",
|
||||
"The input should be followed by a span containing the percentage symbol."
|
||||
);
|
||||
const field = target.querySelector("[name='float_field'] input");
|
||||
await editInput(target, "[name='float_field'] input", "24");
|
||||
assert.strictEqual(field.value, "24", "The value should not be formated yet.");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"24",
|
||||
"The new value should be formatted properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("percentage field with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="percentage" placeholder="Placeholder"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='float_field'] input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='float_field'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PercentageField in form view without rounding error", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="percentage"/>
|
||||
</form>`,
|
||||
});
|
||||
await editInput(target, "[name='float_field'] input", "28");
|
||||
assert.strictEqual(target.querySelector("[name='float_field'] input").value, "28");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getNextTabableElement } from "@web/core/utils/ui";
|
||||
import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
records: [{ foo: "yop" }, { foo: "blip" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("PhoneField");
|
||||
|
||||
QUnit.test("PhoneField in form view on normal screens (readonly)", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
mode: "readonly",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
const phone = target.querySelector(".o_field_phone a");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
phone,
|
||||
"should have rendered the phone number as a link with correct classes"
|
||||
);
|
||||
assert.strictEqual(phone.textContent, "yop", "value should be displayed properly");
|
||||
assert.hasAttrValue(phone, "href", "tel:yop", "should have proper tel prefix");
|
||||
});
|
||||
|
||||
QUnit.test("PhoneField in form view on normal screens (edit)", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'input[type="tel"]',
|
||||
"should have an input for the phone field"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('input[type="tel"]').value,
|
||||
"yop",
|
||||
"input should contain field value in edit mode"
|
||||
);
|
||||
|
||||
const phoneLink = target.querySelector(".o_field_phone a");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
phoneLink,
|
||||
"should have rendered the phone number as a link with correct classes"
|
||||
);
|
||||
assert.strictEqual(phoneLink.textContent, "Call", "link is shown with the right text");
|
||||
assert.hasAttrValue(phoneLink, "href", "tel:yop", "should have proper tel prefix");
|
||||
|
||||
// change value in edit mode
|
||||
await editInput(target, "input[type='tel']", "new");
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector("input[type='tel']").value,
|
||||
"new",
|
||||
"new value should be displayed properly"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PhoneField in editable list view on normal screens", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
|
||||
});
|
||||
|
||||
assert.containsN(target, "tbody td:not(.o_list_record_selector).o_data_cell", 2);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tbody td:not(.o_list_record_selector) a").textContent,
|
||||
"yop",
|
||||
"value should be displayed properly with a link to send SMS"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget a.o_form_uri",
|
||||
2,
|
||||
"should have the correct classnames"
|
||||
);
|
||||
|
||||
// Edit a line and check the result
|
||||
let cell = target.querySelector("tbody td:not(.o_list_record_selector)");
|
||||
await click(cell);
|
||||
assert.hasClass(cell.parentElement, "o_selected_row", "should be set as edit mode");
|
||||
assert.strictEqual(
|
||||
cell.querySelector("input").value,
|
||||
"yop",
|
||||
"should have the corect value in internal input"
|
||||
);
|
||||
await editInput(cell, "input", "new");
|
||||
|
||||
// save
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
cell = target.querySelector("tbody td:not(.o_list_record_selector)");
|
||||
assert.doesNotHaveClass(
|
||||
cell.parentElement,
|
||||
"o_selected_row",
|
||||
"should not be in edit mode anymore"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tbody td:not(.o_list_record_selector) a").textContent,
|
||||
"new",
|
||||
"value should be properly updated"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget a.o_form_uri",
|
||||
2,
|
||||
"should still have links with correct classes"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use TAB to navigate to a PhoneField", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
target.querySelector(".o_field_widget[name=display_name] input").focus();
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector('.o_field_widget[name="display_name"] input'),
|
||||
"display_name should be focused"
|
||||
);
|
||||
assert.strictEqual(
|
||||
getNextTabableElement(target),
|
||||
target.querySelector('[name="foo"] input'),
|
||||
"foo should be focused"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("phone field with placeholder", async function (assert) {
|
||||
serverData.models.partner.fields.foo.default = false;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone" placeholder="Placeholder"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='foo'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("unset and readonly PhoneField", async function (assert) {
|
||||
serverData.models.partner.fields.foo.default = false;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone" readonly="1" placeholder="Placeholder"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
assert.containsNone(
|
||||
target.querySelector(".o_field_widget[name='foo']"),
|
||||
"a",
|
||||
"The readonly field don't contain a link if no value is set"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("href is correctly formatted", async function (assert) {
|
||||
serverData.models.partner.records[0].foo = "+12 345 67 89 00";
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
mode: "readonly",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
const phone = target.querySelector(".o_field_phone a");
|
||||
assert.strictEqual(
|
||||
phone.textContent,
|
||||
"+12 345 67 89 00",
|
||||
"value should be displayed properly with spaces as separators"
|
||||
);
|
||||
assert.hasAttrValue(phone, "href", "tel:+12345678900", "href should not contain any space");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,671 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
getFixture,
|
||||
nextTick,
|
||||
triggerEvent,
|
||||
triggerHotkey,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
},
|
||||
sequence: { type: "integer", string: "Sequence", searchable: true },
|
||||
selection: {
|
||||
string: "Selection",
|
||||
type: "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" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("PriorityField");
|
||||
|
||||
QUnit.test("PriorityField when not set", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority:not(.o_field_empty)",
|
||||
"widget should be considered set, even though there is no value for this field"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star",
|
||||
2,
|
||||
"should have two stars for representing each possible value: no star, one star and two stars"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star",
|
||||
"should have no full star since there is no value"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
|
||||
2,
|
||||
"should have two empty stars since there is no value"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PriorityField tooltip", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// check data-tooltip attribute (used by the tooltip service)
|
||||
const stars = target.querySelectorAll(".o_field_widget .o_priority a.o_priority_star");
|
||||
assert.strictEqual(
|
||||
stars[0].dataset["tooltip"],
|
||||
"Selection: Blocked",
|
||||
"Should set field label and correct selection label as title attribute (tooltip)"
|
||||
);
|
||||
assert.strictEqual(
|
||||
stars[1].dataset["tooltip"],
|
||||
"Selection: Done",
|
||||
"Should set field label and correct selection label as title attribute (tooltip)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PriorityField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority:not(.o_field_empty)",
|
||||
"widget should be considered set"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star",
|
||||
2,
|
||||
"should have two stars for representing each possible value: no star, one star and two stars"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star",
|
||||
"should have one full star since the value is the second value"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
|
||||
"should have one empty star since the value is the second value"
|
||||
);
|
||||
|
||||
// hover last star
|
||||
let stars = target.querySelectorAll(
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o"
|
||||
);
|
||||
await triggerEvent(stars[stars.length - 1], null, "mouseenter");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star",
|
||||
2,
|
||||
"should have two stars for representing each possible value: no star, one star and two stars"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star",
|
||||
2,
|
||||
"should temporary have two full stars since we are hovering the third value"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
|
||||
"should temporary have no empty star since we are hovering the third value"
|
||||
);
|
||||
|
||||
await triggerEvent(stars[stars.length - 1], null, "mouseleave");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star",
|
||||
2,
|
||||
"should have two stars for representing each possible value: no star, one star and two stars"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star",
|
||||
"should temporary have two full stars since we are hovering the third value"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
|
||||
"should temporary have no empty star since we are hovering the third value"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star",
|
||||
2,
|
||||
"should still have two stars"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star",
|
||||
"should still have one full star since the value is the second value"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
|
||||
"should still have one empty star since the value is the second value"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star",
|
||||
2,
|
||||
"should still have two stars"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star",
|
||||
"should still have one full star since the value is the second value"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
|
||||
"should still have one empty star since the value is the second value"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star",
|
||||
2,
|
||||
"should still have two stars"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star",
|
||||
"should still have one full star since the value is the second value"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
|
||||
"should still have one empty star since the value is the second value"
|
||||
);
|
||||
|
||||
// click on the second star in edit mode
|
||||
stars = target.querySelectorAll(".o_field_widget .o_priority a.o_priority_star.fa-star-o");
|
||||
await click(stars[stars.length - 1]);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star",
|
||||
2,
|
||||
"should still have two stars"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star",
|
||||
2,
|
||||
"should now have two full stars since the value is the third value"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
|
||||
"should now have no empty star since the value is the third value"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star",
|
||||
2,
|
||||
"should still have two stars"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star",
|
||||
2,
|
||||
"should now have two full stars since the value is the third value"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
|
||||
"should now have no empty star since the value is the third value"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PriorityField can write after adding a record -- kanban", async function (assert) {
|
||||
serverData.models.partner.fields.selection.selection = [
|
||||
["0", 0],
|
||||
["1", 1],
|
||||
];
|
||||
serverData.models.partner.records[0].selection = "0";
|
||||
|
||||
serverData.views = {
|
||||
"partner,myquickview,form": `<form><field name="display_name" /></form>`,
|
||||
};
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
domain: [["id", "=", 1]],
|
||||
groupBy: ["foo"],
|
||||
arch: `
|
||||
<kanban on_create="quick_create" quick_create_view="myquickview">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_card oe_kanban_global_click">
|
||||
<field name="selection" widget="priority"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "write") {
|
||||
assert.step(`write ${JSON.stringify(args.args)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.containsNone(target, ".o_kanban_record .fa-star");
|
||||
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"), null, true);
|
||||
assert.verifySteps(['write [[1],{"selection":"1"}]']);
|
||||
assert.containsOnce(target, ".o_kanban_record .fa-star");
|
||||
|
||||
await click(target, ".o-kanban-button-new");
|
||||
await click(target, ".o_kanban_quick_create .o_kanban_add");
|
||||
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"), null, true);
|
||||
assert.verifySteps(['write [[6],{"selection":"1"}]']);
|
||||
assert.containsN(target, ".o_kanban_record .fa-star", 2);
|
||||
});
|
||||
|
||||
QUnit.test("PriorityField in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="selection" widget="priority" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority:not(.o_field_empty)",
|
||||
"widget should be considered set"
|
||||
);
|
||||
assert.containsN(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star",
|
||||
2,
|
||||
"should have two stars for representing each possible value: no star, one star and two stars"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star.fa-star",
|
||||
"should have one full star since the value is the second value"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star.fa-star-o",
|
||||
"should have one empty star since the value is the second value"
|
||||
);
|
||||
|
||||
// switch to edit mode and check the result
|
||||
await click(target.querySelector("tbody td:not(.o_list_record_selector)"));
|
||||
|
||||
assert.containsN(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star",
|
||||
2,
|
||||
"should have two stars for representing each possible value: no star, one star and two stars"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star.fa-star",
|
||||
"should have one full star since the value is the second value"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star.fa-star-o",
|
||||
"should have one empty star since the value is the second value"
|
||||
);
|
||||
|
||||
// save
|
||||
await click(target, ".o_list_button_save");
|
||||
|
||||
assert.containsN(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star",
|
||||
2,
|
||||
"should have two stars for representing each possible value: no star, one star and two stars"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star.fa-star",
|
||||
"should have one full star since the value is the second value"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star.fa-star-o",
|
||||
"should have one empty star since the value is the second value"
|
||||
);
|
||||
|
||||
// hover last star
|
||||
await triggerEvent(
|
||||
target.querySelector(".o_data_row"),
|
||||
".o_priority a.o_priority_star.fa-star-o",
|
||||
"mouseenter"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star",
|
||||
2,
|
||||
"should have two stars for representing each possible value: no star, one star and two stars"
|
||||
);
|
||||
assert.containsN(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
"a.o_priority_star.fa-star",
|
||||
2,
|
||||
"should temporary have two full stars since we are hovering the third value"
|
||||
);
|
||||
assert.containsNone(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
"a.o_priority_star.fa-star-o",
|
||||
"should temporary have no empty star since we are hovering the third value"
|
||||
);
|
||||
|
||||
// click on the first star in readonly mode
|
||||
await click(target.querySelector(".o_priority a.o_priority_star.fa-star"));
|
||||
|
||||
assert.containsN(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star",
|
||||
2,
|
||||
"should still have two stars"
|
||||
);
|
||||
assert.containsNone(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star.fa-star",
|
||||
"should now have no full star since the value is the first value"
|
||||
);
|
||||
assert.containsN(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star.fa-star-o",
|
||||
2,
|
||||
"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(target.querySelector("tbody td:not(.o_list_record_selector)"));
|
||||
|
||||
assert.containsN(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star",
|
||||
2,
|
||||
"should still have two stars"
|
||||
);
|
||||
assert.containsNone(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star.fa-star",
|
||||
"should now only have no full star since the value is the first value"
|
||||
);
|
||||
assert.containsN(
|
||||
target.querySelectorAll(".o_data_row")[0],
|
||||
".o_priority a.o_priority_star.fa-star-o",
|
||||
2,
|
||||
"should now have two empty stars since the value is the first value"
|
||||
);
|
||||
|
||||
// Click on second star in edit mode
|
||||
const stars = target.querySelectorAll(".o_priority a.o_priority_star.fa-star-o");
|
||||
await click(stars[stars.length - 1]);
|
||||
|
||||
let rows = target.querySelectorAll(".o_data_row");
|
||||
assert.containsN(
|
||||
rows[rows.length - 1],
|
||||
".o_priority a.o_priority_star",
|
||||
2,
|
||||
"should still have two stars"
|
||||
);
|
||||
assert.containsN(
|
||||
rows[rows.length - 1],
|
||||
".o_priority a.o_priority_star.fa-star",
|
||||
2,
|
||||
"should now have two full stars since the value is the third value"
|
||||
);
|
||||
assert.containsNone(
|
||||
rows[rows.length - 1],
|
||||
".o_priority a.o_priority_star.fa-star-o",
|
||||
"should now have no empty star since the value is the third value"
|
||||
);
|
||||
|
||||
// save
|
||||
await click(target, ".o_list_button_save");
|
||||
rows = target.querySelectorAll(".o_data_row");
|
||||
|
||||
assert.containsN(
|
||||
rows[rows.length - 1],
|
||||
".o_priority a.o_priority_star",
|
||||
2,
|
||||
"should still have two stars"
|
||||
);
|
||||
assert.containsN(
|
||||
rows[rows.length - 1],
|
||||
".o_priority a.o_priority_star.fa-star",
|
||||
2,
|
||||
"should now have two full stars since the value is the third value"
|
||||
);
|
||||
assert.containsNone(
|
||||
rows[rows.length - 1],
|
||||
".o_priority a.o_priority_star.fa-star-o",
|
||||
"should now have no empty star since the value is the third value"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PriorityField with readonly attribute", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
serverData,
|
||||
arch: '<form><field name="selection" widget="priority" readonly="1"/></form>',
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "write") {
|
||||
throw new Error("should not save");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
"span.o_priority_star.fa.fa-star-o",
|
||||
2,
|
||||
"stars of priority widget should rendered with span tag if readonly"
|
||||
);
|
||||
|
||||
await triggerEvent(
|
||||
target.querySelectorAll(".o_priority_star.fa-star-o")[1],
|
||||
null,
|
||||
"mouseenter"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star",
|
||||
"should have no full stars on hover since the field is readonly"
|
||||
);
|
||||
|
||||
await click(target.querySelectorAll(".o_priority_star.fa-star-o")[1]);
|
||||
assert.containsN(
|
||||
target,
|
||||
"span.o_priority_star.fa.fa-star-o",
|
||||
2,
|
||||
"should still have two stars"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
'PriorityField edited by the smart action "Set priority..."',
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="selection" widget="priority"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "a.fa-star");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
const idx = [...target.querySelectorAll(".o_command")]
|
||||
.map((el) => el.textContent)
|
||||
.indexOf("Set priority...ALT + R");
|
||||
assert.ok(idx >= 0);
|
||||
|
||||
await click([...target.querySelectorAll(".o_command")][idx]);
|
||||
await nextTick();
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
|
||||
["Normal", "Blocked", "Done"]
|
||||
);
|
||||
await click(target, "#o_command_2");
|
||||
await nextTick();
|
||||
assert.containsN(target, "a.fa-star", 2);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("PriorityField - auto save record when field toggled", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(_route, { method }) {
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const stars = target.querySelectorAll(
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o"
|
||||
);
|
||||
await click(stars[stars.length - 1]);
|
||||
assert.verifySteps(["write"]);
|
||||
});
|
||||
|
||||
QUnit.test("PriorityField - prevent auto save with autosave option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="priority" options="{'autosave': False}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(_route, { method }) {
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const stars = target.querySelectorAll(
|
||||
".o_field_widget .o_priority a.o_priority_star.fa-star-o"
|
||||
);
|
||||
await click(stars[stars.length - 1]);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,541 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
makeFakeLocalizationService,
|
||||
makeFakeNotificationService,
|
||||
} from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
},
|
||||
int_field2: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
},
|
||||
int_field3: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
},
|
||||
float_field: {
|
||||
string: "Float_field",
|
||||
type: "float",
|
||||
digits: [16, 1],
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
int_field: 10,
|
||||
float_field: 0.44444,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("ProgressBarField");
|
||||
|
||||
QUnit.test("ProgressBarField: max_value should update", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
serverData.models.partner.records[0].float_field = 2;
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
display_name(obj) {
|
||||
obj.int_field = 999;
|
||||
obj.float_field = 5;
|
||||
},
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="display_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,
|
||||
mockRPC(route, { method, args }) {
|
||||
if (method === "write") {
|
||||
assert.deepEqual(
|
||||
args[1],
|
||||
{ int_field: 999, float_field: 5, display_name: "new name" },
|
||||
"New value of progress bar saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar").textContent,
|
||||
"10 / 2",
|
||||
"The initial value of the progress bar should be correct"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=display_name] input", "new name");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar").textContent,
|
||||
"999 / 5",
|
||||
"The value of the progress bar should be correct after the update"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"ProgressBarField: value should update in edit mode when typing in input",
|
||||
async function (assert) {
|
||||
assert.expect(4);
|
||||
serverData.models.partner.records[0].int_field = 99;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(route, { method, args }) {
|
||||
if (method === "write") {
|
||||
assert.strictEqual(
|
||||
args[1].int_field,
|
||||
69,
|
||||
"New value of progress bar saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value .o_input").value +
|
||||
target.querySelector(".o_progressbar").textContent,
|
||||
"99%",
|
||||
"Initial value should be correct"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_progressbar_value .o_input", "69");
|
||||
await click(target, ".o_form_view");
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value .o_input").value,
|
||||
"69",
|
||||
"New value should be different after focusing out of the field"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value .o_input").value,
|
||||
"69",
|
||||
"New value is still displayed after save"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"ProgressBarField: value should update in edit mode when typing in input with field max value",
|
||||
async function (assert) {
|
||||
assert.expect(4);
|
||||
serverData.models.partner.records[0].int_field = 99;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" invisible="1" />
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true, 'max_value': 'float_field'}" />
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(route, { method, args }) {
|
||||
if (method === "write") {
|
||||
assert.strictEqual(
|
||||
args[1].int_field,
|
||||
69,
|
||||
"New value of progress bar saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(target.querySelector(".o_form_view .o_form_editable"), "Form in edit mode");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value .o_input").value +
|
||||
target.querySelector(".o_progressbar").textContent,
|
||||
"99 / 0",
|
||||
"Initial value should be correct"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_progressbar_value .o_input", "69");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value .o_input").value +
|
||||
target.querySelector(".o_progressbar").textContent,
|
||||
"69 / 0",
|
||||
"New value should be different than initial after click"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"ProgressBarField: max value should update in edit mode when typing in input with field max value",
|
||||
async function (assert) {
|
||||
assert.expect(5);
|
||||
serverData.models.partner.records[0].int_field = 99;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<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,
|
||||
mockRPC(route, { method, args }) {
|
||||
if (method === "write") {
|
||||
assert.strictEqual(
|
||||
args[1].float_field,
|
||||
69,
|
||||
"New value of progress bar saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar").textContent +
|
||||
target.querySelector(".o_progressbar_value .o_input").value,
|
||||
"99 / 0",
|
||||
"Initial value should be correct"
|
||||
);
|
||||
assert.ok(target.querySelector(".o_form_view .o_form_editable"), "Form in edit mode");
|
||||
target.querySelector(".o_progressbar input").focus();
|
||||
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar").textContent +
|
||||
target.querySelector(".o_progressbar_value .o_input").value,
|
||||
"99 / 0.44",
|
||||
"Initial value is not formatted when focused"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_progressbar_value .o_input", "69");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar").textContent +
|
||||
target.querySelector(".o_progressbar_value .o_input").value,
|
||||
"99 / 69",
|
||||
"New value should be different than initial after click"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("ProgressBarField: Standard readonly mode is readonly", async function (assert) {
|
||||
serverData.models.partner.records[0].int_field = 99;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<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,
|
||||
mockRPC(route) {
|
||||
assert.step(route);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar").textContent,
|
||||
"99 / 0",
|
||||
"Initial value should be correct"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_progress"));
|
||||
|
||||
assert.containsNone(target, ".o_progressbar_value .o_input", "no input in readonly mode");
|
||||
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/partner/get_views",
|
||||
"/web/dataset/call_kw/partner/read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("ProgressBarField: field is editable in kanban", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
serverData.models.partner.fields.int_field.readonly = true;
|
||||
serverData.models.partner.records[0].int_field = 99;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="int_field" title="ProgressBarTitle" widget="progressbar" options="{'editable': true, 'max_value': 'float_field'}" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
resId: 1,
|
||||
mockRPC(route, { method, args }) {
|
||||
if (method === "write") {
|
||||
assert.strictEqual(args[1].int_field, 69, "New value of progress bar saved");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value .o_input").value,
|
||||
"99",
|
||||
"Initial input value should be correct"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value span").textContent,
|
||||
"100",
|
||||
"Initial max value should be correct"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_title").textContent,
|
||||
"ProgressBarTitle"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_progressbar_value .o_input", "69");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value .o_input").value,
|
||||
"69",
|
||||
"New input value should now be different"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value span").textContent,
|
||||
"100",
|
||||
"Max value is still the same be correct"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_title").textContent,
|
||||
"ProgressBarTitle"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("force readonly in kanban", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
serverData.models.partner.records[0].int_field = 99;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true, 'max_value': 'float_field', 'readonly': True}" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
resId: 1,
|
||||
mockRPC(route, { method, args }) {
|
||||
if (method === "write") {
|
||||
throw new Error("Not supposed to write");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_progressbar").textContent, "99 / 100");
|
||||
assert.containsNone(target, ".o_progressbar_value .o_input");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"ProgressBarField: readonly and editable attrs/options in kanban",
|
||||
async function (assert) {
|
||||
assert.expect(4);
|
||||
serverData.models.partner.records[0].int_field = 29;
|
||||
serverData.models.partner.records[0].int_field2 = 59;
|
||||
serverData.models.partner.records[0].int_field3 = 99;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<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'}" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
"[name='int_field'] .o_progressbar_value .o_input",
|
||||
"the field is still in readonly since there is readonly attribute"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
"[name='int_field2'] .o_progressbar_value .o_input",
|
||||
"the field is still in readonly since the editable option is missing"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"[name='int_field3'] .o_progressbar_value .o_input",
|
||||
"the field is still in readonly since the editable option is missing"
|
||||
);
|
||||
|
||||
await editInput(
|
||||
target,
|
||||
".o_field_progressbar[name='int_field3'] .o_progressbar_value .o_input",
|
||||
"69"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_field_progressbar[name='int_field3'] .o_progressbar_value .o_input"
|
||||
).value,
|
||||
"69",
|
||||
"New value should be different than initial after click"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"ProgressBarField: write float instead of int works, in locale",
|
||||
async function (assert) {
|
||||
assert.expect(4);
|
||||
serverData.models.partner.records[0].int_field = 99;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC: function (route, { method, args }) {
|
||||
if (method === "write") {
|
||||
assert.strictEqual(
|
||||
args[1].int_field,
|
||||
1037,
|
||||
"New value of progress bar saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
registry.category("services").remove("localization");
|
||||
registry
|
||||
.category("services")
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({ thousandsSep: "#", decimalPoint: ":" })
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value .o_input").value +
|
||||
target.querySelector(".o_progressbar").textContent,
|
||||
"99%",
|
||||
"Initial value should be correct"
|
||||
);
|
||||
|
||||
assert.ok(target.querySelector(".o_form_view .o_form_editable"), "Form in edit mode");
|
||||
|
||||
await editInput(target, ".o_field_widget input", "1#037:9");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value .o_input").value,
|
||||
"1k",
|
||||
"New value should be different than initial after click"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"ProgressBarField: write gibbrish instead of int throws warning",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[0].int_field = 99;
|
||||
const mock = () => {
|
||||
assert.step("Show error message");
|
||||
return () => {};
|
||||
};
|
||||
registry.category("services").add("notification", makeFakeNotificationService(mock), {
|
||||
force: true,
|
||||
});
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field" widget="progressbar" options="{'editable': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_progressbar_value .o_input").value,
|
||||
"99",
|
||||
"Initial value in input is correct"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_progressbar_value .o_input", "trente sept virgule neuf");
|
||||
await clickSave(target);
|
||||
assert.containsOnce(target, ".o_form_dirty", "The form has not been saved");
|
||||
assert.verifySteps(["Show error message"], "The error message was shown correctly");
|
||||
}
|
||||
);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,363 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
bar: { string: "Bar", type: "boolean", default: true },
|
||||
int_field: { string: "int_field", type: "integer", sortable: true },
|
||||
trululu: { string: "Trululu", type: "many2one", relation: "partner" },
|
||||
product_id: { string: "Product", type: "many2one", relation: "product" },
|
||||
color: {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
string: "Color",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
bar: true,
|
||||
int_field: 10,
|
||||
trululu: 4,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
display_name: "third record",
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("RadioField");
|
||||
|
||||
QUnit.test("fieldradio widget on a many2one in a new record", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="product_id" widget="radio"/></form>',
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
target.querySelectorAll("div.o_radio_item").length,
|
||||
"should have rendered outer div"
|
||||
);
|
||||
assert.containsN(target, "input.o_radio_input", 2, "should have 2 possible choices");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_radio").textContent.replace(/\s+/g, ""),
|
||||
"xphonexpad"
|
||||
);
|
||||
assert.containsNone(target, "input:checked", "none of the input should be checked");
|
||||
|
||||
await click(target.querySelectorAll("input.o_radio_input")[0]);
|
||||
|
||||
assert.containsOnce(target, "input:checked", "one of the input should be checked");
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector("input.o_radio_input:checked"),
|
||||
"data-value",
|
||||
"37",
|
||||
"should have saved record with correct value"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("required fieldradio widget on a many2one", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="product_id" widget="radio" required="1"/></form>',
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_radio input:checked",
|
||||
"none of the input should be checked"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_title").textContent,
|
||||
"Invalid fields: "
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_content").innerHTML,
|
||||
"<ul><li>Product</li></ul>"
|
||||
);
|
||||
assert.hasClass(target.querySelector(".o_notification"), "border-danger");
|
||||
});
|
||||
|
||||
QUnit.test("fieldradio change value by onchange", async function (assert) {
|
||||
serverData.models.partner.onchanges = {
|
||||
bar(obj) {
|
||||
obj.product_id = obj.bar ? [41] : [37];
|
||||
obj.color = obj.bar ? "red" : "black";
|
||||
},
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" />
|
||||
<field name="product_id" widget="radio" />
|
||||
<field name="color" widget="radio" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, "input[type='checkbox']");
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"input.o_radio_input[data-value='37']:checked",
|
||||
"one of the input should be checked"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"input.o_radio_input[data-value='black']:checked",
|
||||
"the other of the input should be checked"
|
||||
);
|
||||
|
||||
await click(target, "input[type='checkbox']");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"input.o_radio_input[data-value='41']:checked",
|
||||
"the other of the input should be checked"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"input.o_radio_input[data-value='red']:checked",
|
||||
"one of the input should be checked"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("fieldradio widget on a selection in a new record", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="color" widget="radio"/></form>',
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
target.querySelectorAll("div.o_radio_item").length,
|
||||
"should have rendered outer div"
|
||||
);
|
||||
assert.containsN(target, "input.o_radio_input", 2, "should have 2 possible choices");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_radio").textContent.replace(/\s+/g, ""),
|
||||
"RedBlack"
|
||||
);
|
||||
|
||||
// click on 2nd option
|
||||
await click(target.querySelectorAll("input.o_radio_input")[1]);
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector("input.o_radio_input:checked"),
|
||||
"data-value",
|
||||
"black",
|
||||
"should have saved record with correct value"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("fieldradio widget has o_horizontal or o_vertical class", async function (assert) {
|
||||
serverData.models.partner.fields.color2 = serverData.models.partner.fields.color;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="color" widget="radio" />
|
||||
<field name="color2" widget="radio" options="{'horizontal': True}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_radio > div.o_vertical",
|
||||
"should have o_vertical class"
|
||||
);
|
||||
const verticalRadio = target.querySelector(".o_field_radio > div.o_vertical");
|
||||
assert.strictEqual(
|
||||
verticalRadio.querySelector(".o_radio_item:first-child").getBoundingClientRect().right,
|
||||
verticalRadio.querySelector(".o_radio_item:last-child").getBoundingClientRect().right
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_radio > div.o_horizontal",
|
||||
"should have o_horizontal class"
|
||||
);
|
||||
const horizontalRadio = target.querySelector(".o_field_radio > div.o_horizontal");
|
||||
assert.strictEqual(
|
||||
horizontalRadio.querySelector(".o_radio_item:first-child").getBoundingClientRect().top,
|
||||
horizontalRadio.querySelector(".o_radio_item:last-child").getBoundingClientRect().top
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("fieldradio widget with numerical keys encoded as strings", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
serverData.models.partner.fields.selection = {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["0", "Red"],
|
||||
["1", "Black"],
|
||||
],
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="selection" widget="radio"/></form>',
|
||||
mockRPC: function (route, { args, method, model }) {
|
||||
if (model === "partner" && method === "write") {
|
||||
assert.strictEqual(args[1].selection, "1", "should write correct value");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget").textContent.replace(/\s+/g, ""),
|
||||
"RedBlack"
|
||||
);
|
||||
assert.containsNone(target, ".o_radio_input:checked", "no value should be checked");
|
||||
|
||||
await click(target.querySelectorAll("input.o_radio_input")[1]);
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget").textContent.replace(/\s+/g, ""),
|
||||
"RedBlack"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_radio_input[data-index='1']:checked",
|
||||
"'Black' should be checked"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"widget radio on a many2one: domain updated by an onchange",
|
||||
async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
int_field() {},
|
||||
};
|
||||
|
||||
let domain = [];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field" />
|
||||
<field name="trululu" widget="radio" />
|
||||
</form>`,
|
||||
mockRPC(route, { kwargs, method }) {
|
||||
if (method === "onchange") {
|
||||
domain = [["id", "in", [10]]];
|
||||
return Promise.resolve({
|
||||
value: {
|
||||
trululu: false,
|
||||
},
|
||||
domain: {
|
||||
trululu: domain,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (method === "search_read") {
|
||||
assert.deepEqual(kwargs.domain, domain, "sent domain should be correct");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget[name='trululu'] .o_radio_item",
|
||||
3,
|
||||
"should be 3 radio buttons"
|
||||
);
|
||||
|
||||
// trigger an onchange that will update the domain
|
||||
await editInput(target, ".o_field_widget[name='int_field'] input", "2");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget[name='trululu'] .o_radio_item",
|
||||
"should be no more radio button"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("field is empty", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
serverData,
|
||||
arch: `
|
||||
<form edit="0">
|
||||
<field name="trululu" widget="radio" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelector(".o_field_widget[name=trululu]"), "o_field_empty");
|
||||
assert.containsN(target, ".o_radio_input", 3);
|
||||
assert.containsN(target, ".o_radio_input:disabled", 3);
|
||||
assert.containsNone(target, ".o_radio_input:checked");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,509 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
patchDate,
|
||||
patchTimeZone,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
date: { string: "A date", type: "date", searchable: true },
|
||||
datetime: { string: "A datetime", type: "datetime", searchable: true },
|
||||
},
|
||||
records: [],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("RemainingDaysField");
|
||||
|
||||
QUnit.test("RemainingDaysField on a date field in list view", async function (assert) {
|
||||
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
|
||||
serverData.models.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 makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="date" widget="remaining_days" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
const cells = target.querySelectorAll(".o_data_cell");
|
||||
assert.strictEqual(cells[0].textContent, "Today");
|
||||
assert.strictEqual(cells[1].textContent, "Tomorrow");
|
||||
assert.strictEqual(cells[2].textContent, "Yesterday");
|
||||
assert.strictEqual(cells[3].textContent, "In 2 days");
|
||||
assert.strictEqual(cells[4].textContent, "3 days ago");
|
||||
assert.strictEqual(cells[5].textContent, "02/08/2018");
|
||||
assert.strictEqual(cells[6].textContent, "06/08/2017");
|
||||
assert.strictEqual(cells[7].textContent, "");
|
||||
|
||||
assert.hasAttrValue(cells[0].querySelector(".o_field_widget > div"), "title", "10/08/2017");
|
||||
assert.hasClass(cells[0].querySelector(".o_field_widget > div"), "fw-bold text-warning");
|
||||
assert.doesNotHaveClass(
|
||||
cells[1].querySelector(".o_field_widget > div"),
|
||||
"fw-bold text-warning text-danger"
|
||||
);
|
||||
assert.hasClass(cells[2].querySelector(".o_field_widget > div"), "fw-bold text-danger");
|
||||
assert.doesNotHaveClass(
|
||||
cells[3].querySelector(".o_field_widget > div"),
|
||||
"fw-bold text-warning text-danger"
|
||||
);
|
||||
assert.hasClass(cells[4].querySelector(".o_field_widget > div"), "fw-bold text-danger");
|
||||
assert.doesNotHaveClass(
|
||||
cells[5].querySelector(".o_field_widget > div"),
|
||||
"fw-bold text-warning text-danger"
|
||||
);
|
||||
assert.hasClass(cells[6].querySelector(".o_field_widget > div"), "fw-bold text-danger");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"RemainingDaysField on a date field in multi edit list view",
|
||||
async function (assert) {
|
||||
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
|
||||
serverData.models.partner.records = [
|
||||
{ id: 1, date: "2017-10-08" }, // today
|
||||
{ id: 2, date: "2017-10-09" }, // tomorrow
|
||||
{ id: 8, date: false },
|
||||
];
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree multi_edit="1">
|
||||
<field name="date" widget="remaining_days" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
const cells = target.querySelectorAll(".o_data_cell");
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
|
||||
assert.strictEqual(cells[0].textContent, "Today");
|
||||
assert.strictEqual(cells[1].textContent, "Tomorrow");
|
||||
|
||||
// select two records and edit them
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[1], ".o_list_record_selector input");
|
||||
|
||||
await click(rows[0], ".o_data_cell");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"input.o_datepicker_input",
|
||||
"should have date picker input"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_datepicker_input", "10/10/2017");
|
||||
await click(target);
|
||||
|
||||
assert.containsOnce(document.body, ".modal");
|
||||
assert.strictEqual(
|
||||
document.querySelector(".modal .o_field_widget").textContent,
|
||||
"In 2 days",
|
||||
"should have 'In 2 days' value to change"
|
||||
);
|
||||
await click(document.body, ".modal .modal-footer .btn-primary");
|
||||
|
||||
assert.strictEqual(
|
||||
rows[0].querySelector(".o_data_cell").textContent,
|
||||
"In 2 days",
|
||||
"should have 'In 2 days' as date field value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
rows[1].querySelector(".o_data_cell").textContent,
|
||||
"In 2 days",
|
||||
"should have 'In 2 days' as date field value"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"RemainingDaysField, enter wrong value manually in multi edit list view",
|
||||
async function (assert) {
|
||||
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
|
||||
serverData.models.partner.records = [
|
||||
{ id: 1, date: "2017-10-08" }, // today
|
||||
{ id: 2, date: "2017-10-09" }, // tomorrow
|
||||
{ id: 8, date: false },
|
||||
];
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree multi_edit="1">
|
||||
<field name="date" widget="remaining_days" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
const cells = target.querySelectorAll(".o_data_cell");
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
|
||||
assert.strictEqual(cells[0].textContent, "Today");
|
||||
assert.strictEqual(cells[1].textContent, "Tomorrow");
|
||||
|
||||
// select two records and edit them
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[1], ".o_list_record_selector input");
|
||||
|
||||
await click(rows[0], ".o_data_cell");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"input.o_datepicker_input",
|
||||
"should have date picker input"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_datepicker_input", "blabla");
|
||||
await click(target);
|
||||
assert.containsNone(document.body, ".modal");
|
||||
assert.strictEqual(cells[0].textContent, "Today");
|
||||
assert.strictEqual(cells[1].textContent, "Tomorrow");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("RemainingDaysField on a date field in form view", async function (assert) {
|
||||
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
|
||||
serverData.models.partner.records = [
|
||||
{ id: 1, date: "2017-10-08" }, // today
|
||||
];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date" widget="remaining_days" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "10/08/2017");
|
||||
|
||||
assert.containsOnce(target, ".o_form_editable");
|
||||
assert.containsOnce(target, "div.o_field_widget[name='date'] .o_datepicker");
|
||||
|
||||
await click(target.querySelector(".o_datepicker .o_datepicker_input"));
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
|
||||
await click(document.body, ".bootstrap-datetimepicker-widget .day[data-day='10/09/2017']");
|
||||
await click(target, ".o_form_button_save");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "10/09/2017");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"RemainingDaysField on a date field on a new record in form",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date" widget="remaining_days" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_form_editable .o_field_widget[name='date'] .o_datepicker"
|
||||
);
|
||||
await click(target.querySelector(".o_field_widget[name='date'] .o_datepicker input"));
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("RemainingDaysField in form view (readonly)", async function (assert) {
|
||||
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
|
||||
serverData.models.partner.records = [
|
||||
{ id: 1, date: "2017-10-08", datetime: "2017-10-08 10:00:00" }, // today
|
||||
];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date" widget="remaining_days" readonly="1" />
|
||||
<field name="datetime" widget="remaining_days" readonly="1" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='date']").textContent,
|
||||
"Today"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name='date'] > div "),
|
||||
"fw-bold text-warning"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='datetime']").textContent,
|
||||
"Today"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name='datetime'] > div "),
|
||||
"fw-bold text-warning"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("RemainingDaysField on a datetime field in form view", async function (assert) {
|
||||
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
|
||||
serverData.models.partner.records = [
|
||||
{ id: 1, datetime: "2017-10-08 10:00:00" }, // today
|
||||
];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="datetime" widget="remaining_days" />
|
||||
</form>`,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"10/08/2017 11:00:00"
|
||||
);
|
||||
assert.containsOnce(target, "div.o_field_widget[name='datetime'] .o_datepicker");
|
||||
|
||||
await click(target.querySelector(".o_datepicker .o_datepicker_input"));
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
|
||||
await click(document.body, ".bootstrap-datetimepicker-widget .day[data-day='10/09/2017']");
|
||||
await click(document.body, "a[data-action='close']");
|
||||
await click(target, ".o_form_button_save");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"10/09/2017 11:00:00"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"RemainingDaysField on a datetime field in list view in UTC",
|
||||
async function (assert) {
|
||||
patchTimeZone(0);
|
||||
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
|
||||
serverData.models.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 makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="datetime" widget="remaining_days" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[0].textContent, "Today");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[1].textContent, "Tomorrow");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[2].textContent, "Yesterday");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[3].textContent, "In 2 days");
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_data_cell")[4].textContent,
|
||||
"3 days ago"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_data_cell")[5].textContent,
|
||||
"02/08/2018"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_data_cell")[6].textContent,
|
||||
"06/08/2017"
|
||||
);
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[7].textContent, "");
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_data_cell .o_field_widget div"),
|
||||
"title",
|
||||
"10/08/2017"
|
||||
);
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_data_cell div div")[0],
|
||||
"fw-bold text-warning"
|
||||
);
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelectorAll(".o_data_cell div div")[1],
|
||||
"fw-bold text-warning text-danger"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_data_cell div div")[2],
|
||||
"fw-bold text-danger"
|
||||
);
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelectorAll(".o_data_cell div div")[3],
|
||||
"fw-bold text-warning text-danger"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_data_cell div div")[4],
|
||||
"fw-bold text-danger"
|
||||
);
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelectorAll(".o_data_cell div div")[5],
|
||||
"fw-bold text-warning text-danger"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_data_cell div div")[6],
|
||||
"fw-bold text-danger"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"RemainingDaysField on a datetime field in list view in UTC+6",
|
||||
async function (assert) {
|
||||
patchTimeZone(360);
|
||||
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11, UTC+6
|
||||
serverData.models.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 makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="datetime" widget="remaining_days" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[0].textContent, "Tomorrow");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[1].textContent, "Tomorrow");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[2].textContent, "Today");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[3].textContent, "Yesterday");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[4].textContent, "In 2 days");
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_data_cell .o_field_widget div"),
|
||||
"title",
|
||||
"10/09/2017"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("RemainingDaysField on a date field in list view in UTC-6", async function (assert) {
|
||||
patchTimeZone(-360);
|
||||
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
|
||||
serverData.models.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 makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="date" widget="remaining_days" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[0].textContent, "Today");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[1].textContent, "Tomorrow");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[2].textContent, "Yesterday");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[3].textContent, "In 2 days");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[4].textContent, "3 days ago");
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_data_cell .o_field_widget div"),
|
||||
"title",
|
||||
"10/08/2017"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"RemainingDaysField on a datetime field in list view in UTC-8",
|
||||
async function (assert) {
|
||||
patchTimeZone(-560);
|
||||
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11, UTC-8
|
||||
serverData.models.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 makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="datetime" widget="remaining_days" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[0].textContent, "Today");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[1].textContent, "Today");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[2].textContent, "Tomorrow");
|
||||
assert.strictEqual(target.querySelectorAll(".o_data_cell")[3].textContent, "Yesterday");
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_data_cell")[4].textContent,
|
||||
"2 days ago"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, editSelect, editInput, getFixture, clickSave } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
int_field: { string: "int_field", type: "integer", sortable: true },
|
||||
trululu: { string: "Trululu", type: "many2one", relation: "partner" },
|
||||
product_id: { string: "Product", type: "many2one", relation: "product" },
|
||||
color: {
|
||||
type: "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",
|
||||
},
|
||||
],
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("SelectionField");
|
||||
|
||||
QUnit.test("SelectionField in a list view", async function (assert) {
|
||||
serverData.models.partner.records.forEach(function (r) {
|
||||
r.color = "red";
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<tree string="Colors" editable="top"><field name="color"/></tree>',
|
||||
});
|
||||
|
||||
assert.containsN(target, ".o_data_row", 3);
|
||||
await click(target.querySelector(".o_data_cell"));
|
||||
|
||||
const td = target.querySelector("tbody tr.o_selected_row td:not(.o_list_record_selector)");
|
||||
assert.containsOnce(td, "select", "td should have a child 'select'");
|
||||
assert.strictEqual(td.children.length, 1, "select tag should be only child of td");
|
||||
});
|
||||
|
||||
QUnit.test("SelectionField, edition and on many2one field", async function (assert) {
|
||||
serverData.models.partner.onchanges = { product_id: function () {} };
|
||||
serverData.models.partner.records[0].product_id = 37;
|
||||
serverData.models.partner.records[0].trululu = false;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id" widget="selection" />
|
||||
<field name="trululu" widget="selection" />
|
||||
<field name="color" widget="selection" />
|
||||
</form>`,
|
||||
mockRPC(route, { method }) {
|
||||
assert.step(method);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(target, "select", 3);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget[name='product_id'] select option[value='37']",
|
||||
"should have fetched xphone option"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget[name='product_id'] select option[value='41']",
|
||||
"should have fetched xpad option"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='product_id'] select").value,
|
||||
"37",
|
||||
"should have correct product_id value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='trululu'] select").value,
|
||||
"false",
|
||||
"should not have any value in trululu field"
|
||||
);
|
||||
|
||||
await editSelect(target, ".o_field_widget[name='product_id'] select", "41");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='product_id'] select").value,
|
||||
"41",
|
||||
"should have a value of xphone"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='color'] select").value,
|
||||
`"red"`,
|
||||
"should have correct value in color field"
|
||||
);
|
||||
|
||||
assert.verifySteps(["get_views", "read", "name_search", "name_search", "onchange"]);
|
||||
});
|
||||
|
||||
QUnit.test("unset selection field with 0 as key", async function (assert) {
|
||||
// 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.
|
||||
serverData.models.partner.fields.selection = {
|
||||
type: "selection",
|
||||
selection: [
|
||||
[0, "Value O"],
|
||||
[1, "Value 1"],
|
||||
],
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form edit="0"><field name="selection" /></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget").textContent,
|
||||
"Value O",
|
||||
"the displayed value should be 'Value O'"
|
||||
);
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_field_widget"),
|
||||
"o_field_empty",
|
||||
"should not have class o_field_empty"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("unset selection field with string keys", async function (assert) {
|
||||
// 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.
|
||||
serverData.models.partner.fields.selection = {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["0", "Value O"],
|
||||
["1", "Value 1"],
|
||||
],
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form edit="0"><field name="selection" /></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget").textContent,
|
||||
"",
|
||||
"there should be no displayed value"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget"),
|
||||
"o_field_empty",
|
||||
"should have class o_field_empty"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("unset selection on a many2one field", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="trululu" widget="selection" /></form>',
|
||||
mockRPC(route, { args, method }) {
|
||||
if (method === "write") {
|
||||
assert.strictEqual(
|
||||
args[1].trululu,
|
||||
false,
|
||||
"should send 'false' as trululu value"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await editSelect(target, ".o_form_view select", "false");
|
||||
await clickSave(target);
|
||||
});
|
||||
|
||||
QUnit.test("field selection with many2ones and special characters", async function (assert) {
|
||||
// edit the partner with id=4
|
||||
serverData.models.partner.records[2].display_name = "<span>hey</span>";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="trululu" widget="selection" /></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("select option[value='4']").textContent,
|
||||
"<span>hey</span>"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"SelectionField on a many2one: domain updated by an onchange",
|
||||
async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
int_field() {},
|
||||
};
|
||||
|
||||
let domain = [];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field" />
|
||||
<field name="trululu" widget="selection" />
|
||||
</form>`,
|
||||
mockRPC(route, { args, method }) {
|
||||
if (method === "onchange") {
|
||||
domain = [["id", "in", [10]]];
|
||||
return Promise.resolve({
|
||||
domain: {
|
||||
trululu: domain,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (method === "name_search") {
|
||||
assert.deepEqual(args[1], domain, "sent domain should be correct");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget[name='trululu'] option",
|
||||
4,
|
||||
"should be 4 options in the selection"
|
||||
);
|
||||
|
||||
// trigger an onchange that will update the domain
|
||||
await editInput(target, ".o_field_widget[name='int_field'] input", 2);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget[name='trululu'] option",
|
||||
"should be 1 option in the selection"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("required selection widget should not have blank option", async function (assert) {
|
||||
serverData.models.partner.fields.feedback_value = {
|
||||
type: "selection",
|
||||
required: true,
|
||||
selection: [
|
||||
["good", "Good"],
|
||||
["bad", "Bad"],
|
||||
],
|
||||
default: "good",
|
||||
string: "Good",
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="feedback_value" />
|
||||
<field name="color" attrs="{'required': [('feedback_value', '=', 'bad')]}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_widget[name='color'] option")].map(
|
||||
(option) => option.style.display
|
||||
),
|
||||
["", "", ""]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_widget[name='feedback_value'] option")].map(
|
||||
(option) => option.style.display
|
||||
),
|
||||
["none", "", ""]
|
||||
);
|
||||
|
||||
// change value to update widget modifier values
|
||||
await editSelect(target, ".o_field_widget[name='feedback_value'] select", '"bad"');
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_widget[name='color'] option")].map(
|
||||
(option) => option.style.display
|
||||
),
|
||||
["none", "", ""]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"required selection widget should have only one blank option",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.feedback_value = {
|
||||
type: "selection",
|
||||
required: true,
|
||||
selection: [
|
||||
["good", "Good"],
|
||||
["bad", "Bad"],
|
||||
],
|
||||
default: "good",
|
||||
string: "Good",
|
||||
};
|
||||
serverData.models.partner.fields.color.selection = [
|
||||
[false, ""],
|
||||
...serverData.models.partner.fields.color.selection,
|
||||
];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="feedback_value" />
|
||||
<field name="color" attrs="{'required': [('feedback_value', '=', 'bad')]}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target.querySelector(".o_field_widget[name='color']"),
|
||||
"option",
|
||||
3,
|
||||
"Three options in non required field (one blank option)"
|
||||
);
|
||||
|
||||
// change value to update widget modifier values
|
||||
await editSelect(target, ".o_field_widget[name='feedback_value'] select", '"bad"');
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_widget[name='color'] option")].map(
|
||||
(option) => option.style.display
|
||||
),
|
||||
["none", "", ""]
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("selection field with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="trululu" widget="selection" placeholder="Placeholder"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const placeholderOption = target.querySelector(
|
||||
".o_field_widget[name='trululu'] select option"
|
||||
);
|
||||
assert.strictEqual(placeholderOption.textContent, "Placeholder");
|
||||
assert.strictEqual(placeholderOption.value, "false");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
/** @odoo-module **/
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { NameAndSignature } from "@web/core/signature/name_and_signature";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
function getUnique(target) {
|
||||
const src = target.dataset.src;
|
||||
return new URL(src).searchParams.get("unique");
|
||||
}
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Name", type: "char" },
|
||||
product_id: {
|
||||
string: "Product Name",
|
||||
type: "many2one",
|
||||
relation: "product",
|
||||
},
|
||||
sign: { string: "Signature", type: "binary" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "Pop's Chock'lit",
|
||||
product_id: 7,
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 7,
|
||||
display_name: "Veggie Burger",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Signature Field");
|
||||
|
||||
QUnit.test("Set simple field in 'full_name' node option", async function (assert) {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
this._super.apply(this, arguments);
|
||||
assert.step(this.props.signature.name);
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<form>
|
||||
<field name="display_name"/>
|
||||
<field name="sign" widget="signature" options="{'full_name': 'display_name'}" />
|
||||
</form>`,
|
||||
mockRPC: async (route) => {
|
||||
if (route === "/web/sign/get_fonts/") {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div[name=sign] div.o_signature svg",
|
||||
"should have a valid signature widget"
|
||||
);
|
||||
// Click on the widget to open signature modal
|
||||
await click(target, "div[name=sign] div.o_signature");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".modal .modal-body a.o_web_sign_auto_button",
|
||||
'should open a modal with "Auto" button'
|
||||
);
|
||||
assert.verifySteps(["Pop's Chock'lit"]);
|
||||
});
|
||||
|
||||
QUnit.test("Set m2o field in 'full_name' node option", async function (assert) {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
this._super.apply(this, arguments);
|
||||
assert.step(this.props.signature.name);
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<form>
|
||||
<field name="product_id"/>
|
||||
<field name="sign" widget="signature" options="{'full_name': 'product_id'}" />
|
||||
</form>`,
|
||||
mockRPC: async (route) => {
|
||||
if (route === "/web/sign/get_fonts/") {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div[name=sign] div.o_signature svg",
|
||||
"should have a valid signature widget"
|
||||
);
|
||||
|
||||
// Click on the widget to open signature modal
|
||||
await click(target, "div[name=sign] div.o_signature");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".modal .modal-body a.o_web_sign_auto_button",
|
||||
'should open a modal with "Auto" button'
|
||||
);
|
||||
assert.verifySteps(["Veggie Burger"]);
|
||||
});
|
||||
|
||||
QUnit.test("Set size (width and height) in node option", async function (assert) {
|
||||
serverData.models.partner.fields.sign2 = { string: "Signature", type: "binary" };
|
||||
serverData.models.partner.fields.sign3 = { string: "Signature", type: "binary" };
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<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>`,
|
||||
mockRPC: async (route) => {
|
||||
if (route === "/web/sign/get_fonts/") {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(target, ".o_signature", 3);
|
||||
|
||||
const sign = target.querySelector("[name='sign'] .o_signature");
|
||||
assert.strictEqual(sign.style.width, "150px");
|
||||
assert.strictEqual(sign.style.height, "50px");
|
||||
|
||||
const sign2 = target.querySelector("[name='sign2'] .o_signature");
|
||||
assert.strictEqual(sign2.style.width, "300px");
|
||||
assert.strictEqual(sign2.style.height, "100px");
|
||||
|
||||
const sign3 = target.querySelector("[name='sign3'] .o_signature");
|
||||
assert.strictEqual(sign3.style.width, "120px");
|
||||
assert.strictEqual(sign3.style.height, "40px");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"clicking save manually after changing signature should change the unique of the image src",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.foo = { type: "char" };
|
||||
serverData.models.partner.onchanges = { foo: () => {} };
|
||||
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.sign = "3 kb";
|
||||
rec.__last_update = "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;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="sign" widget="signature" />
|
||||
</form>`,
|
||||
mockRPC(route, { method, args }) {
|
||||
if (route === "/web/sign/get_fonts/") {
|
||||
return {};
|
||||
}
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
args[1].__last_update = lastUpdates[index];
|
||||
args[1].sign = "4 kb";
|
||||
index++;
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_signature img")),
|
||||
"1659688620000"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_signature img", true);
|
||||
assert.containsOnce(target, ".modal canvas");
|
||||
|
||||
let canvas = target.querySelector(".modal canvas");
|
||||
canvas.setAttribute("width", "2px");
|
||||
canvas.setAttribute("height", "2px");
|
||||
let ctx = canvas.getContext("2d");
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = "blue";
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(0, 2);
|
||||
ctx.stroke();
|
||||
await triggerEvent(target, ".o_web_sign_signature", "change");
|
||||
await click(target, ".modal-footer .btn-primary");
|
||||
|
||||
const MYB64 = `iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABRJREFUGFdjZGD438DAwNjACGMAACQlBAMW7JulAAAAAElFTkSuQmCC`;
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=sign] img").dataset.src,
|
||||
`data:image/png;base64,${MYB64}`
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=sign] img").dataset.src,
|
||||
`data:image/png;base64,${MYB64}`
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.verifySteps(["write"]);
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_signature img")),
|
||||
"1659692220000"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_signature img", true);
|
||||
assert.containsOnce(target, ".modal canvas");
|
||||
|
||||
canvas = target.querySelector(".modal canvas");
|
||||
canvas.setAttribute("width", "2px");
|
||||
canvas.setAttribute("height", "2px");
|
||||
ctx = canvas.getContext("2d");
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = "blue";
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(2, 0);
|
||||
ctx.stroke();
|
||||
await triggerEvent(target, ".o_web_sign_signature", "change");
|
||||
await click(target, ".modal-footer .btn-primary");
|
||||
|
||||
const MYB64_2 = `iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABVJREFUGFdjZGD438DAwMDACCJAAAAWHgGCN0++VgAAAABJRU5ErkJggg==`;
|
||||
assert.notOk(MYB64 === MYB64_2);
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=sign] img").dataset.src,
|
||||
`data:image/png;base64,${MYB64_2}`
|
||||
);
|
||||
await clickSave(target);
|
||||
assert.verifySteps(["write"]);
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_signature img")),
|
||||
"1659695820000"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("save record with signature field modified by onchange", async function (assert) {
|
||||
const MYB64 = `iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABRJREFUGFdjZGD438DAwNjACGMAACQlBAMW7JulAAAAAElFTkSuQmCC`;
|
||||
|
||||
serverData.models.partner.fields.foo = { type: "char" };
|
||||
serverData.models.partner.onchanges = {
|
||||
foo: (data) => {
|
||||
data.sign = MYB64;
|
||||
},
|
||||
};
|
||||
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.sign = "3 kb";
|
||||
rec.__last_update = "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;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="sign" widget="signature" />
|
||||
</form>`,
|
||||
mockRPC(route, { method, args }) {
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
args[1].__last_update = lastUpdates[index];
|
||||
args[1].sign = "4 kb";
|
||||
index++;
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_signature img")),
|
||||
"1659688620000"
|
||||
);
|
||||
await editInput(target, "[name='foo'] input", "grrr");
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=sign] img").dataset.src,
|
||||
`data:image/png;base64,${MYB64}`
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_signature img")),
|
||||
"1659692220000"
|
||||
);
|
||||
assert.verifySteps(["write"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
qux: { string: "Qux", type: "float", digits: [16, 1], searchable: true },
|
||||
monetary: { string: "Monetary", type: "monetary" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
foo: "yop",
|
||||
int_field: 10,
|
||||
qux: 0.44444,
|
||||
monetary: 9.999999,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("StatInfoField");
|
||||
|
||||
QUnit.test("StatInfoField formats decimal precision", async function (assert) {
|
||||
// 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 makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<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
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".oe_stat_button .o_field_widget .o_stat_value")[0].textContent,
|
||||
"0.4",
|
||||
"Default precision should be [16,1]"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".oe_stat_button .o_field_widget .o_stat_value")[1].textContent,
|
||||
"10.00",
|
||||
"Currency decimal precision should be 2"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("StatInfoField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<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>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".oe_stat_button .o_field_widget .o_stat_info",
|
||||
"should have one stat button"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".oe_stat_button .o_field_widget .o_stat_value").textContent,
|
||||
"10",
|
||||
"should have 10 as value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".oe_stat_button .o_field_widget .o_stat_text").textContent,
|
||||
"int_field",
|
||||
"should have 'int_field' as text"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("StatInfoField in form view with specific label_field", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<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>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".oe_stat_button .o_field_widget .o_stat_info",
|
||||
"should have one stat button"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".oe_stat_button .o_field_widget .o_stat_value").textContent,
|
||||
"10",
|
||||
"should have 10 as value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".oe_stat_button .o_field_widget .o_stat_text").textContent,
|
||||
"yop",
|
||||
"should have 'yop' as text, since it is the value of field foo"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("StatInfoField in form view with no label", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<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>`,
|
||||
});
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".oe_stat_button .o_field_widget .o_stat_info",
|
||||
"should have one stat button"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".oe_stat_button .o_field_widget .o_stat_value").textContent,
|
||||
"10",
|
||||
"should have 10 as value"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".oe_stat_button .o_field_widget .o_stat_text",
|
||||
"should not have any label"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,598 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture, nextTick, triggerHotkey } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
},
|
||||
sequence: { type: "integer", string: "Sequence", searchable: true },
|
||||
selection: {
|
||||
string: "Selection",
|
||||
type: "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" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("StateSelectionField");
|
||||
|
||||
QUnit.test("StateSelectionField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="state_selection"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status.o_status_red",
|
||||
"should have one red status since selection is the second, blocked state"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status.o_status_green",
|
||||
"should not have one green status since selection is the second, blocked state"
|
||||
);
|
||||
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_state_selection .dropdown-toggle").dataset.tooltip,
|
||||
"Blocked",
|
||||
"tooltip attribute has the right text"
|
||||
);
|
||||
|
||||
// Click on the status button to make the dropdown appear
|
||||
await click(target, ".o_field_widget.o_field_state_selection .o_status");
|
||||
assert.containsOnce(document.body, ".dropdown-menu", "there should be a dropdown");
|
||||
assert.containsN(
|
||||
target,
|
||||
".dropdown-menu .dropdown-item",
|
||||
2,
|
||||
"there should be two options in the dropdown"
|
||||
);
|
||||
|
||||
// Click on the first option, "Normal"
|
||||
await click(target.querySelector(".dropdown-menu .dropdown-item"));
|
||||
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown anymore");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status.o_status_red",
|
||||
"should not have one red status since selection is the first, normal state"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status.o_status_green",
|
||||
"should not have one green status since selection is the first, normal state"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status",
|
||||
"should have one grey status since selection is the first, normal state"
|
||||
);
|
||||
|
||||
assert.containsNone(target, ".dropdown-menu", "there should still not be a dropdown");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status.o_status_red",
|
||||
"should still not have one red status since selection is the first, normal state"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status.o_status_green",
|
||||
"should still not have one green status since selection is the first, normal state"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status",
|
||||
"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(target, ".o_field_widget.o_field_state_selection .o_status");
|
||||
assert.containsOnce(target, ".dropdown-menu", "there should be a dropdown");
|
||||
assert.containsN(
|
||||
target,
|
||||
".dropdown-menu .dropdown-item",
|
||||
2,
|
||||
"there should be two options in the dropdown"
|
||||
);
|
||||
|
||||
// Click on the last option, "Done"
|
||||
await click(target, ".dropdown-menu .dropdown-item:last-child");
|
||||
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown anymore");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status.o_status_red",
|
||||
"should not have one red status since selection is the third, done state"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status.o_status_green",
|
||||
"should have one green status since selection is the third, done state"
|
||||
);
|
||||
|
||||
// save
|
||||
await click(target.querySelector(".o_form_button_save"));
|
||||
assert.containsNone(
|
||||
target,
|
||||
".dropdown-menu",
|
||||
"there should still not be a dropdown anymore"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status.o_status_red",
|
||||
"should still not have one red status since selection is the third, done state"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget.o_field_state_selection span.o_status.o_status_green",
|
||||
"should still have one green status since selection is the third, done state"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("StateSelectionField with readonly modifier", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="selection" widget="state_selection" readonly="1"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelector(".o_field_state_selection"), "o_readonly_modifier");
|
||||
assert.isNotVisible(target.querySelector(".dropdown-menu"));
|
||||
await click(target, ".o_field_state_selection span.o_status");
|
||||
assert.isNotVisible(target.querySelector(".dropdown-menu"));
|
||||
});
|
||||
|
||||
QUnit.test("StateSelectionField for list view with hide_label option", async function (assert) {
|
||||
Object.assign(serverData.models.partner.fields, {
|
||||
graph_type: {
|
||||
string: "Graph Type",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["line", "Line"],
|
||||
["bar", "Bar"],
|
||||
],
|
||||
},
|
||||
});
|
||||
serverData.models.partner.records[0].graph_type = "bar";
|
||||
serverData.models.partner.records[1].graph_type = "line";
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="graph_type" widget="state_selection" options="{'hide_label': True}"/>
|
||||
<field name="selection" widget="state_selection"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status",
|
||||
10,
|
||||
"should have ten status selection widgets"
|
||||
);
|
||||
const selection = Array.from(
|
||||
target.querySelectorAll(
|
||||
".o_state_selection_cell .o_field_state_selection[name=selection] span.o_status_label"
|
||||
)
|
||||
);
|
||||
assert.strictEqual(selection.length, 5, "should have five label on selection widgets");
|
||||
assert.strictEqual(
|
||||
selection.filter((el) => el.textContent === "Done").length,
|
||||
1,
|
||||
"should have one Done status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
selection.filter((el) => el.textContent === "Normal").length,
|
||||
3,
|
||||
"should have three Normal status label"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection[name=graph_type] span.o_status",
|
||||
5,
|
||||
"should have five status selection widgets"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection[name=graph_type] span.o_status_label",
|
||||
"should not have status label in selection widgets"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("StateSelectionField in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="foo"/>
|
||||
<field name="selection" widget="state_selection"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status",
|
||||
5,
|
||||
"should have five status selection widgets"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red",
|
||||
"should have one red status"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green",
|
||||
"should have one green status"
|
||||
);
|
||||
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
|
||||
|
||||
// Click on the status button to make the dropdown appear
|
||||
let cell = target.querySelector("tbody td.o_state_selection_cell");
|
||||
await click(
|
||||
target.querySelector(".o_state_selection_cell .o_field_state_selection span.o_status")
|
||||
);
|
||||
assert.doesNotHaveClass(
|
||||
cell.parentElement,
|
||||
"o_selected_row",
|
||||
"should not be in edit mode since we clicked on the state selection widget"
|
||||
);
|
||||
assert.containsOnce(target, ".dropdown-menu", "there should be a dropdown");
|
||||
assert.containsN(
|
||||
target,
|
||||
".dropdown-menu .dropdown-item",
|
||||
2,
|
||||
"there should be two options in the dropdown"
|
||||
);
|
||||
|
||||
// Click on the first option, "Normal"
|
||||
await click(target.querySelector(".dropdown-menu .dropdown-item"));
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status",
|
||||
5,
|
||||
"should still have five status selection widgets"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red",
|
||||
"should now have no red status"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green",
|
||||
"should still have one green status"
|
||||
);
|
||||
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
|
||||
assert.containsNone(target, "tr.o_selected_row", "should not be in edit mode");
|
||||
|
||||
// switch to edit mode and check the result
|
||||
cell = target.querySelector("tbody td.o_state_selection_cell");
|
||||
await click(cell);
|
||||
assert.hasClass(cell.parentElement, "o_selected_row", "should now be in edit mode");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status",
|
||||
5,
|
||||
"should still have five status selection widgets"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red",
|
||||
"should now have no red status"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green",
|
||||
"should still have one green status"
|
||||
);
|
||||
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
|
||||
|
||||
// Click on the status button to make the dropdown appear
|
||||
await click(
|
||||
target.querySelector(".o_state_selection_cell .o_field_state_selection span.o_status")
|
||||
);
|
||||
assert.containsOnce(target, ".dropdown-menu", "there should be a dropdown");
|
||||
assert.containsN(
|
||||
target,
|
||||
".dropdown-menu .dropdown-item",
|
||||
2,
|
||||
"there should be two options in the dropdown"
|
||||
);
|
||||
|
||||
// Click on another row
|
||||
const lastCell = target.querySelectorAll("tbody td.o_state_selection_cell")[4];
|
||||
await click(lastCell);
|
||||
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown anymore");
|
||||
const firstCell = target.querySelector("tbody td.o_state_selection_cell");
|
||||
assert.doesNotHaveClass(
|
||||
firstCell.parentElement,
|
||||
"o_selected_row",
|
||||
"first row should not be in edit mode anymore"
|
||||
);
|
||||
assert.hasClass(
|
||||
lastCell.parentElement,
|
||||
"o_selected_row",
|
||||
"last row should be in edit mode"
|
||||
);
|
||||
|
||||
// Click on the fourth status button to make the dropdown appear
|
||||
await click(
|
||||
target.querySelectorAll(
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status"
|
||||
)[3]
|
||||
);
|
||||
assert.containsOnce(target, ".dropdown-menu", "there should be a dropdown");
|
||||
assert.containsN(
|
||||
target,
|
||||
".dropdown-menu .dropdown-item",
|
||||
2,
|
||||
"there should be two options in the dropdown"
|
||||
);
|
||||
|
||||
// Click on the last option, "Done"
|
||||
await click(target, ".dropdown-menu .dropdown-item:last-child");
|
||||
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown anymore");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status",
|
||||
5,
|
||||
"should still have five status selection widgets"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red",
|
||||
"should still have no red status"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green",
|
||||
2,
|
||||
"should now have two green status"
|
||||
);
|
||||
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
|
||||
|
||||
// save
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status",
|
||||
5,
|
||||
"should have five status selection widgets"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red",
|
||||
"should have no red status"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green",
|
||||
2,
|
||||
"should have two green status"
|
||||
);
|
||||
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
'StateSelectionField edited by the smart action "Set kanban state..."',
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="selection" widget="state_selection"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_status_red");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
const idx = [...target.querySelectorAll(".o_command")]
|
||||
.map((el) => el.textContent)
|
||||
.indexOf("Set kanban state...ALT + SHIFT + R");
|
||||
assert.ok(idx >= 0);
|
||||
|
||||
await click([...target.querySelectorAll(".o_command")][idx]);
|
||||
await nextTick();
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
|
||||
["Normal", "Blocked", "Done"]
|
||||
);
|
||||
await click(target, "#o_command_2");
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_status_green");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("StateSelectionField uses legend_* fields", async function (assert) {
|
||||
serverData.models.partner.fields.legend_normal = { type: "char" };
|
||||
serverData.models.partner.fields.legend_blocked = { type: "char" };
|
||||
serverData.models.partner.fields.legend_done = { type: "char" };
|
||||
serverData.models.partner.records[0].legend_normal = "Custom normal";
|
||||
serverData.models.partner.records[0].legend_blocked = "Custom blocked";
|
||||
serverData.models.partner.records[0].legend_done = "Custom done";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<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(target, ".o_status");
|
||||
let dropdownItemTexts = [...target.querySelectorAll(".dropdown-item")].map(
|
||||
(el) => el.textContent
|
||||
);
|
||||
assert.deepEqual(dropdownItemTexts, ["Custom normal", "Custom done"]);
|
||||
|
||||
await click(target.querySelector(".dropdown-item .o_status"));
|
||||
await click(target, ".o_status");
|
||||
dropdownItemTexts = [...target.querySelectorAll(".dropdown-item")].map(
|
||||
(el) => el.textContent
|
||||
);
|
||||
assert.deepEqual(dropdownItemTexts, ["Custom blocked", "Custom done"]);
|
||||
});
|
||||
|
||||
QUnit.test("works when required in a readonly view ", async function (assert) {
|
||||
serverData.models.partner.records[0].selection = "normal";
|
||||
serverData.models.partner.records = [serverData.models.partner.records[0]];
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="selection" widget="state_selection" required="1"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
mockRPC: (route, args, performRPC) => {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
assert.step("write");
|
||||
}
|
||||
return performRPC(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_state_selection button");
|
||||
const doneItem = target.querySelectorAll(".dropdown-item")[1]; // item "done";
|
||||
await click(doneItem);
|
||||
|
||||
assert.verifySteps(["write"]);
|
||||
assert.hasClass(target.querySelector(".o_field_state_selection span"), "o_status_green");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"StateSelectionField - auto save record when field toggled",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="state_selection"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(_route, { method }) {
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_widget.o_field_state_selection .o_status");
|
||||
await click(target, ".dropdown-menu .dropdown-item:last-child");
|
||||
assert.verifySteps(["write"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"StateSelectionField - prevent auto save with autosave option",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="state_selection" options="{'autosave': False}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(_route, { method }) {
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_widget.o_field_state_selection .o_status");
|
||||
await click(target, ".dropdown-menu .dropdown-item:last-child");
|
||||
assert.verifySteps([]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,684 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { session } from "@web/session";
|
||||
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerHotkey,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
import { EventBus } from "@odoo/owl";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
foo: { string: "Foo", type: "char", default: "My little Foo Value" },
|
||||
bar: { string: "Bar", type: "boolean", default: true },
|
||||
int_field: { string: "int_field", type: "integer", sortable: true },
|
||||
qux: { string: "Qux", type: "float", digits: [16, 1] },
|
||||
p: {
|
||||
string: "one2many field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
relation_field: "trululu",
|
||||
},
|
||||
trululu: { string: "Trululu", type: "many2one", relation: "partner" },
|
||||
product_id: { string: "Product", type: "many2one", relation: "product" },
|
||||
color: {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
string: "Color",
|
||||
},
|
||||
user_id: { string: "User", type: "many2one", relation: "user" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
bar: true,
|
||||
foo: "yop",
|
||||
int_field: 10,
|
||||
qux: 0.44,
|
||||
p: [],
|
||||
trululu: 4,
|
||||
user_id: 17,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
bar: true,
|
||||
foo: "blip",
|
||||
int_field: 9,
|
||||
qux: 13,
|
||||
p: [],
|
||||
trululu: 1,
|
||||
product_id: 37,
|
||||
user_id: 17,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
bar: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
],
|
||||
},
|
||||
user: {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
partner_ids: {
|
||||
string: "one2many partners field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
relation_field: "user_id",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 17,
|
||||
name: "Aline",
|
||||
partner_ids: [1, 2],
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Christine",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("StatusBarField");
|
||||
|
||||
QUnit.test("static statusbar widget on many2one field", async function (assert) {
|
||||
serverData.models.partner.fields.trululu.domain = "[('bar', '=', True)]";
|
||||
serverData.models.partner.records[1].bar = false;
|
||||
|
||||
let count = 0;
|
||||
let fieldsFetched = [];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" />
|
||||
</header>
|
||||
</form>`,
|
||||
mockRPC(route, { method, kwargs }) {
|
||||
if (method === "search_read") {
|
||||
count++;
|
||||
fieldsFetched = kwargs.fields;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
count,
|
||||
1,
|
||||
"once search_read should have been done to fetch the relational values"
|
||||
);
|
||||
assert.deepEqual(
|
||||
fieldsFetched,
|
||||
["display_name"],
|
||||
"search_read should only fetch field display_name"
|
||||
);
|
||||
assert.containsN(target, ".o_statusbar_status button:not(.dropdown-toggle)", 2);
|
||||
assert.containsN(target, ".o_statusbar_status button:disabled", 2);
|
||||
assert.hasClass(
|
||||
target.querySelector('.o_statusbar_status button[data-value="4"]'),
|
||||
"o_arrow_button_current"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"folded statusbar widget on selection field has selected value in the toggler",
|
||||
async function (assert) {
|
||||
registry.category("services").add("ui", {
|
||||
start(env) {
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
value: true,
|
||||
});
|
||||
return {
|
||||
bus: new EventBus(),
|
||||
size: 0,
|
||||
isSmall: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="color" widget="statusbar" />
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_statusbar_status button.dropdown-toggle:contains(Red)");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("static statusbar widget on many2one field with domain", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
patchWithCleanup(session, { uid: 17 });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" domain="[('user_id', '=', uid)]" />
|
||||
</header>
|
||||
</form>`,
|
||||
mockRPC(route, { method, kwargs }) {
|
||||
if (method === "search_read") {
|
||||
assert.deepEqual(
|
||||
kwargs.domain,
|
||||
["|", ["id", "=", 4], ["user_id", "=", 17]],
|
||||
"search_read should sent the correct domain"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("clickable statusbar widget on many2one field", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_statusbar_status button[data-value='4']"),
|
||||
"o_arrow_button_current"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_statusbar_status button[data-value='4']"),
|
||||
"disabled"
|
||||
);
|
||||
|
||||
const clickableButtons = target.querySelectorAll(
|
||||
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current)"
|
||||
);
|
||||
assert.strictEqual(clickableButtons.length, 2);
|
||||
|
||||
await click(clickableButtons[clickableButtons.length - 1]); // (last is visually the first here (css))
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_statusbar_status button[data-value='1']"),
|
||||
"o_arrow_button_current"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_statusbar_status button[data-value='1']"),
|
||||
"disabled"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("statusbar with no status", async function (assert) {
|
||||
serverData.models.product.records = [];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" />
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(target.querySelector(".o_statusbar_status"), "o_field_empty");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_statusbar_status").children.length,
|
||||
0,
|
||||
"statusbar widget should be empty"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("statusbar with tooltip for help text", async function (assert) {
|
||||
serverData.models.partner.fields.product_id.help = "some info about the field";
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" />
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(target.querySelector(".o_statusbar_status"), "o_field_empty");
|
||||
const tooltipInfo = target.querySelector(".o_field_statusbar").attributes[
|
||||
"data-tooltip-info"
|
||||
];
|
||||
assert.strictEqual(
|
||||
JSON.parse(tooltipInfo.value).field.help,
|
||||
"some info about the field",
|
||||
"tooltip text is present on the field"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("statusbar with required modifier", async function (assert) {
|
||||
const mock = () => {
|
||||
assert.step("Show error message");
|
||||
return () => {};
|
||||
};
|
||||
registry.category("services").add("notification", makeFakeNotificationService(mock), {
|
||||
force: true,
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" required="1"/>
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, ".o_form_button_save");
|
||||
|
||||
assert.containsOnce(target, ".o_form_editable", "view should still be in edit");
|
||||
assert.verifySteps(
|
||||
["Show error message"],
|
||||
"should display an 'invalid fields' notification"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("statusbar with no value in readonly", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" />
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(target.querySelector(".o_statusbar_status"), "o_field_empty");
|
||||
assert.containsN(target, ".o_statusbar_status button:visible", 2);
|
||||
});
|
||||
|
||||
QUnit.test("statusbar with domain but no value (create mode)", async function (assert) {
|
||||
serverData.models.partner.fields.trululu.domain = "[('bar', '=', True)]";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" />
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsN(target, ".o_statusbar_status button:disabled", 2);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"clickable statusbar should change m2o fetching domain in edit mode",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.trululu.domain = "[('bar', '=', True)]";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsN(target, ".o_statusbar_status button:not(.dropdown-toggle)", 3);
|
||||
const buttons = target.querySelectorAll(
|
||||
".o_statusbar_status button:not(.dropdown-toggle)"
|
||||
);
|
||||
await click(buttons[buttons.length - 1]);
|
||||
assert.containsN(target, ".o_statusbar_status button:not(.dropdown-toggle)", 2);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"statusbar fold_field option and statusbar_visible attribute",
|
||||
async function (assert) {
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
|
||||
serverData.models.partner.records[0].bar = false;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'fold_field': 'bar'}" />
|
||||
<field name="color" widget="statusbar" statusbar_visible="red" />
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, ".o_statusbar_status .dropdown-toggle");
|
||||
|
||||
const status = target.querySelectorAll(".o_statusbar_status");
|
||||
assert.containsOnce(status[0], ".dropdown-item.disabled");
|
||||
assert.containsOnce(status[status.length - 1], "button.disabled");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("statusbar: choose an item from the 'More' menu", async function (assert) {
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
|
||||
serverData.models.partner.records[0].bar = false;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': '1', 'fold_field': 'bar'}" />
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("[aria-checked='true']").textContent,
|
||||
"aaa",
|
||||
"default status is 'aaa'"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document
|
||||
.querySelector(".o_statusbar_status .dropdown-toggle.o_arrow_button")
|
||||
.textContent.trim(),
|
||||
"More",
|
||||
"button has the correct text"
|
||||
);
|
||||
|
||||
await click(target, ".o_statusbar_status .dropdown-toggle");
|
||||
await click(target, ".o-dropdown .dropdown-item");
|
||||
assert.strictEqual(
|
||||
target.querySelector("[aria-checked='true']").textContent,
|
||||
"second record",
|
||||
"status has changed to the selected dropdown item"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("statusbar with dynamic domain", async function (assert) {
|
||||
serverData.models.partner.fields.trululu.domain = "[('int_field', '>', qux)]";
|
||||
serverData.models.partner.records[2].int_field = 0;
|
||||
|
||||
let rpcCount = 0;
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" />
|
||||
</header>
|
||||
<field name="qux" />
|
||||
<field name="foo" />
|
||||
</form>`,
|
||||
mockRPC(route, { method }) {
|
||||
if (method === "search_read") {
|
||||
rpcCount++;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(target, ".o_statusbar_status button.disabled", 3);
|
||||
assert.strictEqual(rpcCount, 1, "should have done 1 search_read rpc");
|
||||
await editInput(target, ".o_field_widget[name='qux'] input", 9.5);
|
||||
assert.containsN(target, ".o_statusbar_status button.disabled", 2);
|
||||
assert.strictEqual(rpcCount, 2, "should have done 1 more search_read rpc");
|
||||
await editInput(target, ".o_field_widget[name='qux'] input", "hey");
|
||||
assert.strictEqual(rpcCount, 2, "should not have done 1 more search_read rpc");
|
||||
});
|
||||
|
||||
QUnit.test('statusbar edited by the smart action "Move to stage..."', async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': '1'}"/>
|
||||
</header>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_field_widget");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
const movestage = target.querySelectorAll(".o_command");
|
||||
const idx = [...movestage]
|
||||
.map((el) => el.textContent)
|
||||
.indexOf("Move to Trululu...ALT + SHIFT + X");
|
||||
assert.ok(idx >= 0);
|
||||
|
||||
await click(movestage[idx]);
|
||||
await nextTick();
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
|
||||
["first record", "second record", "aaa"]
|
||||
);
|
||||
await click(target, "#o_command_2");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
'smart action "Move to stage..." is unavailable if readonly',
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" readonly="1"/>
|
||||
</header>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_field_widget");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
const movestage = target.querySelectorAll(".o_command");
|
||||
const idx = [...movestage]
|
||||
.map((el) => el.textContent)
|
||||
.indexOf("Move to Trululu...ALT + SHIFT + X");
|
||||
assert.ok(idx < 0);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("hotkey is unavailable if readonly", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" readonly="1"/>
|
||||
</header>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_field_widget");
|
||||
triggerHotkey("alt+shift+x");
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".modal", "command palette should not open");
|
||||
});
|
||||
|
||||
QUnit.test("auto save record when field toggled", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
|
||||
</header>
|
||||
</form>`,
|
||||
mockRPC(_route, { method }) {
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
}
|
||||
},
|
||||
});
|
||||
const clickableButtons = target.querySelectorAll(
|
||||
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current)"
|
||||
);
|
||||
await click(clickableButtons[clickableButtons.length - 1]);
|
||||
assert.verifySteps(["write"]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"clickable statusbar with readonly modifier set to false is editable",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" options="{'clickable': true}" attrs="{'readonly': false}"/>
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
assert.containsN(target, ".o_statusbar_status button:visible", 2);
|
||||
assert.containsNone(target, ".o_statusbar_status button.disabled[disabled]:visible");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"clickable statusbar with readonly modifier set to true is not editable",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" options="{'clickable': true}" attrs="{'readonly': true}"/>
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
assert.containsN(target, ".o_statusbar_status button.disabled[disabled]:visible", 2);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"non-clickable statusbar with readonly modifier set to false is not editable",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="product_id" widget="statusbar" options="{'clickable': false}" attrs="{'readonly': false}"/>
|
||||
</header>
|
||||
</form>`,
|
||||
});
|
||||
assert.containsN(target, ".o_statusbar_status button.disabled[disabled]:visible", 2);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,595 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
clickCreate,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
bar: { string: "Bar", type: "boolean", default: true, searchable: true },
|
||||
txt: {
|
||||
string: "txt",
|
||||
type: "text",
|
||||
},
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
qux: { string: "Qux", type: "float", digits: [16, 1], searchable: true },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
bar: true,
|
||||
foo: "yop",
|
||||
int_field: 10,
|
||||
qux: 0.44444,
|
||||
txt: "some text",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("TextField");
|
||||
|
||||
QUnit.test("text fields are correctly rendered", async function (assert) {
|
||||
serverData.models.partner.fields.foo.type = "text";
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="foo"/></form>',
|
||||
});
|
||||
|
||||
const textarea = target.querySelector(".o_field_text textarea");
|
||||
assert.ok(textarea, "should have a text area");
|
||||
assert.strictEqual(textarea.value, "yop", "should still be 'yop' in edit");
|
||||
|
||||
await editInput(textarea, null, "hello");
|
||||
assert.strictEqual(textarea.value, "hello", "should be 'hello' after first edition");
|
||||
|
||||
await editInput(textarea, null, "hello world");
|
||||
assert.strictEqual(
|
||||
textarea.value,
|
||||
"hello world",
|
||||
"should be 'hello world' after second edition"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_text textarea").value,
|
||||
"hello world",
|
||||
"should be 'hello world' after save"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("text fields in edit mode have correct height", async function (assert) {
|
||||
serverData.models.partner.fields.foo.type = "text";
|
||||
serverData.models.partner.records[0].foo = "f\nu\nc\nk\nm\ni\nl\ng\nr\no\nm";
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="foo"/></form>',
|
||||
});
|
||||
|
||||
const textarea = target.querySelector(".o_field_text textarea");
|
||||
assert.strictEqual(
|
||||
textarea.clientHeight,
|
||||
textarea.scrollHeight - Math.abs(textarea.scrollTop),
|
||||
"textarea should not have a scroll bar"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("text fields in edit mode, no vertical resize", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="txt"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
window.getComputedStyle(target.querySelector("textarea")).resize,
|
||||
"none",
|
||||
"should not have vertical resize"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("text fields should have correct height after onchange", async function (assert) {
|
||||
const damnLongText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Donec est massa, gravida eget dapibus ac, eleifend eget libero.
|
||||
Suspendisse feugiat sed massa eleifend vestibulum. Sed tincidunt
|
||||
velit sed lacinia lacinia. Nunc in fermentum nunc. Vestibulum ante
|
||||
ipsum primis in faucibus orci luctus et ultrices posuere cubilia
|
||||
Curae; Nullam ut nisi a est ornare molestie non vulputate orci.
|
||||
Nunc pharetra porta semper. Mauris dictum eu nulla a pulvinar. Duis
|
||||
eleifend odio id ligula congue sollicitudin. Curabitur quis aliquet
|
||||
nunc, ut aliquet enim. Suspendisse malesuada felis non metus
|
||||
efficitur aliquet.`;
|
||||
|
||||
serverData.models.partner.records[0].txt = damnLongText;
|
||||
serverData.models.partner.records[0].bar = false;
|
||||
serverData.models.partner.onchanges = {
|
||||
bar(obj) {
|
||||
obj.txt = damnLongText;
|
||||
},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" />
|
||||
<field name="txt" attrs="{'invisible': [('bar', '=', True)]}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
let textarea = target.querySelector(".o_field_widget[name='txt'] textarea");
|
||||
const initialHeight = textarea.offsetHeight;
|
||||
|
||||
await editInput(textarea, null, "Short value");
|
||||
|
||||
assert.ok(textarea.offsetHeight < initialHeight, "Textarea height should have shrank");
|
||||
|
||||
await click(target, ".o_field_boolean[name='bar'] input");
|
||||
await click(target, ".o_field_boolean[name='bar'] input");
|
||||
|
||||
textarea = target.querySelector(".o_field_widget[name='txt'] textarea");
|
||||
assert.strictEqual(textarea.offsetHeight, initialHeight, "Textarea height should be reset");
|
||||
});
|
||||
|
||||
QUnit.test("text fields in editable list have correct height", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
serverData.models.partner.records[0].txt = "a\nb\nc\nd\ne\nf";
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<list editable="top"><field name="foo"/><field name="txt"/></list>',
|
||||
});
|
||||
|
||||
// Click to enter edit: in this test we specifically do not set
|
||||
// the focus on the textarea by clicking on another column.
|
||||
// The main goal is to test the resize is actually triggered in this
|
||||
// particular case.
|
||||
await click(target.querySelectorAll(".o_data_cell")[1]);
|
||||
const textarea = target.querySelector("textarea:first-child");
|
||||
|
||||
// make sure the correct data is there
|
||||
assert.strictEqual(textarea.value, serverData.models.partner.records[0].txt);
|
||||
|
||||
// make sure there is no scroll bar
|
||||
assert.strictEqual(
|
||||
textarea.clientHeight,
|
||||
textarea.scrollHeight,
|
||||
"textarea should not have a scroll bar"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("text fields in edit mode should resize on reset", async function (assert) {
|
||||
serverData.models.partner.fields.foo.type = "text";
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
bar(obj) {
|
||||
obj.foo = "a\nb\nc\nd\ne\nf";
|
||||
},
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" />
|
||||
<field name="foo" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// trigger a textarea reset (through onchange) by clicking the box
|
||||
// then check there is no scroll bar
|
||||
await click(target, "div[name='bar'] input");
|
||||
|
||||
const textarea = target.querySelector("textarea");
|
||||
assert.strictEqual(
|
||||
textarea.clientHeight,
|
||||
textarea.scrollHeight,
|
||||
"textarea should not have a scroll bar"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("set row on text fields", async function (assert) {
|
||||
serverData.models.partner.fields.foo.type = "text";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" rows="4"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const textarea = target.querySelector("textarea");
|
||||
assert.strictEqual(
|
||||
textarea.rows,
|
||||
4,
|
||||
"rowCount should be the one set on the field",
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"autoresize of text fields is done when switching to edit mode",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.text_field = { string: "Text field", type: "text" };
|
||||
serverData.models.partner.fields.text_field.default = "some\n\nmulti\n\nline\n\ntext\n";
|
||||
serverData.models.partner.records[0].text_field = "a\nb\nc\nd\ne\nf";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<field name="text_field"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// ensure that autoresize is correctly done
|
||||
let height = target.querySelector(".o_field_widget[name=text_field] textarea")
|
||||
.offsetHeight;
|
||||
// focus the field to manually trigger autoresize
|
||||
await triggerEvent(target, ".o_field_widget[name=text_field] textarea", "focus");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=text_field] textarea").offsetHeight,
|
||||
height,
|
||||
"autoresize should have been done automatically at rendering"
|
||||
);
|
||||
// next assert simply tries to ensure that the textarea isn't stucked to
|
||||
// its minimal size, even after being focused
|
||||
assert.ok(height > 80, "textarea should have an height of at least 80px");
|
||||
|
||||
// create a new record to ensure that autoresize is correctly done
|
||||
await clickCreate(target);
|
||||
height = target.querySelector(".o_field_widget[name=text_field] textarea").offsetHeight;
|
||||
// focus the field to manually trigger autoresize
|
||||
await triggerEvent(target, ".o_field_widget[name=text_field] textarea", "focus");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=text_field] textarea").offsetHeight,
|
||||
height,
|
||||
"autoresize should have been done automatically at rendering"
|
||||
);
|
||||
assert.ok(height > 80, "textarea should have an height of at least 80px");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("autoresize of text fields is done on notebook page show", async function (assert) {
|
||||
serverData.models.partner.fields.text_field = { string: "Text field", type: "text" };
|
||||
serverData.models.partner.fields.text_field.default = "some\n\nmulti\n\nline\n\ntext\n";
|
||||
serverData.models.partner.records[0].text_field = "a\nb\nc\nd\ne\nf";
|
||||
serverData.models.partner.fields.text_field_empty = {
|
||||
string: "Text field",
|
||||
type: "text",
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<notebook>
|
||||
<page string="First Page">
|
||||
<field name="foo"/>
|
||||
</page>
|
||||
<page string="Second Page">
|
||||
<field name="text_field"/>
|
||||
</page>
|
||||
<page string="Third Page">
|
||||
<field name="text_field_empty"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelectorAll(".o_notebook .nav .nav-link")[0], "active");
|
||||
|
||||
await click(target.querySelectorAll(".o_notebook .nav .nav-link")[1]);
|
||||
assert.hasClass(target.querySelectorAll(".o_notebook .nav .nav-link")[1], "active");
|
||||
|
||||
let height = target.querySelector(".o_field_widget[name=text_field] textarea").offsetHeight;
|
||||
assert.ok(height > 80, "textarea should have an height of at least 80px");
|
||||
|
||||
await click(target.querySelectorAll(".o_notebook .nav .nav-link")[2]);
|
||||
assert.hasClass(target.querySelectorAll(".o_notebook .nav .nav-link")[2], "active");
|
||||
|
||||
height = target.querySelector(".o_field_widget[name=text_field_empty] textarea")
|
||||
.offsetHeight;
|
||||
assert.strictEqual(height, 50, "empty textarea should have height of 50px");
|
||||
});
|
||||
|
||||
QUnit.test("text field translatable", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
serverData.models.partner.fields.txt.translate = true;
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
|
||||
force: true,
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="txt" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(route, { args, method }) {
|
||||
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
|
||||
return Promise.resolve([
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
]);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
|
||||
return Promise.resolve([
|
||||
[
|
||||
{ lang: "en_US", source: "yop", value: "yop" },
|
||||
{ lang: "fr_BE", source: "yop", value: "valeur français" },
|
||||
],
|
||||
{ translation_type: "text", translation_show_source: false },
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelector("[name=txt] textarea"), "o_field_translate");
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_text .btn.o_field_translate",
|
||||
"should have a translate button"
|
||||
);
|
||||
await click(target, ".o_field_text .btn.o_field_translate");
|
||||
|
||||
assert.containsOnce(target, ".modal", "there should be a translation modal");
|
||||
});
|
||||
|
||||
QUnit.test("text field translatable in create mode", async function (assert) {
|
||||
serverData.models.partner.fields.txt.translate = true;
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
|
||||
force: true,
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="txt" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_text .btn.o_field_translate",
|
||||
"should have a translate button in create mode"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("text field translatable on notebook page", async function (assert) {
|
||||
serverData.models.partner.fields.txt.translate = true;
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
|
||||
force: true,
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<notebook>
|
||||
<page string="First Page">
|
||||
<field name="txt"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(route, { args, method }) {
|
||||
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
|
||||
return Promise.resolve([
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
]);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
|
||||
return Promise.resolve([
|
||||
[
|
||||
{ lang: "en_US", source: "yop", value: "yop" },
|
||||
{ lang: "fr_BE", source: "yop", value: "valeur français" },
|
||||
],
|
||||
{ translation_type: "text", translation_show_source: false },
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelectorAll(".o_notebook .nav .nav-link")[0], "active");
|
||||
|
||||
assert.hasClass(target.querySelector("[name=txt] textarea"), "o_field_translate");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("[name=txt] textarea").nextElementSibling.textContent,
|
||||
"EN",
|
||||
"The input should be preceded by a translate button"
|
||||
);
|
||||
await click(target, ".o_field_text .btn.o_field_translate");
|
||||
|
||||
assert.containsOnce(target, ".modal", "there should be a translation modal");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"go to next line (and not the next row) when pressing enter",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.foo.type = "text";
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="int_field" />
|
||||
<field name="foo" />
|
||||
<field name="qux" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await click(target.querySelector("tbody tr:first-child .o_list_text"));
|
||||
const textarea = target.querySelector("textarea.o_input");
|
||||
assert.containsOnce(target, textarea, "should have a text area");
|
||||
assert.strictEqual(textarea.value, "yop", 'should still be "yop" in edit');
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("textarea"),
|
||||
document.activeElement,
|
||||
"text area should have the focus"
|
||||
);
|
||||
|
||||
// click on enter
|
||||
await triggerEvent(textarea, null, "keydown", { key: "Enter" });
|
||||
await triggerEvent(textarea, null, "keyup", { key: "Enter" });
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("textarea"),
|
||||
document.activeElement,
|
||||
"text area should still have the focus"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Firefox-specific
|
||||
// Copying from <div style="white-space:pre-wrap"> does not keep line breaks
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1390115
|
||||
QUnit.test(
|
||||
"copying text fields in RO mode should preserve line breaks",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form edit="0">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="txt"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Copying from a div tag with white-space:pre-wrap doesn't work in Firefox
|
||||
assert.strictEqual(
|
||||
target.querySelector('[name="txt"]').firstElementChild.tagName.toLowerCase(),
|
||||
"span",
|
||||
"the field contents should be surrounded by a span tag"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("text field rendering in list view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree><field name="txt"/></tree>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"tbody td.o_list_text",
|
||||
"should have a td with the .o_list_text class"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("field text in editable list view", async function (assert) {
|
||||
serverData.models.partner.fields.foo.type = "text";
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<tree editable="top"><field name="foo"/></tree>',
|
||||
});
|
||||
|
||||
await click(target.querySelector(".o_list_button_add"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("textarea"),
|
||||
document.activeElement,
|
||||
"text area should have the focus"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, editSelect, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
color: {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
},
|
||||
tz_offset: {
|
||||
string: "tz_offset",
|
||||
type: "char",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, color: "red", tz_offset: 0 },
|
||||
{ id: 2, color: "red", tz_offset: 0 },
|
||||
{ id: 3, color: "red", tz_offset: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("TimezoneMismatchField");
|
||||
|
||||
QUnit.test("widget timezone_mismatch in a list view", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
color: function (r) {
|
||||
r.tz_offset = "+4800"; // make sure we have a mismatch
|
||||
},
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: /*xml*/ `
|
||||
<tree string="Colors" editable="top">
|
||||
<field name="tz_offset" invisible="True"/>
|
||||
<field name="color" widget="timezone_mismatch" />
|
||||
</tree>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsN(target, "td:contains(Red)", 3, "should have 3 rows with correct value");
|
||||
|
||||
await click(
|
||||
target
|
||||
.querySelectorAll(".o_data_row")[0]
|
||||
.querySelector("td:not(.o_list_record_selector)")
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget[name=color] select",
|
||||
"td should have a child 'select'"
|
||||
);
|
||||
|
||||
const td = target.querySelector("tbody tr.o_selected_row td:not(.o_list_record_selector)");
|
||||
assert.strictEqual(
|
||||
td.querySelector(".o_field_widget[name=color] select").parentElement.childElementCount,
|
||||
1,
|
||||
"select tag should be only child of td"
|
||||
);
|
||||
|
||||
await editSelect(td, "select", '"black"');
|
||||
assert.containsOnce(td, ".o_tz_warning", "Should display icon alert");
|
||||
assert.ok(
|
||||
td
|
||||
.querySelector("select option:checked")
|
||||
.textContent.match(/Black\s+\([0-9]+\/[0-9]+\/[0-9]+ [0-9]+:[0-9]+:[0-9]+\)/),
|
||||
"Should display the datetime in the selected timezone"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("widget timezone_mismatch in a form view", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
serverData.models.partner.fields.tz = {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["Europe/Brussels", "Europe/Brussels"],
|
||||
["America/Los_Angeles", "America/Los_Angeles"],
|
||||
],
|
||||
};
|
||||
serverData.models.partner.records[0].tz = false;
|
||||
serverData.models.partner.records[0].tz_offset = "+4800";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: /*xml*/ `
|
||||
<form>
|
||||
<field name="tz_offset" invisible="True"/>
|
||||
<field name="tz" widget="timezone_mismatch" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, 'div[name="tz"] select');
|
||||
assert.containsOnce(target, ".o_tz_warning", "warning class should be there.");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"widget timezone_mismatch in a form view edit mode with mismatch",
|
||||
async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
serverData.models.partner.fields.tz = {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["Europe/Brussels", "Europe/Brussels"],
|
||||
["America/Los_Angeles", "America/Los_Angeles"],
|
||||
],
|
||||
};
|
||||
serverData.models.partner.records[0].tz = "America/Los_Angeles";
|
||||
serverData.models.partner.records[0].tz_offset = "+4800";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: /*xml*/ `
|
||||
<form>
|
||||
<field name="tz_offset" invisible="True"/>
|
||||
<field name="tz" widget="timezone_mismatch" options="{'tz_offset_field': 'tz_offset'}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
'div[name="tz"] select option',
|
||||
3,
|
||||
"The select element should have 3 children"
|
||||
);
|
||||
assert.containsOnce(target, ".o_tz_warning", "timezone mismatch is present");
|
||||
assert.notOk(
|
||||
target.querySelector(".o_tz_warning").children.length,
|
||||
"The mismatch element should not have children"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
foo: "yop",
|
||||
},
|
||||
{
|
||||
foo: "blip",
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("UrlField");
|
||||
|
||||
QUnit.test("UrlField in form view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="url"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_field_widget input[type="text"]',
|
||||
"should have an input for the url field"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget input[type="text"]').value,
|
||||
"yop",
|
||||
"input should contain field value"
|
||||
);
|
||||
|
||||
const webLink = target.querySelector(".o_field_url a");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
webLink,
|
||||
"should have rendered the url button as a link with correct classes"
|
||||
);
|
||||
assert.hasAttrValue(webLink, "href", "http://yop", "should have proper href");
|
||||
|
||||
await editInput(target, ".o_field_widget input[type='text']", "limbo");
|
||||
|
||||
// save
|
||||
const editedElement = ".o_field_widget input[type='text']";
|
||||
assert.containsOnce(target, editedElement, "should still have an input for the url field");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
editedElement,
|
||||
"should still have a anchor with correct classes"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(editedElement).value,
|
||||
"limbo",
|
||||
"has the proper value"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("UrlField in form view (readonly)", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="url" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
const matchingEl = target.querySelector("a.o_field_widget.o_form_uri");
|
||||
assert.containsOnce(target, matchingEl, "should have a anchor with correct classes");
|
||||
assert.hasAttrValue(matchingEl, "href", "http://yop", "should have proper href link");
|
||||
assert.hasAttrValue(
|
||||
matchingEl,
|
||||
"target",
|
||||
"_blank",
|
||||
"should have target attribute set to _blank"
|
||||
);
|
||||
assert.strictEqual(matchingEl.textContent, "yop", "the value should be displayed properly");
|
||||
});
|
||||
|
||||
QUnit.test("UrlField takes text from proper attribute", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="foo" widget="url" text="kebeclibre" readonly="1"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="foo"] a').textContent,
|
||||
"kebeclibre",
|
||||
"url text should come from the text attribute"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("UrlField: href attribute and website_path option", async function (assert) {
|
||||
serverData.models.partner.fields.url1 = {
|
||||
string: "Url 1",
|
||||
type: "char",
|
||||
default: "www.url1.com",
|
||||
};
|
||||
serverData.models.partner.fields.url2 = {
|
||||
string: "Url 2",
|
||||
type: "char",
|
||||
default: "www.url2.com",
|
||||
};
|
||||
serverData.models.partner.fields.url3 = {
|
||||
string: "Url 3",
|
||||
type: "char",
|
||||
default: "http://www.url3.com",
|
||||
};
|
||||
serverData.models.partner.fields.url4 = {
|
||||
string: "Url 4",
|
||||
type: "char",
|
||||
default: "https://url4.com",
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
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>`,
|
||||
resId: 1,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="url1"] a').getAttribute("href"),
|
||||
"http://www.url1.com"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="url2"] a').getAttribute("href"),
|
||||
"www.url2.com"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="url3"] a').getAttribute("href"),
|
||||
"http://www.url3.com"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="url4"] a').getAttribute("href"),
|
||||
"https://url4.com"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("UrlField in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree editable="bottom"><field name="foo" widget="url"/></tree>',
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll("tbody td:not(.o_list_record_selector) a").length,
|
||||
2,
|
||||
"should have 2 cells with a link"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_url.o_field_widget[name='foo'] a",
|
||||
2,
|
||||
"should have 2 anchors with correct classes"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget[name='foo'] a"),
|
||||
"href",
|
||||
"http://yop",
|
||||
"should have proper href link"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tbody td:not(.o_list_record_selector)").textContent,
|
||||
"yop",
|
||||
"value should be displayed properly as text"
|
||||
);
|
||||
|
||||
// Edit a line and check the result
|
||||
let cell = target.querySelector("tbody td:not(.o_list_record_selector)");
|
||||
await click(cell);
|
||||
assert.hasClass(cell.parentElement, "o_selected_row", "should be set as edit mode");
|
||||
assert.strictEqual(
|
||||
cell.querySelector("input").value,
|
||||
"yop",
|
||||
"should have the correct value in internal input"
|
||||
);
|
||||
await editInput(cell, "input", "brolo");
|
||||
|
||||
// save
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
cell = target.querySelector("tbody td:not(.o_list_record_selector)");
|
||||
assert.doesNotHaveClass(
|
||||
cell.parentElement,
|
||||
"o_selected_row",
|
||||
"should not be in edit mode anymore"
|
||||
);
|
||||
const resultEl = target.querySelector(".o_field_widget[name='foo'] a");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget[name='foo'] a",
|
||||
2,
|
||||
"should still have anchors with correct classes"
|
||||
);
|
||||
assert.hasAttrValue(resultEl, "href", "http://brolo", "should have proper new href link");
|
||||
assert.strictEqual(resultEl.textContent, "brolo", "value should be properly updated");
|
||||
});
|
||||
|
||||
QUnit.test("UrlField with falsy value", async function (assert) {
|
||||
serverData.models.partner.records[0].foo = false;
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="foo" widget="url"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_field_widget[name=foo] input");
|
||||
assert.strictEqual(target.querySelector("[name=foo] input").value, "");
|
||||
});
|
||||
|
||||
QUnit.test("UrlField: url old content is cleaned on render edit", async function (assert) {
|
||||
serverData.models.partner.fields.foo2 = { string: "Foo2", type: "char", default: "foo2" };
|
||||
serverData.models.partner.onchanges.foo2 = function (record) {
|
||||
record.foo = record.foo2;
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="url" attrs="{'readonly': True}" />
|
||||
<field name="foo2" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=foo]").textContent,
|
||||
"yop",
|
||||
"the starting value should be displayed properly"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=foo2] input").value,
|
||||
"foo2",
|
||||
"input should contain field value in edit mode"
|
||||
);
|
||||
await editInput(target, ".o_field_widget[name=foo2] input", "bonjour");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=foo]").textContent,
|
||||
"bonjour",
|
||||
"Url widget should show the new value and not " +
|
||||
target.querySelector(".o_field_widget[name=foo]").textContent
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("url field with placeholder", async function (assert) {
|
||||
serverData.models.partner.fields.foo.default = false;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="url" placeholder="Placeholder"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='foo'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("url field with non falsy, but non url value", async function (assert) {
|
||||
serverData.models.partner.fields.foo.default = "odoo://hello";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form><field name="foo" widget="url"/></form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='foo'] a").getAttribute("href"),
|
||||
"http://odoo://hello"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,469 @@
|
|||
/** @odoo-module **/
|
||||
import { makeView } from "../helpers";
|
||||
import { setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { FormCompiler } from "@web/views/form/form_compiler";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { getFixture, patchWithCleanup } from "../../helpers/utils";
|
||||
import { createElement } from "@web/core/utils/xml";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
|
||||
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").outerHTML;
|
||||
}
|
||||
|
||||
QUnit.assert.areEquivalent = function (template1, template2) {
|
||||
if (template1.replace(/\s/g, "") === template2.replace(/\s/g, "")) {
|
||||
QUnit.assert.ok(true);
|
||||
} else {
|
||||
QUnit.assert.strictEqual(template1, template2);
|
||||
}
|
||||
};
|
||||
|
||||
QUnit.assert.areContentEquivalent = function (template, content) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(template, "text/xml");
|
||||
const templateContent = doc.documentElement.firstChild.innerHTML;
|
||||
QUnit.assert.areEquivalent(templateContent, content);
|
||||
};
|
||||
|
||||
QUnit.module("Form Compiler", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
// compiler generates a piece of template for the translate alert in multilang
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
});
|
||||
QUnit.test("properly compile simple div", async (assert) => {
|
||||
const arch = /*xml*/ `<form><div>lol</div></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ props.record.isDirty ? 'o_form_dirty' : !props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<div>lol</div>
|
||||
</div>
|
||||
</t>`;
|
||||
|
||||
assert.areEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"label with empty string compiles to FormLabel with empty string",
|
||||
async (assert) => {
|
||||
const arch = /*xml*/ `<form><field name="test"/><label for="test" string=""/></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ props.record.isDirty ? 'o_form_dirty' : !props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<Field id="'test'" name="'test'" record="props.record" fieldInfo="props.archInfo.fieldNodes['test']" readonly="props.archInfo.activeActions?.edit === false and !props.record.isNew" setDirty.alike="props.setFieldAsDirty"/>
|
||||
<FormLabel id="'test'" fieldName="'test'" record="props.record" fieldInfo="props.archInfo.fieldNodes['test']" className="""" string="\`\`" />
|
||||
</div>
|
||||
</t>`;
|
||||
assert.areEquivalent(compileTemplate(arch), expected);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("properly compile simple div with field", async (assert) => {
|
||||
const arch = /*xml*/ `<form><div class="someClass">lol<field name="display_name"/></div></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ props.record.isDirty ? 'o_form_dirty' : !props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<div class="someClass">
|
||||
lol
|
||||
<Field id="'display_name'" name="'display_name'" record="props.record" fieldInfo="props.archInfo.fieldNodes['display_name']" readonly="props.archInfo.activeActions?.edit === false and !props.record.isNew" setDirty.alike="props.setFieldAsDirty"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>`;
|
||||
|
||||
assert.areEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test("properly compile inner groups", async (assert) => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<group>
|
||||
<group><field name="display_name"/></group>
|
||||
<group><field name="charfield"/></group>
|
||||
</group>
|
||||
</form>`;
|
||||
const expected = /*xml*/ `
|
||||
<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:props.record,string:props.record.fields.display_name.string,fieldInfo:props.archInfo.fieldNodes['display_name']}" Component="constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
|
||||
<Field id="'display_name'" name="'display_name'" record="props.record" fieldInfo="props.archInfo.fieldNodes['display_name']" readonly="props.archInfo.activeActions?.edit === false and !props.record.isNew" setDirty.alike="props.setFieldAsDirty" 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:props.record,string:props.record.fields.charfield.string,fieldInfo:props.archInfo.fieldNodes['charfield']}" Component="constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
|
||||
<Field id="'charfield'" name="'charfield'" record="props.record" fieldInfo="props.archInfo.fieldNodes['charfield']" readonly="props.archInfo.activeActions?.edit === false and !props.record.isNew" setDirty.alike="props.setFieldAsDirty" class="scope && scope.className"/>
|
||||
</t>
|
||||
</InnerGroup>
|
||||
</t>
|
||||
</OuterGroup>`;
|
||||
|
||||
assert.areContentEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test("properly compile attributes with nested forms", async (assert) => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<group>
|
||||
<group>
|
||||
<form>
|
||||
<div>
|
||||
<field name="test"/>
|
||||
</div>
|
||||
</form>
|
||||
</group>
|
||||
</group>
|
||||
</form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ props.record.isDirty ? 'o_form_dirty' : !props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" 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 t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ props.record.isDirty ? 'o_form_dirty' : !props.record.isVirtual ? 'o_form_saved' : '' }} {{scope && scope.className || "" }}" class="o_form_nosheet">
|
||||
<div><Field id="'test'" name="'test'" record="props.record" fieldInfo="props.archInfo.fieldNodes['test']" readonly="props.archInfo.activeActions?.edit === false and !props.record.isNew" setDirty.alike="props.setFieldAsDirty"/></div>
|
||||
</div>
|
||||
</t>
|
||||
</InnerGroup>
|
||||
</t>
|
||||
</OuterGroup>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
|
||||
assert.areEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test("properly compile notebook", async (assert) => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<notebook>
|
||||
<page name="p1" string="Page1"><field name="charfield"/></page>
|
||||
<page name="p2" string="Page2"><field name="display_name"/></page>
|
||||
</notebook>
|
||||
</form>`;
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<Notebook defaultPage="props.record.isNew ? undefined : props.activeNotebookPages[0]" onPageUpdate="(page) => this.props.onNotebookPageChange(0, page)">
|
||||
<t t-set-slot="page_1" title="\`Page1\`" name="\`p1\`" isVisible="true">
|
||||
<Field id="'charfield'" name="'charfield'" record="props.record" fieldInfo="props.archInfo.fieldNodes['charfield']" readonly="props.archInfo.activeActions?.edit === false and !props.record.isNew" setDirty.alike="props.setFieldAsDirty"/>
|
||||
</t>
|
||||
<t t-set-slot="page_2" title="\`Page2\`" name="\`p2\`" isVisible="true">
|
||||
<Field id="'display_name'" name="'display_name'" record="props.record" fieldInfo="props.archInfo.fieldNodes['display_name']" readonly="props.archInfo.activeActions?.edit === false and !props.record.isNew" setDirty.alike="props.setFieldAsDirty"/>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
|
||||
assert.areContentEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test("properly compile field without placeholder", async (assert) => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<field name="display_name" placeholder="e.g. Contact's Name or //someinfo..."/>
|
||||
</form>`;
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<Field id="'display_name'" name="'display_name'" record="props.record" fieldInfo="props.archInfo.fieldNodes['display_name']" readonly="props.archInfo.activeActions?.edit === false and !props.record.isNew" setDirty.alike="props.setFieldAsDirty"/>
|
||||
`;
|
||||
|
||||
assert.areContentEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test("properly compile no sheet", async (assert) => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<header>someHeader</header>
|
||||
<div>someDiv</div>
|
||||
</form>`;
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ props.record.isDirty ? 'o_form_dirty' : !props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons readonly="!props.record.isInEdition"/></div>
|
||||
<div>someDiv</div>
|
||||
</div>
|
||||
</t>`;
|
||||
|
||||
assert.areEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test("properly compile sheet", async (assert) => {
|
||||
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 t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ uiService.size < 6 ? "flex-column" : "flex-nowrap h-100" }} {{ props.record.isDirty ? 'o_form_dirty' : !props.record.isVirtual ? '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 border-bottom"><StatusBarButtons readonly="!props.record.isInEdition"/></div>
|
||||
<div>someDiv</div>
|
||||
<div class="o_form_sheet position-relative clearfix">
|
||||
<div>inside sheet</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>after sheet</div>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
|
||||
assert.areEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test("properly compile invisible", async (assert) => {
|
||||
// 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 modifiers="{'invisible': [['display_name', '=', 'take']]}"/>
|
||||
// </form>````
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<field name="display_name" modifiers="{"invisible": true}" />
|
||||
<div class="visible3" modifiers="{"invisible": false}"/>
|
||||
<div modifiers="{"invisible": [["display_name", "=", "take"]]}"/>
|
||||
</form>`;
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<div class="visible3" />
|
||||
<div t-if="!evalDomainFromRecord(props.record,[["display_name","=","take"]])" />
|
||||
`;
|
||||
|
||||
assert.areContentEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test("compile invisible containing string as domain", async (assert) => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<field name="display_name" modifiers="{"invisible": true}" />
|
||||
<div class="visible3" modifiers="{"invisible": false}"/>
|
||||
<div modifiers="{"invisible": "[['display_name', '=', 'take']]"}"/>
|
||||
</form>`;
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<div class="visible3" />
|
||||
<div t-if="!evalDomainFromRecord(props.record,"[['display_name','=','take']]")" />
|
||||
`;
|
||||
assert.areContentEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test("properly compile status bar with content", (assert) => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<header><div>someDiv</div></header>
|
||||
</form>`;
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom">
|
||||
<StatusBarButtons readonly="!props.record.isInEdition">
|
||||
<t t-set-slot="button_0" isVisible="true">
|
||||
<div>someDiv</div>
|
||||
</t>
|
||||
</StatusBarButtons>
|
||||
</div>`;
|
||||
|
||||
assert.areContentEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
|
||||
QUnit.test("properly compile status bar without content", (assert) => {
|
||||
const arch = /*xml*/ `
|
||||
<form>
|
||||
<header></header>
|
||||
</form>`;
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom">
|
||||
<StatusBarButtons readonly="!props.record.isInEdition"/>
|
||||
</div>`;
|
||||
|
||||
assert.areContentEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("Form Renderer", (hooks) => {
|
||||
let target, serverData;
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { type: "char" },
|
||||
charfield: { type: "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "firstRecord", charfield: "content of charfield" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
QUnit.test("compile form with modifiers and attrs - string as domain", async (assert) => {
|
||||
serverData.views = {
|
||||
"partner,1,form": /*xml*/ `
|
||||
<form>
|
||||
<div modifiers="{"invisible": "[['display_name', '=', uid]]"}">
|
||||
<field name="charfield"/>
|
||||
</div>
|
||||
<field name="display_name" attrs="{'readonly': "[['display_name', '=', uid]]"}"/>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsN(target, ".o_form_editable input", 2);
|
||||
});
|
||||
|
||||
QUnit.test("compile notebook with modifiers", async (assert) => {
|
||||
assert.expect(0);
|
||||
|
||||
serverData.views = {
|
||||
"partner,1,form": /*xml*/ `
|
||||
<form>
|
||||
<sheet>
|
||||
<notebook>
|
||||
<page name="p1" attrs="{'invisible': [['display_name', '=', 'lol']]}"><field name="charfield"/></page>
|
||||
<page name="p2"><field name="display_name"/></page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("compile header and buttons", async (assert) => {
|
||||
assert.expect(0);
|
||||
|
||||
serverData.views = {
|
||||
"partner,1,form": /*xml*/ `
|
||||
<form>
|
||||
<header>
|
||||
<button string="ActionButton" class="oe_highlight" name="action_button" type="object"/>
|
||||
</header>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("render field with placeholder", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
class CharField extends owl.Component {
|
||||
setup() {
|
||||
assert.strictEqual(this.props.placeholder, "e.g. Contact's Name or //someinfo...");
|
||||
}
|
||||
}
|
||||
CharField.template = owl.xml`<div/>`;
|
||||
CharField.extractProps = ({ attrs }) => ({ placeholder: attrs.placeholder });
|
||||
|
||||
registry.category("fields").add("char", CharField, { force: true });
|
||||
|
||||
serverData.views = {
|
||||
"partner,1,form": /*xml*/ `
|
||||
<form>
|
||||
<field name="display_name" placeholder="e.g. Contact's Name or //someinfo..." />
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("compile a button with id", async (assert) => {
|
||||
serverData.views = {
|
||||
"partner,1,form": /*xml*/ `
|
||||
<form>
|
||||
<header>
|
||||
<button id="action_button" string="ActionButton"/>
|
||||
</header>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "button[id=action_button]");
|
||||
});
|
||||
|
||||
QUnit.test("compile a button with disabled", async (assert) => {
|
||||
serverData.views = {
|
||||
"partner,1,form": /*xml*/ `
|
||||
<form>
|
||||
<button string="ActionButton" class="demo" name="action_button" type="object" disabled="disabled"/>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
const button = target.querySelector(".demo");
|
||||
assert.ok(button.hasAttribute("disabled"), "The button should have the 'disabled' attribute");
|
||||
});
|
||||
|
||||
QUnit.test("invisible is correctly computed with another t-if", (assert) => {
|
||||
patchWithCleanup(FormCompiler.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
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 modifiers="{"invisible": [["field", "=", "value"]]}" />`;
|
||||
|
||||
const expected = `<t t-translation="off"><div class="myNode" t-if="( myCondition or myOtherCondition ) and !evalDomainFromRecord(props.record,[["field","=","value"]])" t-ref="compiled_view_root"/></t>`;
|
||||
assert.areEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
});
|
||||
13888
odoo-bringout-oca-ocb-web/web/static/tests/views/form/form_view_tests.js
Normal file
13888
odoo-bringout-oca-ocb-web/web/static/tests/views/form/form_view_tests.js
Normal file
File diff suppressed because it is too large
Load diff
4342
odoo-bringout-oca-ocb-web/web/static/tests/views/graph_view_tests.js
Normal file
4342
odoo-bringout-oca-ocb-web/web/static/tests/views/graph_view_tests.js
Normal file
File diff suppressed because it is too large
Load diff
173
odoo-bringout-oca-ocb-web/web/static/tests/views/helpers.js
Normal file
173
odoo-bringout-oca-ocb-web/web/static/tests/views/helpers.js
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { getFixture, mount, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { getDefaultConfig, View } from "@web/views/view";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import {
|
||||
setupControlPanelFavoriteMenuRegistry,
|
||||
setupControlPanelServiceRegistry,
|
||||
} from "../search/helpers";
|
||||
import { addLegacyMockEnvironment } from "../webclient/helpers";
|
||||
import {
|
||||
fakeCompanyService,
|
||||
makeFakeLocalizationService,
|
||||
makeFakeRouterService,
|
||||
makeFakeUserService,
|
||||
} from "../helpers/mock_services";
|
||||
import { commandService } from "@web/core/commands/command_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { createDebugContext } from "@web/core/debug/debug_context";
|
||||
|
||||
import { Component, useSubEnv, xml } from "@odoo/owl";
|
||||
|
||||
import { mapLegacyEnvToWowlEnv } from "@web/legacy/utils";
|
||||
import makeTestEnvironment from "web.test_env";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
const rootDialogTemplate = xml`<Dialog><View t-props="props.viewProps"/></Dialog>`;
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* serverData: Object,
|
||||
* mockRPC?: Function,
|
||||
* type: string,
|
||||
* resModel: string,
|
||||
* [prop:string]: any
|
||||
* }} MakeViewParams
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {MakeViewParams} params
|
||||
* @param {boolean} [inDialog=false]
|
||||
* @returns {Component}
|
||||
*/
|
||||
async function _makeView(params, inDialog = false) {
|
||||
const props = { ...params };
|
||||
const serverData = props.serverData;
|
||||
const mockRPC = props.mockRPC;
|
||||
const config = {
|
||||
...getDefaultConfig(),
|
||||
...props.config,
|
||||
};
|
||||
const legacyParams = props.legacyParams || {};
|
||||
|
||||
delete props.serverData;
|
||||
delete props.mockRPC;
|
||||
delete props.legacyParams;
|
||||
delete props.config;
|
||||
|
||||
if (props.arch) {
|
||||
serverData.views = serverData.views || {};
|
||||
props.viewId = params.viewId || 100000001; // hopefully will not conflict with an id already in views
|
||||
serverData.views[`${props.resModel},${props.viewId},${props.type}`] = props.arch;
|
||||
delete props.arch;
|
||||
props.searchViewId = 100000002; // hopefully will not conflict with an id already in views
|
||||
const searchViewArch = props.searchViewArch || "<search/>";
|
||||
serverData.views[`${props.resModel},${props.searchViewId},search`] = searchViewArch;
|
||||
delete props.searchViewArch;
|
||||
}
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
Object.assign(env, createDebugContext(env)); // This is needed if the views are in debug mode
|
||||
|
||||
/** Legacy Environment, for compatibility sakes
|
||||
* Remove this as soon as we drop the legacy support
|
||||
*/
|
||||
const models = params.serverData.models;
|
||||
if (legacyParams && legacyParams.withLegacyMockServer && models) {
|
||||
legacyParams.models = Object.assign({}, 0);
|
||||
// In lagacy, data may not be sole models, but can contain some other variables
|
||||
// So we filter them out for our WOWL mockServer
|
||||
Object.entries(legacyParams.models).forEach(([k, v]) => {
|
||||
if (!(v instanceof Object) || !("fields" in v)) {
|
||||
delete models[k];
|
||||
}
|
||||
});
|
||||
}
|
||||
await addLegacyMockEnvironment(env, legacyParams);
|
||||
|
||||
const target = getFixture();
|
||||
const viewEnv = Object.assign(Object.create(env), { config });
|
||||
|
||||
await mount(MainComponentsContainer, target, { env });
|
||||
let viewNode;
|
||||
if (inDialog) {
|
||||
let root;
|
||||
class RootDialog extends Component {
|
||||
setup() {
|
||||
root = this;
|
||||
useSubEnv(viewEnv);
|
||||
}
|
||||
}
|
||||
RootDialog.components = { Dialog, View };
|
||||
RootDialog.template = rootDialogTemplate;
|
||||
env.services.dialog.add(RootDialog, { viewProps: props });
|
||||
await nextTick();
|
||||
const rootNode = root.__owl__;
|
||||
const dialogNode = Object.values(rootNode.children)[0];
|
||||
viewNode = Object.values(dialogNode.children)[0];
|
||||
} else {
|
||||
const view = await mount(View, target, { env: viewEnv, props });
|
||||
await nextTick();
|
||||
viewNode = view.__owl__;
|
||||
}
|
||||
const withSearchNode = Object.values(viewNode.children)[0];
|
||||
const concreteViewNode = Object.values(withSearchNode.children)[0];
|
||||
const concreteView = concreteViewNode.component;
|
||||
|
||||
return concreteView;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MakeViewParams} params
|
||||
* @returns {Component}
|
||||
*/
|
||||
export function makeView(params) {
|
||||
return _makeView(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MakeViewParams} params
|
||||
* @returns {Component}
|
||||
*/
|
||||
export function makeViewInDialog(params) {
|
||||
return _makeView(params, true);
|
||||
}
|
||||
|
||||
export function setupViewRegistries() {
|
||||
setupControlPanelFavoriteMenuRegistry();
|
||||
setupControlPanelServiceRegistry();
|
||||
serviceRegistry.add(
|
||||
"user",
|
||||
makeFakeUserService((group) => group === "base.group_allow_export"),
|
||||
{ force: true }
|
||||
);
|
||||
serviceRegistry.add("router", makeFakeRouterService(), { force: true });
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService()), { force: true };
|
||||
serviceRegistry.add("popover", popoverService), { force: true };
|
||||
serviceRegistry.add("company", fakeCompanyService);
|
||||
serviceRegistry.add("command", commandService);
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper sets the legacy env and mounts a MainComponentsContainer
|
||||
* to allow legacy code to use wowl FormViewDialogs.
|
||||
*
|
||||
* TODO: remove this when there's no legacy code using the wowl FormViewDialog.
|
||||
*
|
||||
* @param {Object} serverData
|
||||
* @param {Function} [mockRPC]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function prepareWowlFormViewDialogs(serverData, mockRPC) {
|
||||
setupViewRegistries();
|
||||
const wowlEnv = await makeTestEnv({ serverData, mockRPC });
|
||||
const legacyEnv = makeTestEnvironment();
|
||||
mapLegacyEnvToWowlEnv(legacyEnv, wowlEnv);
|
||||
owl.Component.env = legacyEnv;
|
||||
await mount(MainComponentsContainer, getFixture(), { env: wowlEnv });
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/** @odoo-module **/
|
||||
import { KanbanCompiler } from "@web/views/kanban/kanban_compiler";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
|
||||
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").outerHTML;
|
||||
}
|
||||
|
||||
function assertTemplatesEqual(template1, template2) {
|
||||
if (template1.replace(/\s/g, "") === template2.replace(/\s/g, "")) {
|
||||
QUnit.assert.ok(true);
|
||||
} else {
|
||||
QUnit.assert.strictEqual(template1, template2);
|
||||
}
|
||||
}
|
||||
|
||||
QUnit.module("Kanban Compiler", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
// compiler generates a piece of template for the translate alert in multilang
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
});
|
||||
|
||||
QUnit.test("bootstrap dropdowns with kanban_ignore_dropdown class should be left as is", async () => {
|
||||
const arch = `<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`;
|
||||
const expected = `<t t-translation="off">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</t>`;
|
||||
assertTemplatesEqual(compileTemplate(arch), expected);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
297
odoo-bringout-oca-ocb-web/web/static/tests/views/layout_tests.js
Normal file
297
odoo-bringout-oca-ocb-web/web/static/tests/views/layout_tests.js
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture, mount, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { makeWithSearch, setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
|
||||
import { Layout } from "@web/search/layout";
|
||||
import { getDefaultConfig } from "@web/views/view";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
|
||||
import { Component, xml, useChildSubEnv } from "@odoo/owl";
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Views", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
aaa: {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["a", "A"],
|
||||
["b", "B"],
|
||||
],
|
||||
},
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
},
|
||||
views: {
|
||||
"foo,false,search": /* xml */ `
|
||||
<search>
|
||||
<searchpanel>
|
||||
<field name="aaa" />
|
||||
</searchpanel>
|
||||
</search>`,
|
||||
},
|
||||
};
|
||||
|
||||
setupControlPanelServiceRegistry();
|
||||
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("Layout");
|
||||
|
||||
QUnit.test("Simple rendering", async (assert) => {
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout className="'o_view_sample_data'" display="props.display">
|
||||
<div class="toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const toyEnv = Object.assign(Object.create(env), { config: {} });
|
||||
await mount(ToyComponent, getFixture(), { env: toyEnv });
|
||||
|
||||
assert.containsOnce(target, ".o_view_sample_data");
|
||||
assert.containsNone(target, ".o_control_panel");
|
||||
assert.containsNone(target, ".o_component_with_search_panel");
|
||||
assert.containsNone(target, ".o_search_panel");
|
||||
assert.containsOnce(target, ".o_content > .toy_content");
|
||||
});
|
||||
|
||||
QUnit.test("Simple rendering: with search", async (assert) => {
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout display="props.display">
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<div class="toy_search_bar" />
|
||||
</t>
|
||||
<div class="toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_control_panel .o_cp_top_right .toy_search_bar");
|
||||
assert.containsOnce(target, ".o_component_with_search_panel .o_search_panel");
|
||||
assert.containsNone(target, ".o_cp_searchview");
|
||||
assert.containsOnce(target, ".o_content > .toy_content");
|
||||
});
|
||||
|
||||
QUnit.test("Nested layouts", async (assert) => {
|
||||
// Component C: bottom (no control panel)
|
||||
class ToyC extends Component {
|
||||
get display() {
|
||||
return {
|
||||
controlPanel: false,
|
||||
searchPanel: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
ToyC.template = xml`
|
||||
<Layout className="'toy_c'" display="display">
|
||||
<div class="toy_c_content" />
|
||||
</Layout>`;
|
||||
ToyC.components = { Layout };
|
||||
|
||||
// Component B: center (with custom search panel)
|
||||
class SearchPanel extends Component {}
|
||||
SearchPanel.template = xml`<div class="o_toy_search_panel" />`;
|
||||
|
||||
class ToyB extends Component {
|
||||
setup() {
|
||||
useChildSubEnv({
|
||||
config: {
|
||||
...getDefaultConfig(),
|
||||
SearchPanel,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
ToyB.template = xml`
|
||||
<Layout className="'toy_b'" display="props.display">
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<div class="toy_b_breadcrumbs" />
|
||||
</t>
|
||||
<ToyC/>
|
||||
</Layout>`;
|
||||
ToyB.components = { Layout, ToyC };
|
||||
|
||||
// Component A: top
|
||||
class ToyA extends Component {}
|
||||
ToyA.template = xml`
|
||||
<Layout className="'toy_a'" display="props.display">
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<div class="toy_a_search" />
|
||||
</t>
|
||||
<ToyB display="props.display"/>
|
||||
</Layout>`;
|
||||
ToyA.components = { Layout, ToyB };
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyA,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_content.toy_a .o_content.toy_b .o_content.toy_c"); // Full chain of contents
|
||||
assert.containsN(target, ".o_control_panel", 2); // Component C has hidden its control panel
|
||||
assert.containsN(target, ".o_content.o_component_with_search_panel", 3);
|
||||
assert.containsOnce(target, ".o_search_panel"); // Standard search panel
|
||||
assert.containsN(target, ".o_toy_search_panel", 2); // Custom search panels
|
||||
assert.containsOnce(target, ".toy_a_search");
|
||||
assert.containsOnce(target, ".toy_b_breadcrumbs");
|
||||
assert.containsOnce(target, ".toy_c_content");
|
||||
});
|
||||
|
||||
QUnit.test("Custom control panel", async (assert) => {
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout display="props.display">
|
||||
<div class="o_toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
class ControlPanel extends Component {}
|
||||
ControlPanel.template = xml`<div class="o_toy_search_panel" />`;
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
config: { ControlPanel },
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_toy_content");
|
||||
assert.containsOnce(target, ".o_toy_search_panel");
|
||||
assert.containsNone(target, ".o_control_panel");
|
||||
});
|
||||
|
||||
QUnit.test("Custom search panel", async (assert) => {
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout display="props.display">
|
||||
<div class="o_toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
class SearchPanel extends Component {}
|
||||
SearchPanel.template = xml`<div class="o_toy_search_panel" />`;
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
config: { SearchPanel },
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_toy_content");
|
||||
assert.containsOnce(target, ".o_toy_search_panel");
|
||||
assert.containsNone(target, ".o_search_panel");
|
||||
});
|
||||
|
||||
QUnit.test("Custom banner: no bannerRoute in env", async (assert) => {
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout display="props.display">
|
||||
<div class="o_toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
class Banner extends Component {}
|
||||
Banner.template = xml`<div class="o_toy_banner" />`;
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
config: { Banner },
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_toy_content");
|
||||
assert.containsNone(target, ".o_toy_banner");
|
||||
});
|
||||
|
||||
QUnit.test("Custom banner: with bannerRoute in env", async (assert) => {
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout display="props.display">
|
||||
<div class="o_toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
class Banner extends Component {}
|
||||
Banner.template = xml`<div class="o_toy_banner" />`;
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
config: { Banner, bannerRoute: "toy/banner/route" },
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_toy_content");
|
||||
assert.containsOnce(target, ".o_toy_banner");
|
||||
assert.notOk(target.querySelector(".o_toy_banner").closest(".o_content"));
|
||||
assert.ok(target.querySelector(".o_toy_content").closest(".o_content"));
|
||||
});
|
||||
|
||||
QUnit.test("Simple rendering: with dynamically displayed search", async (assert) => {
|
||||
let displayControlPanelTopRight = true;
|
||||
class ToyComponent extends Component {
|
||||
get display() {
|
||||
return {
|
||||
...this.props.display,
|
||||
controlPanel: {
|
||||
...this.props.display.controlPanel,
|
||||
"top-right": displayControlPanelTopRight,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
ToyComponent.template = xml`
|
||||
<Layout display="display">
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<div class="toy_search_bar" />
|
||||
</t>
|
||||
<div class="toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
const comp = await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_control_panel .o_cp_top_right .toy_search_bar");
|
||||
assert.containsOnce(target, ".o_component_with_search_panel .o_search_panel");
|
||||
assert.containsNone(target, ".o_cp_searchview");
|
||||
assert.containsOnce(target, ".o_content > .toy_content");
|
||||
|
||||
displayControlPanelTopRight = false;
|
||||
comp.render();
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(target, ".o_control_panel .o_cp_top_right .toy_search_bar");
|
||||
assert.containsOnce(target, ".o_component_with_search_panel .o_search_panel");
|
||||
assert.containsNone(target, ".o_cp_searchview");
|
||||
assert.containsOnce(target, ".o_content > .toy_content");
|
||||
});
|
||||
});
|
||||
18023
odoo-bringout-oca-ocb-web/web/static/tests/views/list_view_tests.js
Normal file
18023
odoo-bringout-oca-ocb-web/web/static/tests/views/list_view_tests.js
Normal file
File diff suppressed because it is too large
Load diff
5700
odoo-bringout-oca-ocb-web/web/static/tests/views/pivot_view_tests.js
Normal file
5700
odoo-bringout-oca-ocb-web/web/static/tests/views/pivot_view_tests.js
Normal file
File diff suppressed because it is too large
Load diff
186
odoo-bringout-oca-ocb-web/web/static/tests/views/record_tests.js
Normal file
186
odoo-bringout-oca-ocb-web/web/static/tests/views/record_tests.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { Field } from "@web/views/fields/field";
|
||||
import { Record } from "@web/views/record";
|
||||
import { click, getFixture, mount } from "../helpers/utils";
|
||||
import { setupViewRegistries } from "../views/helpers";
|
||||
|
||||
import { Component, xml, useState } from "@odoo/owl";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Record Component", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
p: {
|
||||
string: "one2many field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
},
|
||||
product_id: {
|
||||
string: "Product",
|
||||
type: "many2one",
|
||||
relation: "product",
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
foo: "yop",
|
||||
int_field: 10,
|
||||
p: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
foo: "blip",
|
||||
int_field: 0,
|
||||
p: [],
|
||||
},
|
||||
{ id: 3, foo: "gnap", int_field: 80 },
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
foo: "abc",
|
||||
int_field: false,
|
||||
},
|
||||
{ id: 5, foo: "blop", int_field: -4 },
|
||||
],
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.test("display a simple field", async function (assert) {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { Record, Field };
|
||||
Parent.template = xml`
|
||||
<Record resModel="'partner'" resId="1" fieldNames="['foo']" t-slot-scope="data">
|
||||
<span>hello</span>
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>`;
|
||||
const env = await makeTestEnv({
|
||||
serverData,
|
||||
mockRPC(route) {
|
||||
assert.step(route);
|
||||
},
|
||||
});
|
||||
await mount(Parent, target, { env });
|
||||
assert.strictEqual(
|
||||
target.innerHTML,
|
||||
'<span>hello</span><div name="foo" class="o_field_widget o_field_char"><span>yop</span></div>'
|
||||
);
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/partner/fields_get",
|
||||
"/web/dataset/call_kw/partner/read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("can be updated with different resId", async function (assert) {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
resId: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
Parent.components = { Record, Field };
|
||||
Parent.template = xml`
|
||||
<Record resModel="'partner'" resId="state.resId" fieldNames="['foo']" t-slot-scope="data">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
<button t-on-click="() => this.state.resId++">Next</button>
|
||||
</Record>`;
|
||||
const env = await makeTestEnv({
|
||||
serverData,
|
||||
mockRPC(route) {
|
||||
assert.step(route);
|
||||
},
|
||||
});
|
||||
await mount(Parent, target, { env, dev: true });
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/partner/fields_get",
|
||||
"/web/dataset/call_kw/partner/read",
|
||||
]);
|
||||
assert.containsOnce(target, ".o_field_char:contains(yop)");
|
||||
await click(target.querySelector("button"));
|
||||
assert.containsOnce(target, ".o_field_char:contains(blip)");
|
||||
assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
|
||||
});
|
||||
|
||||
QUnit.test("predefined fields and values", async function (assert) {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.fields = {
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "char",
|
||||
},
|
||||
bar: {
|
||||
name: "bar",
|
||||
type: "boolean",
|
||||
},
|
||||
};
|
||||
this.values = {
|
||||
foo: "abc",
|
||||
bar: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
Parent.components = { Record, Field };
|
||||
Parent.template = xml`
|
||||
<Record resModel="'partner'" fieldNames="['foo']" fields="fields" initialValues="values" t-slot-scope="data">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, {
|
||||
env: await makeTestEnv({
|
||||
serverData,
|
||||
mockRPC(route) {
|
||||
assert.step(route);
|
||||
},
|
||||
}),
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "abc");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,534 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { SampleServer } from "@web/views/sample_server";
|
||||
|
||||
const {
|
||||
MAIN_RECORDSET_SIZE,
|
||||
SEARCH_READ_LIMIT, // Limits
|
||||
SAMPLE_COUNTRIES,
|
||||
SAMPLE_PEOPLE,
|
||||
SAMPLE_TEXTS, // Text values
|
||||
MAX_COLOR_INT,
|
||||
MAX_FLOAT,
|
||||
MAX_INTEGER,
|
||||
MAX_MONETARY, // Number values
|
||||
SUB_RECORDSET_SIZE, // Records sise
|
||||
} = SampleServer;
|
||||
|
||||
/**
|
||||
* Transforms random results into deterministic ones.
|
||||
*/
|
||||
class DeterministicSampleServer extends SampleServer {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.arrayElCpt = 0;
|
||||
this.boolCpt = 0;
|
||||
this.subRecordIdCpt = 0;
|
||||
}
|
||||
_getRandomArrayEl(array) {
|
||||
return array[this.arrayElCpt++ % array.length];
|
||||
}
|
||||
_getRandomBool() {
|
||||
return Boolean(this.boolCpt++ % 2);
|
||||
}
|
||||
_getRandomSubRecordId() {
|
||||
return (this.subRecordIdCpt++ % SUB_RECORDSET_SIZE) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
let fields;
|
||||
QUnit.module(
|
||||
"Sample Server",
|
||||
{
|
||||
beforeEach() {
|
||||
fields = {
|
||||
"res.users": {
|
||||
display_name: { string: "Name", type: "char" },
|
||||
name: { string: "Reference", type: "char" },
|
||||
email: { string: "Email", type: "char" },
|
||||
phone_number: { string: "Phone number", type: "char" },
|
||||
brol_machin_url_truc: { string: "URL", type: "char" },
|
||||
urlemailphone: { string: "Whatever", type: "char" },
|
||||
active: { string: "Active", type: "boolean" },
|
||||
is_alive: { string: "Is alive", type: "boolean" },
|
||||
description: { string: "Description", type: "text" },
|
||||
birthday: { string: "Birthday", type: "date" },
|
||||
arrival_date: { string: "Date of arrival", type: "datetime" },
|
||||
height: { string: "Height", type: "float" },
|
||||
color: { string: "Color", type: "integer" },
|
||||
age: { string: "Age", type: "integer" },
|
||||
salary: { string: "Salary", type: "monetary" },
|
||||
currency: {
|
||||
string: "Currency",
|
||||
type: "many2one",
|
||||
relation: "res.currency",
|
||||
},
|
||||
manager_id: {
|
||||
string: "Manager",
|
||||
type: "many2one",
|
||||
relation: "res.users",
|
||||
},
|
||||
cover_image_id: {
|
||||
string: "Cover Image",
|
||||
type: "many2one",
|
||||
relation: "ir.attachment",
|
||||
},
|
||||
managed_ids: {
|
||||
string: "Managing",
|
||||
type: "one2many",
|
||||
relation: "res.users",
|
||||
},
|
||||
tag_ids: { string: "Tags", type: "many2many", relation: "tag" },
|
||||
type: {
|
||||
string: "Type",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["client", "Client"],
|
||||
["partner", "Partner"],
|
||||
["employee", "Employee"],
|
||||
],
|
||||
},
|
||||
},
|
||||
"res.country": {
|
||||
display_name: { string: "Name", type: "char" },
|
||||
},
|
||||
hobbit: {
|
||||
display_name: { string: "Name", type: "char" },
|
||||
profession: {
|
||||
string: "Profession",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["gardener", "Gardener"],
|
||||
["brewer", "Brewer"],
|
||||
["adventurer", "Adventurer"],
|
||||
],
|
||||
},
|
||||
age: { string: "Age", type: "integer" },
|
||||
},
|
||||
"ir.attachment": {
|
||||
display_name: { string: "Name", type: "char" },
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
function () {
|
||||
QUnit.module("Basic behaviour");
|
||||
|
||||
QUnit.test("Sample data: people type + all field names", async function (assert) {
|
||||
assert.expect(26);
|
||||
|
||||
const allFieldNames = Object.keys(fields["res.users"]);
|
||||
const server = new DeterministicSampleServer("res.users", fields["res.users"]);
|
||||
const { records } = await server.mockRpc({
|
||||
method: "web_search_read",
|
||||
model: "res.users",
|
||||
fields: allFieldNames,
|
||||
});
|
||||
const rec = records[0];
|
||||
|
||||
function assertFormat(fieldName, regex) {
|
||||
if (regex instanceof RegExp) {
|
||||
assert.ok(
|
||||
regex.test(rec[fieldName].toString()),
|
||||
`Field "${fieldName}" has the correct format`
|
||||
);
|
||||
} else {
|
||||
assert.strictEqual(
|
||||
typeof rec[fieldName],
|
||||
regex,
|
||||
`Field "${fieldName}" is of type ${regex}`
|
||||
);
|
||||
}
|
||||
}
|
||||
function assertBetween(fieldName, min, max) {
|
||||
const val = rec[fieldName];
|
||||
assert.ok(
|
||||
min <= val && val < max && parseInt(val, 10) === val,
|
||||
`Field "${fieldName}" should be an integer between ${min} and ${max}: ${val}`
|
||||
);
|
||||
}
|
||||
|
||||
// Basic fields
|
||||
assert.ok(SAMPLE_PEOPLE.includes(rec.display_name));
|
||||
assert.ok(SAMPLE_PEOPLE.includes(rec.name));
|
||||
assert.strictEqual(
|
||||
rec.email,
|
||||
`${rec.display_name.replace(/ /, ".").toLowerCase()}@sample.demo`
|
||||
);
|
||||
assertFormat("phone_number", /\+1 555 754 000\d/);
|
||||
assertFormat("brol_machin_url_truc", /http:\/\/sample\d\.com/);
|
||||
assert.strictEqual(rec.urlemailphone, false);
|
||||
assert.strictEqual(rec.active, true);
|
||||
assertFormat("is_alive", "boolean");
|
||||
assert.ok(SAMPLE_TEXTS.includes(rec.description));
|
||||
assertFormat("birthday", /\d{4}-\d{2}-\d{2}/);
|
||||
assertFormat("arrival_date", /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
||||
assert.ok(
|
||||
rec.height >= 0 && rec.height <= MAX_FLOAT,
|
||||
"Field height should be between 0 and 100"
|
||||
);
|
||||
assertBetween("color", 0, MAX_COLOR_INT);
|
||||
assertBetween("age", 0, MAX_INTEGER);
|
||||
assertBetween("salary", 0, MAX_MONETARY);
|
||||
|
||||
// check float field have 2 decimal rounding
|
||||
assert.strictEqual(rec.height, parseFloat(parseFloat(rec.height).toFixed(2)));
|
||||
|
||||
const selectionValues = fields["res.users"].type.selection.map((sel) => sel[0]);
|
||||
assert.ok(selectionValues.includes(rec.type));
|
||||
|
||||
// Relational fields
|
||||
assert.strictEqual(rec.currency[0], 1);
|
||||
// Currently we expect the currency name to be a latin string, which
|
||||
// is not important; in most case we only need the ID. The following
|
||||
// assertion can be removed if needed.
|
||||
assert.ok(SAMPLE_TEXTS.includes(rec.currency[1]));
|
||||
|
||||
assert.strictEqual(typeof rec.manager_id[0], "number");
|
||||
assert.ok(SAMPLE_PEOPLE.includes(rec.manager_id[1]));
|
||||
|
||||
assert.strictEqual(rec.cover_image_id, false);
|
||||
|
||||
assert.strictEqual(rec.managed_ids.length, 2);
|
||||
assert.ok(rec.managed_ids.every((id) => typeof id === "number"));
|
||||
|
||||
assert.strictEqual(rec.tag_ids.length, 2);
|
||||
assert.ok(rec.tag_ids.every((id) => typeof id === "number"));
|
||||
});
|
||||
|
||||
QUnit.test("Sample data: country type", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer("res.country", fields["res.country"]);
|
||||
const { records } = await server.mockRpc({
|
||||
method: "web_search_read",
|
||||
model: "res.country",
|
||||
fields: ["display_name"],
|
||||
});
|
||||
|
||||
assert.ok(SAMPLE_COUNTRIES.includes(records[0].display_name));
|
||||
});
|
||||
|
||||
QUnit.test("Sample data: any type", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const { records } = await server.mockRpc({
|
||||
method: "web_search_read",
|
||||
model: "hobbit",
|
||||
fields: ["display_name"],
|
||||
});
|
||||
|
||||
assert.ok(SAMPLE_TEXTS.includes(records[0].display_name));
|
||||
});
|
||||
|
||||
QUnit.module("RPC calls");
|
||||
|
||||
QUnit.test("Send 'search_read' RPC: valid field names", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "web_search_read",
|
||||
model: "hobbit",
|
||||
fields: ["display_name"],
|
||||
});
|
||||
|
||||
assert.deepEqual(Object.keys(result.records[0]), ["id", "display_name"]);
|
||||
assert.strictEqual(result.length, SEARCH_READ_LIMIT);
|
||||
assert.ok(/\w+/.test(result.records[0].display_name), "Display name has been mocked");
|
||||
});
|
||||
|
||||
QUnit.test("Send 'search_read' RPC: invalid field names", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "web_search_read",
|
||||
model: "hobbit",
|
||||
fields: ["name"],
|
||||
});
|
||||
|
||||
assert.deepEqual(Object.keys(result.records[0]), ["id", "name"]);
|
||||
assert.strictEqual(result.length, SEARCH_READ_LIMIT);
|
||||
assert.strictEqual(
|
||||
result.records[0].name,
|
||||
false,
|
||||
`Field "name" doesn't exist => returns false`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'web_read_group' RPC: no group", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
server.setExistingGroups(null);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "web_read_group",
|
||||
model: "hobbit",
|
||||
groupBy: ["profession"],
|
||||
});
|
||||
|
||||
assert.deepEqual(result, {
|
||||
groups: [
|
||||
{
|
||||
__domain: [],
|
||||
profession: "adventurer",
|
||||
profession_count: 5,
|
||||
},
|
||||
{
|
||||
__domain: [],
|
||||
profession: "brewer",
|
||||
profession_count: 5,
|
||||
},
|
||||
{
|
||||
__domain: [],
|
||||
profession: "gardener",
|
||||
profession_count: 6,
|
||||
},
|
||||
],
|
||||
length: 3,
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("Send 'web_read_group' RPC: 2 groups", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
const existingGroups = [
|
||||
{ value: "gardener", profession_count: 0 }, // fake group
|
||||
{ value: "adventurer", profession_count: 0 }, // fake group
|
||||
];
|
||||
server.setExistingGroups(existingGroups);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "web_read_group",
|
||||
model: "hobbit",
|
||||
groupBy: ["profession"],
|
||||
fields: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, 2);
|
||||
assert.strictEqual(result.groups.length, 2);
|
||||
|
||||
assert.deepEqual(
|
||||
result.groups.map((g) => g.value),
|
||||
["gardener", "adventurer"]
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
result.groups.reduce((acc, g) => acc + g.profession_count, 0),
|
||||
MAIN_RECORDSET_SIZE
|
||||
);
|
||||
assert.ok(result.groups.every((g) => g.profession_count === g.__data.length));
|
||||
});
|
||||
|
||||
QUnit.test("Send 'web_read_group' RPC: all groups", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
const existingGroups = [
|
||||
{ value: "gardener", profession_count: 0 }, // fake group
|
||||
{ value: "brewer", profession_count: 0 }, // fake group
|
||||
{ value: "adventurer", profession_count: 0 }, // fake group
|
||||
];
|
||||
server.setExistingGroups(existingGroups);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "web_read_group",
|
||||
model: "hobbit",
|
||||
groupBy: ["profession"],
|
||||
fields: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, 3);
|
||||
assert.strictEqual(result.groups.length, 3);
|
||||
|
||||
assert.deepEqual(
|
||||
result.groups.map((g) => g.value),
|
||||
["gardener", "brewer", "adventurer"]
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
result.groups.reduce((acc, g) => acc + g.profession_count, 0),
|
||||
MAIN_RECORDSET_SIZE
|
||||
);
|
||||
assert.ok(result.groups.every((g) => g.profession_count === g.__data.length));
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read_group' RPC: no group", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "read_group",
|
||||
model: "hobbit",
|
||||
fields: [],
|
||||
groupBy: [],
|
||||
});
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{
|
||||
__count: MAIN_RECORDSET_SIZE,
|
||||
__domain: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read_group' RPC: groupBy", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "read_group",
|
||||
model: "hobbit",
|
||||
fields: [],
|
||||
groupBy: ["profession"],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, 3);
|
||||
assert.deepEqual(
|
||||
result.map((g) => g.profession),
|
||||
["adventurer", "brewer", "gardener"]
|
||||
);
|
||||
assert.strictEqual(
|
||||
result.reduce((acc, g) => acc + g.profession_count, 0),
|
||||
MAIN_RECORDSET_SIZE
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read_group' RPC: groupBy and field", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "read_group",
|
||||
model: "hobbit",
|
||||
fields: ["age:sum"],
|
||||
groupBy: ["profession"],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, 3);
|
||||
assert.deepEqual(
|
||||
result.map((g) => g.profession),
|
||||
["adventurer", "brewer", "gardener"]
|
||||
);
|
||||
assert.strictEqual(
|
||||
result.reduce((acc, g) => acc + g.profession_count, 0),
|
||||
MAIN_RECORDSET_SIZE
|
||||
);
|
||||
assert.strictEqual(
|
||||
result.reduce((acc, g) => acc + g.age, 0),
|
||||
server.data.hobbit.records.reduce((acc, g) => acc + g.age, 0)
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read_group' RPC: multiple groupBys and lazy", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "read_group",
|
||||
model: "hobbit",
|
||||
fields: [],
|
||||
groupBy: ["profession", "age"],
|
||||
});
|
||||
|
||||
assert.ok("profession" in result[0]);
|
||||
assert.notOk("age" in result[0]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Send 'read_group' RPC: multiple groupBys and not lazy",
|
||||
async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "read_group",
|
||||
model: "hobbit",
|
||||
fields: [],
|
||||
groupBy: ["profession", "age"],
|
||||
lazy: false,
|
||||
});
|
||||
|
||||
assert.ok("profession" in result[0]);
|
||||
assert.ok("age" in result[0]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"Send 'read_group' RPC: multiple groupBys among which a many2many",
|
||||
async function (assert) {
|
||||
const server = new DeterministicSampleServer("res.users", fields["res.users"]);
|
||||
const result = await server.mockRpc({
|
||||
method: "read_group",
|
||||
model: "res.users",
|
||||
fields: [],
|
||||
groupBy: ["height", "tag_ids"],
|
||||
lazy: false,
|
||||
});
|
||||
assert.ok(typeof result[0].tag_ids[0] === "number");
|
||||
assert.ok(typeof result[0].tag_ids[1] === "string");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Send 'read' RPC: no id", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "read",
|
||||
model: "hobbit",
|
||||
args: [[], ["display_name"]],
|
||||
});
|
||||
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read' RPC: one id", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: "read",
|
||||
model: "hobbit",
|
||||
args: [[1], ["display_name"]],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, 1);
|
||||
assert.ok(/\w+/.test(result[0].display_name), "Display name has been mocked");
|
||||
assert.strictEqual(result[0].id, 1);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read' RPC: more than all available ids", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
|
||||
const amount = MAIN_RECORDSET_SIZE + 3;
|
||||
const ids = new Array(amount).fill().map((_, i) => i + 1);
|
||||
const result = await server.mockRpc({
|
||||
method: "read",
|
||||
model: "hobbit",
|
||||
args: [ids, ["display_name"]],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, MAIN_RECORDSET_SIZE);
|
||||
});
|
||||
|
||||
// To be implemented if needed
|
||||
// QUnit.test("Send 'read_progress_bar' RPC", async function (assert) { ... });
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/** @odoo-module */
|
||||
import { ViewButton } from "@web/views/view_button/view_button";
|
||||
import { useViewButtons } from "@web/views/view_button/view_button_hook";
|
||||
import { setupViewRegistries } from "./helpers";
|
||||
import { click, getFixture, mount } from "../helpers/utils";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
import { useRef, Component, xml } from "@odoo/owl";
|
||||
|
||||
QUnit.module("UseViewButton tests", (hooks) => {
|
||||
let target;
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.test("action can be prevented", async (assert) => {
|
||||
registry.category("services").add(
|
||||
"action",
|
||||
{
|
||||
start() {
|
||||
return {
|
||||
doActionButton() {
|
||||
assert.step("doActionButton");
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
let executeInHook;
|
||||
let executeInHandler;
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
const rootRef = useRef("root");
|
||||
useViewButtons({}, rootRef, {
|
||||
beforeExecuteAction: () => {
|
||||
assert.step("beforeExecuteAction in hook");
|
||||
return executeInHook;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onClick() {
|
||||
const getResParams = () => ({
|
||||
resIds: [3],
|
||||
resId: 3,
|
||||
});
|
||||
const clickParams = {};
|
||||
const beforeExecute = () => {
|
||||
assert.step("beforeExecuteAction on handler");
|
||||
return executeInHandler;
|
||||
};
|
||||
this.env.onClickViewButton({ beforeExecute, getResParams, clickParams });
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div t-ref="root" t-on-click="onClick" class="myComponent">Some text</div>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
await mount(MyComponent, target, { env, props: {} });
|
||||
|
||||
await click(target, ".myComponent");
|
||||
assert.verifySteps([
|
||||
"beforeExecuteAction on handler",
|
||||
"beforeExecuteAction in hook",
|
||||
"doActionButton",
|
||||
]);
|
||||
|
||||
executeInHook = false;
|
||||
await click(target, ".myComponent");
|
||||
assert.verifySteps(["beforeExecuteAction on handler", "beforeExecuteAction in hook"]);
|
||||
|
||||
executeInHandler = false;
|
||||
await click(target, ".myComponent");
|
||||
assert.verifySteps(["beforeExecuteAction on handler"]);
|
||||
});
|
||||
|
||||
QUnit.test("ViewButton clicked in Dropdown close the Dropdown", async (assert) => {
|
||||
registry.category("services").add(
|
||||
"action",
|
||||
{
|
||||
start() {
|
||||
return {
|
||||
doActionButton() {
|
||||
assert.step("doActionButton");
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
const rootRef = useRef("root");
|
||||
useViewButtons({}, rootRef);
|
||||
}
|
||||
}
|
||||
MyComponent.components = { Dropdown, DropdownItem, ViewButton };
|
||||
MyComponent.template = xml`
|
||||
<div t-ref="root" class="myComponent">
|
||||
<Dropdown>
|
||||
<t t-set-slot="toggler">dropdown</t>
|
||||
<DropdownItem>
|
||||
<ViewButton tag="'a'" clickParams="{ type:'action' }" string="'coucou'" record="{ resId: 1 }" />
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
await mount(MyComponent, target, { env });
|
||||
|
||||
await click(target, ".dropdown-toggle");
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
|
||||
await click(target, "a[type=action]");
|
||||
assert.verifySteps(["doActionButton"]);
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,425 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerHotkey,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { contains } from "@web/../tests/utils";
|
||||
import { makeView } from "@web/../tests/views/helpers";
|
||||
import { createWebClient } from "@web/../tests/webclient/helpers";
|
||||
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
|
||||
import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
|
||||
|
||||
QUnit.module("ViewDialogs", (hooks) => {
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
foo: { string: "Foo", type: "char" },
|
||||
bar: { string: "Bar", type: "boolean" },
|
||||
instrument: {
|
||||
string: "Instruments",
|
||||
type: "many2one",
|
||||
relation: "instrument",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, foo: "blip", display_name: "blipblip", bar: true },
|
||||
{ id: 2, foo: "ta tata ta ta", display_name: "macgyver", bar: false },
|
||||
{ id: 3, foo: "piou piou", display_name: "Jack O'Neill", bar: true },
|
||||
],
|
||||
},
|
||||
instrument: {
|
||||
fields: {
|
||||
name: { string: "name", type: "char" },
|
||||
badassery: {
|
||||
string: "level",
|
||||
type: "many2many",
|
||||
relation: "badassery",
|
||||
domain: [["level", "=", "Awsome"]],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
badassery: {
|
||||
fields: {
|
||||
level: { string: "level", type: "char" },
|
||||
},
|
||||
records: [{ id: 1, level: "Awsome" }],
|
||||
},
|
||||
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "name", type: "char" },
|
||||
partner: { string: "Doors", type: "one2many", relation: "partner" },
|
||||
},
|
||||
records: [{ id: 1, name: "The end" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
setupControlPanelServiceRegistry();
|
||||
});
|
||||
|
||||
QUnit.module("FormViewDialog");
|
||||
|
||||
QUnit.test("formviewdialog buttons in footer are positioned properly", async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,form": `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Custom Button" type="object" class="btn-primary"/>
|
||||
</footer>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
const webClient = await createWebClient({ serverData });
|
||||
webClient.env.services.dialog.add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(target, ".modal-body button", "should not have any button in body");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".modal-footer button:not(.d-none)",
|
||||
"should have only one button in footer"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("modifiers are considered on multiple <footer/> tags", async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,form": `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<footer attrs="{'invisible': [('bar','=',False)]}">
|
||||
<button>Hello</button>
|
||||
<button>World</button>
|
||||
</footer>
|
||||
<footer attrs="{'invisible': [('bar','!=',False)]}">
|
||||
<button>Foo</button>
|
||||
</footer>
|
||||
</form>`,
|
||||
};
|
||||
const webClient = await createWebClient({ serverData });
|
||||
webClient.env.services.dialog.add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
assert.deepEqual(
|
||||
getVisibleButtonTexts(),
|
||||
["Hello", "World"],
|
||||
"only the first button section should be visible"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_field_boolean input"));
|
||||
|
||||
assert.deepEqual(
|
||||
getVisibleButtonTexts(),
|
||||
["Foo"],
|
||||
"only the second button section should be visible"
|
||||
);
|
||||
|
||||
function getVisibleButtonTexts() {
|
||||
return [...target.querySelectorAll(".modal-footer button:not(.d-none)")].map((x) =>
|
||||
x.innerHTML.trim()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("formviewdialog buttons in footer are not duplicated", async function (assert) {
|
||||
serverData.models.partner.fields.poney_ids = {
|
||||
string: "Poneys",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
};
|
||||
serverData.models.partner.records[0].poney_ids = [];
|
||||
serverData.views = {
|
||||
"partner,false,form": `
|
||||
<form string="Partner">
|
||||
<field name="poney_ids"><tree editable="top"><field name="display_name"/></tree></field>
|
||||
<footer><button string="Custom Button" type="object" class="my_button"/></footer>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
webClient.env.services.dialog.add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(target, ".modal button.my_button", "should have 1 buttons in modal");
|
||||
|
||||
await click(target, ".o_field_x2many_list_row_add a");
|
||||
triggerHotkey("escape");
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".modal button.btn-primary",
|
||||
"should still have 1 buttons in modal"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Form dialog and subview with _view_ref contexts", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
serverData.models.instrument.records = [{ id: 1, name: "Tromblon", badassery: [1] }];
|
||||
serverData.models.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.
|
||||
serverData.views = {
|
||||
"instrument,false,form": `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="badassery" widget="many2many" context="{'tree_view_ref': 'some_other_tree_view'}"/>
|
||||
</form>`,
|
||||
"badassery,false,list": `
|
||||
<tree>
|
||||
<field name="level"/>
|
||||
</tree>`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<form>
|
||||
<field name="name"/>
|
||||
<field name="instrument" context="{'tree_view_ref': 'some_tree_view'}" open_target="new"/>
|
||||
</form>`,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "get_formview_id") {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (args.method === "get_views" && args.model === "instrument") {
|
||||
assert.deepEqual(
|
||||
args.kwargs.context,
|
||||
{
|
||||
lang: "en",
|
||||
tree_view_ref: "some_tree_view",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
"1 The correct _view_ref should have been sent to the server, first time"
|
||||
);
|
||||
}
|
||||
if (args.method === "get_views" && args.model === "badassery") {
|
||||
assert.deepEqual(
|
||||
args.kwargs.context,
|
||||
{
|
||||
base_model_name: "instrument",
|
||||
lang: "en",
|
||||
tree_view_ref: "some_other_tree_view",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
"2 The correct _view_ref should have been sent to the server for the subview"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
await click(target, '.o_field_widget[name="instrument"] button.o_external_button');
|
||||
});
|
||||
|
||||
QUnit.test("click on view buttons in a FormViewDialog", async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,form": `
|
||||
<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>`,
|
||||
};
|
||||
|
||||
function mockRPC(route, args) {
|
||||
assert.step(args.method || route);
|
||||
}
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
patchWithCleanup(webClient.env.services.action, {
|
||||
doActionButton: (params) => {
|
||||
assert.step(params.name);
|
||||
params.onClose();
|
||||
},
|
||||
});
|
||||
webClient.env.services.dialog.add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_dialog .o_form_view");
|
||||
assert.containsN(target, ".o_dialog .o_form_view button", 2);
|
||||
assert.verifySteps(["/web/webclient/load_menus", "get_views", "read"]);
|
||||
await click(target.querySelector(".o_dialog .o_form_view .btn1"));
|
||||
assert.containsOnce(target, ".o_dialog .o_form_view");
|
||||
assert.verifySteps(["method1", "read"]); // should re-read the record
|
||||
await click(target.querySelector(".o_dialog .o_form_view .btn2"));
|
||||
assert.containsNone(target, ".o_dialog .o_form_view");
|
||||
assert.verifySteps(["method2"]); // should not read as we closed
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"formviewdialog is not closed when button handlers return a rejected promise",
|
||||
async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,form": `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<group><field name="foo"/></group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
let reject = true;
|
||||
function mockRPC(route, args) {
|
||||
if (args.method === "create" && reject) {
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
webClient.env.services.dialog.add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
context: { answer: 42 },
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(target, ".modal-body button", "should not have any button in body");
|
||||
assert.containsN(target, ".modal-footer button", 3, "should have 3 buttons in footer");
|
||||
|
||||
await click(target, ".modal .o_form_button_save");
|
||||
assert.containsOnce(target, ".modal", "modal should still be opened");
|
||||
|
||||
reject = false;
|
||||
await click(target, ".modal .o_form_button_save");
|
||||
assert.containsNone(target, ".modal", "modal should be closed");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("FormViewDialog with remove button", async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,form": `<form><field name="foo"/></form>`,
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
webClient.env.services.dialog.add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
removeRecord: () => assert.step("remove"),
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_dialog .o_form_view");
|
||||
assert.containsOnce(target, ".o_dialog .modal-footer .o_form_button_remove");
|
||||
await click(target.querySelector(".o_dialog .modal-footer .o_form_button_remove"));
|
||||
assert.verifySteps(["remove"]);
|
||||
assert.containsNone(target, ".o_dialog .o_form_view");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Save a FormViewDialog when a required field is empty don't close the dialog",
|
||||
async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,form": `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<group><field name="foo" required="1"/></group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="save" special="save" class="btn-primary"/>
|
||||
</footer>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
webClient.env.services.dialog.add(FormViewDialog, {
|
||||
resModel: "partner",
|
||||
context: { answer: 42 },
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
await click(target, '.modal button[name="save"]');
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(target, ".modal", "modal should still be opened");
|
||||
await editInput(target, "[name='foo'] input", "new");
|
||||
|
||||
await click(target, '.modal button[name="save"]');
|
||||
assert.containsNone(target, ".modal", "modal should be closed");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("display a dialog if onchange result is a warning from within a dialog", async function (assert) {
|
||||
serverData.views = {
|
||||
"instrument,false,form": `<form><field name="display_name" /></form>`
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form><field name="instrument"/></form>`,
|
||||
resId: 2,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "onchange" && args.model === "instrument") {
|
||||
assert.step("onchange warning")
|
||||
return Promise.resolve({
|
||||
warning: {
|
||||
title: "Warning",
|
||||
message: "You must first select a partner",
|
||||
type: "dialog",
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await editInput(target, ".o_field_widget[name=instrument] input", "tralala");
|
||||
await contains(".o_m2o_dropdown_option_create_edit a");
|
||||
|
||||
await click(target.querySelector(".o_m2o_dropdown_option_create_edit a"));
|
||||
await contains(".modal.o_inactive_modal");
|
||||
assert.containsN(document.body, ".modal", 2);
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".modal:not(.o_inactive_modal) .modal-body").textContent,
|
||||
"You must first select a partner"
|
||||
);
|
||||
|
||||
await click(document.body.querySelector(".modal:not(.o_inactive_modal) button"))
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".modal:not(.o_inactive_modal) .modal-title").textContent,
|
||||
"Create Instruments"
|
||||
);
|
||||
|
||||
assert.verifySteps(["onchange warning"])
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,525 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import {
|
||||
click,
|
||||
clickOpenedDropdownItem,
|
||||
getFixture,
|
||||
nextTick,
|
||||
editInput,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { createWebClient } from "@web/../tests/webclient/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { session } from "@web/session";
|
||||
import { useSetupAction } from "@web/webclient/actions/action_hook";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
|
||||
import {
|
||||
editFavoriteName,
|
||||
removeFacet,
|
||||
saveFavorite,
|
||||
toggleFavoriteMenu,
|
||||
toggleFilterMenu,
|
||||
toggleMenuItem,
|
||||
toggleSaveFavorite,
|
||||
} from "@web/../tests/search/helpers";
|
||||
|
||||
QUnit.module("ViewDialogs", (hooks) => {
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
foo: { string: "Foo", type: "char" },
|
||||
bar: { string: "Bar", type: "boolean" },
|
||||
instrument: {
|
||||
string: "Instruments",
|
||||
type: "many2one",
|
||||
relation: "instrument",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, foo: "blip", display_name: "blipblip", bar: true },
|
||||
{ id: 2, foo: "ta tata ta ta", display_name: "macgyver", bar: false },
|
||||
{ id: 3, foo: "piou piou", display_name: "Jack O'Neill", bar: true },
|
||||
],
|
||||
},
|
||||
instrument: {
|
||||
fields: {
|
||||
name: { string: "name", type: "char" },
|
||||
badassery: {
|
||||
string: "level",
|
||||
type: "many2many",
|
||||
relation: "badassery",
|
||||
domain: [["level", "=", "Awsome"]],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
badassery: {
|
||||
fields: {
|
||||
level: { string: "level", type: "char" },
|
||||
},
|
||||
records: [{ id: 1, level: "Awsome" }],
|
||||
},
|
||||
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "name", type: "char" },
|
||||
partner: { string: "Doors", type: "one2many", relation: "partner" },
|
||||
},
|
||||
records: [{ id: 1, name: "The end" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("SelectCreateDialog");
|
||||
|
||||
QUnit.test(
|
||||
"SelectCreateDialog use domain, group_by and search default",
|
||||
async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
serverData.views = {
|
||||
"partner,false,list": `
|
||||
<tree string="Partner">
|
||||
<field name="display_name"/>
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
`,
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<field name="foo" filter_domain="[('display_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;
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "web_read_group") {
|
||||
assert.deepEqual(
|
||||
args.kwargs,
|
||||
{
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
domain: [
|
||||
"&",
|
||||
["display_name", "like", "a"],
|
||||
"&",
|
||||
["display_name", "ilike", "piou"],
|
||||
["foo", "ilike", "piou"],
|
||||
],
|
||||
fields: ["display_name", "foo", "bar"],
|
||||
groupby: ["bar"],
|
||||
orderby: "",
|
||||
expand: false,
|
||||
expand_orderby: null,
|
||||
expand_limit: null,
|
||||
lazy: true,
|
||||
limit: 80,
|
||||
offset: 0,
|
||||
},
|
||||
"should search with the complete domain (domain + search), and group by 'bar'"
|
||||
);
|
||||
} else if (args.method === "web_search_read") {
|
||||
if (search === 0) {
|
||||
assert.deepEqual(
|
||||
args.kwargs,
|
||||
{
|
||||
context: {
|
||||
bin_size: true,
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
}, // not part of the test, may change
|
||||
domain: [
|
||||
"&",
|
||||
["display_name", "like", "a"],
|
||||
"&",
|
||||
["display_name", "ilike", "piou"],
|
||||
["foo", "ilike", "piou"],
|
||||
],
|
||||
fields: ["display_name", "foo"],
|
||||
limit: 80,
|
||||
offset: 0,
|
||||
order: "",
|
||||
count_limit: 10001,
|
||||
},
|
||||
"should search with the complete domain (domain + search)"
|
||||
);
|
||||
} else if (search === 1) {
|
||||
assert.deepEqual(
|
||||
args.kwargs,
|
||||
{
|
||||
context: {
|
||||
bin_size: true,
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
}, // not part of the test, may change
|
||||
domain: [["display_name", "like", "a"]],
|
||||
fields: ["display_name", "foo"],
|
||||
limit: 80,
|
||||
offset: 0,
|
||||
order: "",
|
||||
count_limit: 10001,
|
||||
},
|
||||
"should search with the domain"
|
||||
);
|
||||
}
|
||||
search++;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
webClient.env.services.dialog.add(SelectCreateDialog, {
|
||||
noCreate: true,
|
||||
resModel: "partner",
|
||||
domain: [["display_name", "like", "a"]],
|
||||
context: {
|
||||
search_default_groupby_bar: true,
|
||||
search_default_foo: "piou",
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
await removeFacet(target.querySelector(".modal"), "Bar");
|
||||
await removeFacet(target.querySelector(".modal"));
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("SelectCreateDialog correctly evaluates domains", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
serverData.views = {
|
||||
"partner,false,list": `
|
||||
<tree string="Partner">
|
||||
<field name="display_name"/>
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
`,
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<field name="foo"/>
|
||||
</search>
|
||||
`,
|
||||
};
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "web_search_read") {
|
||||
assert.deepEqual(
|
||||
args.kwargs.domain,
|
||||
[["id", "=", 2]],
|
||||
"should have correctly evaluated the domain"
|
||||
);
|
||||
}
|
||||
};
|
||||
patchWithCleanup(session.user_context, { uid: 2 });
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
webClient.env.services.dialog.add(SelectCreateDialog, {
|
||||
noCreate: true,
|
||||
readonly: true, //Not used
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", session.user_context.uid]],
|
||||
});
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
QUnit.test("SelectCreateDialog list view in readonly", async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,list": `
|
||||
<tree string="Partner" editable="bottom">
|
||||
<field name="display_name"/>
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
`,
|
||||
"partner,false,search": `
|
||||
<search/>
|
||||
`,
|
||||
};
|
||||
const webClient = await createWebClient({ serverData });
|
||||
webClient.env.services.dialog.add(SelectCreateDialog, {
|
||||
resModel: "partner",
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
// click on the first row to see if the list is editable
|
||||
target.querySelectorAll(".o_list_view tbody tr td")[1].click();
|
||||
await nextTick();
|
||||
assert.equal(
|
||||
target.querySelectorAll(".o_list_view tbody tr td .o_field_char input").length,
|
||||
0,
|
||||
"list view should not be editable in a SelectCreateDialog"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("SelectCreateDialog cascade x2many in create mode", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
|
||||
serverData.views = {
|
||||
"partner,false,form": `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="instrument" widget="one2many" mode="tree"/>
|
||||
</form>
|
||||
`,
|
||||
"instrument,false,form": `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="badassery">
|
||||
<tree>
|
||||
<field name="level"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
"badassery,false,list": `<tree><field name="level"/></tree>`,
|
||||
"badassery,false,search": `<search><field name="level"/></search>`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="partner" widget="one2many" >
|
||||
<tree editable="top">
|
||||
<field name="display_name"/>
|
||||
<field name="instrument"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
mockRPC: (route, args) => {
|
||||
if (route === "/web/dataset/call_kw/partner/get_formview_id") {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/instrument/get_formview_id") {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/instrument/create") {
|
||||
assert.deepEqual(
|
||||
args.args,
|
||||
[{ badassery: [[6, false, [1]]], name: "ABC" }],
|
||||
"The method create should have been called with the right arguments"
|
||||
);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_x2many_list_row_add a");
|
||||
|
||||
await editInput(target, ".o_field_widget[name=instrument] input", "ABC");
|
||||
await clickOpenedDropdownItem(target, "instrument", "Create and edit...");
|
||||
|
||||
assert.containsOnce(target, ".modal .modal-lg");
|
||||
|
||||
await click(target.querySelector(".modal .o_field_x2many_list_row_add a"));
|
||||
|
||||
assert.containsN(target, ".modal .modal-lg", 2);
|
||||
|
||||
await click(target.querySelector(".modal .o_data_row input[type=checkbox]"));
|
||||
await nextTick(); // wait for the select button to be enabled
|
||||
await click(target.querySelector(".modal .o_select_button"));
|
||||
|
||||
assert.containsOnce(target, ".modal .modal-lg");
|
||||
assert.strictEqual(target.querySelector(".modal .o_data_cell").innerText, "Awsome");
|
||||
|
||||
await click(target.querySelector(".modal .o_form_button_save"));
|
||||
});
|
||||
|
||||
QUnit.test("SelectCreateDialog: save current search", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
serverData.views = {
|
||||
"partner,false,list": `
|
||||
<tree>
|
||||
<field name="display_name"/>
|
||||
</tree>
|
||||
`,
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<filter name="bar" help="Bar" domain="[('bar', '=', True)]"/>
|
||||
</search>
|
||||
`,
|
||||
};
|
||||
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
useSetupAction({
|
||||
getContext: () => ({ shouldBeInFilterContext: true }),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const mockRPC = (_, args) => {
|
||||
if (args.model === "ir.filters" && args.method === "create_or_replace") {
|
||||
const irFilter = args.args[0];
|
||||
assert.deepEqual(
|
||||
irFilter.domain,
|
||||
`[("bar", "=", True)]`,
|
||||
"should save the correct domain"
|
||||
);
|
||||
const expectedContext = {
|
||||
group_by: [], // default groupby is an empty list
|
||||
shouldBeInFilterContext: true,
|
||||
};
|
||||
assert.deepEqual(
|
||||
irFilter.context,
|
||||
expectedContext,
|
||||
"should save the correct context"
|
||||
);
|
||||
return 7; // fake serverSideId
|
||||
}
|
||||
if (args.method === "get_views") {
|
||||
assert.equal(args.kwargs.options.load_filters, true, "Missing load_filters option");
|
||||
}
|
||||
};
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
webClient.env.services.dialog.add(SelectCreateDialog, {
|
||||
context: { shouldNotBeInFilterContext: false },
|
||||
resModel: "partner",
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_data_row", 3, "should contain 3 records");
|
||||
|
||||
// filter on bar
|
||||
await toggleFilterMenu(target);
|
||||
await toggleMenuItem(target, "Bar");
|
||||
|
||||
assert.containsN(target, ".o_data_row", 2, "should contain 2 records");
|
||||
|
||||
// save filter
|
||||
await toggleFavoriteMenu(target);
|
||||
await toggleSaveFavorite(target);
|
||||
await editFavoriteName(target, "some name");
|
||||
await saveFavorite(target);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"SelectCreateDialog calls on_selected with every record matching the domain",
|
||||
async function (assert) {
|
||||
assert.expect(1);
|
||||
serverData.views = {
|
||||
"partner,false,list": `
|
||||
<tree limit="2" string="Partner">
|
||||
<field name="display_name"/>
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
`,
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<field name="foo"/>
|
||||
</search>
|
||||
`,
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
webClient.env.services.dialog.add(SelectCreateDialog, {
|
||||
resModel: "partner",
|
||||
onSelected: function (records) {
|
||||
assert.equal(records.join(","), "1,2,3");
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
await click(target, "thead .o_list_record_selector input");
|
||||
await click(target, ".o_list_selection_box .o_list_select_domain");
|
||||
await click(target, ".modal .o_select_button");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"SelectCreateDialog calls on_selected with every record matching without selecting a domain",
|
||||
async function (assert) {
|
||||
assert.expect(1);
|
||||
serverData.views = {
|
||||
"partner,false,list": `
|
||||
<tree limit="2" string="Partner">
|
||||
<field name="display_name"/>
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
`,
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<field name="foo"/>
|
||||
</search>
|
||||
`,
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
webClient.env.services.dialog.add(SelectCreateDialog, {
|
||||
resModel: "partner",
|
||||
onSelected: function (records) {
|
||||
assert.equal(records.join(","), "1,2");
|
||||
},
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
await click(target, "thead .o_list_record_selector input");
|
||||
await click(target, ".o_list_selection_box");
|
||||
await click(target, ".modal .o_select_button");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("SelectCreateDialog: default props, create a record", async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,list": `<tree><field name="display_name"/></tree>`,
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<filter name="bar" help="Bar" domain="[('bar', '=', True)]"/>
|
||||
</search>`,
|
||||
"partner,false,form": `<form><field name="display_name"/></form>`,
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
webClient.env.services.dialog.add(SelectCreateDialog, {
|
||||
onSelected: (resIds) => assert.step(`onSelected ${resIds}`),
|
||||
resModel: "partner",
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.containsN(target, ".o_dialog .o_list_view .o_data_row", 3);
|
||||
assert.containsN(target, ".o_dialog footer button", 3);
|
||||
assert.containsOnce(target, ".o_dialog footer button.o_select_button");
|
||||
assert.containsOnce(target, ".o_dialog footer button.o_create_button");
|
||||
assert.containsOnce(target, ".o_dialog footer button.o_form_button_cancel");
|
||||
|
||||
await click(target.querySelector(".o_dialog footer button.o_create_button"));
|
||||
|
||||
assert.containsN(target, ".o_dialog", 2);
|
||||
assert.containsOnce(target, ".o_dialog .o_form_view");
|
||||
|
||||
await editInput(target, ".o_dialog .o_form_view .o_field_widget input", "hello");
|
||||
await click(target.querySelector(".o_dialog .o_form_button_save"));
|
||||
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
assert.verifySteps(["onSelected 4"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
/** @odoo-module */
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeMockServer } from "../helpers/mock_server";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
|
||||
QUnit.module("View service", (hooks) => {
|
||||
let serverData;
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
const views = {
|
||||
"take.five,99,list": `<list><field name="display_name" /></list>`,
|
||||
};
|
||||
|
||||
const models = {
|
||||
"take.five": {
|
||||
fields: {},
|
||||
records: [],
|
||||
},
|
||||
};
|
||||
|
||||
const fakeUiService = {
|
||||
start(env) {
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
get() {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
serverData = { models, views };
|
||||
registry
|
||||
.category("services")
|
||||
.add("views", viewService)
|
||||
.add("orm", ormService)
|
||||
.add("ui", fakeUiService);
|
||||
});
|
||||
|
||||
QUnit.test("stores calls in cache in success", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route.includes("get_views")) {
|
||||
assert.step("get_views");
|
||||
}
|
||||
};
|
||||
|
||||
await makeMockServer(serverData, mockRPC);
|
||||
const env = await makeTestEnv();
|
||||
|
||||
await env.services.views.loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
context: { default_field_value: 1 },
|
||||
},
|
||||
{}
|
||||
);
|
||||
await env.services.views.loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
context: { default_field_value: 2 },
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
assert.verifySteps(["get_views"]);
|
||||
});
|
||||
|
||||
QUnit.test("stores calls in cache when failed", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route.includes("get_views")) {
|
||||
assert.step("get_views");
|
||||
return Promise.reject("my little error");
|
||||
}
|
||||
};
|
||||
|
||||
await makeMockServer(serverData, mockRPC);
|
||||
const env = await makeTestEnv();
|
||||
|
||||
try {
|
||||
await env.services.views.loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
},
|
||||
{}
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(error, "my little error");
|
||||
}
|
||||
|
||||
try {
|
||||
await env.services.views.loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
},
|
||||
{}
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(error, "my little error");
|
||||
}
|
||||
|
||||
assert.verifySteps(["get_views", "get_views"]);
|
||||
});
|
||||
|
||||
QUnit.test("loadViews stores fields in cache", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route.includes("get_views")) {
|
||||
assert.step("get_views");
|
||||
}
|
||||
if (route.includes("fields_get")) {
|
||||
assert.step("fields_get");
|
||||
}
|
||||
};
|
||||
|
||||
await makeMockServer(serverData, mockRPC);
|
||||
const env = await makeTestEnv();
|
||||
|
||||
await env.services.views.loadViews(
|
||||
{
|
||||
resModel: "take.five",
|
||||
views: [[99, "list"]],
|
||||
context: { default_field_value: 1 },
|
||||
},
|
||||
{}
|
||||
);
|
||||
await env.services.views.loadFields("take.five");
|
||||
|
||||
assert.verifySteps(["get_views"]);
|
||||
});
|
||||
|
||||
QUnit.test("store loadFields calls in cache in success", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route.includes("fields_get")) {
|
||||
assert.step("fields_get");
|
||||
}
|
||||
};
|
||||
|
||||
await makeMockServer(serverData, mockRPC);
|
||||
const env = await makeTestEnv();
|
||||
|
||||
await env.services.views.loadFields("take.five");
|
||||
await env.services.views.loadFields("take.five");
|
||||
|
||||
assert.verifySteps(["fields_get"]);
|
||||
});
|
||||
|
||||
QUnit.test("store loadFields calls in cache when failed", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route.includes("fields_get")) {
|
||||
assert.step("fields_get");
|
||||
return Promise.reject("my little error");
|
||||
}
|
||||
};
|
||||
|
||||
await makeMockServer(serverData, mockRPC);
|
||||
const env = await makeTestEnv();
|
||||
|
||||
try {
|
||||
await env.services.views.loadFields("take.five");
|
||||
} catch (error) {
|
||||
assert.strictEqual(error, "my little error");
|
||||
}
|
||||
try {
|
||||
await env.services.views.loadFields("take.five");
|
||||
} catch (error) {
|
||||
assert.strictEqual(error, "my little error");
|
||||
}
|
||||
|
||||
assert.verifySteps(["fields_get", "fields_get"]);
|
||||
});
|
||||
});
|
||||
1656
odoo-bringout-oca-ocb-web/web/static/tests/views/view_tests.js
Normal file
1656
odoo-bringout-oca-ocb-web/web/static/tests/views/view_tests.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,157 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { AttachDocumentWidget } from "@web/views/widgets/attach_document/attach_document";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Widgets", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("AttachDocument");
|
||||
|
||||
QUnit.test("attach document widget calls action with attachment ids", async function (assert) {
|
||||
let fileInput;
|
||||
patchWithCleanup(AttachDocumentWidget.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
fileInput = this.fileInput;
|
||||
},
|
||||
});
|
||||
|
||||
serviceRegistry.add("http", {
|
||||
start: () => ({
|
||||
post: (route, params) => {
|
||||
assert.step("post");
|
||||
assert.strictEqual(route, "/web/binary/upload_attachment");
|
||||
assert.strictEqual(params.model, "partner");
|
||||
assert.strictEqual(params.id, 1);
|
||||
return '[{ "id": 5 }, { "id": 2 }]';
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
mockRPC(route, args) {
|
||||
assert.step(args.method);
|
||||
if (args.method === "my_action") {
|
||||
assert.deepEqual(args.model, "partner");
|
||||
assert.deepEqual(args.args, [1]);
|
||||
assert.deepEqual(args.kwargs.attachment_ids, [5, 2]);
|
||||
return true;
|
||||
}
|
||||
if (args.method === "write") {
|
||||
assert.deepEqual(args.args[1], { display_name: "yop" });
|
||||
}
|
||||
if (args.method === "read") {
|
||||
assert.deepEqual(args.args[0], [1]);
|
||||
}
|
||||
},
|
||||
arch: `
|
||||
<form>
|
||||
<widget name="attach_document" action="my_action" string="Attach document"/>
|
||||
<field name="display_name" required="1"/>
|
||||
</form>`,
|
||||
});
|
||||
assert.verifySteps(["get_views", "read"]);
|
||||
|
||||
await editInput(target, "[name='display_name'] input", "yop");
|
||||
await click(target, ".o_attach_document");
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
await nextTick();
|
||||
assert.verifySteps(["write", "read", "post", "my_action", "read"]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"attach document widget calls action with attachment ids on a new record",
|
||||
async function (assert) {
|
||||
let fileInput;
|
||||
patchWithCleanup(AttachDocumentWidget.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
fileInput = this.fileInput;
|
||||
},
|
||||
});
|
||||
|
||||
serviceRegistry.add("http", {
|
||||
start: () => ({
|
||||
post: (route, params) => {
|
||||
assert.step("post");
|
||||
assert.strictEqual(route, "/web/binary/upload_attachment");
|
||||
assert.strictEqual(params.model, "partner");
|
||||
assert.strictEqual(params.id, 2);
|
||||
return '[{ "id": 5 }, { "id": 2 }]';
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
mockRPC(route, args) {
|
||||
assert.step(args.method);
|
||||
if (args.method === "my_action") {
|
||||
assert.deepEqual(args.model, "partner");
|
||||
assert.deepEqual(args.args, [2]);
|
||||
assert.deepEqual(args.kwargs.attachment_ids, [5, 2]);
|
||||
return true;
|
||||
}
|
||||
if (args.method === "create") {
|
||||
assert.deepEqual(args.args[0], { display_name: "yop" });
|
||||
}
|
||||
if (args.method === "read") {
|
||||
assert.deepEqual(args.args[0], [2]);
|
||||
}
|
||||
},
|
||||
arch: `
|
||||
<form>
|
||||
<widget name="attach_document" action="my_action" string="Attach document"/>
|
||||
<field name="display_name" required="1"/>
|
||||
</form>`,
|
||||
});
|
||||
assert.verifySteps(["get_views", "onchange"]);
|
||||
|
||||
await editInput(target, "[name='display_name'] input", "yop");
|
||||
await click(target, ".o_attach_document");
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
await nextTick();
|
||||
assert.verifySteps(["create", "read", "post", "my_action", "read"]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/** @odoo-module **/
|
||||
import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { NameAndSignature } from "@web/core/signature/name_and_signature";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Widgets", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Name", type: "char" },
|
||||
product_id: {
|
||||
string: "Product Name",
|
||||
type: "many2one",
|
||||
relation: "product",
|
||||
},
|
||||
__last_update: { type: "datetime" },
|
||||
sign: { string: "Signature", type: "binary" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "Pop's Chock'lit",
|
||||
product_id: 7,
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 7,
|
||||
display_name: "Veggie Burger",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Signature Widget");
|
||||
|
||||
QUnit.test("Signature widget renders a Sign button", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
this._super.apply(this, arguments);
|
||||
assert.strictEqual(this.props.signature.name, "");
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<form>
|
||||
<header>
|
||||
<widget name="signature" string="Sign"/>
|
||||
</header>
|
||||
</form>`,
|
||||
mockRPC: async (route, args) => {
|
||||
if (route === "/web/sign/get_fonts/") {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector("button.o_sign_button"),
|
||||
"btn-secondary",
|
||||
"The button must have the 'btn-secondary' class as \"highlight=0\""
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_widget_signature button.o_sign_button",
|
||||
"Should have a signature widget button"
|
||||
);
|
||||
assert.containsNone(target, ".modal-dialog", "Should not have any modal");
|
||||
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await click(target, ".o_widget_signature button.o_sign_button");
|
||||
assert.containsOnce(target, ".modal-dialog", "Should have one modal opened");
|
||||
});
|
||||
|
||||
QUnit.test("Signature widget: full_name option", async function (assert) {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
setup() {
|
||||
this._super.apply(this, arguments);
|
||||
assert.step(this.props.signature.name);
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<form>
|
||||
<header>
|
||||
<widget name="signature" string="Sign" full_name="display_name"/>
|
||||
</header>
|
||||
<field name="display_name"/>
|
||||
</form>`,
|
||||
mockRPC: async (route) => {
|
||||
if (route === "/web/sign/get_fonts/") {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await click(target, "span.o_sign_label");
|
||||
assert.containsOnce(target, ".modal .modal-body a.o_web_sign_auto_button");
|
||||
assert.verifySteps(["Pop's Chock'lit"]);
|
||||
});
|
||||
|
||||
QUnit.test("Signature widget: highlight option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<form>
|
||||
<header>
|
||||
<widget name="signature" string="Sign" highlight="1"/>
|
||||
</header>
|
||||
</form>`,
|
||||
mockRPC: async (route, args) => {
|
||||
if (route === "/web/sign/get_fonts/") {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector("button.o_sign_button"),
|
||||
"btn-primary",
|
||||
"The button must have the 'btn-primary' class as \"highlight=1\""
|
||||
);
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await click(target, ".o_widget_signature button.o_sign_button");
|
||||
assert.containsNone(target, ".modal .modal-body a.o_web_sign_auto_button");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeFakeLocalizationService } from "../../helpers/mock_services";
|
||||
import { getFixture, click, clickSave } from "../../helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "../helpers";
|
||||
|
||||
let serverData;
|
||||
let fixture;
|
||||
|
||||
QUnit.module("Widgets", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
fixture = getFixture();
|
||||
setupViewRegistries();
|
||||
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
id: { type: "integer", string: "ID" },
|
||||
sun: { type: "boolean", string: "Sun" },
|
||||
mon: { type: "boolean", string: "Mon" },
|
||||
tue: { type: "boolean", string: "Tue" },
|
||||
wed: { type: "boolean", string: "Wed" },
|
||||
thu: { type: "boolean", string: "Thu" },
|
||||
fri: { type: "boolean", string: "Fri" },
|
||||
sat: { type: "boolean", string: "Sat" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
sun: false,
|
||||
mon: false,
|
||||
tue: false,
|
||||
wed: false,
|
||||
thu: false,
|
||||
fri: false,
|
||||
sat: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
QUnit.module("WeekDays");
|
||||
|
||||
QUnit.test("simple week recurrence widget", async (assert) => {
|
||||
assert.expect(13);
|
||||
|
||||
let writeCall = 0;
|
||||
registry.category("services", makeFakeLocalizationService({ weekStart: 1 }));
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<widget name="week_days" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
mockRPC(route, { args, method }) {
|
||||
if (method === "write") {
|
||||
writeCall++;
|
||||
if (writeCall === 1) {
|
||||
assert.ok(args[1].sun, "value of sunday should be true");
|
||||
}
|
||||
if (writeCall === 2) {
|
||||
assert.notOk(args[1].sun, "value of sunday should be false");
|
||||
assert.ok(args[1].mon, "value of monday should be true");
|
||||
assert.ok(args[1].tue, "value of tuesday should be true");
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const labelsTexts = [...fixture.querySelectorAll(".o_recurrent_weekday_label")].map((el) =>
|
||||
el.innerText.trim()
|
||||
);
|
||||
assert.deepEqual(
|
||||
labelsTexts,
|
||||
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||
"labels should be short week names"
|
||||
);
|
||||
|
||||
assert.containsNone(
|
||||
fixture,
|
||||
".form-check input:disabled",
|
||||
"all inputs should be enabled in readonly mode"
|
||||
);
|
||||
|
||||
await click(fixture.querySelector("td:nth-child(7) input"));
|
||||
assert.ok(
|
||||
fixture.querySelector("td:nth-child(7) input").checked,
|
||||
"sunday checkbox should be checked"
|
||||
);
|
||||
await clickSave(fixture);
|
||||
|
||||
await click(fixture.querySelector("td:nth-child(1) input"));
|
||||
assert.ok(
|
||||
fixture.querySelector("td:nth-child(1) input").checked,
|
||||
"monday checkbox should be checked"
|
||||
);
|
||||
|
||||
await click(fixture.querySelector("td:nth-child(2) input"));
|
||||
assert.ok(
|
||||
fixture.querySelector("td:nth-child(2) input").checked,
|
||||
"tuesday checkbox should be checked"
|
||||
);
|
||||
|
||||
// uncheck Sunday checkbox and check write call
|
||||
await click(fixture.querySelector("td:nth-child(7) input"));
|
||||
assert.notOk(
|
||||
fixture.querySelector("td:nth-child(7) input").checked,
|
||||
"sunday checkbox should be unchecked"
|
||||
);
|
||||
|
||||
await clickSave(fixture);
|
||||
assert.notOk(
|
||||
fixture.querySelector("td:nth-child(7) input").checked,
|
||||
"sunday checkbox should be unchecked"
|
||||
);
|
||||
assert.ok(
|
||||
fixture.querySelector("td:nth-child(1) input").checked,
|
||||
"monday checkbox should be checked"
|
||||
);
|
||||
assert.ok(
|
||||
fixture.querySelector("td:nth-child(2) input").checked,
|
||||
"tuesday checkbox should be checked"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"week recurrence widget show week start as per language configuration",
|
||||
async (assert) => {
|
||||
registry.category("services", makeFakeLocalizationService({ weekStart: 5 }));
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<widget name="week_days" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
const labels = [...fixture.querySelectorAll(".o_recurrent_weekday_label")].map((el) =>
|
||||
el.textContent.trim()
|
||||
);
|
||||
assert.deepEqual(
|
||||
labels,
|
||||
["Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu"],
|
||||
"labels should be short week names"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue