Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -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);
});

View file

@ -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"]);
});
});

View file

@ -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]);
}
});
});

View file

@ -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");
});
});

View file

@ -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([]);
});
});

View file

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

View file

@ -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"]);
});
});

View file

@ -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"]);
});
});

View file

@ -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);
}

View file

@ -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"]]']);
}
);
});

View file

@ -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');
});
});

View file

@ -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"
);
}
);
});

View file

@ -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,
""
);
})
});

View file

@ -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"
);
});
});

View file

@ -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"
);
});
});

View file

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

View file

@ -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"
);
});
});

View file

@ -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)"
);
});
});

View file

@ -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",
]);
});
});

View file

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

View file

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

View file

@ -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"
);
});
});

View file

@ -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."
);
});
});

View file

@ -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"
);
});
});

View file

@ -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);
});
});

View file

@ -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"
);
});
});

View file

@ -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);
}
);
});

View file

@ -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");
});
});

View file

@ -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"
);
});
});

View file

@ -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
});
});

View file

@ -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>");
});
});

View file

@ -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"));
}
);
});

View file

@ -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"
);
});
});

View file

@ -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");
});
});

View file

@ -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"
);
}
);
});

View file

@ -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'"
);
});
});

View file

@ -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");
});
});

View file

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

View file

@ -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"
);
});
});

View file

@ -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");
});
});

View file

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

View file

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

View file

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

View file

@ -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);
});
});

View file

@ -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");
});
});

View file

@ -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();
// });
});

View file

@ -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");
});
});

View file

@ -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");
});
});

View file

@ -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([]);
});
});

View file

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

View file

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

View file

@ -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"
);
}
);
});

View file

@ -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");
});
});

View file

@ -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"]);
});
});

View file

@ -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"
);
});
});

View file

@ -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([]);
}
);
});

View file

@ -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);
}
);
});

View file

@ -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"
);
});
});

View file

@ -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"
);
}
);
});

View file

@ -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"
);
});
});

View file

@ -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="&quot;&quot;" 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; scope.className || &quot;&quot; }}" 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) =&gt; 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 &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }} {{ 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="{&quot;invisible&quot;: true}" />
<div class="visible3" modifiers="{&quot;invisible&quot;: false}"/>
<div modifiers="{&quot;invisible&quot;: [[&quot;display_name&quot;, &quot;=&quot;, &quot;take&quot;]]}"/>
</form>`;
const expected = /*xml*/ `
<div class="visible3" />
<div t-if="!evalDomainFromRecord(props.record,[[&quot;display_name&quot;,&quot;=&quot;,&quot;take&quot;]])" />
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
});
QUnit.test("compile invisible containing string as domain", async (assert) => {
const arch = /*xml*/ `
<form>
<field name="display_name" modifiers="{&quot;invisible&quot;: true}" />
<div class="visible3" modifiers="{&quot;invisible&quot;: false}"/>
<div modifiers="{&quot;invisible&quot;: &quot;[['display_name', '=', 'take']]&quot;}"/>
</form>`;
const expected = /*xml*/ `
<div class="visible3" />
<div t-if="!evalDomainFromRecord(props.record,&quot;[['display_name','=','take']]&quot;)" />
`;
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="{&quot;invisible&quot;: &quot;[['display_name', '=', uid]]&quot;}">
<field name="charfield"/>
</div>
<field name="display_name" attrs="{'readonly': &quot;[['display_name', '=', uid]]&quot;}"/>
</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="{&quot;invisible&quot;: [[&quot;field&quot;, &quot;=&quot;, &quot;value&quot;]]}" />`;
const expected = `<t t-translation="off"><div class="myNode" t-if="( myCondition or myOtherCondition ) and !evalDomainFromRecord(props.record,[[&quot;field&quot;,&quot;=&quot;,&quot;value&quot;]])" t-ref="compiled_view_root"/></t>`;
assert.areEquivalent(compileTemplate(arch), expected);
});
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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 });
}

View file

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

View 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");
});
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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");
});
});

View file

@ -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) { ... });
}
);

View file

@ -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");
});
});

View file

@ -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"])
});
});

View file

@ -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"]);
});
});

View file

@ -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"]);
});
});

File diff suppressed because it is too large Load diff

View file

@ -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"]);
}
);
});

View file

@ -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");
});
});

View file

@ -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"
);
}
);
});