vanilla 17.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:47:08 +02:00
parent d72e748793
commit a9bcec8e91
1986 changed files with 1613876 additions and 568976 deletions

View file

@ -34,10 +34,11 @@ QUnit.test("defaults", (assert) => {
filtersInfo: {},
formViewId: false,
hasEditDialog: false,
hasQuickCreate: true,
quickCreate: true,
quickCreateViewId: null,
isDateHidden: false,
isTimeHidden: false,
popoverFields: {},
popoverFieldNodes: {},
scale: "week",
scales: ["day", "week", "month", "year"],
showUnusualDays: false,
@ -87,15 +88,33 @@ QUnit.test("hasEditDialog", (assert) => {
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("quickCreate", (assert) => {
check(assert, "quick_create", "", "quickCreate", true);
check(assert, "quick_create", "true", "quickCreate", true);
check(assert, "quick_create", "True", "quickCreate", true);
check(assert, "quick_create", "1", "quickCreate", true);
check(assert, "quick_create", "false", "quickCreate", false);
check(assert, "quick_create", "False", "quickCreate", false);
check(assert, "quick_create", "0", "quickCreate", false);
check(assert, "quick_create", "12", "quickCreate", true);
});
QUnit.test("quickCreateViewId", (assert) => {
let arch = parseArch(
`<calendar date_start="start_date" quick_create="0" quick_create_view_id="12" />`
);
assert.strictEqual(arch.quickCreate, false);
assert.strictEqual(arch.quickCreateViewId, null);
arch = parseArch(
`<calendar date_start="start_date" quick_create="1" quick_create_view_id="12" />`
);
assert.strictEqual(arch.quickCreate, true);
assert.strictEqual(arch.quickCreateViewId, 12);
arch = parseArch(`<calendar date_start="start_date" quick_create="1"/>`);
assert.strictEqual(arch.quickCreate, true);
assert.strictEqual(arch.quickCreateViewId, null);
});
QUnit.test("isDateHidden", (assert) => {

View file

@ -62,7 +62,7 @@ QUnit.module("CalendarView - CommonPopover", ({ beforeEach }) => {
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "July 16, 2021 (All day)");
assert.strictEqual(dateTimeLabels, "July 16, 2021");
});
QUnit.test("date duration: is all day and two days duration", async (assert) => {
@ -77,7 +77,7 @@ QUnit.module("CalendarView - CommonPopover", ({ beforeEach }) => {
});
const dateTimeGroup = target.querySelector(`.list-group`);
const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim();
assert.strictEqual(dateTimeLabels, "July 16-17, 2021 (2 days)");
assert.strictEqual(dateTimeLabels, "July 16-17, 2021 2 days");
});
QUnit.test("time duration: 1 hour diff", async (assert) => {
@ -152,10 +152,7 @@ QUnit.module("CalendarView - CommonPopover", ({ beforeEach }) => {
});
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)"
);
assert.strictEqual(dateTimeLabels, "July 16, 2021 08:00 - 11:15 (3 hours, 15 minutes)");
});
QUnit.test("isTimeHidden is true", async (assert) => {
@ -177,10 +174,7 @@ QUnit.module("CalendarView - CommonPopover", ({ beforeEach }) => {
});
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)"
);
assert.strictEqual(dateTimeLabels, "July 16, 2021 08:00 - 11:15 (3 hours, 15 minutes)");
});
QUnit.test("canDelete is true", async (assert) => {

View file

@ -67,7 +67,9 @@ QUnit.module("CalendarView - CommonRenderer", ({ beforeEach }) => {
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");
const dayHeader = target.querySelector(".fc-day-header");
assert.strictEqual(dayHeader.querySelector(".o_cw_day_name").textContent, "Friday");
assert.strictEqual(dayHeader.querySelector(".o_cw_day_number").textContent, "16");
});
QUnit.test("Day: click all day slot", async (assert) => {
@ -152,11 +154,35 @@ QUnit.module("CalendarView - CommonRenderer", ({ beforeEach }) => {
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 dateNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const dates = ["11", "12", "13", "14", "15", "16", "17"];
const els = target.querySelectorAll(".fc-day-header");
for (let i = 0; i < els.length; i++) {
assert.strictEqual(els[i].textContent, dates[i]);
assert.strictEqual(els[i].querySelector(".o_cw_day_name").textContent, dateNames[i]);
assert.strictEqual(els[i].querySelector(".o_cw_day_number").textContent, dates[i]);
}
});
QUnit.test("Day: automatically scroll to 6am", async (assert) => {
// Make calendar scrollable
target.style.height = "500px";
await start({ model: { scale: "day" } });
const containerDimensions = target.querySelector(".fc-scroller").getBoundingClientRect();
const dayStartDimensions = target
.querySelector('tr[data-time="06:00:00"')
.getBoundingClientRect();
assert.ok(Math.abs(dayStartDimensions.y - containerDimensions.y) <= 2);
});
QUnit.test("Week: automatically scroll to 6am", async (assert) => {
// Make calendar scrollable
target.style.height = "500px";
await start({ model: { scale: "week" } });
const containerDimensions = target.querySelector(".fc-scroller").getBoundingClientRect();
const dayStartDimensions = target
.querySelector('tr[data-time="06:00:00"')
.getBoundingClientRect();
assert.ok(Math.abs(dayStartDimensions.y - containerDimensions.y) <= 2);
});
});

View file

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

View file

@ -1,7 +1,7 @@
/** @odoo-module **/
import { CalendarFilterPanel } from "@web/views/calendar/filter_panel/calendar_filter_panel";
import { click, getFixture, triggerEvent } from "../../helpers/utils";
import { click, getFixture } from "../../helpers/utils";
import { makeEnv, makeFakeModel, mountComponent } from "./helpers";
let target;
@ -47,13 +47,13 @@ QUnit.module("CalendarView - FilterPanel", ({ beforeEach }) => {
assert.containsN(sections[0], ".o_calendar_filter_item", 4);
assert.strictEqual(
sections[0].textContent.trim(),
"AttendeesMitchell AdminMarc DemoBrandon FreemanEverybody's calendar"
"AttendeesMitchell AdminBrandon FreemanMarc DemoEverybody'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");
assert.strictEqual(sections[1].textContent.trim(), "UsersBrandon FreemanMarc Demo");
});
QUnit.test("section can collapse", async (assert) => {
@ -101,12 +101,12 @@ QUnit.module("CalendarView - FilterPanel", ({ beforeEach }) => {
assert.hasAttrValue(
filters[1].querySelector(".o_cw_filter_avatar"),
"data-src",
"/web/image/res.partner/6/avatar_128"
"/web/image/res.partner/4/avatar_128"
);
assert.hasAttrValue(
filters[2].querySelector(".o_cw_filter_avatar"),
"data-src",
"/web/image/res.partner/4/avatar_128"
"/web/image/res.partner/6/avatar_128"
);
});
@ -148,7 +148,7 @@ QUnit.module("CalendarView - FilterPanel", ({ beforeEach }) => {
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"]);
assert.verifySteps(["partner_ids 1", "partner_ids 2"]);
});
QUnit.test("click on filter", async (assert) => {
@ -172,42 +172,10 @@ QUnit.module("CalendarView - FilterPanel", ({ beforeEach }) => {
await click(filters[3], "input");
assert.verifySteps([
"partner_ids 3 false",
"partner_ids 6 true",
"partner_ids 4 false",
"partner_ids 6 true",
"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

@ -90,18 +90,18 @@ QUnit.module("CalendarView - YearPopover", ({ beforeEach }) => {
QUnit.test("group records", async (assert) => {
await start({});
assert.containsN(target, ".o_cw_body > div", 5);
assert.containsN(target, ".o_cw_body > a", 5);
assert.containsN(target, ".o_cw_body > div", 4);
assert.containsN(target, ".o_cw_body > a", 1);
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(sectionTitles[0].textContent.trim(), "July 16, 2021R114:00R2");
assert.strictEqual(sectionTitles[1].textContent.trim(), "July 13-17, 2021R4");
assert.strictEqual(sectionTitles[2].textContent.trim(), "July 15-17, 2021R3");
assert.strictEqual(sectionTitles[3].textContent.trim(), "July 15-19, 2021R5");
assert.strictEqual(
target.querySelector(".o_cw_body").textContent.trim(),
"July 16, 2021R114:00 R2July 13-17, 2021R4July 15-17, 2021R3July 15-19, 2021R5 Create"
"July 16, 2021R114:00R2July 13-17, 2021R4July 15-17, 2021R3July 15-19, 2021R5 Create"
);
});
@ -113,8 +113,8 @@ QUnit.module("CalendarView - YearPopover", ({ beforeEach }) => {
editRecord: () => assert.step("edit"),
},
});
assert.containsOnce(target, ".o_cw_body > a");
await click(target, ".o_cw_body > a");
assert.containsOnce(target, ".o_cw_body a.o_cw_popover_link");
await click(target, ".o_cw_body a.o_cw_popover_link");
assert.verifySteps(["edit"]);
});
});

View file

@ -34,18 +34,18 @@ QUnit.module("CalendarView - YearRenderer", ({ beforeEach }) => {
// check "title format"
assert.strictEqual(monthHeaders.length, 12);
const monthTitles = [
"Jan 2021",
"Feb 2021",
"Mar 2021",
"Apr 2021",
"January 2021",
"February 2021",
"March 2021",
"April 2021",
"May 2021",
"Jun 2021",
"Jul 2021",
"Aug 2021",
"Sep 2021",
"Oct 2021",
"Nov 2021",
"Dec 2021",
"June 2021",
"July 2021",
"August 2021",
"September 2021",
"October 2021",
"November 2021",
"December 2021",
];
for (let i = 0; i < 12; i++) {
assert.strictEqual(monthHeaders[i].textContent, monthTitles[i]);
@ -137,18 +137,24 @@ QUnit.module("CalendarView - YearRenderer", ({ beforeEach }) => {
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
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({});
await start({});
const dayHeaders = target
.querySelector(".fc-month-container")
.querySelectorAll(".fc-day-header");
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"]);
});
assert.deepEqual(
[...dayHeaders].map((el) => el.textContent),
["S", "M", "T", "W", "T", "F", "S"]
);
}
);
});

View file

@ -1,7 +1,9 @@
/** @odoo-module **/
import { uiService } from "@web/core/ui/ui_service";
import { createElement } from "@web/core/utils/xml";
import { registry } from "@web/core/registry";
import { Field } from "@web/views/fields/field";
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
import { click, getFixture, mount, nextTick, triggerEvent } from "../../helpers/utils";
import { setupViewRegistries } from "@web/../tests/views/helpers";
@ -206,6 +208,8 @@ export const FAKE_FIELDS = {
};
function makeFakeModelState() {
const fakeFieldNode = createElement("field", { name: "name" });
const fakeModels = { event: FAKE_FIELDS };
return {
canCreate: true,
canDelete: true,
@ -226,9 +230,18 @@ function makeFakeModelState() {
isTimeHidden: false,
hasAllDaySlot: true,
hasEditDialog: false,
hasQuickCreate: false,
popoverFields: {
name: { rawAttrs: {}, options: {} },
quickCreate: false,
popoverFieldNodes: {
name: Field.parseFieldNode(fakeFieldNode, fakeModels, "event", "calendar"),
},
activeFields: {
name: {
context: "{}",
invisible: false,
readonly: false,
required: false,
onChange: false,
},
},
rangeEnd: makeFakeDate().endOf("month"),
rangeStart: makeFakeDate().startOf("month"),
@ -263,16 +276,15 @@ async function scrollTo(el, scrollParam) {
}
export function findPickedDate(target) {
return target.querySelector(".ui-datepicker-current-day");
return target.querySelector(".o_datetime_picker .o_selected");
}
export async function pickDate(target, date) {
const [year, month, day] = date.split("-");
const iMonth = parseInt(month, 10) - 1;
const day = date.split("-")[2];
const iDay = parseInt(day, 10) - 1;
const el = target.querySelectorAll(
`.ui-datepicker-calendar td[data-year="${year}"][data-month="${iMonth}"]`
)[iDay];
const el = target.querySelectorAll(`.o_datetime_picker .o_date_item_cell:not(.o_out_of_range)`)[
iDay
];
el.scrollIntoView();
await click(el);
}
@ -497,9 +509,40 @@ export async function resizeEventToTime(target, eventId, dateTime) {
await nextTick();
}
export async function resizeEventToDate(target, eventId, date) {
const event = findEvent(target, eventId);
const slot = findAllDaySlot(target, date);
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,
y: resizerRect.y + resizerRect.height / 2,
};
await triggerEventForCalendar(resizer, "mousedown", resizerPos);
// Find slot position
await scrollTo(slot, false);
const slotRect = slot.getBoundingClientRect();
const toPos = {
x: slotRect.x + slotRect.width / 2,
y: slotRect.y + slotRect.height / 2,
};
await triggerEventForCalendar(slot, "mousemove", toPos);
await triggerEventForCalendar(slot, "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 click(target, `.o_view_scale_selector .scale_button_selection`);
await click(target, `.o_view_scale_selector .o_scale_button_${scale}`);
await nextTick();
}

View file

@ -1,10 +1,17 @@
/** @odoo-module **/
/* global ace */
import { registry } from "@web/core/registry";
import { getFixture, triggerEvents } from "@web/../tests/helpers/utils";
import {
click,
clickSave,
editInput,
getFixture,
nextTick,
triggerEvent,
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;
@ -33,7 +40,6 @@ QUnit.module("Fields", (hooks) => {
};
setupViewRegistries();
registry.category("services").add("cookie", fakeCookieService);
});
QUnit.module("AceEditorField");
@ -46,7 +52,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<field name="foo" widget="ace" />
<field name="foo" widget="code" />
</form>`,
});
@ -57,7 +63,96 @@ QUnit.module("Fields", (hooks) => {
"should have rendered something with ace editor"
);
assert.ok(target.querySelector(".o_field_ace").textContent.includes("yop"));
assert.ok(target.querySelector(".o_field_code").textContent.includes("yop"));
});
QUnit.test("AceEditorField mark as dirty as soon at onchange", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="foo" widget="code" />
</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_code").textContent.includes("yop"));
// edit the foo field
const aceEditor = target.querySelector(".ace_editor");
ace.edit(aceEditor).setValue("blip");
await nextTick();
assert.containsOnce(target, ".o_form_status_indicator_buttons");
assert.doesNotHaveClass(
target.querySelector(".o_form_status_indicator_buttons"),
"invisible"
);
// revert edition
ace.edit(aceEditor).setValue("yop");
await nextTick();
assert.containsOnce(target, ".o_form_status_indicator_buttons");
assert.hasClass(target.querySelector(".o_form_status_indicator_buttons"), "invisible");
});
QUnit.test("AceEditorField on html fields works", async function (assert) {
assert.expect(8);
serverData.models.partner.fields.htmlField = {
string: "HTML Field",
type: "html",
};
serverData.models.partner.records.push({
id: 3,
htmlField: "<p>My little HTML Test</p>",
});
serverData.models.partner.onchanges = { htmlField: function () {} };
await makeView({
type: "form",
resModel: "partner",
resId: 3,
serverData,
arch: `
<form>
<field name="foo"/>
<field name="htmlField" widget="code" />
</form>`,
mockRPC(route, args) {
if (args.method) {
assert.step(args.method);
if (args.method === "web_save") {
assert.deepEqual(args.args[1], { foo: "DEF" });
}
if (args.method === "onchange") {
throw new Error("Should not call onchange, htmlField wasn't changed");
}
}
},
});
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_code").textContent.includes("My little HTML Test")
);
// Modify foo and save
await editInput(target, ".o_field_widget[name=foo] textarea", "DEF");
await clickSave(target);
assert.verifySteps(["get_views", "web_read", "web_save"]);
});
QUnit.test("AceEditorField doesn't crash when editing", async (assert) => {
@ -69,7 +164,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="display_name" />
<field name="foo" widget="ace" />
<field name="foo" widget="code" />
</form>`,
});
@ -86,15 +181,17 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: /* xml */ `
<form>
<field name="foo" widget="ace" />
<field name="foo" widget="code" />
</form>`,
});
assert.ok(target.querySelector(".o_field_ace").textContent.includes("yop"));
assert.ok(target.querySelector(".o_field_code").textContent.includes("yop"));
await pagerNext(target);
await nextTick();
await nextTick();
assert.ok(target.querySelector(".o_field_ace").textContent.includes("blip"));
assert.ok(target.querySelector(".o_field_code").textContent.includes("blip"));
});
QUnit.test(
@ -120,9 +217,74 @@ QUnit.module("Fields", (hooks) => {
},
});
assert.verifySteps(["get_views: []", 'read: [[1],["foo","display_name"]]']);
assert.verifySteps(["get_views: []", "web_read: [[1]]"]);
await pagerNext(target);
assert.verifySteps(['read: [[2],["foo","display_name"]]']);
assert.verifySteps(["web_read: [[2]]"]);
}
);
QUnit.test("AceEditorField only trigger onchanges when blurred", async (assert) => {
serverData.models.partner.onchanges = {
foo: (obj) => {},
};
serverData.models.partner.records.forEach((rec) => {
rec.foo = false;
});
await makeView({
type: "form",
resModel: "partner",
resId: 1,
resIds: [1, 2],
serverData,
arch: `<form>
<field name="display_name" />
<field name="foo" widget="code" />
</form>`,
mockRPC(route, args) {
if (args.method) {
assert.step(`${args.method}: ${JSON.stringify(args.args)}`);
}
},
});
assert.verifySteps(["get_views: []", "web_read: [[1]]"]);
const textArea = target.querySelector(".ace_editor textarea");
await click(textArea);
textArea.focus();
textArea.value = "a";
await triggerEvent(textArea, null, "input", {});
await triggerEvents(textArea, null, ["blur"]);
assert.verifySteps(['onchange: [[1],{"foo":"a"},["foo"],{"display_name":{},"foo":{}}]']);
await click(target, ".o_form_button_save");
assert.verifySteps(['web_save: [[1],{"foo":"a"}]']);
});
QUnit.test("Save and Discard buttons will become invisible after saving", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="display_name" />
<field name="foo" widget="code" />
</form>`,
});
const textArea = target.querySelector(".ace_editor textarea");
await click(textArea);
textArea.focus();
textArea.value = "a";
await triggerEvent(textArea, null, "input", {});
assert.containsOnce(target, ".o_form_status_indicator_buttons");
assert.doesNotHaveClass(
target.querySelector(".o_form_status_indicator_buttons"),
"invisible"
);
await click(target, ".o_form_button_save");
assert.containsOnce(target, ".o_form_status_indicator_buttons");
assert.hasClass(target.querySelector(".o_form_status_indicator_buttons"), "invisible");
});
});

View file

@ -91,7 +91,7 @@ QUnit.module("Fields", (hooks) => {
await click(target, ".o_form_button_save");
var newRecord = _.last(serverData.models.partner.records);
var newRecord = serverData.models.partner.records.at(-1);
assert.strictEqual(newRecord.product_id, 37, "should have saved record with correct value");
});
@ -122,7 +122,7 @@ QUnit.module("Fields", (hooks) => {
await click(target.querySelector(".o_form_button_save"));
var newRecord = _.last(serverData.models.partner.records);
var newRecord = serverData.models.partner.records.at(-1);
assert.strictEqual(
newRecord.color,
"black",
@ -151,12 +151,18 @@ QUnit.module("Fields", (hooks) => {
QUnit.test(
"BadgeSelectionField widget on a selection unchecking selected value",
async function (assert) {
async (assert) => {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: '<form><field name="color" widget="selection_badge"/></form>',
mockRPC(_, { method, model, args }) {
if (method === "web_save" && model === "partner") {
assert.step("web_save");
assert.deepEqual(args[1], { color: false });
}
},
});
assert.containsOnce(
@ -165,18 +171,20 @@ QUnit.module("Fields", (hooks) => {
"should have rendered outer div"
);
assert.containsN(target, "span.o_selection_badge", 2, "should have 2 possible choices");
assert.containsN(target, "span.o_selection_badge.active", 1, "one is active");
assert.strictEqual(
target.querySelector("span.o_selection_badge").textContent,
target.querySelector("span.o_selection_badge.active").textContent,
"Red",
"one of them should be Red"
"the active one should be Red"
);
// click again on red option
await click(target.querySelector("span.o_selection_badge.active"));
// click again on red option and save to update the server data
await click(target, "span.o_selection_badge.active");
assert.verifySteps([]);
await click(target, ".o_form_button_save");
assert.verifySteps(["web_save"], "should have created a new record");
await click(target.querySelector(".o_form_button_save"));
var newRecord = _.last(serverData.models.partner.records);
const newRecord = serverData.models.partner.records.at(-1);
assert.strictEqual(
newRecord.color,
false,
@ -184,4 +192,49 @@ QUnit.module("Fields", (hooks) => {
);
}
);
QUnit.test(
"BadgeSelectionField widget on a selection unchecking selected value (required field)",
async (assert) => {
serverData.models.partner.fields.color.required = true;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: '<form><field name="color" widget="selection_badge"/></form>',
mockRPC(_, { method, model, args }) {
if (method === "web_save" && model === "partner") {
assert.step("web_save");
assert.deepEqual(args[1], { color: "red" });
}
},
});
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.containsN(target, "span.o_selection_badge.active", 1, "one is active");
assert.strictEqual(
target.querySelector("span.o_selection_badge.active").textContent,
"Red",
"the active one should be Red"
);
// click again on red option and save to update the server data
await click(target, "span.o_selection_badge.active");
assert.verifySteps([]);
await click(target, ".o_form_button_save");
assert.verifySteps(["web_save"], "should have created a new record");
const newRecord = serverData.models.partner.records.at(-1);
assert.strictEqual(
newRecord.color,
"red",
"the new value should be red"
);
}
);
});

View file

@ -1,6 +1,7 @@
/** @odoo-module **/
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { makeServerError } from "@web/../tests/helpers/mock_server";
import { makeMockXHR } from "@web/../tests/helpers/mock_services";
import {
click,
@ -12,13 +13,16 @@ import {
} 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 { errorService } from "@web/core/errors/error_service";
import { registry } from "@web/core/registry";
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";
const serviceRegistry = registry.category("services");
let serverData;
let target;
@ -94,13 +98,7 @@ QUnit.module("Fields", (hooks) => {
}
const MockXHR = makeMockXHR("", send);
patchWithCleanup(
browser,
{
XMLHttpRequest: MockXHR,
},
{ pure: true }
);
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
await makeView({
serverData,
@ -167,13 +165,7 @@ QUnit.module("Fields", (hooks) => {
}
const MockXHR = makeMockXHR("", send);
patchWithCleanup(
browser,
{
XMLHttpRequest: MockXHR,
},
{ pure: true }
);
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
await makeView({
serverData,
@ -308,6 +300,36 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("icons are displayed exactly once", async (assert) => {
assert.expect(3);
patchWithCleanup(odoo, { debug: true });
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<field name="document" filename="foo"/>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
".o_field_binary .o_select_file_button",
"only one select file icon should be visible"
);
assert.containsOnce(
target,
".o_field_binary .o_download_file_button",
"only one download file icon should be visible"
);
assert.containsOnce(
target,
".o_field_binary .o_clear_file_button",
"only one clear file icon should be visible"
);
});
QUnit.test(
"binary fields input value is empty when clearing after uploading",
async function (assert) {
@ -380,13 +402,7 @@ QUnit.module("Fields", (hooks) => {
}
const MockXHR = makeMockXHR("", download);
patchWithCleanup(
browser,
{
XMLHttpRequest: MockXHR,
},
{ pure: true }
);
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
serverData.models.partner.onchanges = {
product_id: function (obj) {
@ -408,7 +424,10 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
});
await click(target, ".o_form_button_create");
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .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")
@ -429,7 +448,7 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test("Binary field in list view", async function (assert) {
QUnit.test("BinaryField in list view (formatter)", async function (assert) {
serverData.models.partner.records[0].document = BINARY_FILE;
await makeView({
@ -438,7 +457,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<tree>
<field name="document" filename="yooo"/>
<field name="document"/>
</tree>`,
resId: 1,
});
@ -449,7 +468,28 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("Binary field for new record has no download button", async function (assert) {
QUnit.test("BinaryField in list view with filename", async function (assert) {
serverData.models.partner.records[0].document = BINARY_FILE;
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree>
<field name="document" filename="foo" widget="binary"/>
<field name="foo"/>
</tree>`,
resId: 1,
});
assert.strictEqual(
target.querySelector(".o_data_row .o_data_cell").textContent,
"coucou.txt"
);
});
QUnit.test("BinaryField for new record has no download button", async function (assert) {
serverData.models.partner.fields.document.default = BINARY_FILE;
await makeView({
serverData,
@ -466,8 +506,10 @@ QUnit.module("Fields", (hooks) => {
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");
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,
@ -528,14 +570,13 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test('isUploading state should be set to false after upload', async function(assert) {
assert.expect(1);
QUnit.test("isUploading state should be set to false after upload", async function (assert) {
serviceRegistry.add("error", errorService);
serverData.models.partner.onchanges = {
document: function (obj) {
if (obj.document) {
const error = new RPCError();
error.exceptionName = "odoo.exceptions.ValidationError";
throw error;
throw makeServerError({ type: "ValidationError" });
}
},
};
@ -552,16 +593,19 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_field_binary .o_input_file", file);
assert.equal(
target.querySelector(".o_select_file_button").innerText,
"UPLOAD YOUR FILE",
"Upload your file",
"displayed value should be upload your file"
);
assert.containsOnce(target, ".o_error_dialog");
});
QUnit.test("doesn't crash if value is not a string", async (assert) => {
serverData.models.partner.records = [{
id: 1,
document: {},
}]
serverData.models.partner.records = [
{
id: 1,
document: {},
},
];
await makeView({
type: "form",
@ -573,9 +617,6 @@ QUnit.module("Fields", (hooks) => {
<field name="document"/>
</form>`,
});
assert.equal(
target.querySelector(".o_field_binary input").value,
""
);
})
assert.equal(target.querySelector(".o_field_binary input").value, "");
});
});

View file

@ -56,7 +56,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_kanban_record .o_field_widget .o_favorite > a").textContent,
" Remove from Favorites",
"Remove from Favorites",
'the label should say "Remove from Favorites"'
);
@ -69,11 +69,94 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_kanban_record .o_field_widget .o_favorite > a").textContent,
" Add to Favorites",
"Add to Favorites",
'the label should say "Add to Favorites"'
);
});
QUnit.test("FavoriteField saves changes by default", 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>`,
mockRPC(route, args) {
if (args.method === "web_save" && args.model === "partner") {
assert.step("save");
assert.deepEqual(args.args, [[1], { bar: false }]);
}
},
domain: [["id", "=", 1]],
});
// 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"'
);
assert.verifySteps(["save"]);
});
QUnit.test(
"FavoriteField does not save if autosave option is set to false",
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" options="{'autosave': False}"/>
</div>
</t>
</templates>
</kanban>`,
mockRPC(route, args) {
if (args.method === "web_save" && args.model === "partner") {
assert.step("save");
}
},
domain: [["id", "=", 1]],
});
// 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"'
);
assert.verifySteps([]);
}
);
QUnit.test("FavoriteField in form view", async function (assert) {
await makeView({
type: "form",
@ -97,7 +180,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_field_widget .o_favorite > a").textContent,
" Remove from Favorites",
"Remove from Favorites",
'the label should say "Remove from Favorites"'
);
@ -110,7 +193,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_field_widget .o_favorite > a").textContent,
" Add to Favorites",
"Add to Favorites",
'the label should say "Add to Favorites"'
);
@ -121,7 +204,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_field_widget .o_favorite > a").textContent,
" Add to Favorites",
"Add to Favorites",
'the label should say "Add to Favorites"'
);
@ -134,7 +217,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_field_widget .o_favorite > a").textContent,
" Remove from Favorites",
"Remove from Favorites",
'the label should say "Remove from Favorites"'
);
@ -147,7 +230,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_field_widget .o_favorite > a").textContent,
" Remove from Favorites",
"Remove from Favorites",
'the label should say "Remove from Favorites"'
);
});
@ -159,7 +242,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<tree editable="bottom">
<field name="bar" widget="boolean_favorite" nolabel="1" />
<field name="bar" widget="boolean_favorite" nolabel="1" options="{'autosave': False}"/>
</tree>`,
});

View file

@ -190,9 +190,10 @@ QUnit.module("Fields", (hooks) => {
"the row is now selected, in edition"
);
assert.ok(
!cell.querySelector(".o-checkbox input:checked").disabled,
"input should now be enabled"
!cell.querySelector(".o-checkbox input:not(:checked)").disabled,
"input should now be enabled and unchecked"
);
await click(cell, ".o-checkbox");
await click(cell);
assert.notOk(
cell.querySelector(".o-checkbox input:checked").disabled,
@ -220,10 +221,9 @@ QUnit.module("Fields", (hooks) => {
"should now have only 3 checked input"
);
// Re-Edit the line and fake-check the checkbox
// Fake-check the checkbox
await click(cell);
await click(cell, ".o-checkbox");
await click(cell, ".o-checkbox");
// Save
await clickSave(target);
@ -273,4 +273,34 @@ QUnit.module("Fields", (hooks) => {
"checkbox should still be disabled"
);
});
QUnit.test("onchange return value before toggle checkbox", async function (assert) {
serverData.models.partner.onchanges = {
bar(obj) {
obj.bar = true;
},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form><field name="bar"/></form>`,
});
assert.containsOnce(
target,
".o_field_boolean input:checked",
"checkbox should still be checked"
);
await click(target, ".o_field_boolean .o-checkbox");
await nextTick();
assert.containsOnce(
target,
".o_field_boolean input:checked",
"checkbox should still be checked"
);
});
});

View file

@ -0,0 +1,83 @@
/** @odoo-module **/
import { getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { click } from "../../helpers/utils";
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 },
barOff: {
string: "Bar Off",
type: "boolean",
default: true,
searchable: true,
},
},
records: [{ id: 1, bar: true, barOff: false }],
},
},
};
setupViewRegistries();
});
QUnit.module("BooleanIconField");
QUnit.test("boolean_icon field in form view", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<label for="bar" string="Bar" />
<field name="bar" widget="boolean_icon" options="{'icon': 'fa-recycle'}" />
<field name="barOff" widget="boolean_icon" options="{'icon': 'fa-trash'}" />
</form>`,
});
assert.containsN(target, ".o_field_boolean_icon button", 2, "icon buttons are visible");
assert.strictEqual(
target.querySelector("[name='bar'] button").dataset.tooltip,
"Bar",
"first button has the label as tooltip"
);
assert.hasClass(
target.querySelector("[name='bar'] button"),
"btn-primary",
"active boolean button has the right class"
);
assert.hasClass(
target.querySelector("[name='bar'] button"),
"fa-recycle",
"first button has the right icon"
);
assert.hasClass(
target.querySelector("[name='barOff'] button"),
"btn-outline-secondary",
"inactive boolean button has the right class"
);
assert.hasClass(
target.querySelector("[name='barOff'] button"),
"fa-trash",
"second button has the right icon"
);
await click(target.querySelector("[name='bar'] button"));
assert.hasClass(
target.querySelector("[name='bar'] button"),
"btn-outline-secondary",
"boolean button is now inactive"
);
});
});

View file

@ -249,13 +249,13 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
if (method === "web_save") {
assert.step("web_save");
}
},
});
await click(target, ".o_field_widget[name='bar'] input");
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
});
QUnit.test("BooleanToggleField - autosave option set to false", async function (assert) {
@ -269,8 +269,8 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
if (method === "web_save") {
assert.step("web_save");
}
},
});

View file

@ -190,7 +190,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, { args, method }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(args[1].foo, false, "the foo value should be false");
}
},
@ -407,60 +407,63 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("translation dialog should close if field is not there anymore", async function (assert) {
// In this test, we simulate the case where the field is removed from the view
// this can happend for example if the user click the back button of the browser.
serverData.models.partner.fields.foo.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: `
QUnit.test(
"translation dialog should close if field is not there anymore",
async function (assert) {
// In this test, we simulate the case where the field is removed from the view
// this can happend for example if the user click the back button of the browser.
serverData.models.partner.fields.foo.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>
<sheet>
<group>
<field name="int_field" />
<field name="foo" attrs="{'invisible': [('int_field', '==', 9)]}"/>
<field name="foo" invisible="int_field == 9"/>
</group>
</sheet>
</form>`,
mockRPC(route, { args, method, model }) {
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return Promise.resolve([
["en_US", "English"],
["fr_BE", "French (Belgium)"],
["es_ES", "Spanish"],
]);
}
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" },
{ lang: "es_ES", source: "yop", value: "yop español" },
],
{ translation_type: "char", translation_show_source: false },
]);
}
},
});
mockRPC(route, { args, method, model }) {
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return Promise.resolve([
["en_US", "English"],
["fr_BE", "French (Belgium)"],
["es_ES", "Spanish"],
]);
}
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" },
{ lang: "es_ES", source: "yop", value: "yop español" },
],
{ translation_type: "char", translation_show_source: false },
]);
}
},
});
assert.hasClass(target.querySelector("[name=foo] input"), "o_field_translate");
assert.hasClass(target.querySelector("[name=foo] input"), "o_field_translate");
await click(target, ".o_field_char .btn.o_field_translate");
assert.containsOnce(target, ".modal", "a translate modal should be visible");
await editInput(target, ".o_field_widget[name=int_field] input", "9");
await nextTick();
assert.containsNone(target, "[name=foo] input", "the field foo should be invisible");
assert.containsNone(target, ".modal", "a translate modal should not be visible");
});
await click(target, ".o_field_char .btn.o_field_translate");
assert.containsOnce(target, ".modal", "a translate modal should be visible");
await editInput(target, ".o_field_widget[name=int_field] input", "9");
await nextTick();
assert.containsNone(target, "[name=foo] input", "the field foo should be invisible");
assert.containsNone(target, ".modal", "a translate modal should not be visible");
}
);
QUnit.test("html field translatable", async function (assert) {
assert.expect(5);
@ -718,6 +721,77 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test(
"input field: change value before pending onchange returns (2)",
async function (assert) {
serverData.models.partner.onchanges = {
int_field(obj) {
if (obj.int_field === 7) {
obj.foo = "blabla";
} else {
obj.foo = "tralala";
}
},
};
const def = makeDeferred();
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<field name="int_field" />
<field name="foo" />
</sheet>
</form>`,
async mockRPC(route, { method }) {
if (method === "onchange") {
await def;
}
},
});
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] input").value,
"yop",
"should contain the correct value"
);
// trigger a deferred onchange
await editInput(target, ".o_field_widget[name='int_field'] input", "7");
// insert a value in input foo
target.querySelector(".o_field_widget[name=foo] input").value = "test";
await triggerEvent(target, ".o_field_widget[name=foo] input", "input");
// complete the onchange
def.resolve();
await nextTick();
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] input").value,
"test",
"The onchange value should not be applied because the input is in edition"
);
// apply the value of the input foo
await triggerEvent(target, ".o_field_widget[name=foo] input", "change");
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] input").value,
"test"
);
// trigger another onchange (not deferred)
await editInput(target, ".o_field_widget[name='int_field'] input", "10");
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] input").value,
"tralala",
"the onchange value should be applied because the input is not in edition"
);
}
);
QUnit.test(
"input field: change value before pending onchange returns (with fieldDebounce)",
async function (assert) {
@ -792,6 +866,30 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test("onchange return value before editing input", async function (assert) {
serverData.models.partner.onchanges = {
foo(obj) {
obj.foo = "yop";
},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="foo" />
</form>`,
});
assert.strictEqual(target.querySelector("[name='foo'] input").value, "yop");
await editInput(target, "[name='foo'] input", "tralala");
assert.strictEqual(target.querySelector("[name='foo'] input").value, "yop");
});
QUnit.test(
"input field: change value before pending onchange renaming",
async function (assert) {
@ -1047,4 +1145,94 @@ QUnit.module("Fields", (hooks) => {
"Placeholder"
);
});
QUnit.test(
"char field: correct value is used to evaluate the modifiers",
async function (assert) {
serverData.models.partner.onchanges = {
foo: (obj) => {
if (obj.foo === "a") {
obj.display_name = false;
} else if (obj.foo === "b") {
obj.display_name = "";
}
},
};
serverData.models.partner.records[0].foo = false;
serverData.models.partner.records[0].display_name = false;
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: `
<form>
<field name="foo" />
<field name="display_name" invisible="'' == display_name"/>
</form>`,
});
assert.containsOnce(target, "[name='display_name'] input");
await editInput(target, "[name='foo'] input", "a");
assert.containsOnce(target, "[name='display_name'] input");
await editInput(target, "[name='foo'] input", "b");
assert.containsNone(target, "[name='display_name'] input");
}
);
QUnit.test(
"edit a char field should display the status indicator buttons without flickering",
async function (assert) {
serverData.models.partner.records[0].p = [2];
serverData.models.partner.onchanges = {
foo() {},
};
const def = makeDeferred();
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: `
<form>
<field name="p">
<tree editable="bottom">
<field name="foo"/>
</tree>
</field>
</form>`,
async mockRPC(route, { method }) {
if (method === "onchange") {
assert.step("onchange");
await def;
}
},
});
assert.containsOnce(
target,
".o_form_status_indicator_buttons.invisible",
"form view is not dirty"
);
await click(target, ".o_data_cell");
await editInput(target, "[name='foo'] input", "a");
assert.verifySteps(["onchange"]);
assert.containsOnce(
target,
".o_form_status_indicator_buttons:not(.invisible)",
"form view is dirty"
);
def.resolve();
await nextTick();
assert.containsOnce(
target,
".o_form_status_indicator_buttons:not(.invisible)",
"form view is dirty"
);
}
);
});

View file

@ -68,7 +68,7 @@ QUnit.module("Fields", (hooks) => {
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"}]',
'onchange [[1],{"hex_color":"#fefefe"},["hex_color"],{"hex_color":{},"display_name":{}}]',
]);
assert.strictEqual(target.querySelector(".o_field_color input").value, "#fefefe");
assert.strictEqual(
@ -113,31 +113,34 @@ QUnit.module("Fields", (hooks) => {
assert.containsN(
target,
'.o_field_color input:disabled',
".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: `
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"
);
});
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 = {
@ -170,7 +173,7 @@ QUnit.module("Fields", (hooks) => {
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":""}]',
'onchange [[1],{"foo":"someValue"},["foo"],{"foo":{},"hex_color":{},"display_name":{}}]',
]);
assert.strictEqual(target.querySelector(".o_field_color input").value, "#fefefe");
assert.strictEqual(

View file

@ -224,7 +224,11 @@ QUnit.module("Fields", (hooks) => {
</tree>`,
domain: [["id", "<", 0]],
});
await click(target.querySelector(".o_list_button_add"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_add"
)
);
const date_column_width = target
.querySelector('.o_list_table thead th[data-name="date_field"]')
.style.width.replace("px", "");

View file

@ -2,7 +2,13 @@
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { click, getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import {
click,
getFixture,
editInput,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
const serviceRegistry = registry.category("services");
@ -73,8 +79,9 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("CopyClipboardField on unset field", async function (assert) {
QUnit.test("CopyClipboardField: show copy button even on empty field", async function (assert) {
serverData.models.partner.records[0].char_field = false;
serverData.models.partner.records[0].text_field = false;
await makeView({
serverData,
@ -85,26 +92,25 @@ QUnit.module("Fields", (hooks) => {
<sheet>
<group>
<field name="char_field" widget="CopyClipboardChar" />
<field name="text_field" widget="CopyClipboardText" />
</group>
</sheet>
</form>`,
resId: 1,
});
assert.containsNone(
assert.containsOnce(
target,
'.o_field_copy[name="char_field"] .o_clipboard_button',
"char_field (unset) should not contain a button"
'.o_field_CopyClipboardChar[name="char_field"] .o_clipboard_button'
);
assert.containsOnce(
target.querySelector(".o_field_widget[name=char_field]"),
"input",
"char_field (unset) should contain an input field"
target,
'.o_field_CopyClipboardText[name="text_field"] .o_clipboard_button'
);
});
QUnit.test(
"CopyClipboardField on readonly unset fields in create mode",
"CopyClipboardField: show copy button even on readonly empty field",
async function (assert) {
serverData.models.partner.fields.display_name.readonly = true;
@ -122,10 +128,9 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
assert.containsNone(
assert.containsOnce(
target,
'.o_field_copy[name="display_name"] .o_clipboard_button',
"the readonly unset field should not contain a button"
'.o_field_CopyClipboardChar[name="display_name"] .o_clipboard_button'
);
}
);
@ -190,42 +195,9 @@ QUnit.module("Fields", (hooks) => {
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,
},
});
QUnit.module("CopyClipboardButtonField");
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) {
QUnit.test("CopyClipboardButtonField in form view", async function (assert) {
patchWithCleanup(browser, {
navigator: {
clipboard: {
@ -267,4 +239,52 @@ Ho-ho-hoooo Merry Christmas`,
"yop",
]);
});
QUnit.test("CopyClipboardButtonField can be disabled", 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" disabled="1" widget="CopyClipboardButton"/>
<field name="char_field" disabled="char_field == 'yop'" widget="CopyClipboardButton"/>
<field name="char_field" widget="char"/>
</div>
</sheet>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
".o_clipboard_button.o_btn_text_copy[disabled]",
"The inner button should be disabled."
);
assert.containsOnce(
target,
".o_clipboard_button.o_btn_char_copy[disabled]",
"The inner button should be disabled."
);
await editInput(target, ".o_input", "yip");
assert.containsNone(
target,
".o_clipboard_button.o_btn_char_copy[disabled]",
"The inner button should not be disabled."
);
});
});

View file

@ -1,6 +1,6 @@
/** @odoo-module **/
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { getPickerCell, zoomOut } from "@web/../tests/core/datetime/datetime_test_helpers";
import {
click,
clickCreate,
@ -16,8 +16,7 @@ import {
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";
import { localization } from "@web/core/l10n/localization";
let serverData;
let target;
@ -70,7 +69,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("DateField");
QUnit.test("DateField: toggle datepicker", async function (assert) {
QUnit.test("DateField: toggle datepicker", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -81,29 +80,21 @@ QUnit.module("Fields", (hooks) => {
<field name="date" />
</form>`,
});
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be closed initially"
);
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed initially");
await click(target, ".o_datepicker .o_datepicker_input");
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(target, ".o_field_date input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
// focus another field
await click(target, ".o_field_widget[name='foo'] input");
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
target,
".o_datetime_picker",
"datepicker should close itself when the user clicks outside"
);
});
QUnit.test("DateField: toggle datepicker far in the future", async function (assert) {
QUnit.test("DateField: toggle datepicker far in the future", async (assert) => {
serverData.models.partner.records = [
{
id: 1,
@ -124,29 +115,21 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be closed initially"
);
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed initially");
await click(target, ".o_datepicker .o_datepicker_input");
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(target, ".o_field_date input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
// focus another field
await click(target, ".o_field_widget[name='foo'] input");
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
target,
".o_datetime_picker",
"datepicker should close itself when the user clicks outside"
);
});
QUnit.test("date field is empty if no date is set", async function (assert) {
QUnit.test("date field is empty if no date is set", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -157,7 +140,7 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(
target,
".o_field_widget .o_datepicker_input",
".o_field_widget input",
"should have one input in the form view"
);
assert.strictEqual(
@ -167,47 +150,24 @@ QUnit.module("Fields", (hooks) => {
);
});
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>',
});
QUnit.test("DateField: set an invalid date when the field is already set", async (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");
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");
}
);
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) {
QUnit.test("DateField: set an invalid date when the field is not set yet", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -216,25 +176,42 @@ QUnit.module("Fields", (hooks) => {
arch: '<form><field name="date"/></form>',
});
await click(target, ".o_datepicker .o_datepicker_input");
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 (assert) => {
await makeView({
type: "form",
resModel: "partner",
resId: 4,
serverData,
arch: '<form><field name="date"/></form>',
});
await click(target, ".o_field_date 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/']");
await click(getPickerCell("22"));
// re-open datepicker
await click(target, ".o_datepicker .o_datepicker_input");
await click(target, ".o_field_date input");
assert.strictEqual(
document.body.querySelector(".day.active").textContent,
target.querySelector(".o_date_item_cell.o_selected").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) {
QUnit.test("DateField in form view (with positive time zone offset)", async (assert) => {
assert.expect(7);
patchTimeZone(120); // Should be ignored by date fields
@ -246,7 +223,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: '<form><field name="date"/></form>',
mockRPC(route, { args }) {
if (route === "/web/dataset/call_kw/partner/write") {
if (route === "/web/dataset/call_kw/partner/web_save") {
assert.strictEqual(
args[1].date,
"2017-02-22",
@ -257,39 +234,28 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date 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");
await click(target, ".o_field_date input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
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"
target,
".o_date_item_cell.o_selected",
"datepicker should have a selected day"
);
// select 22 Feb 2017
await zoomOut();
await zoomOut();
await click(getPickerCell("2017"));
await click(getPickerCell("Feb"));
await click(getPickerCell("22"));
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date input").value,
"02/22/2017",
"the selected date should be displayed in the input"
);
@ -303,7 +269,7 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("DateField in form view (with negative time zone offset)", async function (assert) {
QUnit.test("DateField in form view (with negative time zone offset)", async (assert) => {
patchTimeZone(-120); // Should be ignored by date fields
await makeView({
@ -315,13 +281,13 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date input").value,
"02/03/2017",
"the date should be correct in edit mode"
);
});
QUnit.test("DateField dropdown disappears on scroll", async function (assert) {
QUnit.test("DateField dropdown doesn't disappear on scroll", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -335,22 +301,14 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
await click(target, ".o_datepicker .o_datepicker_input");
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(target, ".o_field_date input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
await triggerScroll(target, { top: 50 });
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be closed"
);
assert.containsOnce(target, ".o_datetime_picker", "datepicker should still be opened");
});
QUnit.test("DateField with label opens datepicker on click", async function (assert) {
QUnit.test("DateField with label opens datepicker on click", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -364,14 +322,10 @@ QUnit.module("Fields", (hooks) => {
});
await click(target.querySelector("label.o_form_label"));
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
});
QUnit.test("DateField with warn_future option", async function (assert) {
QUnit.test("DateField with warn_future option", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -379,42 +333,36 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<field name="date" options="{ 'datepicker': { 'warn_future': true } }" />
<field name="date" options="{'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/']");
await click(target, ".o_field_date input");
await zoomOut();
await zoomOut();
await click(getPickerCell("2030"));
await click(getPickerCell("Dec"));
await click(getPickerCell("31"));
assert.containsOnce(
target,
".o_datepicker_warning",
".fa-exclamation-triangle",
"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
await editInput(target, ".o_field_widget[name='date'] input", "");
assert.containsNone(
target,
".o_datepicker_warning",
".fa-exclamation-triangle",
"the warning in the form view should be hidden"
);
});
QUnit.test(
"DateField with warn_future option: do not overwrite datepicker option",
async function (assert) {
async (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;
@ -428,7 +376,7 @@ QUnit.module("Fields", (hooks) => {
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 } }" />
<field name="date" options="{'warn_future': true}" />
</form>`,
});
@ -447,7 +395,7 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test("DateField in editable list view", async function (assert) {
QUnit.test("DateField in editable list view", async (assert) => {
await makeView({
type: "list",
resModel: "partner",
@ -465,45 +413,33 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(
target,
"input.o_datepicker_input",
".o_field_date input",
"the view should have a date input for editable mode"
);
assert.strictEqual(
target.querySelector("input.o_datepicker_input"),
target.querySelector(".o_field_date input"),
document.activeElement,
"date input should have the focus"
);
assert.strictEqual(
target.querySelector("input.o_datepicker_input").value,
target.querySelector(".o_field_date 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"
);
await click(target, ".o_field_date input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
await zoomOut();
await zoomOut();
await click(getPickerCell("2017"));
await click(getPickerCell("Feb"));
await click(getPickerCell("22"));
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date input").value,
"02/22/2017",
"the selected date should be displayed in the input"
);
@ -517,44 +453,41 @@ QUnit.module("Fields", (hooks) => {
);
});
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";
QUnit.test("multi edition of DateField in list view: clear date in input", async (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>',
});
await makeView({
serverData,
type: "list",
resModel: "partner",
arch: '<tree multi_edit="1"><field name="date"/></tree>',
});
const rows = target.querySelectorAll(".o_data_row");
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");
// 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");
await click(rows[0], ".o_data_cell");
assert.containsOnce(target, "input.o_datepicker_input");
await editInput(target, ".o_datepicker_input", "");
assert.containsOnce(target, ".o_field_date input");
await editInput(target, ".o_field_date input", "");
assert.containsOnce(document.body, ".modal");
await click(target, ".modal .modal-footer .btn-primary");
assert.containsOnce(target, ".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,
""
);
}
);
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) {
QUnit.test("DateField remove value", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -569,16 +502,16 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date input").value,
"02/03/2017",
"the date should be correct in edit mode"
);
const input = target.querySelector(".o_datepicker_input");
const input = target.querySelector(".o_field_date input");
input.value = "";
await triggerEvents(input, null, ["input", "change", "focusout"]);
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date input").value,
"",
"should have correctly removed the value"
);
@ -592,93 +525,32 @@ QUnit.module("Fields", (hooks) => {
);
});
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.",
QUnit.test("field date should select its content onclick when there is one", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: '<form><field name="date"/></form>',
});
registry.category("services").remove("localization");
registry
.category("services")
.add(
"localization",
makeFakeLocalizationService({ dateFormat: strftimeToLuxonFormat("%d-%m/%Y") })
);
patchWithCleanup(luxon.Settings, {
defaultLocale: "no",
const input = target.querySelector(".o_field_date input");
await click(input);
input.focus();
assert.containsOnce(target, ".o_datetime_picker");
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"
);
});
QUnit.test("DateField supports custom format", async (assert) => {
patchWithCleanup(localization, {
dateFormat: "dd-MM-yyyy",
});
await makeView({
@ -690,27 +562,60 @@ QUnit.module("Fields", (hooks) => {
});
const dateViewForm = target.querySelector(".o_field_date input").value;
await click(target, ".o_datepicker .o_datepicker_input");
await click(target, ".o_field_date input");
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date 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 click(getPickerCell("22"));
const dateEditForm = target.querySelector(".o_field_date 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) {
QUnit.test("DateField supports internationalization", async (assert) => {
patchWithCleanup(luxon.Settings, {
defaultLocale: "nb-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_field_date input");
assert.strictEqual(
target.querySelector(".o_field_date input").value,
dateViewForm,
"input date field should be the same as it was in the view form"
);
assert.strictEqual(
target.querySelector(".o_zoom_out strong").textContent,
"februar 2017",
"Norwegian locale should be correctly applied"
);
await click(getPickerCell("22"));
const dateEditForm = target.querySelector(".o_field_date 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"
);
});
QUnit.test("DateField: hit enter should update value", async (assert) => {
patchTimeZone(120);
await makeView({
@ -725,23 +630,23 @@ QUnit.module("Fields", (hooks) => {
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");
await triggerEvent(input, null, "keydown", { key: "Enter" });
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");
await triggerEvent(input, null, "keydown", { key: "Enter" });
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) {
QUnit.test("DateField: allow to use compute dates (+5d for instance)", async (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";
@ -755,20 +660,20 @@ QUnit.module("Fields", (hooks) => {
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");
await editInput(target, ".o_field_widget[name=date] 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");
await editInput(target, ".o_field_widget[name=date] 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");
await editInput(target, ".o_field_widget[name=date] input", "+5d");
assert.strictEqual(target.querySelector(".o_field_widget input").value, "02/20/2021");
});
});

View file

@ -1,17 +1,25 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import {
getPickerApplyButton,
getPickerCell,
getTimePickers,
zoomOut,
} from "@web/../tests/core/datetime/datetime_test_helpers";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import {
click,
clickSave,
editInput,
editSelect,
getFixture,
patchTimeZone,
patchWithCleanup,
triggerEvent,
triggerEvents,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
let serverData;
let target;
@ -55,7 +63,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("DatetimeField");
QUnit.test("DatetimeField in form view", async function (assert) {
QUnit.test("DatetimeField in form view", async (assert) => {
patchTimeZone(120);
await makeView({
@ -70,56 +78,32 @@ QUnit.module("Fields", (hooks) => {
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"
"the datetime should be correctly displayed"
);
// datepicker should not open on focus
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
assert.containsNone(target, ".o_datetime_picker");
await click(target, ".o_datepicker_input");
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
await click(target, ".o_field_datetime input");
assert.containsOnce(target, ".o_datetime_picker");
// 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]);
// select 22 April 2018 at 8:25
await zoomOut();
await zoomOut();
await click(getPickerCell("2018"));
await click(getPickerCell("Apr"));
await click(getPickerCell("22"));
const [hourSelect, minuteSelect] = getTimePickers().at(0);
await editSelect(hourSelect, null, "8");
await editSelect(minuteSelect, null, "25");
// Close the datepicker
await click(target, ".o_form_view_container");
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be closed"
);
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
const newExpectedDateString = "04/22/2017 08:25:35";
const newExpectedDateString = "04/22/2018 08:25:00";
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_datetime input").value,
newExpectedDateString,
"the selected date should be displayed in the input"
);
@ -134,8 +118,8 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
"DatetimeField does not trigger fieldChange before datetime completly picked",
async function (assert) {
"DatetimeField only triggers fieldChange when a day is picked and when an hour/minute is selected",
async (assert) => {
patchTimeZone(120);
serverData.models.partner.onchanges = {
@ -154,60 +138,38 @@ QUnit.module("Fields", (hooks) => {
},
});
await click(target, ".o_datepicker_input");
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
await click(target, ".o_field_datetime input");
// 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.containsOnce(target, ".o_datetime_picker");
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
// select 22 April 2018 at 8:25
await zoomOut();
await zoomOut();
await click(getPickerCell("2018"));
await click(getPickerCell("Apr"));
await click(getPickerCell("22"));
assert.verifySteps([]);
const [hourSelect, minuteSelect] = getTimePickers().at(0);
await editSelect(hourSelect, null, "8");
await editSelect(minuteSelect, null, "25");
assert.verifySteps([]);
// Close the datepicker
await click(target);
assert.containsNone(target, ".o_datetime_picker");
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
"04/22/2017 08:25:35"
target.querySelector(".o_field_datetime input").value,
"04/22/2018 08:25:00"
);
assert.verifySteps(["onchange"], "should have done only one onchange");
assert.verifySteps(["onchange"]);
}
);
QUnit.test("DatetimeField with datetime formatted without second", async function (assert) {
QUnit.test("DatetimeField with datetime formatted without second", async (assert) => {
patchTimeZone(0);
serverData.models.partner.fields.datetime.default = "2017-08-02 12:00:05";
@ -234,14 +196,13 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
target.querySelector(".o_field_datetime input").value,
expectedDateString,
"the datetime should be correctly displayed in readonly"
"the datetime should be correctly displayed"
);
await click(target, ".o_form_button_cancel");
assert.containsNone(document.body, ".modal", "there should not be a Warning dialog");
assert.containsNone(target, ".modal", "there should not be a Warning dialog");
});
QUnit.test("DatetimeField in editable list view", async function (assert) {
QUnit.test("DatetimeField in editable list view", async (assert) => {
patchTimeZone(120);
await makeView({
@ -256,69 +217,54 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
cell.textContent,
expectedDateString,
"the datetime should be correctly displayed in readonly"
"the datetime should be correctly displayed"
);
// switch to edit mode
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.containsOnce(
target,
"input.o_datepicker_input",
".o_field_datetime input",
"the view should have a date input for editable mode"
);
assert.strictEqual(
target.querySelector("input.o_datepicker_input"),
target.querySelector(".o_field_datetime input"),
document.activeElement,
"date input should have the focus"
);
assert.strictEqual(
target.querySelector("input.o_datepicker_input").value,
target.querySelector(".o_field_datetime 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");
assert.containsNone(target, ".o_datetime_picker");
// 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]);
await click(target, ".o_field_datetime input");
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be closed"
);
assert.containsOnce(target, ".o_datetime_picker");
const newExpectedDateString = "04/22/2017 08:25:35";
// select 22 April 2018 at 8:25
await zoomOut();
await zoomOut();
await click(getPickerCell("2018"));
await click(getPickerCell("Apr"));
await click(getPickerCell("22"));
const [hourSelect, minuteSelect] = getTimePickers().at(0);
await editSelect(hourSelect, null, "8");
await editSelect(minuteSelect, null, "25");
// Apply changes
await click(getPickerApplyButton());
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
const newExpectedDateString = "04/22/2018 08:25:00";
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_datetime input").value,
newExpectedDateString,
"the selected datetime should be displayed in the input"
"the date should be updated in the input"
);
// save
@ -332,7 +278,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.test(
"multi edition of DatetimeField in list view: edit date in input",
async function (assert) {
async (assert) => {
await makeView({
serverData,
type: "list",
@ -348,10 +294,12 @@ QUnit.module("Fields", (hooks) => {
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(target, ".o_field_datetime input");
await editInput(target, ".o_field_datetime input", "10/02/2019 09:00:00");
assert.containsOnce(target, ".modal");
assert.containsOnce(document.body, ".modal");
await click(target.querySelector(".modal .modal-footer .btn-primary"));
assert.strictEqual(
@ -367,7 +315,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.test(
"multi edition of DatetimeField in list view: clear date in input",
async function (assert) {
async (assert) => {
serverData.models.partner.records[1].datetime = "2017-02-08 10:00:00";
await makeView({
@ -385,10 +333,12 @@ QUnit.module("Fields", (hooks) => {
await click(rows[0], ".o_data_cell");
assert.containsOnce(target, "input.o_datepicker_input");
await editInput(target, ".o_datepicker_input", "");
assert.containsOnce(target, ".o_field_datetime input");
await editInput(target, ".o_field_datetime input", "");
assert.containsOnce(target, ".modal");
assert.containsOnce(document.body, ".modal");
await click(target, ".modal .modal-footer .btn-primary");
assert.strictEqual(
@ -402,7 +352,7 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test("DatetimeField remove value", async function (assert) {
QUnit.test("DatetimeField remove value", async (assert) => {
assert.expect(4);
patchTimeZone(120);
@ -414,7 +364,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: '<form><field name="datetime"/></form>',
mockRPC(route, { args }) {
if (route === "/web/dataset/call_kw/partner/write") {
if (route === "/web/dataset/call_kw/partner/web_save") {
assert.strictEqual(
args[1].datetime,
false,
@ -425,16 +375,16 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_datetime input").value,
"02/08/2017 12:00:00",
"the date time should be correct in edit mode"
);
const input = target.querySelector(".o_datepicker_input");
const input = target.querySelector(".o_field_datetime input");
input.value = "";
await triggerEvents(input, null, ["input", "change", "focusout"]);
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_datetime input").value,
"",
"should have an empty input"
);
@ -449,8 +399,8 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
"DatetimeField with date/datetime widget (with day change)",
async function (assert) {
"DatetimeField with date/datetime widget (with day change) does not care about widget",
async (assert) => {
patchTimeZone(-240);
serverData.models.partner.records[0].p = [2];
@ -484,16 +434,16 @@ QUnit.module("Fields", (hooks) => {
// 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",
target.querySelector(".modal .o_field_date[name='datetime'] input").value,
"02/07/2017 22:00:00",
"the datetime (date widget) should be correctly displayed in form view"
);
}
);
QUnit.test(
"DatetimeField with date/datetime widget (without day change)",
async function (assert) {
"DatetimeField with date/datetime widget (without day change) does not care about widget",
async (assert) => {
patchTimeZone(-240);
serverData.models.partner.records[0].p = [2];
@ -527,44 +477,14 @@ QUnit.module("Fields", (hooks) => {
// 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",
target.querySelector(".modal .o_field_date[name='datetime'] input").value,
"02/08/2017 06:00:00",
"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) {
QUnit.test("datetime field: hit enter should update value", async (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
@ -609,36 +529,7 @@ QUnit.module("Fields", (hooks) => {
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) {
QUnit.test("DateTimeField with label opens datepicker on click", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -652,10 +543,66 @@ QUnit.module("Fields", (hooks) => {
});
await click(target.querySelector("label.o_form_label"));
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
});
QUnit.test("datetime field: use picker with arabic numbering system", async (assert) => {
patchWithCleanup(luxon.Settings, {
defaultLocale: "ar-001",
defaultNumberingSystem: "arab",
});
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: /* xml */ `
<form string="Partners">
<field name="datetime" />
</form>
`,
});
const getInput = () => target.querySelector("[name=datetime] input");
assert.strictEqual(getInput().value, "٠٢/٠٨/٢٠١٧ ١١:٠٠:٠٠");
await click(getInput());
await editSelect(getTimePickers()[0][1], null, 45);
assert.strictEqual(getInput().value, "٠٢/٠٨/٢٠١٧ ١١:٤٥:٠٠");
});
QUnit.test("list datetime with date widget test", async (assert) => {
await makeView({
type: "list",
resModel: "partner",
arch: /* xml */ `
<tree editable="bottom">
<field name="datetime" widget="datetime" options="{'show_time': false}"/>
<field name="datetime" widget="datetime"/>
</tree>`,
serverData,
});
const dates = target.querySelectorAll(".o_field_cell");
assert.strictEqual(
dates[0].textContent,
"02/08/2017",
"for datetime field only date should be visible with show_time as false and readonly"
);
assert.strictEqual(
dates[1].textContent,
"02/08/2017 11:00:00",
"for datetime field both date and time should be visible with show_time by default true"
);
await click(dates[0]);
assert.strictEqual(
target.querySelector(".o_field_datetime input").value,
"02/08/2017 11:00:00",
"for datetime field both date and time should be visible with show_time as false and edit"
);
});
});

View file

@ -9,14 +9,34 @@ import {
getFixture,
makeDeferred,
nextTick,
patchDate,
patchWithCleanup,
triggerEvent,
} from "@web/../tests/helpers/utils";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { getPickerCell } from "@web/../tests/core/datetime/datetime_test_helpers";
import * as dsHelpers from "@web/../tests/core/domain_selector_tests";
import { registry } from "@web/core/registry";
let serverData;
let target;
function replaceNotificationService(assert) {
registry.category("services").add(
"notification",
{
start() {
return {
add(message) {
assert.step(message);
},
};
},
},
{ force: true }
);
}
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
@ -89,19 +109,20 @@ QUnit.module("Fields", (hooks) => {
},
},
};
setupViewRegistries();
});
QUnit.module("DomainField");
QUnit.test(
"The domain editor should not crash the view when given a dynamic filter",
"The domain editor should not crash the view when given a dynamic filter (allow_expressions=False)",
async function (assert) {
// dynamic filters (containing variables, such as uid, parent or today)
// are not handled by the domain editor, but it shouldn't crash the view
// are handled by the domain editor
serverData.models.partner.records[0].foo = `[("int_field", "=", uid)]`;
replaceNotificationService(assert);
await makeView({
type: "form",
resModel: "partner",
@ -115,20 +136,51 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
target.querySelector(".o_edit_mode").textContent,
" This domain is not supported. Reset domain",
"The widget should not crash the view, but gracefully admit its failure."
dsHelpers.getCurrentValue(target),
"uid",
"The widget should show the dynamic filter."
);
assert.doesNotHaveClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain should not involve non-literals"]);
}
);
QUnit.test(
"The domain editor should not crash the view when given a dynamic filter (allow_expressions=True)",
async function (assert) {
// dynamic filters (containing variables, such as uid, parent or today)
// are handled by the domain editor
serverData.models.partner.records[0].foo = `[("int_field", "=", uid)]`;
replaceNotificationService(assert);
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="foo" widget="domain" options="{'model': 'partner', 'allow_expressions':True}" />
<field name="int_field" invisible="1" />
</form>`,
});
assert.strictEqual(
dsHelpers.getCurrentValue(target),
"uid",
"The widget should show the dynamic filter."
);
assert.doesNotHaveClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain involves non-literals. Their evaluation might fail."]);
}
);
QUnit.test(
"The domain editor should not crash the view when given a dynamic filter ( datetime )",
async function (assert) {
// dynamic filters (containing variables, such as uid, parent or today)
// are not handled by the domain editor, but it shouldn't crash the view
serverData.models.partner.records[0].foo = `[("datetime", "=", context_today())]`;
serverData.models.partner.fields.datetime = { string: "A date", type: "datetime" };
serverData.models.partner.records[0].foo = `[("datetime", "=", context_today())]`;
await makeView({
type: "form",
@ -141,27 +193,20 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
// The input field should display that the date is invalid
assert.equal(target.querySelector(".o_datepicker_input").value, "Invalid DateTime");
assert.equal(dsHelpers.getCurrentValue(target), "context_today()");
await dsHelpers.clearNotSupported(target);
// Change the date in the datepicker
await click(target, ".o_datepicker_input");
await click(target, ".o_datetime_input");
// Select a date in the datepicker
await click(
document.body.querySelector(
`.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]`
)
);
await click(getPickerCell("15"));
// Close the datepicker
await click(
document.body.querySelector(
`.bootstrap-datetimepicker-widget a[data-action="close"]`
)
);
await click(target);
await clickDiscard(target);
// Open the datepicker again
await click(target, ".o_datepicker_input");
assert.equal(dsHelpers.getCurrentValue(target), "context_today()");
}
);
@ -183,48 +228,32 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
// As the domain is empty, there should be a button to add the first
// domain part
assert.containsOnce(
target,
".o_domain_add_first_node_button",
"there should be a button to create first domain element"
);
// As the domain is empty, there should be a button to add a new rule
assert.containsOnce(target, dsHelpers.SELECTORS.addNewRule);
// Clicking on the button should add the [["id", "=", "1"]] domain, so
// there should be a field selector in the DOM
await click(target, ".o_domain_add_first_node_button");
assert.containsOnce(target, ".o_field_selector", "there should be a field selector");
await dsHelpers.addNewRule(target);
assert.containsOnce(target, ".o_model_field_selector", "there should be a field selector");
// Focusing the field selector input should open the field selector
// popover
await click(target, ".o_field_selector");
assert.containsOnce(
document.body,
".o_field_selector_popover",
"field selector popover should be visible"
);
assert.containsOnce(
document.body,
".o_field_selector_search input",
"field selector popover should contain a search input"
);
await click(target, ".o_model_field_selector");
assert.containsOnce(document.body, ".o_model_field_selector_popover");
assert.containsOnce(document.body, ".o_model_field_selector_popover_search input");
// The popover should contain the list of partner_type fields and so
// there should be the "Color index" field
assert.strictEqual(
document.body.querySelector(".o_field_selector_item").textContent,
"Color index",
"field selector popover should contain 'Color index' field"
document.body.querySelector(".o_model_field_selector_popover_item_name").textContent,
"Color index"
);
// Clicking on this field should close the popover, then changing the
// associated value should reveal one matched record
await click(document.body.querySelector(".o_field_selector_item"));
await click(document.body.querySelector(".o_model_field_selector_popover_item_name"));
const input = target.querySelector(".o_domain_leaf_value_input");
input.value = 2;
await triggerEvent(input, null, "change");
await dsHelpers.editValue(target, 2);
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent.trim().substr(0, 2),
@ -235,10 +264,7 @@ QUnit.module("Fields", (hooks) => {
// Saving the form view should show a readonly domain containing the
// "color" field
await clickSave(target);
assert.ok(
target.querySelector(".o_field_domain").textContent.includes("Color index"),
"field selector readonly value should now contain 'Color index'"
);
assert.ok(target.querySelector(".o_field_domain").textContent.includes("Color index"));
});
QUnit.test("using binary field in domain widget", async function (assert) {
@ -260,9 +286,13 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
await click(target, ".o_domain_add_first_node_button");
await click(target, ".o_field_selector");
await click(document.body.querySelector(".o_field_selector_item[data-name='image']"));
await dsHelpers.addNewRule(target);
await click(target, ".o_model_field_selector");
await click(
document.body.querySelector(
".o_model_field_selector_popover_item[data-name='image'] button"
)
);
});
QUnit.test("domain field is correctly reset on every view change", async function (assert) {
@ -290,15 +320,15 @@ QUnit.module("Fields", (hooks) => {
// selector to change this
assert.containsOnce(
target,
".o_field_domain .o_field_selector",
".o_field_domain .o_model_field_selector",
"there should be a field selector"
);
// Focusing its input should open the field selector popover
await click(target.querySelector(".o_field_selector"));
await click(target.querySelector(".o_model_field_selector"));
assert.containsOnce(
document.body,
".o_field_selector_popover",
".o_model_field_selector_popover",
"field selector popover should be visible"
);
@ -306,11 +336,11 @@ QUnit.module("Fields", (hooks) => {
// popover should contain the list of "product" fields
assert.containsOnce(
document.body,
".o_field_selector_item",
".o_model_field_selector_popover_item",
"field selector popover should contain only one field"
);
assert.strictEqual(
document.body.querySelector(".o_field_selector_item").textContent,
document.body.querySelector(".o_model_field_selector_popover_item").textContent,
"Product Name",
"field selector popover should contain 'Product Name' field"
);
@ -319,22 +349,22 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_field_widget[name='bar'] input", "partner_type");
// Refocusing the field selector input should open the popover again
await click(target.querySelector(".o_field_selector"));
await click(target.querySelector(".o_model_field_selector"));
assert.containsOnce(
document.body,
".o_field_selector_popover",
".o_model_field_selector_popover",
"field selector popover should be visible"
);
// Now the list of fields should be the ones of the "partner_type" model
assert.containsN(
document.body,
".o_field_selector_item",
".o_model_field_selector_popover_item",
2,
"field selector popover should contain two fields"
);
assert.strictEqual(
document.body.querySelector(".o_field_selector_item").textContent,
document.body.querySelector(".o_model_field_selector_popover_item").textContent,
"Color index",
"field selector popover should contain 'Color index' field"
);
@ -405,16 +435,8 @@ QUnit.module("Fields", (hooks) => {
}
},
});
assert.containsOnce(
target,
".o_field_widget[name='foo']:not(.o_field_empty)",
"there should be a domain field, not considered empty"
);
assert.containsNone(
target,
".o_field_widget[name='foo'] .text-warning",
"should not display that the domain is invalid"
);
assert.containsOnce(target, ".o_field_widget[name='foo']:not(.o_field_empty)");
assert.containsNone(target, ".o_field_widget[name='foo'] .text-warning");
});
QUnit.test("basic domain field: show the selection", async function (assert) {
@ -533,7 +555,7 @@ QUnit.module("Fields", (hooks) => {
"2 record(s)"
);
await editInput(target, ".o_domain_debug_input", "[['id', '<', 40]]");
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['id', '<', 40]]");
// the count should not be re-computed when editing with the textarea
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
@ -588,7 +610,7 @@ QUnit.module("Fields", (hooks) => {
throw new Error("should not save");
}
if (route === "/web/domain/validate") {
return JSON.stringify(domain) === "[[\"abc\",\"=\",1]]";
return JSON.stringify(domain) === '[["abc","=",1]]';
}
},
});
@ -601,7 +623,7 @@ QUnit.module("Fields", (hooks) => {
"2 record(s)"
);
await editInput(target, ".o_domain_debug_input", "[['abc']]");
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['abc', '=', 1]]");
// the count should not be re-computed when editing with the textarea
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
@ -609,6 +631,9 @@ QUnit.module("Fields", (hooks) => {
);
assert.verifySteps([]);
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['abc']]");
assert.verifySteps([]);
await clickSave(target);
assert.hasClass(
target.querySelector(".o_field_domain"),
@ -673,7 +698,7 @@ QUnit.module("Fields", (hooks) => {
"2 record(s)"
);
await editInput(target, ".o_domain_debug_input", "[['id', '<', 40]]");
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['id', '<', 40]]");
// the count should not be re-computed when editing with the textarea
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
@ -732,11 +757,7 @@ QUnit.module("Fields", (hooks) => {
patchWithCleanup(odoo, { debug: true });
let rawDomain = `
[
["date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -365), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S")]
]
`;
let rawDomain = `[("date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -365), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S"))]`;
serverData.models.partner.records[0].foo = rawDomain;
serverData.models.partner.fields.bar.type = "char";
serverData.models.partner.records[0].bar = "partner";
@ -745,7 +766,7 @@ QUnit.module("Fields", (hooks) => {
"partner,false,form": `
<form>
<field name="bar"/>
<field name="foo" widget="domain" options="{'model': 'bar'}"/>
<field name="foo" widget="domain" options="{'model': 'bar', 'allow_expressions':True}"/>
</form>`,
"partner,false,search": `<search />`,
};
@ -764,7 +785,7 @@ QUnit.module("Fields", (hooks) => {
const webClient = await createWebClient({
serverData,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(args[1].foo, rawDomain);
}
if (route === "/web/domain/validate") {
@ -774,21 +795,18 @@ QUnit.module("Fields", (hooks) => {
});
await doAction(webClient, 1);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
rawDomain = `
[
["date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -1), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S")]
]
`;
await editInput(target, ".o_domain_debug_input", rawDomain);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
rawDomain = `[("date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -1), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S"))]`;
await editInput(target, dsHelpers.SELECTORS.debugArea, rawDomain);
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
await clickSave(target);
});
QUnit.test("domain field: edit through selector (dynamic content)", async function (assert) {
patchWithCleanup(odoo, { debug: true });
patchDate(2020, 8, 5, 0, 0, 0);
let rawDomain = `[("date", ">=", context_today())]`;
serverData.models.partner.records[0].foo = rawDomain;
@ -799,7 +817,7 @@ QUnit.module("Fields", (hooks) => {
"partner,false,form": `
<form>
<field name="bar"/>
<field name="foo" widget="domain" options="{'model': 'bar'}"/>
<field name="foo" widget="domain" options="{'model': 'bar', 'allow_expressions':True}"/>
</form>`,
"partner,false,search": `<search />`,
};
@ -824,29 +842,39 @@ QUnit.module("Fields", (hooks) => {
assert.verifySteps(["/web/webclient/load_menus"]);
await doAction(webClient, 1);
assert.verifySteps(["/web/action/load", "get_views", "read", "search_count", "fields_get"]);
assert.verifySteps([
"/web/action/load",
"get_views",
"web_read",
"search_count",
"fields_get",
]);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
await dsHelpers.clearNotSupported(target);
rawDomain = `[("date", ">=", "2020-09-05")]`;
assert.containsOnce(target, ".o_datetime_input", "there should be a datepicker");
assert.verifySteps(["search_count"]);
// Open and close the datepicker
await click(target, ".o_datepicker_input");
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
await click(target, ".o_datetime_input");
assert.containsOnce(target, ".o_datetime_picker");
await triggerEvent(window, null, "scroll");
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.containsOnce(target, ".o_datetime_picker");
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
assert.verifySteps([]);
// Manually input a date
rawDomain = `[("date", ">=", "2020-09-09")]`;
await editInput(target, ".o_datepicker_input", "09/09/2020");
await editInput(target, ".o_datetime_input", "09/09/2020");
assert.verifySteps(["search_count"]);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
// Save
await clickSave(target);
assert.verifySteps(["write", "read", "search_count"]);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.verifySteps(["web_save", "search_count"]);
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
});
QUnit.test("domain field without model", async function (assert) {
@ -874,13 +902,15 @@ QUnit.module("Fields", (hooks) => {
"should contain an error message saying the model is missing"
);
assert.verifySteps([]);
await editInput(target, ".o_field_widget[name=model_name] input", "test");
assert.notStrictEqual(
target.querySelector('.o_field_widget[name="display_name"]').innerText,
"Select a model to add a filter.",
"should not contain an error message anymore"
await editInput(target, ".o_field_widget[name=model_name] input", "partner");
assert.strictEqual(
target
.querySelector('.o_field_widget[name="display_name"] .o_field_domain_panel')
.innerText.toLowerCase(),
"5 record(s)"
);
assert.verifySteps(["test"]);
assert.verifySteps(["partner"]);
});
QUnit.test("domain field in kanban view", async function (assert) {
@ -936,20 +966,27 @@ QUnit.module("Fields", (hooks) => {
<form>
<field name="display_name" widget="domain" options="{'model': 'partner', 'in_dialog': True}"/>
</form>`,
mockRPC: (route) => {
if (route === "/web/domain/validate") {
return true;
}
},
});
assert.containsNone(target, ".o_domain_leaf");
assert.containsNone(target, dsHelpers.SELECTORS.condition);
assert.containsNone(target, ".modal");
await click(target, ".o_field_domain_dialog_button");
assert.containsOnce(target, ".modal");
await click(target, ".modal .o_domain_add_first_node_button");
await click(target, `.modal ${dsHelpers.SELECTORS.addNewRule}`);
await click(target, ".modal-footer .btn-primary");
assert.containsOnce(target, ".o_domain_leaf");
assert.strictEqual(target.querySelector(".o_domain_leaf").textContent, "ID = 1");
assert.containsOnce(target, dsHelpers.SELECTORS.condition);
assert.strictEqual(dsHelpers.getConditionText(target), "ID = 1");
});
QUnit.test("invalid value in domain field with 'inDialog' options", async function (assert) {
serverData.models.partner.fields.display_name.default = "[]";
patchWithCleanup(odoo, {
debug: true,
});
await makeView({
type: "form",
resModel: "partner",
@ -958,33 +995,59 @@ QUnit.module("Fields", (hooks) => {
<form>
<field name="display_name" widget="domain" options="{'model': 'partner', 'in_dialog': True}"/>
</form>`,
mockRPC: (route, args) => {
if (args.method === "search_count") {
const domain = args.args[0];
if (domain.length && domain[0][0] === "id" && domain[0][2] === "01/01/2002") {
throw new Error("Invalid Domain");
}
}
},
});
assert.containsNone(target, ".o_domain_leaf");
assert.containsNone(target, dsHelpers.SELECTORS.condition);
assert.containsNone(target, ".modal");
assert.containsNone(target, ".o_field_domain .text-warning");
await click(target, ".o_field_domain_dialog_button");
assert.containsOnce(target, ".modal");
await click(target, ".modal .o_domain_add_first_node_button");
await editInput(target, ".o_domain_leaf_value_input", "01/01/2002");
await click(target, `.modal ${dsHelpers.SELECTORS.addNewRule}`);
await editInput(target, dsHelpers.SELECTORS.debugArea, "[(0, '=', expr)]");
await click(target, ".modal-footer .btn-primary");
assert.containsOnce(target, ".o_domain_leaf");
assert.strictEqual(target.querySelector(".o_domain_leaf").textContent, 'ID = "01/01/2002"');
assert.strictEqual(
target.querySelector(".o_field_domain .text-warning").textContent.trim(),
"Invalid domain"
);
assert.containsOnce(target, ".modal", "the domain is invalid: the dialog is not closed");
});
QUnit.test(
"edit domain button is available even while loading records count",
async function (assert) {
serverData.models.partner.fields.display_name.default = "[]";
patchWithCleanup(odoo, {
debug: true,
});
const searchCountDeffered = makeDeferred();
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="display_name" widget="domain" options="{'model': 'partner', 'in_dialog': True}"/>
</form>`,
mockRPC: async (route) => {
if (route === "/web/dataset/call_kw/partner/search_count") {
await searchCountDeffered;
}
if (route === "/web/domain/validate") {
return true;
}
},
});
assert.containsNone(target, ".modal");
assert.containsOnce(target, ".o_field_domain_dialog_button");
await click(target, ".o_field_domain_dialog_button");
searchCountDeffered.resolve();
assert.containsOnce(target, ".modal");
await click(target, ".modal-footer .btn-primary");
assert.containsNone(target, ".modal");
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent,
"5 record(s) "
);
}
);
QUnit.test(
"quick check on save if domain has been edited via the debug input",
async function (assert) {
@ -1013,7 +1076,7 @@ QUnit.module("Fields", (hooks) => {
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
"0 record(s)"
);
await editInput(target, ".o_domain_debug_input", "[['id', '!=', False]]");
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['id', '!=', False]]");
await click(target, "button.o_form_button_save");
assert.verifySteps(["/web/domain/validate"]);
assert.strictEqual(
@ -1022,4 +1085,226 @@ QUnit.module("Fields", (hooks) => {
);
}
);
QUnit.test("domain field can be foldable", async function (assert) {
serverData.models.partner.records[0].foo = "[]";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="domain" options="{'model': 'partner_type', 'foldable': true}" />
</group>
</sheet>
</form>`,
});
// As the domain is empty, the "Match all records" span should be visible
assert.strictEqual(
target.querySelector(".o_field_domain span").textContent,
"Match all records"
);
// Unfold the domain
await click(target, ".o_field_domain > div > div");
// There should be a button to add a new rule
assert.containsOnce(target, dsHelpers.SELECTORS.addNewRule);
// Clicking on the button should add the [["id", "=", "1"]] domain, so
// there should be a field selector in the DOM
await dsHelpers.addNewRule(target);
assert.containsOnce(target, ".o_model_field_selector");
// Focusing the field selector input should open the field selector
// popover
await click(target, ".o_model_field_selector");
assert.containsOnce(document.body, ".o_model_field_selector_popover");
assert.containsOnce(document.body, ".o_model_field_selector_popover_search input");
// The popover should contain the list of partner_type fields and so
// there should be the "Color index" field
assert.strictEqual(
document.body.querySelector(".o_model_field_selector_popover_item_name").textContent,
"Color index"
);
// Clicking on this field should close the popover, then changing the
// associated value should reveal one matched record
await click(document.body.querySelector(".o_model_field_selector_popover_item_name"));
await dsHelpers.editValue(target, 2);
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent.trim().substr(0, 2),
"1 ",
"changing color value to 2 should reveal only one record"
);
// Saving the form view should show a readonly domain containing the
// "color" field
await clickSave(target);
assert.ok(target.querySelector(".o_field_domain").textContent.includes("Color index"));
// Fold domain selector
await click(target, ".o_field_domain a i");
assert.containsOnce(target, ".o_field_domain .o_facet_values:contains('Color index = 2')");
});
QUnit.test("add condition in empty foldable domain", async function (assert) {
patchWithCleanup(odoo, { debug: true });
serverData.models.partner.records[0].foo = '[("id", "=", 1)]';
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="domain" options="{'model': 'partner_type', 'foldable': true}" />
</group>
</sheet>
</form>`,
});
// As the domain is not empty, the "Add condition" button should not be available
assert.containsNone(target, ".o_domain_add_first_node_button");
// Unfold the domain and delete the condition
await click(target, ".o_field_domain > div > div");
await dsHelpers.clickOnButtonDeleteNode(target);
// Fold domain selector
await click(target, ".o_field_domain a i");
// As the domain is empty, the "Add condition" button should now be available
assert.containsOnce(target, ".o_domain_add_first_node_button");
// Click on "Add condition"
await click(target, ".o_domain_add_first_node_button");
// Domain is now unfolded with the default condition
assert.containsOnce(target, ".o_model_field_selector");
assert.strictEqual(
target.querySelector(dsHelpers.SELECTORS.debugArea).value,
'[("id", "=", 1)]'
);
});
QUnit.test(
"foldable domain field unfolds and hides caret when domain is invalid",
async function (assert) {
serverData.models.partner.records[0].foo = "[";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="domain" options="{'model': 'partner_type', 'foldable': true}" />
</group>
</sheet>
</form>`,
});
assert.strictEqual(
target.querySelector(".o_field_domain span").textContent,
" Invalid domain "
);
assert.containsNone(target, ".fa-caret-down");
assert.strictEqual(
target.querySelector(".o_domain_selector_row").textContent,
" This domain is not supported. Reset domain"
);
await click(target, ".o_domain_selector_row button");
assert.strictEqual(
target.querySelector(".o_field_domain span").textContent,
"Match all records"
);
}
);
QUnit.test("allow_expressions = true", async function (assert) {
serverData.models.partner.records[0].foo = "[]";
patchWithCleanup(odoo, { debug: true });
replaceNotificationService(assert);
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="domain" options="{'model': 'partner_type', 'allow_expressions':True}" />
</group>
</sheet>
</form>`,
mockRPC(route) {
if (route === "/web/domain/validate") {
return true;
}
},
context: { path: "name", name: "name" },
});
await editInput(target, dsHelpers.SELECTORS.debugArea, `[("name", "=", [name])]`);
assert.doesNotHaveClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain involves non-literals. Their evaluation might fail."]);
await editInput(
target,
dsHelpers.SELECTORS.debugArea,
`["&", ("name", "=", "name"), (path, "=", "other name")]`
);
assert.doesNotHaveClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain involves non-literals. Their evaluation might fail."]);
});
QUnit.test("allow_expressions = false (default)", async function (assert) {
serverData.models.partner.records[0].foo = "[]";
patchWithCleanup(odoo, { debug: true });
replaceNotificationService(assert);
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="domain" options="{'model': 'partner_type' }" />
</group>
</sheet>
</form>`,
context: { path: "name", name: "name" },
});
await editInput(target, dsHelpers.SELECTORS.debugArea, `[("name", "=", name)]`);
assert.hasClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain should not involve non-literals"]);
await editInput(
target,
dsHelpers.SELECTORS.debugArea,
`["&", ("name", "=", "name"), (path, "=", 1)]`
);
assert.hasClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain should not involve non-literals"]);
});
});

View file

@ -0,0 +1,164 @@
/** @odoo-module **/
import { click, getFixture, triggerEvent, triggerHotkey } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
QUnit.module("Dynamic placeholder", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
char: {
string: "Char",
type: "char",
},
placeholder: {
string: "Placeholder",
type: "char",
default: "partner",
},
product_id: {
string: "Product",
type: "many2one",
relation: "product",
searchable: true,
},
},
records: [
{
id: 1,
char: "yop",
product_id: 37,
},
{
id: 2,
char: "blip",
product_id: 41,
},
],
},
product: {
fields: {
name: { string: "Product Name", type: "char", searchable: true },
},
records: [
{
id: 37,
name: "xphone",
},
{
id: 41,
name: "xpad",
},
],
},
},
};
setupViewRegistries();
});
QUnit.test("dynamic placeholder close on click out", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: /* xml */ `
<form>
<field name="placeholder" invisible="1"/>
<sheet>
<group>
<field
name="char"
options="{
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'placeholder'
}"
/>
</group>
</sheet>
</form>
`,
});
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
assert.containsOnce(target, ".o_model_field_selector_popover");
await click(target, ".o_content");
assert.containsNone(target, ".o_model_field_selector_popover");
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
await click(target, ".o_model_field_selector_popover_item_relation");
await click(target, ".o_content");
assert.containsNone(target, ".o_model_field_selector_popover");
});
QUnit.test("dynamic placeholder close with escape", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: /* xml */ `
<form>
<field name="placeholder" invisible="1"/>
<sheet>
<group>
<field
name="char"
options="{
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'placeholder'
}"
/>
</group>
</sheet>
</form>
`,
});
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
assert.containsOnce(target, ".o_model_field_selector_popover");
await triggerHotkey("Escape");
assert.containsNone(target, ".o_model_field_selector_popover");
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
await click(target, ".o_model_field_selector_popover_item_relation");
await triggerHotkey("Escape");
assert.containsNone(target, ".o_model_field_selector_popover");
});
QUnit.test("dynamic placeholder close when clicking on the cross", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: /* xml */ `
<form>
<field name="placeholder" invisible="1"/>
<sheet>
<group>
<field
name="char"
options="{
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'placeholder'
}"
/>
</group>
</sheet>
</form>
`,
});
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
assert.containsOnce(target, ".o_model_field_selector_popover");
await click(target, ".o_model_field_selector_popover_close");
assert.containsNone(target, ".o_model_field_selector_popover");
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
await click(target, ".o_model_field_selector_popover_item_relation");
await click(target, ".o_model_field_selector_popover_close");
assert.containsNone(target, ".o_model_field_selector_popover");
});
});

View file

@ -75,6 +75,12 @@ QUnit.module("Fields", (hooks) => {
"should have rendered the email button as a link with correct classes"
);
assert.hasAttrValue(emailBtn, "href", "mailto:yop", "should have proper mailto prefix");
assert.hasAttrValue(
emailBtn,
"target",
"_blank",
"should have target attribute set to _blank"
);
// change value in edit mode
await editInput(target, ".o_field_email input[type='email']", "new");

View file

@ -0,0 +1,123 @@
/** @odoo-module **/
import { editSelect, getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
// Note: the containsN always check for one more as there will be an invisible empty option every time.
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
program: {
fields: {
program_type: {
type: "selection",
selection: [
["coupon", "Coupons"],
["promotion", "Promotion"],
["gift_card", "gift_card"],
],
required: true,
}
},
records: [
{ id: 1, program_type: "coupon" },
{ id: 2, program_type: "gift_card" },
],
},
}
}
setupViewRegistries();
});
QUnit.module("utils");
QUnit.test("FilterableSelectionField test whitelist", async (assert) => {
await makeView({
type: "form",
resModel: "program",
resId: 1,
serverData,
arch: `
<form>
<field name="program_type" widget="filterable_selection" options="{'whitelisted_values': ['coupons', 'promotion']}"/>
</form>`,
});
assert.containsN(target, "select option", 3);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"coupon\"']",
);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"promotion\"']",
);
});
QUnit.test("FilterableSelectionField test blacklist", async (assert) => {
await makeView({
type: "form",
resModel: "program",
resId: 1,
serverData,
arch: `
<form>
<field name="program_type" widget="filterable_selection" options="{'blacklisted_values': ['gift_card']}"/>
</form>`,
});
assert.containsN(target, "select option", 3);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"coupon\"']",
);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"promotion\"']",
);
});
QUnit.test("FilterableSelectionField test with invalid value", async (assert) => {
// The field should still display the current value in the list
await makeView({
type: "form",
resModel: "program",
resId: 2,
serverData,
arch: `
<form>
<field name="program_type" widget="filterable_selection" options="{'blacklisted_values': ['gift_card']}"/>
</form>`,
});
assert.containsN(target, "select option", 4);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"gift_card\"']",
);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"coupon\"']",
);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"promotion\"']",
);
await editSelect(target, ".o_field_widget[name='program_type'] select", '"coupon"');
assert.containsN(target, "select option", 3);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"coupon\"']",
);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"promotion\"']",
);
});
});

View file

@ -1,7 +1,9 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
let serverData;
let target;
@ -39,7 +41,7 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC(route, { args }) {
if (route === "/web/dataset/call_kw/partner/write") {
if (route === "/web/dataset/call_kw/partner/web_save") {
// 2.3 / 0.5 = 4.6
assert.strictEqual(args[1].qux, 4.6, "the correct float value should be saved");
}
@ -60,4 +62,41 @@ QUnit.module("Fields", (hooks) => {
"The new value should be saved and displayed properly."
);
});
QUnit.test("FloatFactorField comma as decimal point", async function (assert) {
assert.expect(3);
registry.category("services").remove("localization");
registry.category("services").add(
"localization",
makeFakeLocalizationService({
decimalPoint: ",",
thousandsSep: "",
})
);
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/web_save") {
// 2.3 / 0.5 = 4.6
assert.strictEqual(args[1].qux, 4.6, "the correct float value should be saved");
assert.step("save");
}
},
});
await editInput(target, ".o_field_widget[name='qux'] input", "2,3");
await clickSave(target);
assert.verifySteps(["save"]);
});
});

View file

@ -1,5 +1,6 @@
/** @odoo-module **/
import { Component, xml } from "@odoo/owl";
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";
@ -24,6 +25,9 @@ QUnit.module("Fields", (hooks) => {
{ id: 3, float_field: -3.89859 },
{ id: 4, float_field: false },
{ id: 5, float_field: 9.1 },
{ id: 100, float_field: 2.034567e3 },
{ id: 101, float_field: 3.75675456e6 },
{ id: 102, float_field: 6.67543577586e12 },
],
},
},
@ -34,6 +38,66 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("FloatField");
QUnit.test("human readable format 1", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 101,
arch: `<form><field name="float_field" options="{'human_readable': 'true'}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"4M",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("human readable format 2", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 100,
arch: `<form><field name="float_field" options="{'human_readable': 'true', 'decimals': 1}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"2.0k",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("human readable format 3", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 102,
arch: `<form><field name="float_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"6.6754T",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("still human readable when readonly", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 102,
arch: `<form><field readonly="true" name="float_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget span").textContent,
"6.6754T",
"The value should be rendered in human readable format when input is readonly."
);
});
QUnit.test("unset field should be set to 0", async function (assert) {
await makeView({
type: "form",
@ -251,8 +315,7 @@ QUnit.module("Fields", (hooks) => {
});
// switch to edit mode
var cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
await click(cell);
await click(target.querySelector("tr.o_data_row td:not(.o_list_record_selector)"));
assert.containsOnce(
target,
@ -268,7 +331,11 @@ QUnit.module("Fields", (hooks) => {
);
await editInput(target, 'div[name="float_field"] input', "18.8958938598598");
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.strictEqual(
target.querySelector(".o_field_widget").textContent,
"18.896",
@ -389,6 +456,81 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("field with enable_formatting option as false", async function (assert) {
registry.category("services").remove("localization");
registry
.category("services")
.add(
"localization",
makeFakeLocalizationService({ thousandsSep: ",", grouping: [3, 0] })
);
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `<form><field name="float_field" options="{'enable_formatting': false}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"0.36",
"Integer value must not be formatted"
);
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",
"Integer value must be not formatted if input type is number."
);
});
QUnit.test(
"field with enable_formatting option as false 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]" options="{'enable_formatting': false}" />
</tree>`,
});
// switch to edit mode
await click(target.querySelector("tr.o_data_row td:not(.o_list_record_selector)"));
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.2458938598598",
"The value should not be formatted on blur."
);
await editInput(target, 'div[name="float_field"] input', "18.8958938598598");
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.strictEqual(
target.querySelector(".o_field_widget").textContent,
"18.8958938598598",
"The new value should not be rounded as well."
);
}
);
QUnit.test("float_field field with placeholder", async function (assert) {
await makeView({
type: "form",
@ -407,14 +549,17 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("float field can be updated by another field/widget", async function (assert) {
class MyWidget extends owl.Component {
class MyWidget extends Component {
static template = xml`<button t-on-click="onClick">do it</button>`;
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);
const myWidget = {
component: MyWidget,
};
registry.category("view_widgets").add("wi", myWidget);
await makeView({
type: "form",
resModel: "partner",

View file

@ -39,7 +39,7 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC(route, args) {
if (route === "/web/dataset/call_kw/partner/write") {
if (route === "/web/dataset/call_kw/partner/web_save") {
// 48 / 60 = 0.8
assert.strictEqual(
args.args[1].qux,
@ -89,7 +89,7 @@ QUnit.module("Fields", (hooks) => {
<field name="qux" widget="float_time"/>
</form>`,
mockRPC(route, args) {
if (route === "/web/dataset/call_kw/partner/write") {
if (route === "/web/dataset/call_kw/partner/web_save") {
assert.strictEqual(
args.args[1].qux,
9.5,

View file

@ -35,7 +35,7 @@ QUnit.module("Fields", (hooks) => {
<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") {
if (route === "/web/dataset/call_kw/partner/web_save") {
// 1.000 / 0.125 = 8
assert.step(args[1].float_field.toString());
}

View file

@ -1,9 +1,10 @@
/** @odoo-module **/
import { markup } from "@odoo/owl";
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { currencies } from "@web/core/currency";
import { localization } from "@web/core/l10n/localization";
import { session } from "@web/session";
import {
formatFloat,
formatFloatFactor,
@ -14,6 +15,7 @@ import {
formatMonetary,
formatPercentage,
formatReference,
formatText,
formatX2many,
} from "@web/views/fields/formatters";
@ -26,135 +28,6 @@ QUnit.module("Fields", (hooks) => {
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) {
@ -234,10 +107,21 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("formatMany2one", function (assert) {
assert.strictEqual(formatMany2one(false), "");
assert.strictEqual(formatMany2one([false, "M2O value"]), "M2O value");
assert.strictEqual(formatMany2one([1, false]), "Unnamed");
assert.strictEqual(formatMany2one([1, "M2O value"]), "M2O value");
assert.strictEqual(formatMany2one([1, "M2O value"], { escape: true }), "M2O%20value");
});
QUnit.test("formatText", function (assert) {
assert.strictEqual(formatText(false), "");
assert.strictEqual(formatText("value"), "value");
assert.strictEqual(formatText(1), "1");
assert.strictEqual(formatText(1.5), "1.5");
assert.strictEqual(formatText(markup("<p>This is a Test</p>")), "<p>This is a Test</p>");
assert.strictEqual(formatText([1, 2, 3, 4, 5]), "1,2,3,4,5");
assert.strictEqual(formatText({ a: 1, b: 2 }), "[object Object]");
});
QUnit.test("formatX2many", function (assert) {
// Results are cast as strings since they're lazy translated.
assert.strictEqual(String(formatX2many({ currentIds: [] })), "No records");
@ -246,7 +130,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("formatMonetary", function (assert) {
patchWithCleanup(session.currencies, {
patchWithCleanup(currencies, {
10: {
digits: [69, 2],
position: "after",
@ -265,67 +149,24 @@ QUnit.module("Fields", (hooks) => {
});
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"
);
const field = {
type: "monetary",
currency_field: "c_x",
};
let data = {
c_x: [11],
c_y: 12,
};
assert.deepEqual(formatMonetary(200, { field, currencyId: 10, data }), "200.00\u00a0€");
assert.deepEqual(formatMonetary(200, { field, data }), "$\u00a0200.00");
assert.deepEqual(formatMonetary(200, { field, currencyField: "c_y", data }), "200.00\u00a0&");
// 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");
const floatField = { type: "float" };
data = {
currency_id: [11],
};
assert.deepEqual(formatMonetary(200, { field: floatField, data }), "$\u00a0200.00");
});
QUnit.test("formatPercentage", function (assert) {

View file

@ -0,0 +1,101 @@
/** @odoo-module **/
import { onMounted } from "@odoo/owl";
import { getFixture, getNodesTextContent, patchWithCleanup } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { GaugeField } from "@web/views/fields/gauge/gauge_field";
let serverData;
let target;
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
int_field: {
string: "int_field",
type: "integer",
},
another_int_field: {
string: "another_int_field",
type: "integer",
},
},
records: [
{ id: 1, int_field: 10, another_int_field: 45 },
{ id: 2, int_field: 4, another_int_field: 10 },
],
},
},
};
setupViewRegistries();
});
QUnit.module("GaugeField");
QUnit.test("GaugeField in kanban view", async function (assert) {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<field name="another_int_field"/>
<templates>
<t t-name="kanban-box">
<div>
<field name="int_field" widget="gauge" options="{'max_field': 'another_int_field'}"/>
</div>
</t>
</templates>
</kanban>`,
});
assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2);
assert.containsN(target, ".o_field_widget[name=int_field] .oe_gauge canvas", 2);
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_gauge_value")), [
"10",
"4",
]);
});
QUnit.test("GaugeValue supports max_value option", async function (assert) {
patchWithCleanup(GaugeField.prototype, {
setup() {
super.setup();
onMounted(() => {
assert.step("gauge mounted");
assert.strictEqual(this.chart.config.options.plugins.tooltip.callbacks.label({}), "Max: 120");
});
}
});
serverData.models.partner.records = serverData.models.partner.records.slice(0,1);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="int_field" widget="gauge" options="{'max_value': 120}"/>
</div>
</t>
</templates>
</kanban>`,
});
assert.verifySteps(["gauge mounted"]);
assert.containsN(target, ".o_field_widget[name=int_field] .oe_gauge canvas", 1);
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_gauge_value")), [
"10",
]);
});
});

View file

@ -9,7 +9,7 @@ import {
} 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 { htmlField } from "@web/views/fields/html/html_field";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { session } from "@web/session";
@ -38,7 +38,7 @@ QUnit.module("Fields", ({ beforeEach }) => {
setupViewRegistries();
// Explicitly removed by web_editor, we need to add it back
registry.category("fields").add("html", HtmlField, { force: true });
registry.category("fields").add("html", htmlField, { force: true });
});
QUnit.module("HtmlField");
@ -295,4 +295,99 @@ QUnit.module("Fields", ({ beforeEach }) => {
await click(target, ".modal button.btn-primary"); // save
});
QUnit.test("html fields: spellcheck is disabled on blur", 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.strictEqual(textarea.spellcheck, true, "by default, spellcheck is enabled");
textarea.focus();
await editInput(textarea, null, "nev walue");
textarea.blur();
assert.strictEqual(
textarea.spellcheck,
false,
"spellcheck is disabled once the field has lost its focus"
);
textarea.focus();
assert.strictEqual(
textarea.spellcheck,
true,
"spellcheck is re-enabled once the field is focused"
);
});
QUnit.test(
"Setting an html field to empty string is saved as a false value",
async function (assert) {
assert.expect(1);
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<group>
<field name="txt" />
</group>
</sheet>
</form>`,
resId: 1,
mockRPC(route, { args, method }) {
if (method === "web_save") {
assert.strictEqual(args[1].txt, false, "the txt value should be false");
}
},
});
await editInput(target, ".o_field_widget[name=txt] textarea", "");
await clickSave(target);
}
);
QUnit.test(
"html field: correct value is used to evaluate the modifiers",
async function (assert) {
serverData.models.partner.fields.foo = { string: "foo", type: "char" };
serverData.models.partner.onchanges = {
foo: (obj) => {
if (obj.foo === "a") {
obj.txt = false;
} else if (obj.foo === "b") {
obj.txt = "";
}
},
};
serverData.models.partner.records[0].foo = false;
serverData.models.partner.records[0].txt = false;
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: `
<form>
<field name="foo" />
<field name="txt" invisible="'' == txt"/>
</form>`,
});
assert.containsOnce(target, "[name='txt'] textarea");
await editInput(target, "[name='foo'] input", "a");
assert.containsOnce(target, "[name='txt'] textarea");
await editInput(target, "[name='foo'] input", "b");
assert.containsNone(target, "[name='txt'] textarea");
}
);
});

View file

@ -24,7 +24,7 @@ let target;
function getUnique(target) {
const src = target.dataset.src;
return new URL(src).searchParams.get("unique");
return new URL(src, window.location).searchParams.get("unique");
}
QUnit.module("Fields", (hooks) => {
@ -87,7 +87,7 @@ QUnit.module("Fields", (hooks) => {
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].write_date = "2017-02-08 10:00:00";
serverData.models.partner.records[0].document = MY_IMAGE;
await makeView({
@ -99,12 +99,16 @@ QUnit.module("Fields", (hooks) => {
<form>
<field name="document" widget="image" options="{'size': [90, 90]}" />
</form>`,
mockRPC(route, { args }) {
if (route === "/web/dataset/call_kw/partner/read") {
mockRPC(route, { args, kwargs }) {
if (route === "/web/dataset/call_kw/partner/web_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"
kwargs.specification,
{
display_name: {},
document: {},
write_date: {},
},
"The fields document, display_name and write_date should be present when reading an image"
);
}
},
@ -295,7 +299,7 @@ QUnit.module("Fields", (hooks) => {
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
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
// 1659692220000, 1659695820000
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
@ -312,8 +316,8 @@ QUnit.module("Fields", (hooks) => {
<field name="document" widget="image" />
</form>`,
mockRPC(_route, { method, args }) {
if (method === "write") {
args[1].__last_update = lastUpdates[index];
if (method === "web_save") {
args[1].write_date = lastUpdates[index];
args[1].document = "4 kb";
index++;
}
@ -381,7 +385,7 @@ QUnit.module("Fields", (hooks) => {
};
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
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
// 1659692220000
const lastUpdates = ["2022-08-05 09:37:00"];
@ -398,8 +402,8 @@ QUnit.module("Fields", (hooks) => {
<field name="document" widget="image" />
</form>`,
mockRPC(_route, { method, args }) {
if (method === "write") {
args[1].__last_update = lastUpdates[index];
if (method === "web_save") {
args[1].write_date = lastUpdates[index];
args[1].document = "3 kb";
index++;
}
@ -517,7 +521,7 @@ QUnit.module("Fields", (hooks) => {
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";
serverData.models.partner.records[0].write_date = "2022-08-05 08:37:00";
await makeView({
type: "form",
@ -547,7 +551,7 @@ QUnit.module("Fields", (hooks) => {
"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";
serverData.models.partner.records[0].write_date = "2022-08-05 08:37:00";
await makeView({
type: "form",
@ -583,7 +587,7 @@ QUnit.module("Fields", (hooks) => {
);
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].write_date = "2017-02-08 10:00:00";
serverData.models.partner.records[0].document = MY_IMAGE;
serverData.models.partner_type.fields.image = {
name: "image",
@ -708,7 +712,7 @@ QUnit.module("Fields", (hooks) => {
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));
await new Promise((resolve) => setTimeout(resolve, 100));
// Wait for a render
await nextTick();
}
@ -728,7 +732,7 @@ QUnit.module("Fields", (hooks) => {
);
await clickSave(target);
await click(target, ".o_form_button_create");
await click(target, ".o_control_panel_main_buttons .d-none .o_form_button_create");
assert.strictEqual(
target.querySelector("img[data-alt='Binary file']").dataset.src,
"/web/static/img/placeholder.png",
@ -751,7 +755,7 @@ QUnit.module("Fields", (hooks) => {
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
rec.document = "3 kb";
rec.__last_update = "2022-08-05 08:37:00";
rec.write_date = "2022-08-05 08:37:00";
await makeView({
resId: 1,
@ -765,14 +769,14 @@ QUnit.module("Fields", (hooks) => {
</form>`,
mockRPC(route, { method, args }) {
assert.step(method);
if (method === "write") {
if (method === "web_save") {
// 1659692220000
args[1].__last_update = "2022-08-05 09:37:00";
args[1].write_date = "2022-08-05 09:37:00";
}
},
});
assert.verifySteps(["get_views", "read"]);
assert.verifySteps(["get_views", "web_read"]);
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
assert.verifySteps([]);
@ -785,7 +789,7 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
await clickSave(target);
assert.verifySteps(["write", "read"]);
assert.verifySteps(["web_save"]);
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
});
@ -793,11 +797,11 @@ QUnit.module("Fields", (hooks) => {
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";
rec.write_date = "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";
rec2.write_date = "2022-08-05 09:37:00";
await makeView({
resIds: [1, 2],
@ -822,11 +826,11 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
"unique in url does not change on record change if no_reload option is set",
"unique in url does not change on record change if reload option is set to false",
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";
rec.write_date = "2022-08-05 08:37:00";
await makeView({
resIds: [1, 2],
@ -836,8 +840,8 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<field name="document" widget="image" required="1" options="{'no_reload': true}" />
<field name="__last_update" />
<field name="document" widget="image" required="1" options="{'reload': false}" />
<field name="write_date" />
</form>`,
});
@ -850,12 +854,7 @@ QUnit.module("Fields", (hooks) => {
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 editInput(target, "div[name='write_date'] > div > input", "2022-08-05 08:39:00");
await click(target, ".o_form_button_save");
assert.strictEqual(
getUnique(target.querySelector(".o_field_image img")),
@ -895,7 +894,7 @@ QUnit.module("Fields", (hooks) => {
],
};
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
serverData.models.partner.records[0].write_date = "2017-02-08 10:00:00";
patchDate(2017, 1, 6, 11, 0, 0);
@ -906,16 +905,15 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" />
<field name="user"/>
<field name="related" widget="image"/>
</group>
</sheet>
<field name="foo" />
<field name="user"/>
<field name="related" widget="image"/>
</form>`,
async mockRPC(route, { args }, performRpc) {
if (route === "/web/dataset/call_kw/partner/read") {
if (
route === "/web/dataset/call_kw/partner/web_read" ||
route === "/web/dataset/call_kw/partner/web_save"
) {
const res = await performRpc(...arguments);
// The mockRPC doesn't implement related fields
res[0].related = "3 kb";

View file

@ -33,6 +33,9 @@ QUnit.module("Fields", (hooks) => {
{ id: 1, int_field: 10 },
{ id: 2, int_field: false },
{ id: 3, int_field: 8069 },
{ id: 100, int_field: 2.034567e3 },
{ id: 101, int_field: 3.75675456e6 },
{ id: 102, int_field: 6.67543577586e12 },
],
},
},
@ -43,6 +46,66 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("IntegerField");
QUnit.test("human readable format 1", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 101,
arch: `<form><field name="int_field" options="{'human_readable': 'true'}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"4M",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("human readable format 2", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 100,
arch: `<form><field name="int_field" options="{'human_readable': 'true', 'decimals': 1}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"2.0k",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("human readable format 3", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 102,
arch: `<form><field name="int_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"6.6754T",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("still human readable when readonly", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 102,
arch: `<form><field readonly="true" name="int_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget span").textContent,
"6.6754T",
"The value should be rendered in human readable format when input is readonly."
);
});
QUnit.test("should be 0 when unset", async function (assert) {
await makeView({
type: "form",
@ -264,7 +327,11 @@ QUnit.module("Fields", (hooks) => {
"The value should be displayed properly in the input."
);
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.strictEqual(
target.querySelector("td:not(.o_list_record_selector)").textContent,
"-28",
@ -289,6 +356,32 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("IntegerField with enable_formatting option as false", async function (assert) {
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 3,
arch: `<form><field name="int_field" options="{'enable_formatting': false}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"8069",
"Integer value must not be formatted"
);
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(
"no need to focus out of the input to save the record after correcting an invalid input",
async function (assert) {
@ -329,16 +422,12 @@ QUnit.module("Fields", (hooks) => {
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");
@ -383,4 +472,25 @@ QUnit.module("Fields", (hooks) => {
await triggerEvent(target, ".o_field_widget input", "keydown", { key: "Enter" });
assert.strictEqual(target.querySelector(".o_field_widget input").value, "8,069");
});
QUnit.test("value is formatted on click out (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", "change"); // triggered when clicking out
assert.strictEqual(target.querySelector(".o_field_widget input").value, "8,069");
});
});

View file

@ -1,7 +1,5 @@
/** @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";
@ -85,7 +83,6 @@ QUnit.module("Fields", (hooks) => {
};
setupViewRegistries();
registry.category("services").add("cookie", fakeCookieService);
});
async function reloadKanbanView(target) {

View file

@ -181,7 +181,11 @@ QUnit.module("Fields", (hooks) => {
);
// save and check the result
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.strictEqual(
target.querySelectorAll(".o_field_widget .badge:not(:empty)").length,
3,

View file

@ -51,7 +51,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("Many2ManyBinaryField");
QUnit.test("widget many2many_binary", async function (assert) {
assert.expect(24);
assert.expect(21);
const fakeHTTPService = {
start() {
@ -95,8 +95,33 @@ QUnit.module("Fields", (hooks) => {
if (args.method !== "get_views") {
assert.step(route);
}
if (route === "/web/dataset/call_kw/ir.attachment/read") {
assert.deepEqual(args.args[1], ["name", "mimetype"]);
if (args.method === "web_read" && args.model === "turtle") {
assert.deepEqual(args.kwargs.specification, {
display_name: {},
picture_ids: {
fields: {
mimetype: {},
name: {},
},
},
});
}
if (args.method === "web_save" && args.model === "turtle") {
assert.deepEqual(args.kwargs.specification, {
display_name: {},
picture_ids: {
fields: {
mimetype: {},
name: {},
},
},
});
}
if (args.method === "web_read" && args.model === "ir.attachment") {
assert.deepEqual(args.kwargs.specification, {
mimetype: {},
name: {},
});
}
},
});
@ -138,10 +163,7 @@ QUnit.module("Fields", (hooks) => {
"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",
]);
assert.verifySteps(["/web/dataset/call_kw/turtle/web_read"]);
// Set and trigger the change of a file for the input
const fileInput = target.querySelector('input[type="file"]');
@ -181,10 +203,8 @@ QUnit.module("Fields", (hooks) => {
"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",
"/web/dataset/call_kw/ir.attachment/web_read",
"/web/dataset/call_kw/turtle/web_save",
]);
});

View file

@ -1,6 +1,15 @@
/** @odoo-module **/
import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
import {
click,
clickSave,
editInput,
getFixture,
getNodesTextContent,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
@ -41,7 +50,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("Many2ManyCheckBoxesField", async function (assert) {
serverData.models.partner.records[0].timmy = [12];
const commands = [[[6, false, [12, 14]]], [[6, false, [14]]]];
const commands = [[[4, 14]], [[3, 12]]];
await makeView({
type: "form",
resModel: "partner",
@ -54,8 +63,8 @@ QUnit.module("Fields", (hooks) => {
</group>
</form>`,
mockRPC(route, args) {
if (args.method === "write") {
assert.step("write");
if (args.method === "web_save") {
assert.step("web_save");
assert.deepEqual(args.args[1].timmy, commands.shift());
}
},
@ -82,7 +91,7 @@ QUnit.module("Fields", (hooks) => {
assert.notOk(checkboxes[0].checked);
assert.ok(checkboxes[1].checked);
assert.verifySteps(["write", "write"]);
assert.verifySteps(["web_save", "web_save"]);
});
QUnit.test("Many2ManyCheckBoxesField (readonly)", async function (assert) {
@ -95,7 +104,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<group>
<field name="timmy" widget="many2many_checkboxes" attrs="{'readonly': true}" />
<field name="timmy" widget="many2many_checkboxes" readonly="True" />
</group>
</form>`,
});
@ -125,6 +134,50 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("Many2ManyCheckBoxesField does not read added record", async function (assert) {
serverData.models.partner.records[0].timmy = [];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<group>
<field name="timmy" widget="many2many_checkboxes" />
</group>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
},
});
assert.containsN(target, "div.o_field_widget div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsNone(target, "div.o_field_widget div.form-check input:checked");
await click(target.querySelector("div.o_field_widget div.form-check input"));
assert.containsN(target, "div.o_field_widget div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsOnce(target, "div.o_field_widget div.form-check input:checked");
await clickSave(target);
assert.containsN(target, "div.o_field_widget div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsOnce(target, "div.o_field_widget div.form-check input:checked");
assert.verifySteps(["get_views", "web_read", "name_search", "web_save"]);
});
QUnit.test(
"Many2ManyCheckBoxesField: start non empty, then remove twice",
async function (assert) {
@ -190,6 +243,32 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test(
"Many2ManyCheckBoxesField: many2many read, field context is properly sent",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_checkboxes" context="{ 'hello': 'world' }" />
</form>`,
mockRPC(route, args) {
if (args.method === "web_read" && args.model === "partner") {
assert.step(`${args.method} ${args.model}`);
assert.strictEqual(args.kwargs.specification.timmy.context.hello, "world");
} else if (args.method === "name_search" && args.model === "partner_type") {
assert.step(`${args.method} ${args.model}`);
assert.strictEqual(args.kwargs.context.hello, "world");
}
},
});
assert.verifySteps(["web_read partner", "name_search partner_type"]);
}
);
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
@ -219,10 +298,8 @@ QUnit.module("Fields", (hooks) => {
<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]]);
if (method === "web_save") {
assert.deepEqual(args[1].timmy, [[3, records[records.length - 1].id]]);
}
},
});
@ -274,11 +351,9 @@ QUnit.module("Fields", (hooks) => {
<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 === "web_save") {
assert.deepEqual(args[1].timmy, [[3, records[0].id]]);
assert.step("web_save");
}
if (method === "name_search") {
assert.step("name_search");
@ -303,7 +378,7 @@ QUnit.module("Fields", (hooks) => {
assert.notOk(
target.querySelector(".o_field_widget[name='timmy'] input[type='checkbox']").checked
);
assert.verifySteps(["name_search", "write"]);
assert.verifySteps(["name_search", "web_save"]);
});
QUnit.test("Many2ManyCheckBoxesField in a one2many", async function (assert) {
@ -326,9 +401,20 @@ QUnit.module("Fields", (hooks) => {
</field>
</form>`,
mockRPC(route, args) {
if (args.method === "write") {
if (args.method === "web_save") {
assert.deepEqual(args.args[1], {
p: [[1, 1, { timmy: [[6, false, [15, 12]]] }]],
p: [
[
1,
1,
{
timmy: [
[4, 12],
[3, 14],
],
},
],
],
});
}
},
@ -352,7 +438,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("Many2ManyCheckBoxesField with default values", async function (assert) {
assert.expect(7);
serverData.models.partner.fields.timmy.default = [3];
serverData.models.partner.fields.timmy.default = [[4, 3]];
serverData.models.partner.fields.timmy.type = "many2many";
serverData.models.partner_type.records.push({ id: 3, display_name: "bronze" });
@ -365,10 +451,10 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_checkboxes"/>
</form>`,
mockRPC: function (route, args) {
if (args.method === "create") {
if (args.method === "web_save") {
assert.deepEqual(
args.args[0].timmy,
[[6, false, [12]]],
args.args[1].timmy,
[[4, 12]],
"correct values should have been sent to create"
);
}
@ -408,4 +494,131 @@ QUnit.module("Fields", (hooks) => {
await clickSave(target);
});
QUnit.test("Many2ManyCheckBoxesField batches successive changes", async function (assert) {
serverData.models.partner.records[0].timmy = [];
serverData.models.partner.onchanges = {
timmy: () => {},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<group>
<field name="timmy" widget="many2many_checkboxes" />
</group>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
},
});
assert.containsN(target, "div.o_field_widget div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsNone(target, "div.o_field_widget div.form-check input:checked");
let mockSetTimeout;
patchWithCleanup(browser, { setTimeout: (fn) => (mockSetTimeout = fn) });
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]);
// checkboxes are updated directly
assert.containsN(target, "div.o_field_widget div.form-check input:checked", 2);
// but no onchanges has been fired yet
assert.verifySteps(["get_views", "web_read", "name_search"]);
// execute the setTimeout callback
mockSetTimeout();
await nextTick();
assert.verifySteps(["onchange"]);
});
QUnit.test("Many2ManyCheckBoxesField sends batched changes on save", async function (assert) {
serverData.models.partner.records[0].timmy = [];
serverData.models.partner.onchanges = {
timmy: () => {},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<group>
<field name="timmy" widget="many2many_checkboxes" />
</group>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
},
});
assert.containsN(target, "div.o_field_widget div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsNone(target, "div.o_field_widget div.form-check input:checked");
patchWithCleanup(browser, { setTimeout: () => {} }); // never call it
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]);
// checkboxes are updated directly
assert.containsN(target, "div.o_field_widget div.form-check input:checked", 2);
// but no onchanges has been fired yet
assert.verifySteps(["get_views", "web_read", "name_search"]);
// save
await clickSave(target);
assert.verifySteps(["onchange", "web_save"]);
});
QUnit.test("Many2ManyCheckBoxesField in a notebook tab", async function (assert) {
serverData.models.partner.records[0].timmy = [];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<notebook>
<page string="Page 1">
<field name="timmy" widget="many2many_checkboxes" />
</page>
<page string="Page 2">
<field name="int_field" />
</page>
</notebook>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
},
});
assert.containsOnce(target, "div.o_field_widget[name=timmy]");
assert.containsN(target, "div.o_field_widget[name=timmy] div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsNone(target, "div.o_field_widget[name=timmy] div.form-check input:checked");
patchWithCleanup(browser, { setTimeout: () => {} }); // never call it
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]);
// checkboxes are updated directly
assert.containsN(target, "div.o_field_widget div.form-check input:checked", 2);
// go to the other tab
await click(target.querySelectorAll(".o_notebook .nav-link")[1]);
assert.containsNone(target, "div.o_field_widget[name=timmy]");
assert.containsOnce(target, "div.o_field_widget[name=int_field]");
// save
await clickSave(target);
assert.verifySteps(["get_views", "web_read", "name_search", "web_save"]);
});
});

View file

@ -1,6 +1,5 @@
/** @odoo-module **/
import { Component, xml } from "@odoo/owl";
import {
addRow,
click,
@ -17,10 +16,11 @@ import { editSearch, validateSearch } from "@web/../tests/search/helpers";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { Deferred } from "@web/core/utils/concurrency";
import { session } from "@web/session";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";
import { companyService } from "@web/webclient/company_service";
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
let target;
let serverData;
@ -222,7 +222,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("Many2ManyField");
QUnit.test("many2many kanban: edition", async function (assert) {
assert.expect(31);
assert.expect(29);
serverData.views = {
"partner_type,false,form": '<form><field name="display_name"/></form>',
@ -260,37 +260,41 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, args) {
if (route === "/web/dataset/call_kw/partner_type/write") {
if (
route === "/web/dataset/call_kw/partner_type/web_save" &&
args.args[0].length !== 0
) {
assert.strictEqual(
args.args[1].display_name,
"new name",
"should write 'new_name'"
);
}
if (route === "/web/dataset/call_kw/partner_type/create") {
if (
route === "/web/dataset/call_kw/partner_type/web_save" &&
args.args[0].length === 0
) {
assert.strictEqual(
args.args[0].display_name,
args.args[1].display_name,
"A new type",
"should create 'A new type'"
);
}
if (route === "/web/dataset/call_kw/partner/write") {
var commands = args.args[1].timmy;
assert.strictEqual(commands.length, 1, "should have generated one command");
assert.strictEqual(
commands[0][0],
6,
"generated command should be REPLACE WITH"
);
if (
route === "/web/dataset/call_kw/partner/web_save" &&
args.args[0].length !== 0
) {
const commands = args.args[1].timmy;
// get the created type's id
var createdType = _.findWhere(serverData.models.partner_type.records, {
display_name: "A new type",
const createdType = serverData.models.partner_type.records.find((record) => {
return record.display_name === "A new type";
});
var ids = _.sortBy([12, 15, 18].concat(createdType.id), _.identity.bind(_));
assert.ok(
_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), ids),
"new value should be " + ids
);
assert.deepEqual(commands, [
[4, 15],
[4, 18],
[4, createdType.id],
[3, 14],
]);
}
},
});
@ -632,7 +636,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
"many2many list (non editable): create a new record and click on action button",
"many2many list (non editable): create a new record and click on action button 1",
async function (assert) {
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
@ -659,8 +663,8 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
mockRPC: async (route, args) => {
assert.step(args.method);
if (args.method === "create") {
assert.deepEqual(args.args[0], { display_name: "Hello" });
if (args.method === "web_save") {
assert.deepEqual(args.args[1], { display_name: "Hello" });
}
},
});
@ -673,20 +677,25 @@ QUnit.module("Fields", (hooks) => {
let modal = target.querySelector(".modal");
await click(modal, ".o_create_button");
assert.verifySteps(["get_views", "read", "get_views", "web_search_read", "onchange"]);
assert.verifySteps([
"get_views",
"web_read",
"get_views",
"web_search_read",
"onchange",
]);
modal = target.querySelector(".modal");
await editInput(modal, "[name='display_name'] input", "Hello");
assert.strictEqual(modal.querySelector("[name='display_name'] input").value, "Hello");
await click(modal, ".o_statusbar_buttons [name='myaction']");
assert.strictEqual(modal.querySelector("[name='display_name'] input").value, "Hello");
assert.verifySteps(["create", "read", "action: myaction"]);
assert.verifySteps(["web_save", "action: myaction"]);
}
);
QUnit.test(
"many2many list (non editable): create a new record and click on action button",
"many2many list (non editable): create a new record and click on action button 2",
async function (assert) {
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
@ -713,8 +722,8 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
mockRPC: async (route, args) => {
assert.step(args.method);
if (args.method === "create") {
assert.deepEqual(args.args[0], { display_name: "Hello" });
if (args.method === "web_save" && args.args[0].length === 0) {
assert.deepEqual(args.args[1], { display_name: "Hello" });
}
},
});
@ -727,7 +736,13 @@ QUnit.module("Fields", (hooks) => {
let modal = target.querySelector(".modal");
await click(modal, ".o_create_button");
assert.verifySteps(["get_views", "read", "get_views", "web_search_read", "onchange"]);
assert.verifySteps([
"get_views",
"web_read",
"get_views",
"web_search_read",
"onchange",
]);
modal = target.querySelector(".modal");
await editInput(modal, "[name='display_name'] input", "Hello");
@ -753,10 +768,57 @@ QUnit.module("Fields", (hooks) => {
["Hello (edited)"]
);
assert.verifySteps(["create", "read", "action: myaction", "write", "read", "read"]);
assert.verifySteps(["web_save", "action: myaction", "web_save", "web_read"]);
}
);
QUnit.test("add a new record in a many2many non editable list", async function (assert) {
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,form": '<form><field name="display_name"/></form>',
"partner_type,false,search": '<search><field name="display_name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree>
<field name="display_name"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
if (args.method === "web_save") {
// should not read the record as we're closing the dialog
assert.deepEqual(args.kwargs.specification, {});
}
},
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
await click(target.querySelector(".o_dialog .o_create_button"));
await editInput(
target.querySelector(".o_dialog"),
".o_field_widget[name=display_name] input",
"a name"
);
await click(target.querySelector(".o_dialog .o_form_button_save"));
assert.verifySteps([
"get_views",
"onchange",
"get_views",
"web_search_read",
"get_views",
"onchange",
"web_save",
"web_read",
]);
});
QUnit.test("add record in a many2many non editable list with context", async function (assert) {
assert.expect(1);
@ -794,10 +856,8 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_field_widget[name=int_field] input", "2");
await click(target.querySelector(".o_field_x2many_list_row_add a"));
});
QUnit.test("many2many list (editable): edition", async function (assert) {
assert.expect(29);
QUnit.test("many2many list (editable): edition concurrence", async function (assert) {
assert.expect(5);
serverData.models.partner.records[0].timmy = [12, 14];
serverData.models.partner_type.records.push({ id: 15, display_name: "bronze", color: 6 });
serverData.models.partner_type.fields.float_field = { string: "Float", type: "float" };
@ -820,9 +880,49 @@ QUnit.module("Fields", (hooks) => {
</field>
</form>`,
mockRPC(route, args) {
if (args.method !== "get_views") {
assert.step(_.last(route.split("/")));
assert.step(args.method);
if (args.method === "web_save") {
//check that delete command is not duplicate
assert.deepEqual(args.args, [
[1],
{
timmy: [[3, 12]],
},
]);
}
},
resId: 1,
});
const t = target.querySelector(".o_list_record_remove");
click(t);
click(t);
await clickSave(target);
assert.verifySteps(["get_views", "web_read", "web_save"]);
});
QUnit.test("many2many list (editable): edition", async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
serverData.models.partner_type.records.push({ id: 15, display_name: "bronze", color: 6 });
serverData.models.partner_type.fields.float_field = { string: "Float", type: "float" };
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,search": '<search><field name="display_name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree editable="top">
<field name="display_name"/>
<field name="float_field"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
if (args.method === "write") {
assert.deepEqual(args.args[1].timmy, [
[6, false, [12, 15]],
@ -895,7 +995,7 @@ QUnit.module("Fields", (hooks) => {
"new name",
"value of subrecord should have been updated"
);
assert.verifySteps(["read", "read"]);
assert.verifySteps(["get_views", "web_read"]);
// add new subrecords
await click(target.querySelector(".o_field_x2many_list_row_add a"));
@ -943,11 +1043,10 @@ QUnit.module("Fields", (hooks) => {
);
assert.verifySteps([
"get_views", // list view in dialog
"web_search_read", // list view in dialog
"read", // relational field (updated)
"write", // save main record
"read", // main record
"read", // relational field
"web_read", // relational field (updated)
"web_save", // save main record
]);
});
@ -1050,14 +1149,20 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<field name="timmy" widget="many2many" can_create="false" can_write="false"/>
<field name="timmy" widget="many2many" can_create="False" can_write="False"/>
</form>`,
mockRPC(route, args) {
if (route === "/web/dataset/call_kw/partner/create") {
assert.deepEqual(args.args[0], { timmy: [[6, false, [12]]] });
if (
route === "/web/dataset/call_kw/partner/web_save" &&
args.args[0].length === 0
) {
assert.deepEqual(args.args[1], { timmy: [[4, 12]] });
}
if (route === "/web/dataset/call_kw/partner/write") {
assert.deepEqual(args.args[1], { timmy: [[6, false, []]] });
if (
route === "/web/dataset/call_kw/partner/web_save" &&
args.args[0].length !== 0
) {
assert.deepEqual(args.args[1], { timmy: [[3, 12]] });
}
},
});
@ -1377,7 +1482,10 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("many2many list: list of id as default value", async function (assert) {
serverData.models.partner.fields.turtles.default = [2, 3];
serverData.models.partner.fields.turtles.default = [
[4, 2],
[4, 3],
];
serverData.models.partner.fields.turtles.type = "many2many";
await makeView({
@ -1401,6 +1509,49 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test(
"context and domain dependent on an x2m must contain the list of current ids for the x2m",
async function (assert) {
assert.expect(2);
serverData.models.partner.fields.turtles.default = [
[4, 2],
[4, 3],
];
serverData.models.partner.fields.turtles.type = "many2many";
serverData.views = {
"turtle,false,list": '<tree><field name="display_name"/></tree>',
"turtle,false,search": '<search><field name="display_name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="turtles" context="{'test': turtles}" domain="[('id', 'in', turtles)]">
<tree>
<field name="turtle_foo"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
if (args.method === "web_search_read") {
assert.deepEqual(args.kwargs.domain, [
"&",
["id", "in", [2, 3]],
"!",
["id", "in", [2, 3]],
]);
assert.deepEqual(args.kwargs.context.test, [2, 3]);
}
},
});
await addRow(target);
}
);
QUnit.test("many2many list with x2many: add a record", async function (assert) {
serverData.models.partner_type.fields.m2m = {
string: "M2M",
@ -1428,10 +1579,7 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
mockRPC(route, args) {
if (args.method !== "get_views") {
assert.step(_.last(route.split("/")) + " on " + args.model);
}
if (args.model === "turtle") {
assert.step(JSON.stringify(args.args[0])); // the read ids
assert.step(route.split("/").at(-1) + " on " + args.model);
}
},
});
@ -1466,19 +1614,11 @@ QUnit.module("Fields", (hooks) => {
);
assert.verifySteps([
"read on partner",
"web_read on partner",
"web_search_read on partner_type",
"read on turtle",
"[1,2,3]",
"read on partner_type",
"read on turtle",
"[1,2]",
"web_read on partner_type",
"web_search_read on partner_type",
"read on turtle",
"[2,3]",
"read on partner_type",
"read on turtle",
"[2,3]",
"web_read on partner_type",
]);
});
@ -1537,20 +1677,71 @@ QUnit.module("Fields", (hooks) => {
assert.step(args.method);
},
});
assert.verifySteps(["get_views", "read", "read"]);
assert.verifySteps(["get_views", "web_read"]);
await click($(target).find("td.o_data_cell:first")[0]);
assert.verifySteps(["get_views", "read"]);
await click(target.querySelector("td.o_data_cell"));
assert.verifySteps(["get_views", "web_read"]);
await click($('.modal-body input[type="checkbox"]')[0]);
await click($(".modal .modal-footer .btn-primary").first()[0]);
assert.verifySteps(["write", "onchange", "read"]);
await click(target.querySelector(".modal-body input[type=checkbox]"));
await click(target.querySelector(".modal .modal-footer .btn-primary"));
assert.verifySteps(["web_save"]);
// there is nothing left to save -> should not do a 'write' RPC
await clickSave(target);
assert.verifySteps([]);
});
QUnit.test("many2many concurrency edition", async function (assert) {
serverData.models.partner.fields.turtles.type = "many2many";
serverData.models.partner.onchanges.turtles = function () {};
serverData.models.turtle.records.push({
id: 4,
display_name: "Bloop",
turtle_bar: true,
turtle_foo: "Bloop",
partner_ids: [],
});
serverData.models.partner.records[0].turtles = [1, 2, 3, 4];
serverData.views = {
"turtle,false,list": '<tree><field name="display_name"/></tree>',
"turtle,false,search": '<search><field name="display_name" string="Name"/></search>',
};
const def = new Deferred();
let firstOnChange = false;
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="turtles">
<tree>
<field name="turtle_foo"/>
</tree>
</field>
</form>`,
resId: 1,
mockRPC: async (route, args) => {
if (args.method === "onchange") {
if (!firstOnChange) {
firstOnChange = true;
await def;
}
}
},
});
assert.containsN(target, ".o_data_row", 4);
await click(target.querySelector(".o_data_row .o_list_record_remove"));
await click(target.querySelector(".o_data_row .o_list_record_remove"));
await click(target, ".o_field_x2many_list_row_add a");
await click(target.querySelectorAll(".modal .o_data_row td.o_data_cell")[0]);
def.resolve();
await nextTick();
assert.containsN(target, ".o_data_row", 3);
});
QUnit.test(
"many2many widget: creates a new record with a context containing the parentID",
async function (assert) {
@ -1584,13 +1775,18 @@ QUnit.module("Fields", (hooks) => {
{},
[],
{
turtle_trululu: "",
turtle_foo: {},
turtle_trululu: {
fields: {
display_name: {},
},
},
},
]);
}
},
});
assert.verifySteps(["get_views", "read", "read"]);
assert.verifySteps(["get_views", "web_read"]);
await addRow(target);
assert.verifySteps(["get_views", "web_search_read"]);
@ -1607,10 +1803,10 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("onchange with 40+ commands for a many2many", async function (assert) {
// this test ensures that the basic_model correctly handles more LINK_TO
// commands than the limit of the dataPoint (40 for x2many kanban)
assert.expect(25);
assert.expect(20);
// create a lot of partner_types that will be linked by the onchange
var commands = [[5]];
const commands = [];
for (var i = 0; i < 45; i++) {
var id = 100 + i;
serverData.models.partner_type.records.push({ id: id, display_name: "type " + id });
@ -1643,22 +1839,20 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
mockRPC(route, args) {
assert.step(args.method);
if (args.method === "write") {
assert.strictEqual(args.args[1].timmy[0][0], 6, "should send a command 6");
assert.strictEqual(
args.args[1].timmy[0][2].length,
45,
"should replace with 45 ids"
if (args.method === "web_save") {
assert.deepEqual(
args.args[1].timmy,
commands.map((c) => [c[0], c[1]]),
"should send all commands"
);
}
},
});
assert.verifySteps(["get_views", "read"]);
assert.verifySteps(["get_views", "web_read"]);
await editInput(target, ".o_field_widget[name=foo] input", "trigger onchange");
assert.verifySteps(["onchange", "read"]);
assert.verifySteps(["onchange"]);
assert.strictEqual(
$(target).find(".o_x2m_control_panel .o_pager_counter").text().trim(),
"1-40 / 45",
@ -1669,9 +1863,8 @@ QUnit.module("Fields", (hooks) => {
40,
"there should be 40 records displayed on page 1"
);
await click($(target).find(".o_field_widget[name=timmy] .o_pager_next")[0]);
assert.verifySteps(["read"]);
assert.verifySteps([]);
assert.strictEqual(
$(target).find(".o_x2m_control_panel .o_pager_counter").text().trim(),
"41-45 / 45",
@ -1720,21 +1913,15 @@ QUnit.module("Fields", (hooks) => {
"there should be 40 records displayed on page 1"
);
assert.verifySteps(["write", "read", "read", "read"]);
assert.verifySteps(["web_save", "web_read"]);
});
QUnit.test("default_get, onchange, onchange on m2m", async function (assert) {
assert.expect(1);
serverData.models.partner.onchanges.int_field = function (obj) {
if (obj.int_field === 2) {
assert.deepEqual(obj.timmy, [
[6, false, [12]],
[1, 12, { display_name: "gold" }],
]);
}
obj.timmy = [[5], [1, 12, { display_name: "gold" }]];
};
serverData.models.partner.onchanges.int_field = function () {};
let firstOnChange = true;
await makeView({
type: "form",
@ -1751,14 +1938,30 @@ QUnit.module("Fields", (hooks) => {
<field name="int_field"/>
</sheet>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
if (firstOnChange) {
firstOnChange = false;
return {
value: {
timmy: [[1, 12, { display_name: "gold" }]],
},
};
} else {
assert.deepEqual(args.args[1], {
display_name: false,
int_field: 2,
timmy: [[1, 12, { display_name: "gold" }]],
});
}
}
},
});
await editInput(target, ".o_field_widget[name=int_field] input", 2);
});
QUnit.test("many2many list add *many* records, remove, re-add", async function (assert) {
assert.expect(5);
serverData.models.partner.fields.timmy.domain = [["color", "=", 2]];
serverData.models.partner.fields.timmy.onChange = true;
serverData.models.partner_type.fields.product_ids = {
@ -1767,8 +1970,8 @@ QUnit.module("Fields", (hooks) => {
relation: "product",
};
for (var i = 0; i < 50; i++) {
var new_record_partner_type = { id: 100 + i, display_name: "batch" + i, color: 2 };
for (let i = 0; i < 50; i++) {
const new_record_partner_type = { id: 100 + i, display_name: "batch" + i, color: 2 };
serverData.models.partner_type.records.push(new_record_partner_type);
}
@ -1806,7 +2009,7 @@ QUnit.module("Fields", (hooks) => {
// First round: add 51 records in batch
await click(target.querySelector(".o_field_x2many_list_row_add a"));
var $modal = $(".modal-lg");
let $modal = $(".modal-lg");
assert.equal($modal.length, 1, "There should be one modal");
@ -1821,26 +2024,43 @@ QUnit.module("Fields", (hooks) => {
"We should have added all the records present in the search view to the m2m field"
); // the 50 in batch + 'gold'
assert.containsNone(
target,
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_cp_pager",
"pager should not be displayed"
);
await clickSave(target);
assert.containsOnce(
target,
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_cp_pager",
"pager should not be displayed"
);
const pagerValue = target.querySelector(
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_pager_value"
);
assert.strictEqual(pagerValue.textContent, "1-40", "The pager should be updated.");
// Secound round: remove one record
var trash_buttons = $(target).find(
const trash_buttons = $(target).find(
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_list_record_remove"
);
await click(trash_buttons.first()[0]);
var pager_limit = $(target).find(
const pager_limit = $(target).find(
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_pager_limit"
);
assert.equal(pager_limit.text(), "50", "We should have 50 records in the m2m field");
assert.strictEqual(pager_limit.text(), "50", "We should have 50 records in the m2m field");
// Third round: re-add 1 records
await click($(target).find(".o_field_x2many_list_row_add a")[0]);
$modal = $(".modal-lg");
assert.equal($modal.length, 1, "There should be one modal");
assert.strictEqual($modal.length, 1, "There should be one modal");
await click($modal.find("thead input[type=checkbox]")[0]);
await nextTick();
@ -1849,8 +2069,8 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
$(target).find(".o_data_row").length,
51,
"We should have 51 records in the m2m field"
41,
"We should have 41 records in the m2m field"
);
});
@ -1931,12 +2151,13 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("many2many basic keys in field evalcontext -- in list", async (assert) => {
assert.expect(6);
assert.expect(5);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.models.partner.records.push({ id: 7, display_name: "default partner" });
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
@ -1964,13 +2185,12 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<tree editable="top">
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': uid, 'allowed_company_ids': allowed_company_ids, 'company_id': current_company_id}"/>
</tree>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.uid, 7);
assert.deepEqual(args.kwargs.context.allowed_company_ids, [3]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
@ -1978,22 +2198,22 @@ QUnit.module("Fields", (hooks) => {
await click(target.querySelector(".o_data_cell"));
await editInput(target, ".o_field_many2many_selection input", "indianapolis");
await nextTick();
await clickOpenedDropdownItem(target, "timmy", "Create and edit...");
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
"default partner"
);
});
QUnit.test("many2many basic keys in field evalcontext -- in form", async (assert) => {
assert.expect(6);
assert.expect(5);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.models.partner.records.push({ id: 7, display_name: "default partner" });
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
@ -2022,13 +2242,12 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': uid, 'allowed_company_ids': allowed_company_ids, 'company_id': current_company_id}"/>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.default_partner_id, 7);
assert.deepEqual(args.kwargs.context.allowed_company_ids, [3]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
@ -2040,19 +2259,20 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
"default partner"
);
});
QUnit.test(
"many2many basic keys in field evalcontext -- in a x2many in form",
async (assert) => {
assert.expect(6);
assert.expect(5);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.models.partner.records.push({ id: 7, display_name: "default partner" });
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
@ -2085,15 +2305,14 @@ QUnit.module("Fields", (hooks) => {
<form>
<field name="p">
<tree editable="top">
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': uid, 'allowed_company_ids': allowed_company_ids, 'company_id': current_company_id}"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.default_partner_id, 7);
assert.deepEqual(args.kwargs.context.allowed_company_ids, [3]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
@ -2105,73 +2324,44 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
"default partner"
);
}
);
QUnit.test("many2many field calling replaceWith (add + remove)", async function (assert) {
serverData.models.partner.records[0].p = [1];
QUnit.test(
"`this` inside rendererProps should reference the component",
async function (assert) {
class CustomX2manyField extends X2ManyField {
setup() {
super.setup();
this.selectCreate = (params) => {
assert.step("selectCreate");
assert.strictEqual(this.num, 2);
};
this.num = 1;
}
class MyX2Many extends Component {
onClick() {
this.props.value.replaceWith([2, 3]);
async onAdd({ context, editable } = {}) {
this.num = 2;
assert.step("onAdd");
super.onAdd(...arguments);
}
}
}
MyX2Many.template = xml`
<span class="ids" t-esc="this.props.value.resIds"/>
<button class="my_btn" t-on-click="onClick">To id</button>`;
registry.category("fields").add("my_x2many", MyX2Many);
const customX2ManyField = {
...x2ManyField,
component: CustomX2manyField,
};
registry.category("fields").add("custom", customX2ManyField);
await makeView({
type: "form",
resModel: "turtle",
serverData,
arch: `
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="partner_ids" widget="my_x2many"/>
</form>`,
resId: 2,
});
assert.strictEqual(target.querySelector(".ids").innerText, "2,4");
await click(target.querySelector(".my_btn"));
assert.strictEqual(target.querySelector(".ids").innerText, "2,3");
});
QUnit.test("`this` inside rendererProps should reference the component", async function (assert) {
class CustomX2manyField extends X2ManyField {
setup() {
super.setup();
this.selectCreate = (params) => {
assert.step("selectCreate");
assert.strictEqual(this.num, 2);
};
this.num = 1;
}
async onAdd({ context, editable } = {}) {
this.num = 2;
assert.step("onAdd");
super.onAdd(...arguments);
}
}
registry.category("fields").add("custom_x2many", CustomX2manyField);
serverData.views = {
"partner_type,false,list": `<tree><field name="display_name"/></tree>`,
"partner_type,false,search": `<search></search>`,
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="custom_x2many">
<field name="timmy" widget="custom">
<tree editable="top">
<field name="display_name"/>
</tree>
@ -2180,9 +2370,10 @@ QUnit.module("Fields", (hooks) => {
</form>
</field>
</form>`,
resId: 1,
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
assert.verifySteps(["onAdd", "selectCreate"]);
});
resId: 1,
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
assert.verifySteps(["onAdd", "selectCreate"]);
}
);
});

View file

@ -1,6 +1,16 @@
/** @odoo-module **/
import { click, clickSave, getFixture, selectDropdownItem } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
import {
click,
clickSave,
getFixture,
patchWithCleanup,
selectDropdownItem,
triggerEvent,
editInput,
clickOpenedDropdownItem,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { triggerHotkey } from "../../helpers/utils";
@ -59,13 +69,13 @@ QUnit.module("Fields", (hooks) => {
assert.containsN(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_avatar img",
2,
"should have 2 records"
);
assert.strictEqual(
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .badge img").dataset
.src,
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .o_avatar img")
.dataset.src,
"/web/image/partner/2/avatar_128",
"should have correct avatar image"
);
@ -116,13 +126,13 @@ QUnit.module("Fields", (hooks) => {
);
assert.containsN(
target,
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:not(.o_m2m_avatar_empty)",
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img",
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)",
".o_data_row:nth-child(3) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img",
5,
"should have 5 records"
);
@ -149,21 +159,21 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:nth-child(2) img.o_m2m_avatar"
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar: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"
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar: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"
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:nth-child(4) img.o_m2m_avatar"
).dataset.src,
"/web/image/partner/5/avatar_128",
"should have correct avatar image"
@ -175,7 +185,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.containsN(
target,
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_tag:not(.o_m2m_avatar_empty)",
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img",
4,
"should have 4 records"
);
@ -213,16 +223,20 @@ QUnit.module("Fields", (hooks) => {
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",
".o_data_row.o_selected_row .o_many2many_tags_avatar_cell .o_avatar img",
1,
"should have 1 many2many badges in edit mode"
);
await selectDropdownItem(target, "partner_ids", "second record");
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.containsN(
target,
".o_data_row:first-child .o_field_many2many_tags_avatar .o_tag",
".o_data_row:first-child .o_field_many2many_tags_avatar .o_avatar img",
2,
"should have 2 records"
);
@ -273,16 +287,14 @@ QUnit.module("Fields", (hooks) => {
);
QUnit.test("widget many2many_tags_avatar in kanban view", async function (assert) {
assert.expect(13);
assert.expect(21);
const records = [];
for (let id = 5; id <= 15; id++) {
records.push({
serverData.models.partner.records.push({
id,
display_name: `record ${id}`,
});
}
serverData.models.partner.records = serverData.models.partner.records.concat(records);
serverData.models.turtle.records.push({
id: 4,
@ -294,6 +306,8 @@ QUnit.module("Fields", (hooks) => {
serverData.models.turtle.records[2].partner_ids = [1, 2, 4, 5];
serverData.views = {
"turtle,false,form": '<form><field name="display_name"/></form>',
"partner,false,list": '<tree><field name="display_name"/></tree>',
"partner,false,search": "<search/>",
};
await makeView({
@ -325,23 +339,21 @@ QUnit.module("Fields", (hooks) => {
);
},
});
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.containsOnce(
target,
".o_kanban_record:first-child .o_field_many2many_tags_avatar .o_quick_assign",
"should have the assign icon"
);
assert.containsN(
target,
".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag",
3,
"should have 3 records"
".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_avatar img",
2,
"should have 2 records"
);
assert.containsN(
target,
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_tag",
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_avatar img",
2,
"should have 2 records"
);
@ -349,14 +361,14 @@ QUnit.module("Fields", (hooks) => {
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",
"/web/image/partner/5/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",
"/web/image/partner/4/avatar_128",
"should have correct avatar image"
);
assert.containsOnce(
@ -376,7 +388,7 @@ QUnit.module("Fields", (hooks) => {
assert.containsN(
target,
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_tag",
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_avatar img",
2,
"should have 2 records"
);
@ -394,28 +406,94 @@ QUnit.module("Fields", (hooks) => {
"9+",
"should have 9+ in o_m2m_avatar_empty"
);
assert.containsNone(target, ".o_field_many2many_tags_avatar .o_field_many2many_selection");
// 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"
const o_kanban_record = target.querySelector(".o_kanban_record:nth-child(2)");
await click(o_kanban_record, ".o_field_tags > .o_m2m_avatar_empty");
const popover = document.querySelector(".o-overlay-container");
assert.strictEqual(
document.activeElement,
popover.querySelector("input"),
"the input inside the popover should have the focus"
);
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 3, "Should have 3 tags");
// delete inside the popover
await click(popover.querySelector(".o_tag .o_delete"));
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 2, "Should have 2 tag");
assert.strictEqual(
o_kanban_record.querySelectorAll(".o_tag").length,
2,
"Should have 2 tags"
);
// select first input
await click(popover.querySelector(".o-autocomplete--dropdown-item"));
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 3, "Should have 3 tags");
assert.strictEqual(
o_kanban_record.querySelectorAll(".o_tag").length,
2,
"Should have 2 tags"
);
// load more
await click(popover.querySelector(".o_m2o_dropdown_option_search_more"));
// first item
await click(document.querySelector(".o_dialog .o_list_table .o_data_row .o_data_cell"));
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 4, "Should have 4 tags");
assert.strictEqual(
o_kanban_record.querySelectorAll(".o_tag").length,
2,
"Should have 2 tags"
);
assert.strictEqual(
tag.dataset["tooltipTemplate"],
"web.TagsList.Tooltip",
"uses the proper tooltip template"
o_kanban_record.querySelector("img.o_m2m_avatar").dataset.src,
"/web/image/partner/5/avatar_128",
"should have correct avatar image"
);
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 add/remove tags in kanban view",
async function (assert) {
assert.expect(3);
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>`,
async mockRPC(route, { method, args }) {
if (method === "web_save") {
const command = args[1].partner_ids[0];
assert.step(`web_save: ${command[0]}-${command[1]}`);
}
},
});
await click(target, ".o_kanban_record:first-child .o_quick_assign");
// add and directly remove an item
await click(target, ".o_popover .o-autocomplete--dropdown-item:first-child");
await click(target, ".o_popover .o_tag .o_delete");
assert.verifySteps(["web_save: 4-1", "web_save: 3-1"]);
}
);
QUnit.test("widget many2many_tags_avatar delete tag", async function (assert) {
await makeView({
type: "form",
@ -432,25 +510,192 @@ QUnit.module("Fields", (hooks) => {
assert.containsN(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_tag",
2,
"should have 2 records"
);
await click(
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .badge .o_delete")
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .o_tag .o_delete")
);
assert.containsOnce(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_tag",
"should have 1 record"
);
await clickSave(target);
assert.containsOnce(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_tag",
"should have 1 record"
);
});
QUnit.test(
"widget many2many_tags_avatar quick add tags and close in kanban view with keyboard navigation",
async function (assert) {
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>`,
});
await click(target, ".o_kanban_record:first-child .o_quick_assign");
// add and directly close the dropdown
await triggerEvent(target, null, "keydown", { key: "Tab" });
await triggerEvent(document.activeElement, null, "keydown", { key: "Enter" });
await triggerEvent(target, null, "keydown", { key: "Escape" });
assert.containsOnce(
target,
".o_kanban_record:first-child .o_field_many2many_tags_avatar .o_tag",
"should assign the user"
);
assert.containsNone(
target,
".o_kanban_record:first-child .o_field_many2many_tags_avatar .o_popover",
"should have close the popover"
);
}
);
QUnit.test(
"widget many2many_tags_avatar in kanban view missing access rights",
async function (assert) {
assert.expect(1);
await makeView({
type: "kanban",
resModel: "turtle",
serverData,
arch: `
<kanban edit="0" create="0">
<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>`,
});
assert.containsNone(
target,
".o_kanban_record:first-child .o_field_many2many_tags_avatar .o_quick_assign",
"should not have the assign icon"
);
}
);
QUnit.test("widget many2many_tags_avatar", async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await makeView({
type: "form",
resModel: "turtle",
serverData,
arch: `
<form>
<sheet>
<field name="partner_ids" widget="many2many_tags_avatar"/>
</sheet>
</form>`,
resId: 1,
});
assert.deepEqual(
[...target.querySelectorAll("[name='partner_ids'] .o_tag")].map((el) => el.textContent),
[]
);
assert.strictEqual(
target.querySelector("[name='partner_ids'] .o_input_dropdown input").value,
""
);
await editInput(target, "[name='partner_ids'] .o_input_dropdown input", "first record");
await triggerEvent(target, "[name='partner_ids'] .o_input_dropdown input", "keydown", {
key: "Enter",
});
assert.deepEqual(
[...target.querySelectorAll("[name='partner_ids'] .o_tag")].map((el) => el.textContent),
["first record"]
);
assert.strictEqual(
target.querySelector("[name='partner_ids'] .o_input_dropdown input").value,
""
);
await editInput(target, "[name='partner_ids'] .o_input_dropdown input", "abc");
await triggerEvent(target, "[name='partner_ids'] .o_input_dropdown input", "keydown", {
key: "Enter",
});
assert.deepEqual(
[...target.querySelectorAll("[name='partner_ids'] .o_tag")].map((el) => el.textContent),
["first record", "abc"]
);
assert.strictEqual(
target.querySelector("[name='partner_ids'] .o_input_dropdown input").value,
""
);
});
QUnit.test(
"Many2ManyTagsAvatarField: make sure that the arch context is passed to the form view call",
async function (assert) {
serverData.views = {
"partner,false,form": `<form><field name="display_name"/></form>`,
};
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await makeView({
type: "list",
resModel: "turtle",
serverData,
arch: `<list editable="top">
<field name="partner_ids" widget="many2many_tags_avatar" context="{ 'append_coucou': 'test_value' }"/>
</list>`,
mockRPC(route, args) {
if (args.method === "onchange" && args.model === "partner") {
if (args.kwargs.context.append_coucou === "test_value") {
assert.step("onchange with context given");
}
}
},
});
await click(target.querySelector("div[name=partner_ids]"));
await editInput(target, `div[name="partner_ids"] input`, "A new partner");
await clickOpenedDropdownItem(target, "partner_ids", "Create and edit...");
assert.containsOnce(target, ".modal .o_form_view", "Here we should have opened the modal form view");
assert.verifySteps(["onchange with context given"]);
}
);
});

View file

@ -1,9 +1,10 @@
/** @odoo-module **/
import { makeServerError } from "@web/../tests/helpers/mock_server";
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
import { browser } from "@web/core/browser/browser";
import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
import {
addRow,
click,
clickDiscard,
clickDropdown,
@ -19,7 +20,6 @@ import {
triggerEvent,
triggerHotkey,
} from "@web/../tests/helpers/utils";
import { RPCError } from "@web/core/network/rpc_service";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
@ -38,7 +38,6 @@ QUnit.module("Fields", (hooks) => {
string: "one2many turtle field",
type: "one2many",
relation: "turtle",
relation_field: "turtle_trululu",
},
timmy: { string: "pokemon", type: "many2many", relation: "partner_type" },
},
@ -113,13 +112,14 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("Many2ManyTagsField");
QUnit.test("Many2ManyTagsField with and without color", async function (assert) {
assert.expect(12);
assert.expect(14);
serverData.models.partner.fields.partner_ids = {
string: "Partner",
type: "many2many",
relation: "partner",
};
serverData.models.partner.fields.color = { string: "Color index", type: "integer" };
await makeView({
type: "form",
@ -130,17 +130,19 @@ QUnit.module("Fields", (hooks) => {
<field name="partner_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="timmy" widget="many2many_tags"/>
</form>`,
mockRPC: (route, { args, method, model }) => {
if (method === "read" && model === "partner_type") {
mockRPC: (route, { args, method, model, kwargs }) => {
if (method === "web_read" && model === "partner_type") {
assert.deepEqual(args, [[12]]);
assert.deepEqual(
args,
[[12], ["display_name"]],
kwargs.specification,
{ display_name: {} },
"should not read any color field"
);
} else if (method === "read" && model === "partner") {
} else if (method === "web_read" && model === "partner") {
assert.deepEqual(args, [[1]]);
assert.deepEqual(
args,
[[1], ["display_name", "color"]],
kwargs.specification,
{ display_name: {}, color: {} },
"should read color field"
);
}
@ -163,8 +165,8 @@ QUnit.module("Fields", (hooks) => {
const autocomplete = target.querySelector("[name='timmy'] .o-autocomplete.dropdown");
assert.strictEqual(
autocomplete.querySelectorAll("li").length,
3,
"autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')"
4,
"autocomplete dropdown should have 4 entries (2 values + 'Search More...' + 'Search and Edit...')"
);
await clickOpenedDropdownItem(target, "timmy", "gold");
assert.containsOnce(target, "[name=timmy] .o_tag");
@ -182,7 +184,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("Many2ManyTagsField with color: rendering and edition", async function (assert) {
assert.expect(26);
assert.expect(24);
serverData.models.partner.records[0].timmy = [12, 14];
serverData.models.partner_type.records.push({ id: 13, display_name: "red", color: 8 });
@ -195,22 +197,22 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True }"/>
</form>`,
resId: 1,
mockRPC: (route, { args, method, model }) => {
if (route === "/web/dataset/call_kw/partner/write") {
mockRPC: (route, { args, method, model, kwargs }) => {
if (route === "/web/dataset/call_kw/partner/web_save") {
var commands = args[1].timmy;
assert.strictEqual(commands.length, 1, "should have generated one command");
assert.strictEqual(
commands[0][0],
6,
"generated command should be REPLACE WITH"
);
assert.deepEqual(commands[0][2], [12, 13], "new value should be [12, 13]");
}
if (method === "read" && model === "partner_type") {
assert.strictEqual(commands.length, 2, "should have generated two commands");
assert.strictEqual(commands.map((cmd) => cmd[0]).join("-"), "4-3");
assert.deepEqual(
args[1],
["display_name", "color"],
"should read the color field"
commands.map((cmd) => cmd[1]),
[13, 14],
"Should add 13, remove 14"
);
}
if ((method === "web_read" || method === "web_save") && model === "partner_type") {
assert.deepEqual(
kwargs.specification,
{ display_name: {}, color: {} },
"should read color field"
);
}
},
@ -245,8 +247,8 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
autocompleteDropdown.querySelectorAll("li").length,
2,
"autocomplete dropdown should have 2 entry"
3,
"autocomplete dropdown should have 3 entry"
);
assert.strictEqual(
@ -349,14 +351,14 @@ QUnit.module("Fields", (hooks) => {
assert.containsNone(target, ".badge.dropdown-toggle", "the tags should not be dropdowns");
// click on the tag: should do nothing and open the form view
click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
await click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
assert.verifySteps(["selectRecord"]);
await nextTick();
assert.containsNone(target, ".o_colorlist");
await click(target.querySelectorAll(".o_list_record_selector")[1]);
click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
await click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
assert.verifySteps(["selectRecord"]);
await nextTick();
@ -384,14 +386,14 @@ QUnit.module("Fields", (hooks) => {
assert.containsNone(target, ".badge.dropdown-toggle", "the tags should not be dropdowns");
// click on the tag: should do nothing and open the form view
click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
await click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
assert.verifySteps(["selectRecord"]);
await nextTick();
assert.containsNone(target, ".o_colorlist");
await click(target.querySelectorAll(".o_list_record_selector")[1]);
click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
await click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
assert.verifySteps([]);
await nextTick();
@ -439,8 +441,8 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
autocompleteDropdown.querySelectorAll("li").length,
2,
"autocomplete dropdown should have 2 entry"
3,
"autocomplete dropdown should have 3 entries"
);
assert.strictEqual(
@ -464,6 +466,116 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("use binary field as the domain", async (assert) => {
serverData.models.partner.fields.domain = { string: "Domain", type: "binary" };
serverData.models.partner.records[0].domain = [["id", "<", 50]];
serverData.models.partner.records[0].timmy = [12];
serverData.models.partner_type.records.push({ id: 99, display_name: "red", color: 8 });
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_tags" domain="domain"/>
<field name="domain" invisible="1"/>
</form>`,
resId: 1,
});
assert.containsOnce(target, ".o_field_many2many_tags .badge", "should contain 1 tag");
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".badge")),
["gold"],
"should have fetched and rendered gold partner tag"
);
await clickDropdown(target, "timmy");
const autocompleteDropdown = target.querySelector(".o-autocomplete--dropdown-menu");
assert.strictEqual(
autocompleteDropdown.querySelectorAll("li").length,
3,
"autocomplete dropdown should have 3 entries"
);
assert.deepEqual(
getNodesTextContent(autocompleteDropdown.querySelectorAll("li")),
["silver", "Search More...", "Start typing..."],
"should contain newly added tag 'silver'"
);
assert.strictEqual(
autocompleteDropdown.querySelector("li a").textContent,
"silver",
"autocomplete dropdown should contain 'silver'"
);
await clickOpenedDropdownItem(target, "timmy", "silver");
assert.strictEqual(
target.querySelectorAll(".o_field_many2many_tags .badge").length,
2,
"should contain 2 tags"
);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".badge")),
["gold", "silver"],
"should contain newly added tag 'silver'"
);
});
QUnit.test("Domain: allow python code domain in fieldInfo", async function (assert) {
assert.expect(4);
serverData.models.partner.fields.timmy.domain =
"foo and [('color', '>', 3)] or [('color', '<', 3)]";
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="foo"/>
<field name="timmy" widget="many2many_tags"></field>
</form>`,
resId: 1,
});
// foo set => only silver (id=5) selectable
await clickDropdown(target, "timmy");
let autocompleteDropdown = target.querySelector(".o-autocomplete--dropdown-menu");
assert.containsN(
autocompleteDropdown,
"li",
3,
"autocomplete should contain 'silver'm 'Search More...' and 'Start typing...' options"
);
assert.strictEqual(
autocompleteDropdown.querySelector("li a").textContent,
"silver",
"autocomplete dropdown should contain 'silver'"
);
await clickOpenedDropdownItem(target, "timmy", "Start typing...");
// set foo = "" => only gold (id=2) selectable
const textInput = target.querySelector("[name=foo] input");
textInput.focus();
await editInput(textInput, null, "");
await clickDropdown(target, "timmy");
autocompleteDropdown = target.querySelector(".o-autocomplete--dropdown-menu");
assert.containsN(
autocompleteDropdown,
"li",
3,
"autocomplete should contain 'gold'm 'Search More...' and 'Start typing...' options"
);
assert.strictEqual(
autocompleteDropdown.querySelector("li a").textContent,
"gold",
"autocomplete dropdown should contain 'gold'"
);
});
QUnit.test("Many2ManyTagsField in a new record", async function (assert) {
assert.expect(7);
@ -473,15 +585,11 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: '<form><field name="timmy" widget="many2many_tags"/></form>',
mockRPC: (route, { args }) => {
if (route === "/web/dataset/call_kw/partner/create") {
var commands = args[0].timmy;
if (route === "/web/dataset/call_kw/partner/web_save") {
const commands = args[1].timmy;
assert.strictEqual(commands.length, 1, "should have generated one command");
assert.strictEqual(
commands[0][0],
6,
"generated command should be REPLACE WITH"
);
assert.ok(_.isEqual(commands[0][2], [12]), "new value should be [12]");
assert.strictEqual(commands[0][0], 4, "generated command should be LINK TO");
assert.strictEqual(commands[0][1], 12, "new value should be 12");
}
},
});
@ -495,8 +603,8 @@ QUnit.module("Fields", (hooks) => {
const autocomplete = target.querySelector("[name='timmy'] .o-autocomplete.dropdown");
assert.strictEqual(
autocomplete.querySelectorAll("li").length,
3,
"autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')"
4,
"autocomplete dropdown should have 4 entries (2 values + 'Search More...' + 'Search and Edit...')"
);
await clickOpenedDropdownItem(target, "timmy", "gold");
@ -524,7 +632,7 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_tags" options="{'color_field': 'color'}"/>
</form>`,
mockRPC: (route, { args, method }) => {
if (method === "write") {
if (method === "web_save") {
assert.step(JSON.stringify(args[1]));
}
},
@ -610,7 +718,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("Many2ManyTagsField in editable list", async function (assert) {
assert.expect(7);
assert.expect(5);
serverData.models.partner.records[0].timmy = [12];
@ -624,7 +732,7 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_tags"/>
</tree>`,
mockRPC: (route, { kwargs, method, model }) => {
if (method === "read" && model === "partner_type") {
if (method === "web_read" && model === "partner_type") {
assert.strictEqual(
kwargs.context.take,
"five",
@ -680,43 +788,10 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test(
"Many2ManyTagsField loads records according to limit defined on widget prototype",
async function (assert) {
patchWithCleanup(Many2ManyTagsField, {
limit: 30,
});
serverData.models.partner.fields.partner_ids = {
string: "Partner",
type: "many2many",
relation: "partner",
};
serverData.models.partner.records[0].partner_ids = [];
for (var i = 15; i < 50; i++) {
serverData.models.partner.records.push({ id: i, display_name: "walter" + i });
serverData.models.partner.records[0].partner_ids.push(i);
}
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: '<form><field name="partner_ids" widget="many2many_tags"/></form>',
resId: 1,
});
assert.strictEqual(
target.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length,
30,
"should have rendered 30 tags even though 35 records linked"
);
}
);
QUnit.test("Many2ManyTagsField keeps focus when being edited", async function (assert) {
serverData.models.partner.records[0].timmy = [12];
serverData.models.partner.onchanges.foo = function (obj) {
obj.timmy = [[5]]; // DELETE command
obj.timmy = [[3, 12]];
};
await makeView({
@ -1069,15 +1144,21 @@ QUnit.module("Fields", (hooks) => {
arch: '<form><field name="timmy" widget="many2many_tags"/></form>',
resId: 1,
mockRPC(route, args) {
if (args.method === "read" && args.model === "partner_type") {
assert.step(args.kwargs.context.hello);
if (args.method === "web_read" && args.model === "partner") {
assert.step(`${args.method} ${args.model}`);
assert.strictEqual(args.kwargs.specification.timmy.context.hello, "world");
}
if (args.method === "web_read" && args.model === "partner_type") {
assert.step(`${args.method} ${args.model}`);
assert.strictEqual(args.kwargs.context.hello, "world");
}
},
});
assert.verifySteps(["world"]);
assert.verifySteps(["web_read partner"]);
await selectDropdownItem(target, "timmy", "silver");
assert.verifySteps(["world"]);
assert.verifySteps(["web_read partner_type"]);
});
QUnit.test("Many2ManyTagsField: select multiple records", async function (assert) {
@ -1482,12 +1563,10 @@ QUnit.module("Fields", (hooks) => {
arch: '<form><field name="timmy" widget="many2many_tags"/></form>',
mockRPC(route, args) {
if (args.method === "name_create") {
const error = new RPCError("Something went wrong");
error.exceptionName = "odoo.exceptions.ValidationError";
throw error;
throw makeServerError({ type: "ValidationError" });
}
if (args.method === "create") {
assert.deepEqual(args.args[0], {
if (args.method === "web_save") {
assert.deepEqual(args.args[1], {
color: 8,
name: "new partner",
});
@ -1553,6 +1632,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<tree editable="bottom">
<field name="timmy" widget="many2many_tags"/>
<field name="name"/>
</tree>`,
});
@ -1620,8 +1700,7 @@ QUnit.module("Fields", (hooks) => {
type: "form",
resModel: "partner",
serverData,
arch:
'<form><field name="timmy" widget="many2many_tags" placeholder="Placeholder"/></form>',
arch: '<form><field name="timmy" widget="many2many_tags" placeholder="Placeholder"/></form>',
});
assert.strictEqual(
@ -1649,7 +1728,7 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
target.querySelector(".o_field_many2many_tags .o-autocomplete--dropdown-menu")
.textContent,
"goldsilver"
"goldsilverSearch More..."
);
});
@ -1674,6 +1753,52 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(target, "[name='timmy'].o_field_invalid");
});
QUnit.test("set a required many2many_tags and save directly", async function (assert) {
let def;
const form = await makeView({
type: "form",
resModel: "partner",
serverData,
arch: '<form><field name="timmy" widget="many2many_tags" required="1"/></form>',
async mockRPC(route, args) {
assert.step(args.method);
if (args.method === "web_read") {
await def;
}
},
});
patchWithCleanup(form.env.services.notification, {
add: () => assert.step("notification"),
});
assert.verifySteps(["get_views", "onchange"]);
assert.containsNone(target, ".o_tag");
def = makeDeferred();
await clickDropdown(target, "timmy");
await clickOpenedDropdownItem(target, "timmy", "gold");
assert.containsOnce(target, ".o_tag");
assert.strictEqual(
target.querySelector(".o_tag").textContent,
"",
"The tag is displayed, but the web read is not finished yet"
);
assert.verifySteps(["name_search", "web_read"]);
await clickSave(target);
assert.doesNotHaveClass(target, "[name='timmy']", "o_field_invalid");
assert.verifySteps([]);
def.resolve();
await nextTick();
assert.strictEqual(target.querySelector(".o_tag").textContent, "gold");
assert.verifySteps(["web_save"]);
});
QUnit.test("Many2ManyTagsField with option 'no_quick_create' set to true", async (assert) => {
serverData.views = {
"partner_type,false,form": `<form><field name="name"/><field name="color"/></form>`,
@ -1767,7 +1892,7 @@ QUnit.module("Fields", (hooks) => {
arch: `<form><field name="timmy" widget="many2many_tags" context="{ 'append_coucou': True }"/></form>`,
async mockRPC(route, args, performRPC) {
const result = await performRPC(route, args);
if (args.method === "read") {
if (args.method === "web_read") {
if (args.kwargs.context.append_coucou) {
assert.step("read with context given");
result[0].display_name += " coucou";
@ -1799,7 +1924,7 @@ QUnit.module("Fields", (hooks) => {
arch: `<list editable="top"><field name="timmy" widget="many2many_tags" context="{ 'append_coucou': True }"/></list>`,
async mockRPC(route, args, performRPC) {
const result = await performRPC(route, args);
if (args.method === "read") {
if (args.method === "web_read") {
if (args.kwargs.context.append_coucou) {
assert.step("read with context given");
result[0].display_name += " coucou";
@ -1823,4 +1948,89 @@ QUnit.module("Fields", (hooks) => {
assert.verifySteps(["name search with context given", "read with context given"]);
assert.strictEqual(target.querySelector(".o_field_tags").innerText, "gold coucou");
});
QUnit.test("Many2ManyTagsField doesn't use virtualId for 'name_search'", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: `<form>
<field name="turtles" widget="many2many_tags"/>
<field name="turtles">
<tree>
<field name="display_name"/>
</tree>
<form>
<field name="display_name"/>
</form>
</field>
</form>`,
async mockRPC(route, { method, kwargs }) {
if (method === "name_search") {
assert.step("name_search");
// no virtualId in domain
assert.deepEqual(kwargs.args, ["!", ["id", "in", [2]]]);
}
},
});
await addRow(target);
assert.containsOnce(target, ".modal");
await editInput(target, ".modal [name='display_name'] input", "yop");
await click(target.querySelector(".modal .o_form_button_save"));
assert.containsNone(target, ".modal");
assert.deepEqual(
[...target.querySelectorAll("[name='turtles'] .o_tag_badge_text")].map(
(el) => el.textContent
),
["donatello", "yop"]
);
assert.deepEqual(
[...target.querySelectorAll("[name='turtles'] .o_data_row")].map(
(el) => el.textContent
),
["donatello", "yop"]
);
await click(target.querySelector("[name='turtles'] input"));
assert.verifySteps(["name_search"]);
});
QUnit.test(
"Many2ManyTagsField: quickly remove several tags with backspace",
async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
serverData.models.partner.onchanges.timmy = () => {};
const def = makeDeferred();
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_tags"/>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.step(`onchange ${JSON.stringify(args.args[1].timmy)}`);
return def;
}
},
resId: 1,
});
assert.containsN(target, ".o_field_many2many_tags .badge", 2);
target.querySelectorAll(".o_field_many2many_tags .badge")[1].focus();
triggerHotkey("BackSpace");
triggerHotkey("BackSpace");
def.resolve();
await nextTick();
assert.containsN(target, ".o_field_many2many_tags .badge", 1);
assert.verifySteps(["onchange [[3,14]]"]);
}
);
});

View file

@ -10,6 +10,7 @@ import {
selectDropdownItem,
triggerEvent,
clickDiscard,
clickOpenedDropdownItem,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { browser } from "@web/core/browser/browser";
@ -87,7 +88,7 @@ QUnit.module("Fields", (hooks) => {
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_field_many2one_avatar > div");
assert.containsOnce(target, ".o_input_dropdown");
assert.strictEqual(target.querySelector(".o_input_dropdown input").value, "Aline");
@ -186,7 +187,7 @@ QUnit.module("Fields", (hooks) => {
});
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id'] span span")),
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id']")),
["Aline", "Christine", "Aline", ""]
);
const imgs = target.querySelectorAll(".o_m2o_avatar > img");
@ -204,7 +205,7 @@ QUnit.module("Fields", (hooks) => {
});
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id'] span span")),
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id']")),
["Aline", "Christine", "Aline", ""]
);
@ -226,8 +227,7 @@ QUnit.module("Fields", (hooks) => {
type: "form",
resModel: "partner",
serverData,
arch:
'<form><field name="user_id" widget="many2one_avatar" placeholder="Placeholder"/></form>',
arch: '<form><field name="user_id" widget="many2one_avatar" placeholder="Placeholder"/></form>',
});
assert.strictEqual(
@ -255,7 +255,7 @@ QUnit.module("Fields", (hooks) => {
});
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"));
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id']"));
assert.hasClass(target.querySelector(".o_data_row"), "o_selected_row");
assert.verifySteps([]);
@ -280,7 +280,7 @@ QUnit.module("Fields", (hooks) => {
});
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"));
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id']"));
assert.hasClass(target.querySelector(".o_data_row"), "o_selected_row");
assert.verifySteps([]);
@ -304,12 +304,41 @@ QUnit.module("Fields", (hooks) => {
</tree>`,
});
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id'] span span"));
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id']"));
assert.containsNone(target, ".o_selected_row");
assert.verifySteps(["openRecord"]);
});
QUnit.test(
"readonly many2one_avatar in form view should contain a link",
async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `<form><field name="user_id" widget="many2one_avatar" readonly="1"/></form>`,
});
assert.containsOnce(target, "[name='user_id'] a");
}
);
QUnit.test(
"readonly many2one_avatar in list view should not contain a link",
async function (assert) {
await makeView({
type: "list",
serverData,
resModel: "partner",
arch: `<tree><field name="user_id" widget="many2one_avatar"/></tree>`,
});
assert.containsNone(target, "[name='user_id'] a");
}
);
QUnit.test("cancelling create dialog should clear value in the field", async function (assert) {
serverData.views = {
"user,false,form": `
@ -332,11 +361,171 @@ QUnit.module("Fields", (hooks) => {
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 clickOpenedDropdownItem(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");
});
QUnit.test("widget many2one_avatar in kanban view (load more dialog)", async function (assert) {
assert.expect(1);
for (let id = 1; id <= 10; id++) {
serverData.models.user.records.push({
id,
display_name: `record ${id}`,
});
}
serverData.views = {
"user,false,list": '<tree><field name="display_name"/></tree>',
"user,false,search": "<search/>",
};
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="oe_kanban_footer">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_right">
<field name="user_id" widget="many2one_avatar"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>`,
});
// open popover
await click(
target.querySelector(
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > a.o_quick_assign"
)
);
// load more
await click(
document.querySelector(".o-overlay-container .o_m2o_dropdown_option_search_more")
);
await click(document.querySelector(".o_dialog .o_list_table .o_data_row .o_data_cell"));
assert.strictEqual(
target.querySelector(
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
).dataset.src,
"/web/image/user/1/avatar_128",
"should have correct avatar image"
);
});
QUnit.test("widget many2one_avatar in kanban view", async function (assert) {
assert.expect(5);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="oe_kanban_footer">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_right">
<field name="user_id" widget="many2one_avatar"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>`,
});
assert.strictEqual(
target.querySelector(
".o_kanban_record:nth-child(1) .o_field_many2one_avatar .o_m2o_avatar > img"
).dataset.src,
"/web/image/user/17/avatar_128",
"should have correct avatar image"
);
assert.containsOnce(
target,
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign",
"should have the quick assign icon"
);
// open popover
await click(
target.querySelector(
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
)
);
const popover = document.querySelector(".o-overlay-container");
assert.strictEqual(
document.activeElement,
popover.querySelector("input"),
"the input inside the popover should have the focus"
);
// select first input
await click(popover.querySelector(".o-autocomplete--dropdown-item"));
assert.strictEqual(
target.querySelector(
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
).dataset.src,
"/web/image/user/17/avatar_128",
"should have correct avatar image"
);
assert.containsNone(
target,
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign",
"should not have the quick assign icon"
);
});
QUnit.test(
"widget many2one_avatar in kanban view without access rights",
async function (assert) {
assert.expect(2);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban edit="0" create="0">
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="oe_kanban_footer">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_right">
<field name="user_id" widget="many2one_avatar"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>`,
});
assert.strictEqual(
target.querySelector(
".o_kanban_record:nth-child(1) .o_field_many2one_avatar .o_m2o_avatar > img"
).dataset.src,
"/web/image/user/17/avatar_128",
"should have correct avatar image"
);
assert.containsNone(
target,
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign",
"should not have the quick assign icon"
);
}
);
});

View file

@ -10,7 +10,6 @@ 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";
@ -133,8 +132,8 @@ QUnit.module("Fields", (hooks) => {
</form>
`,
async mockRPC(route, args, performRPC) {
if (args.method === CREATE && args.model === SALE_ORDER_LINE) {
const selectedId = args.args[0][PRODUCT_FIELD_NAME];
if (args.method === "web_save" && args.model === SALE_ORDER_LINE) {
const selectedId = args.args[1][PRODUCT_FIELD_NAME];
assert.equal(
selectedId,
selectedRecordTest.id,
@ -172,8 +171,8 @@ QUnit.module("Fields", (hooks) => {
<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];
if (args.method === "web_save" && args.model === SALE_ORDER_LINE) {
const selectedId = args.args[1][PRODUCT_FIELD_NAME];
assert.equal(
selectedId,
selectedRecordTest.id,

View file

@ -10,7 +10,7 @@ import {
patchWithCleanup,
triggerEvent,
} from "@web/../tests/helpers/utils";
import { session } from "@web/session";
import { currencies } from "@web/core/currency";
let serverData;
let target;
@ -252,15 +252,12 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("with currency digits != 2 - float field", async function (assert) {
// need to also add it to the session (as currencies are loaded there)
patchWithCleanup(session, {
currencies: {
...session.currencies,
3: {
name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
patchWithCleanup(currencies, {
3: {
name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
});
@ -313,15 +310,12 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("with currency digits != 2 - monetary field", async function (assert) {
// need to also add it to the session (as currencies are loaded there)
patchWithCleanup(session, {
currencies: {
...session.currencies,
3: {
name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
patchWithCleanup(currencies, {
3: {
name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
});
@ -406,7 +400,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<tree editable="bottom">
<field name="float_field" widget="monetary"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" column_invisible="1"/>
</tree>`,
});
@ -501,7 +495,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<tree editable="bottom">
<field name="monetary_field"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" column_invisible="1"/>
</tree>`,
});
@ -685,7 +679,7 @@ QUnit.module("Fields", (hooks) => {
string: "m2m",
type: "many2many",
relation: "partner",
default: [[6, false, [2]]],
default: [[4, 2]],
};
serverData.views = {
"partner,false,list": `
@ -777,7 +771,11 @@ QUnit.module("Fields", (hooks) => {
</tree>`,
});
await click(target.querySelector(".o_list_button_add"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_add"
)
);
assert.containsOnce(
target,
".o_selected_row .o_field_widget[name=float_field] input",
@ -865,15 +863,12 @@ QUnit.module("Fields", (hooks) => {
},
];
patchWithCleanup(session, {
currencies: {
...session.currencies,
1: {
name: "USD",
symbol: "$",
position: "before",
digits: [0, 4],
},
patchWithCleanup(currencies, {
1: {
name: "USD",
symbol: "$",
position: "before",
digits: [0, 4],
},
});

View file

@ -2,13 +2,12 @@
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { registry } from "@web/core/registry";
import { getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { getFixture, nextTick, patchWithCleanup, triggerEvent } 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;
import { Component, mount, useState, xml } from "@odoo/owl";
let serverData;
let target;
@ -358,4 +357,18 @@ QUnit.module("Fields", (hooks) => {
await testInputElements(target.querySelectorAll("main > input"));
}
);
QUnit.test("select all content on focus", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `<form><field name="monetary"/></form>`,
});
const input = target.querySelector(".o_field_widget[name='monetary'] input");
await triggerEvent(input, null, "focus");
assert.strictEqual(input.selectionStart, 0);
assert.strictEqual(input.selectionEnd, 4);
});
});

View file

@ -83,8 +83,8 @@ QUnit.module("Fields", (hooks) => {
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") });
if (method === "web_save") {
assert.deepEqual(args[1], { document: btoa("test") });
}
},
});

View file

@ -27,15 +27,16 @@ QUnit.module("Fields", (hooks) => {
searchable: true,
},
float_field: {
string: "Float_field",
string: "float_field",
type: "float",
digits: [0, 1],
},
sortable: true,
searchable: true,
},
},
records: [
{ id: 1, foo: "yop", int_field: 10 },
{ id: 2, foo: "gnap", int_field: 80 },
{ id: 3, foo: "dop", float_field: 65.6},
{ id: 3, foo: "blip", float_field: 33.3333 },
],
onchanges: {},
},
@ -69,74 +70,17 @@ QUnit.module("Fields", (hooks) => {
"should have a pie chart"
);
assert.strictEqual(
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie_info .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"
target
.querySelector(".o_field_percent_pie.o_field_widget .o_pie")
.style.background.replaceAll(/\s+/g, " "),
"conic-gradient( var(--PercentPieField-color-active) 0% 10%, var(--PercentPieField-color-static) 0% 100% )",
"pie should have a background computed for its value of 10%"
);
});
@ -162,69 +106,17 @@ QUnit.module("Fields", (hooks) => {
"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")
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie_info .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%"
target
.querySelector(".o_field_percent_pie.o_field_widget .o_pie")
.style.background.replaceAll(/\s+/g, " "),
"conic-gradient( var(--PercentPieField-color-active) 0% 80%, var(--PercentPieField-color-static) 0% 100% )",
"pie should have a background computed for its value of 80%"
);
});
@ -243,60 +135,72 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 3,
});
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")
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_value")
.textContent,
"66%",
"should have 66% as pie value since float_field=65.6"
"33.33%",
"should have 33.33% as pie value since float_field=33.3333 and its value is rounded to 2 decimals"
);
assert.strictEqual(
target
.querySelector(".o_field_percent_pie.o_field_widget .o_pie")
.style.background.replaceAll(/\s+/g, " "),
"conic-gradient( var(--PercentPieField-color-active) 0% 33.3333%, var(--PercentPieField-color-static) 0% 100% )",
"pie should have a background computed for its value of 33.3333%"
);
});
// 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();
// });
QUnit.test(
"hide the string when the PercentPieField widget is used in the view",
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");
assert.isNotVisible(
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_text")
);
}
);
QUnit.test(
"show the string when the PercentPieField widget is used in a button with the class oe_stat_button",
async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<div name="button_box" class="oe_button_box">
<button type="object" class="oe_stat_button">
<field name="int_field" widget="percentpie"/>
</button>
</div>
</form>`,
resId: 2,
});
assert.containsOnce(target, ".o_field_percent_pie.o_field_widget .o_pie");
assert.isVisible(
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_text")
);
}
);
});

View file

@ -41,7 +41,7 @@ QUnit.module("Fields", (hooks) => {
<field name="float_field" widget="percentage"/>
</form>`,
mockRPC(route, { args, method }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].float_field,
0.24,

View file

@ -140,7 +140,11 @@ QUnit.module("Fields", (hooks) => {
await editInput(cell, "input", "new");
// save
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
cell = target.querySelector("tbody td:not(.o_list_record_selector)");
assert.doesNotHaveClass(
cell.parentElement,
@ -260,4 +264,49 @@ QUnit.module("Fields", (hooks) => {
);
assert.hasAttrValue(phone, "href", "tel:+12345678900", "href should not contain any space");
});
QUnit.test(
"New record, fill in phone field, then click on call icon and save",
async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="display_name" required="1"/>
<field name="foo" widget="phone"/>
</group>
</sheet>
</form>`,
});
await editInput(target, "div[name='display_name'] input[type='text']", "TEST");
await editInput(target, "div[name='foo'] input[type='tel']", "+12345678900");
target.querySelector(".o_field_widget[name=foo] input").focus();
await click(target.querySelector(".o_phone_form_link"));
assert.doesNotHaveClass(
target.querySelector(".o_form_status_indicator_buttons"),
"invisible",
"save button should be visible"
);
await clickSave(target);
assert.deepEqual(
target.querySelector(".o_field_widget[name=display_name] input").value,
"TEST"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name=foo] input").value,
"+12345678900"
);
assert.hasClass(
target.querySelector(".o_form_status_indicator_buttons"),
"invisible",
"save button should be invisible"
);
}
);
});

View file

@ -333,20 +333,24 @@ QUnit.module("Fields", (hooks) => {
</templates>
</kanban>`,
mockRPC(route, args) {
if (args.method === "write") {
assert.step(`write ${JSON.stringify(args.args)}`);
if (args.method === "web_save") {
assert.step(`web_save ${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"}]']);
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"));
assert.verifySteps(['web_save [[1],{"selection":"1"}]']);
assert.containsOnce(target, ".o_kanban_record .fa-star");
await click(target, ".o-kanban-button-new");
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o-kanban-button-new"
);
await nextTick();
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"}]']);
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"));
assert.verifySteps(['web_save [[6],{"selection":"1"}]']);
assert.containsN(target, ".o_kanban_record .fa-star", 2);
});
@ -404,7 +408,10 @@ QUnit.module("Fields", (hooks) => {
);
// save
await click(target, ".o_list_button_save");
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
);
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
@ -514,7 +521,10 @@ QUnit.module("Fields", (hooks) => {
);
// save
await click(target, ".o_list_button_save");
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
);
rows = target.querySelectorAll(".o_data_row");
assert.containsN(
@ -627,8 +637,8 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
if (method === "web_save") {
assert.step("web_save");
}
},
});
@ -637,7 +647,7 @@ QUnit.module("Fields", (hooks) => {
".o_field_widget .o_priority a.o_priority_star.fa-star-o"
);
await click(stars[stars.length - 1]);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
});
QUnit.test("PriorityField - prevent auto save with autosave option", async function (assert) {
@ -667,5 +677,4 @@ QUnit.module("Fields", (hooks) => {
await click(stars[stars.length - 1]);
assert.verifySteps([]);
});
});

View file

@ -1,9 +1,6 @@
/** @odoo-module **/
import {
makeFakeLocalizationService,
makeFakeNotificationService,
} from "@web/../tests/helpers/mock_services";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import {
click,
clickSave,
@ -87,7 +84,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.deepEqual(
args[1],
{ int_field: 999, float_field: 5, display_name: "new name" },
@ -129,7 +126,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].int_field,
69,
@ -181,7 +178,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].int_field,
69,
@ -228,7 +225,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].float_field,
69,
@ -256,6 +253,7 @@ QUnit.module("Fields", (hooks) => {
);
await editInput(target, ".o_progressbar_value .o_input", "69");
target.querySelector(".o_progressbar_value .o_input").blur(); // because clickSave does not trigger blur
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_progressbar").textContent +
@ -296,7 +294,7 @@ QUnit.module("Fields", (hooks) => {
assert.verifySteps([
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/read",
"/web/dataset/call_kw/partner/web_read",
]);
});
@ -322,7 +320,7 @@ QUnit.module("Fields", (hooks) => {
</kanban>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(args[1].int_field, 69, "New value of progress bar saved");
}
},
@ -370,18 +368,18 @@ QUnit.module("Fields", (hooks) => {
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>`,
<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") {
if (method === "web_save") {
throw new Error("Not supposed to write");
}
},
@ -465,7 +463,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC: function (route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].int_field,
1037,
@ -493,6 +491,7 @@ QUnit.module("Fields", (hooks) => {
assert.ok(target.querySelector(".o_form_view .o_form_editable"), "Form in edit mode");
await editInput(target, ".o_field_widget input", "1#037:9");
target.querySelector(".o_progressbar_value .o_input").blur(); // because clickSave does not trigger blur
await clickSave(target);
assert.strictEqual(
@ -507,13 +506,6 @@ QUnit.module("Fields", (hooks) => {
"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,
@ -534,8 +526,39 @@ QUnit.module("Fields", (hooks) => {
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");
assert.containsOnce(
target,
".o_form_status_indicator span.text-danger",
"The form has not been saved"
);
assert.strictEqual(
target.querySelector(".o_form_button_save").disabled,
true,
"save button is disabled"
);
}
);
QUnit.test(
"ProgressBarField: color is correctly set when value > max value",
async function (assert) {
serverData.models.partner.records[0].float_field = 101;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="float_field" widget="progressbar" options="{'overflow_class': 'bg-warning'}"/>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
".o_progressbar .bg-warning",
"As the value has excedded the max value, the color should be set to bg-warning"
);
}
);
});

View file

@ -1,6 +1,6 @@
/** @odoo-module **/
import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
import { click, clickSave, getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
@ -86,11 +86,19 @@ QUnit.module("Fields", (hooks) => {
target.querySelector(".o_field_radio").textContent.replace(/\s+/g, ""),
"xphonexpad"
);
assert.containsNone(target, "input:checked", "none of the input should be checked");
assert.containsNone(
target,
"input.o_radio_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");
assert.containsOnce(
target,
"input.o_radio_input:checked",
"one of the input should be checked"
);
await clickSave(target);
@ -148,7 +156,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
await click(target, "input[type='checkbox']");
await click(target, ".o_field_boolean input[type='checkbox']");
assert.containsOnce(
target,
@ -161,7 +169,7 @@ QUnit.module("Fields", (hooks) => {
"the other of the input should be checked"
);
await click(target, "input[type='checkbox']");
await click(target, ".o_field_boolean input[type='checkbox']");
assert.containsOnce(
target,
"input.o_radio_input[data-value='41']:checked",
@ -205,6 +213,52 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("Two RadioField with same selection", async function (assert) {
serverData.models.partner.fields.color_2 = serverData.models.partner.fields.color;
serverData.models.partner.records[0].color = "black";
serverData.models.partner.records[0].color_2 = "black";
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: `
<form>
<group>
<field name="color" widget="radio"/>
</group>
<group>
<field name="color_2" widget="radio"/>
</group>
</form>`,
});
assert.hasAttrValue(
target.querySelector("[name='color'] input.o_radio_input:checked"),
"data-value",
"black"
);
assert.hasAttrValue(
target.querySelector("[name='color_2'] input.o_radio_input:checked"),
"data-value",
"black"
);
// click on Red
await click(target.querySelector("[name='color_2'] label"));
assert.hasAttrValue(
target.querySelector("[name='color'] input.o_radio_input:checked"),
"data-value",
"black"
);
assert.hasAttrValue(
target.querySelector("[name='color_2'] input.o_radio_input:checked"),
"data-value",
"red"
);
});
QUnit.test("fieldradio widget has o_horizontal or o_vertical class", async function (assert) {
serverData.models.partner.fields.color2 = serverData.models.partner.fields.color;
@ -261,7 +315,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: '<form><field name="selection" widget="radio"/></form>',
mockRPC: function (route, { args, method, model }) {
if (model === "partner" && method === "write") {
if (model === "partner" && method === "web_save") {
assert.strictEqual(args[1].selection, "1", "should write correct value");
}
},
@ -288,61 +342,6 @@ QUnit.module("Fields", (hooks) => {
);
});
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",

View file

@ -12,9 +12,9 @@ import {
clickSave,
triggerHotkey,
nextTick,
makeDeferred,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { makeView, makeViewInDialog, setupViewRegistries } from "@web/../tests/views/helpers";
let target;
let serverData;
@ -252,9 +252,7 @@ QUnit.module("Fields", (hooks) => {
"name_search", // for the select
"name_search", // for the spawned many2one
"name_create",
"create",
"read",
"name_get",
"web_save",
],
"The name_create method should have been called"
);
@ -408,7 +406,7 @@ QUnit.module("Fields", (hooks) => {
patchWithCleanup(actionService, {
start() {
const service = this._super(...arguments);
const service = super.start(...arguments);
return {
...service,
doAction(action) {
@ -422,7 +420,7 @@ QUnit.module("Fields", (hooks) => {
},
});
await makeView({
await makeViewInDialog({
type: "form",
resModel: "partner",
resId: 1,
@ -431,7 +429,7 @@ QUnit.module("Fields", (hooks) => {
<form>
<sheet>
<group>
<field name="reference" string="custom label" open_target="new" />
<field name="reference" string="custom label"/>
</group>
</sheet>
</form>`,
@ -460,7 +458,7 @@ QUnit.module("Fields", (hooks) => {
"the name_search should be done on the newly set model"
);
}
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(model, "partner", "should write on the current model");
assert.deepEqual(
args,
@ -502,11 +500,13 @@ QUnit.module("Fields", (hooks) => {
await click(target, ".o_external_button");
assert.strictEqual(
target.querySelector(".modal .modal-title").textContent.trim(),
target
.querySelector(".o_dialog:not(.o_inactive_modal) .modal-title")
.textContent.trim(),
"Open: custom label",
"dialog title should display the custom string label"
);
await click(target, ".modal .o_form_button_cancel");
await click(target, ".o_dialog:not(.o_inactive_modal) .o_form_button_cancel");
await editSelect(target, ".o_field_widget select", "partner_type");
assert.strictEqual(
@ -526,16 +526,10 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("Many2One 'Search More...' updates on resModel change", async function (assert) {
// Patch the Many2XAutocomplete default search limit options
patchWithCleanup(Many2XAutocomplete.defaultProps, {
searchLimit: -1,
});
QUnit.test("Many2One 'Search more...' updates on resModel change", async function (assert) {
serverData.views = {
"product,false,list": '<tree><field name="display_name"/></tree>',
"product,false,search": '<search/>',
"product,false,search": "<search/>",
};
await makeView({
@ -546,14 +540,25 @@ QUnit.module("Fields", (hooks) => {
});
// Selecting a relation
await editSelect(target.querySelector("div.o_field_reference"), "select.o_input", "partner_type");
await editSelect(
target.querySelector("div.o_field_reference"),
"select.o_input",
"partner_type"
);
// Selecting another relation
await editSelect(target.querySelector("div.o_field_reference"), "select.o_input", "product");
await editSelect(
target.querySelector("div.o_field_reference"),
"select.o_input",
"product"
);
// Opening the Search More... option
await click(target.querySelector("div.o_field_reference"), "input.o_input");
await click(target.querySelector("div.o_field_reference"), ".o_m2o_dropdown_option_search_more");
await click(
target.querySelector("div.o_field_reference"),
".o_m2o_dropdown_option_search_more"
);
assert.strictEqual(
target.querySelector("div.modal td.o_data_cell").innerText,
@ -562,82 +567,44 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("computed reference field changed by onchange to 'False,0' value", async function (assert) {
assert.expect(1);
QUnit.test(
"computed reference field changed by onchange to 'False,0' value",
async function (assert) {
assert.expect(1);
serverData.models.partner.onchanges = {
bar(obj) {
if (!obj.bar) {
obj.reference_char = "False,0";
}
},
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
serverData.models.partner.onchanges = {
bar(obj) {
if (!obj.bar) {
obj.reference_char = "False,0";
}
},
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="bar"/>
<field name="reference_char" widget="reference"/>
</form>`,
mockRPC(route, { args, method }) {
if (method === "create") {
assert.deepEqual(args[0], {
bar: false,
reference_char: "False,0",
});
}
},
});
mockRPC(route, { args, method }) {
if (method === "web_save") {
assert.deepEqual(args[1], {
bar: false,
reference_char: "False,0",
});
}
},
});
// trigger the onchange to set a value for the reference field
await click(target, ".o_field_boolean input");
// trigger the onchange to set a value for the reference field
await click(target, ".o_field_boolean input");
// save
await clickSave(target);
});
QUnit.test("ReferenceField with model field", async function (assert) {
serverData.models.partner.onchanges = {
color(obj) {
if (obj.color === "black") {
obj.model_id = 20;
obj.reference = "product,37";
} else {
obj.model_id = 17;
obj.reference = "partner,1";
}
},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="color" />
<field name="model_id" invisible="1"/>
<field name="reference" options="{'model_field': 'model_id'}" />
</form>`,
mockRPC(route, { args, method }) {
if (method === "write") {
assert.step("write");
assert.strictEqual(args[1].reference, "partner,4");
}
},
});
await editSelect(target, "select", '"black"');
await editSelect(target, "select", '"red"');
await editInput(target, ".o_field_widget[name=reference] input", "aaa");
await click(target, ".ui-autocomplete .ui-menu-item:first-child");
await clickSave(target);
assert.verifySteps(["write"]);
});
// save
await clickSave(target);
}
);
QUnit.test("interact with reference field changed by onchange", async function (assert) {
assert.expect(2);
@ -659,8 +626,8 @@ QUnit.module("Fields", (hooks) => {
<field name="reference"/>
</form>`,
mockRPC(route, { args, method }) {
if (method === "create") {
assert.deepEqual(args[0], {
if (method === "web_save") {
assert.deepEqual(args[1], {
bar: false,
reference: "partner,4",
});
@ -707,14 +674,8 @@ QUnit.module("Fields", (hooks) => {
</group>
</sheet>
</form>`,
mockRPC(route, { method, model }) {
if (method === "name_get") {
assert.step(model);
}
},
});
assert.verifySteps(["product"], "the first name_get should have been done");
assert.strictEqual(
target.querySelector(".o_field_widget[name='reference'] select").value,
"product",
@ -729,7 +690,6 @@ QUnit.module("Fields", (hooks) => {
// trigger onchange
await editInput(target, ".o_field_widget[name=int_field] input", 12);
assert.verifySteps(["partner_type"], "the second name_get should have been done");
assert.strictEqual(
target.querySelector(".o_field_widget[name='reference'] select").value,
"partner_type",
@ -784,7 +744,6 @@ QUnit.module("Fields", (hooks) => {
obj.foo = "product," + obj.int_field;
},
};
let nbNameGet = 0;
await makeView({
type: "form",
@ -800,23 +759,26 @@ QUnit.module("Fields", (hooks) => {
</group>
</sheet>
</form>`,
mockRPC(route, { model, method }) {
if (model === "product" && method === "name_get") {
mockRPC(route, { model, method, args }) {
if (
model === "product" &&
method === "read" &&
args[1].length === 1 &&
args[1][0] === "display_name"
) {
nbNameGet++;
}
},
});
assert.strictEqual(nbNameGet, 1, "the first name_get should have been done");
assert.strictEqual(
target.querySelector(".o_field_widget[name=foo]").textContent,
"xphone",
"foo field should be correctly set"
);
// trigger onchange
await editInput(target, ".o_field_widget[name=int_field] input", 41);
await nextTick();
assert.strictEqual(nbNameGet, 2, "the second name_get should have been done");
assert.strictEqual(
target.querySelector(".o_field_widget[name=foo]").textContent,
@ -869,7 +831,6 @@ QUnit.module("Fields", (hooks) => {
<field name="reference" options="{'model_field': 'model_id'}" />
</form>`,
});
assert.containsNone(
target,
"select",
@ -891,6 +852,7 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_field_widget[name='model_id'] input", "Partner");
await click(target, ".ui-autocomplete .ui-menu-item:first-child");
await nextTick();
assert.strictEqual(
target.querySelector(".o_field_widget[name='reference'] input").value,
"",
@ -946,7 +908,7 @@ QUnit.module("Fields", (hooks) => {
);
QUnit.test("Reference field with default value in list view", async function (assert) {
assert.expect(2);
assert.expect(1);
await makeView({
type: "list",
@ -960,19 +922,29 @@ QUnit.module("Fields", (hooks) => {
mockRPC: (route, { method, args }) => {
if (method === "onchange") {
return {
value: {reference: "partner,2"},
value: {
reference: {
id: { id: 2, model: "partner" },
display_name: "second record",
},
},
};
} else if (method === "create") {
assert.strictEqual(args.length, 1);
assert.strictEqual(args[0].reference, "partner,2");
} else if (method === "web_save") {
assert.strictEqual(args[1].reference, "partner,2");
}
},
});
await click(target, '.o_list_button_add');
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_add"
);
await click(target, '.o_list_char[name="display_name"] input');
await editInput(target, '.o_list_char[name="display_name"] input', "Blabla");
await click(target, '.o_list_button_save');
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
);
});
QUnit.test(
@ -1003,6 +975,8 @@ QUnit.module("Fields", (hooks) => {
// Select the second product without changing the model
await click(target, ".o_list_table .reference_field");
await nextTick();
await click(target, ".o_list_table .reference_field input");
// Enter to select it
@ -1051,6 +1025,7 @@ QUnit.module("Fields", (hooks) => {
);
await click(target.querySelector(".o_list_table .o_data_cell"));
await nextTick();
await editInput(target, ".o_list_table [name='name'] input", "plop");
await click(target, ".o_form_view");
assert.strictEqual(
@ -1093,8 +1068,13 @@ QUnit.module("Fields", (hooks) => {
await click(target, ".o_list_table td.o_list_many2one");
await click(target, ".o_list_table .o_list_many2one input");
//Select the "Partner" option, different from original "Product"
const dropdownItems = [...target.querySelectorAll(".o_list_table .o_list_many2one .o_input_dropdown .dropdown-item")];
await click(dropdownItems.filter(item => item.text === "Partner")[0]);
const dropdownItems = [
...target.querySelectorAll(
".o_list_table .o_list_many2one .o_input_dropdown .dropdown-item"
),
];
await click(dropdownItems.filter((item) => item.text === "Partner")[0]);
await nextTick();
assert.strictEqual(target.querySelector(".reference_field input").value, "");
assert.strictEqual(target.querySelector(".o_list_many2one input").value, "Partner");
//Void the associated, required, "reference" field and make sure the form marks the field as required
@ -1158,4 +1138,56 @@ QUnit.module("Fields", (hooks) => {
"the selection list of the reference field should exist when hide_model=False and no model_field specified."
);
});
QUnit.test("reference field should await fetch model before render", async function (assert) {
serverData.models.partner.records[0].model_id = 20;
const def = makeDeferred();
makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="model_id" invisible="1"/>
<field name="reference" options="{'model_field': 'model_id'}" />
</form>`,
async mockRPC(route, args) {
if (args.method === "read" && args.model === "ir.model") {
await def;
}
},
});
await nextTick();
await nextTick();
assert.containsNone(target, ".o_form_view");
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_form_view");
});
QUnit.test("reference char with list view pager navigation", async function (assert) {
assert.expect(2);
serverData.models.partner.records[0].reference_char = "product,37";
serverData.models.partner.records[1].reference_char = "product,41";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form edit="0"><field name="reference_char" widget="reference" string="Record"/></form>`,
resIds: [1, 2],
});
assert.strictEqual(
target.querySelector(".o_field_reference .o_form_uri").textContent,
"xphone"
);
await click(target, ".o_pager_next");
assert.strictEqual(
target.querySelector(".o_field_reference .o_form_uri").textContent,
"xpad"
);
});
});

View file

@ -8,6 +8,7 @@ import {
patchTimeZone,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { getPickerCell } from "../../core/datetime/datetime_test_helpers";
let serverData;
let target;
@ -116,22 +117,18 @@ QUnit.module("Fields", (hooks) => {
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"
);
assert.containsOnce(target, ".o_field_remaining_days input");
await editInput(target, ".o_datepicker_input", "10/10/2017");
await editInput(target, ".o_field_remaining_days input", "10/10/2017");
await click(target);
assert.containsOnce(document.body, ".modal");
assert.containsOnce(target, ".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");
await click(target, ".modal .modal-footer .btn-primary");
assert.strictEqual(
rows[0].querySelector(".o_data_cell").textContent,
@ -177,15 +174,11 @@ QUnit.module("Fields", (hooks) => {
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"
);
assert.containsOnce(target, ".o_field_remaining_days input");
await editInput(target, ".o_datepicker_input", "blabla");
await editInput(target, ".o_field_remaining_days input", "blabla");
await click(target);
assert.containsNone(document.body, ".modal");
assert.containsNone(target, ".modal");
assert.strictEqual(cells[0].textContent, "Today");
assert.strictEqual(cells[1].textContent, "Tomorrow");
}
@ -211,16 +204,12 @@ QUnit.module("Fields", (hooks) => {
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");
assert.containsOnce(target, "div.o_field_widget[name='date'] input");
await click(target.querySelector(".o_datepicker .o_datepicker_input"));
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(target, ".o_field_remaining_days input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
await click(document.body, ".bootstrap-datetimepicker-widget .day[data-day='10/09/2017']");
await click(getPickerCell("9").at(0));
await click(target, ".o_form_button_save");
assert.strictEqual(target.querySelector(".o_field_widget input").value, "10/09/2017");
});
@ -238,12 +227,9 @@ QUnit.module("Fields", (hooks) => {
</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");
assert.containsOnce(target, ".o_form_editable .o_field_widget[name='date'] input");
await click(target, ".o_field_widget[name='date'] input");
assert.containsOnce(target, ".o_datetime_picker");
}
);
@ -303,17 +289,12 @@ QUnit.module("Fields", (hooks) => {
target.querySelector(".o_field_widget input").value,
"10/08/2017 11:00:00"
);
assert.containsOnce(target, "div.o_field_widget[name='datetime'] .o_datepicker");
assert.containsOnce(target, "div.o_field_widget[name='datetime'] input");
await click(target.querySelector(".o_datepicker .o_datepicker_input"));
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(target, ".o_field_widget input");
assert.containsOnce(target, ".o_datetime_picker", "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(getPickerCell("9").at(0));
await click(target, ".o_form_button_save");
assert.strictEqual(
target.querySelector(".o_field_widget input").value,

View file

@ -1,6 +1,6 @@
/** @odoo-module **/
import { click, editSelect, editInput, getFixture, clickSave } from "@web/../tests/helpers/utils";
import { click, clickSave, editSelect, getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
@ -147,7 +147,7 @@ QUnit.module("Fields", (hooks) => {
"should have correct value in color field"
);
assert.verifySteps(["get_views", "read", "name_search", "name_search", "onchange"]);
assert.verifySteps(["get_views", "web_read", "name_search", "name_search", "onchange"]);
});
QUnit.test("unset selection field with 0 as key", async function (assert) {
@ -225,7 +225,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: '<form><field name="trululu" widget="selection" /></form>',
mockRPC(route, { args, method }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].trululu,
false,
@ -257,59 +257,6 @@ QUnit.module("Fields", (hooks) => {
);
});
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",
@ -329,7 +276,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="feedback_value" />
<field name="color" attrs="{'required': [('feedback_value', '=', 'bad')]}" />
<field name="color" required="feedback_value == 'bad'" />
</form>`,
});
@ -382,7 +329,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="feedback_value" />
<field name="color" attrs="{'required': [('feedback_value', '=', 'bad')]}" />
<field name="color" required="feedback_value == 'bad'" />
</form>`,
});
@ -421,4 +368,189 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(placeholderOption.textContent, "Placeholder");
assert.strictEqual(placeholderOption.value, "false");
});
QUnit.test("SelectionField in kanban view", async function (assert) {
assert.expect(3);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="color" widget="selection" />
</div>
</t>
</templates>
</kanban>`,
domain: [["id", "=", 1]],
});
assert.containsOnce(
target,
".o_field_widget[name='color'] select",
"SelectionKanbanField widget applied to selection field"
);
assert.containsN(
target.querySelector(".o_field_widget[name='color']"),
"option",
3,
"Three options are displayed (one blank option)"
);
assert.deepEqual(
[...target.querySelectorAll(".o_field_widget[name='color'] option")].map(
(option) => option.value
),
["false", '"red"', '"black"']
);
});
QUnit.test("SelectionField - auto save record in kanban view", async function (assert) {
assert.expect(2);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="color" widget="selection" />
</div>
</t>
</templates>
</kanban>`,
domain: [["id", "=", 1]],
mockRPC(_route, { method }) {
if (method === "web_save") {
assert.step("web_save");
}
},
});
await editSelect(target, ".o_field_widget[name='color'] select", '"black"');
assert.verifySteps(["web_save"]);
});
QUnit.test(
"SelectionField don't open form view on click in kanban view",
async function (assert) {
assert.expect(1);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="color" widget="selection" />
</div>
</t>
</templates>
</kanban>`,
domain: [["id", "=", 1]],
selectRecord: () => {
assert.step("selectRecord");
},
});
await click(target, ".o_field_widget[name='color'] select");
assert.verifySteps([]);
}
);
QUnit.test("SelectionField is disabled if field readonly", async function (assert) {
assert.expect(1);
serverData.models.partner.fields.color.readonly = true;
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="color" widget="selection" />
</div>
</t>
</templates>
</kanban>
`,
domain: [["id", "=", 1]],
});
assert.containsOnce(
target,
".o_field_widget[name='color'] span",
"field should be readonly"
);
});
QUnit.test("SelectionField is disabled with a readonly attribute", async function (assert) {
assert.expect(1);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="color" widget="selection" readonly="1" />
</div>
</t>
</templates>
</kanban>
`,
domain: [["id", "=", 1]],
});
assert.containsOnce(
target,
".o_field_widget[name='color'] span",
"field should be readonly"
);
});
QUnit.test("SelectionField in kanban view with handle widget", async function (assert) {
// When records are draggable, most pointerdown events are default prevented. This test
// comes with a fix that blacklists "select" elements, i.e. pointerdown events on such
// elements aren't default prevented, because if they were, the select element can't be
// opened. The test is a bit artificial but there's no other way to test the scenario, as
// using editSelect simply triggers a "change" event, which obviously always works.
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<field name="int_field" widget="handle"/>
<templates>
<t t-name="kanban-box">
<div>
<field name="color" widget="selection"/>
</div>
</t>
</templates>
</kanban>`,
});
const ev = new PointerEvent("pointerdown", { bubbles: true, cancelable: true });
const select = target.querySelector(".o_kanban_record .o_field_widget[name=color] select");
select.dispatchEvent(ev);
assert.notOk(ev.defaultPrevented);
});
});

View file

@ -4,8 +4,11 @@ import {
clickSave,
editInput,
getFixture,
makeDeferred,
nextTick,
patchWithCleanup,
triggerEvent,
triggerEvents,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { NameAndSignature } from "@web/core/signature/name_and_signature";
@ -60,10 +63,67 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("Signature Field");
QUnit.test("signature can be drawn", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form>
<field name="sign" widget="signature" />
</form>`,
mockRPC: async (route) => {
if (route === "/web/sign/get_fonts/") {
return {};
}
},
});
assert.containsNone(target, "div[name=sign] img.o_signature");
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 .o_web_sign_name_and_signature");
assert.containsNone(target, ".modal .btn.btn-primary:not([disabled])");
// Use a drag&drop simulation to draw a signature
const def = makeDeferred();
const jSignatureEl = target.querySelector(".modal .o_web_sign_signature");
$(jSignatureEl).on("change", def.resolve);
const { x, y, width, height } = target
.querySelector("canvas.jSignature")
.getBoundingClientRect();
await triggerEvents(jSignatureEl, "canvas.jSignature", [
["mousedown", { clientX: x + 1, clientY: y + 1 }],
["mousemove", { clientX: x + width - 1, clientY: height + height - 1 }],
["mouseup", { clientX: x + width - 1, clientY: height + height - 1 }],
]);
await def; // makes sure the signature stroke is taken into account by jSignature
await nextTick(); // await owl rendering
assert.containsOnce(target, ".modal .btn.btn-primary:not([disabled])");
// Click on "Adopt and Sign" button
await click(target, ".modal .btn.btn-primary:not([disabled])");
assert.containsNone(target, ".modal");
// The signature widget should now display the signature img
assert.containsNone(target, "div[name=sign] div.o_signature svg");
assert.containsOnce(target, "div[name=sign] img.o_signature");
const signImgSrc = target.querySelector("div[name=sign] img.o_signature").dataset.src;
assert.notOk(signImgSrc.includes("placeholder"));
assert.ok(signImgSrc.startsWith("data:image/png;base64,"));
});
QUnit.test("Set simple field in 'full_name' node option", async function (assert) {
patchWithCleanup(NameAndSignature.prototype, {
setup() {
this._super.apply(this, arguments);
super.setup(...arguments);
assert.step(this.props.signature.name);
},
});
@ -96,13 +156,18 @@ QUnit.module("Fields", (hooks) => {
".modal .modal-body a.o_web_sign_auto_button",
'should open a modal with "Auto" button'
);
assert.hasClass(
target.querySelector(".o_web_sign_auto_button"),
"active",
"'Auto' panel is visible by default"
);
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);
super.setup(...arguments);
assert.step(this.props.signature.name);
},
});
@ -183,7 +248,7 @@ QUnit.module("Fields", (hooks) => {
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
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
// 1659692220000, 1659695820000
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
@ -203,9 +268,9 @@ QUnit.module("Fields", (hooks) => {
if (route === "/web/sign/get_fonts/") {
return {};
}
if (method === "write") {
assert.step("write");
args[1].__last_update = lastUpdates[index];
if (method === "web_save") {
assert.step("web_save");
args[1].write_date = lastUpdates[index];
args[1].sign = "4 kb";
index++;
}
@ -216,7 +281,7 @@ QUnit.module("Fields", (hooks) => {
"1659688620000"
);
await click(target, ".o_field_signature img", true);
await click(target, ".o_field_signature img", { skipVisibilityCheck: true });
assert.containsOnce(target, ".modal canvas");
let canvas = target.querySelector(".modal canvas");
@ -244,13 +309,13 @@ QUnit.module("Fields", (hooks) => {
);
await clickSave(target);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
assert.strictEqual(
getUnique(target.querySelector(".o_field_signature img")),
"1659692220000"
);
await click(target, ".o_field_signature img", true);
await click(target, ".o_field_signature img", { skipVisibilityCheck: true });
assert.containsOnce(target, ".modal canvas");
canvas = target.querySelector(".modal canvas");
@ -272,7 +337,7 @@ QUnit.module("Fields", (hooks) => {
`data:image/png;base64,${MYB64_2}`
);
await clickSave(target);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
assert.strictEqual(
getUnique(target.querySelector(".o_field_signature img")),
"1659695820000"
@ -292,7 +357,7 @@ QUnit.module("Fields", (hooks) => {
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
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
// 1659692220000, 1659695820000
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
@ -309,9 +374,9 @@ QUnit.module("Fields", (hooks) => {
<field name="sign" widget="signature" />
</form>`,
mockRPC(route, { method, args }) {
if (method === "write") {
assert.step("write");
args[1].__last_update = lastUpdates[index];
if (method === "web_save") {
assert.step("web_save");
args[1].write_date = lastUpdates[index];
args[1].sign = "4 kb";
index++;
}
@ -332,6 +397,6 @@ QUnit.module("Fields", (hooks) => {
getUnique(target.querySelector(".o_field_signature img")),
"1659692220000"
);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
});
});

View file

@ -82,26 +82,34 @@ QUnit.module("Fields", (hooks) => {
".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"
);
assert.containsNone(target, ".o_content .dropdown-menu", "there should not be a dropdown");
// 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.containsOnce(
document.body,
".o_content .dropdown-menu",
"there should be a dropdown"
);
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
".o_content .dropdown-menu .dropdown-item",
3,
"there should be three options in the dropdown"
);
assert.hasClass(
target.querySelector(".dropdown-menu .dropdown-item:nth-child(2)"),
"active",
"current value has a checkmark"
);
// 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");
await click(target.querySelector(".o_content .dropdown-menu .dropdown-item"));
assert.containsNone(
target,
".o_content .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",
@ -118,7 +126,11 @@ QUnit.module("Fields", (hooks) => {
"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_content .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",
@ -137,17 +149,21 @@ QUnit.module("Fields", (hooks) => {
// 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.containsOnce(target, ".o_content .dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
3,
"there should be three 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");
await click(target, ".o_content .dropdown-menu .dropdown-item:last-child");
assert.containsNone(
target,
".o_content .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",
@ -163,7 +179,7 @@ QUnit.module("Fields", (hooks) => {
await click(target.querySelector(".o_form_button_save"));
assert.containsNone(
target,
".dropdown-menu",
".o_content .dropdown-menu",
"there should still not be a dropdown anymore"
);
assert.containsNone(
@ -193,6 +209,21 @@ QUnit.module("Fields", (hooks) => {
assert.isNotVisible(target.querySelector(".dropdown-menu"));
});
QUnit.test("StateSelectionField for form view with hide_label option", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="selection" widget="state_selection" options="{'hide_label': False}"/>
</form>
`,
resId: 1,
});
assert.containsOnce(target, ".o_status_label");
});
QUnit.test("StateSelectionField for list view with hide_label option", async function (assert) {
Object.assign(serverData.models.partner.fields, {
graph_type: {
@ -214,7 +245,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<tree>
<field name="graph_type" widget="state_selection" options="{'hide_label': True}"/>
<field name="selection" widget="state_selection"/>
<field name="selection" widget="state_selection" options="{'hide_label': False}"/>
</tree>`,
});
@ -281,7 +312,7 @@ QUnit.module("Fields", (hooks) => {
".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");
assert.containsNone(target, ".o_content .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");
@ -293,16 +324,16 @@ QUnit.module("Fields", (hooks) => {
"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.containsOnce(target, ".o_content .dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
".o_content .dropdown-menu .dropdown-item",
3,
"there should be three options in the dropdown"
);
// Click on the first option, "Normal"
await click(target.querySelector(".dropdown-menu .dropdown-item"));
await click(target.querySelector(".o_content .dropdown-menu .dropdown-item"));
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
@ -319,7 +350,7 @@ QUnit.module("Fields", (hooks) => {
".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, ".o_content .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
@ -342,24 +373,28 @@ QUnit.module("Fields", (hooks) => {
".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, ".o_content .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.containsOnce(target, ".o_content .dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
".o_content .dropdown-menu .dropdown-item",
3,
"there should be three 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");
assert.containsNone(
target,
".o_content .dropdown-menu",
"there should not be a dropdown anymore"
);
const firstCell = target.querySelector("tbody td.o_state_selection_cell");
assert.doesNotHaveClass(
firstCell.parentElement,
@ -378,17 +413,21 @@ QUnit.module("Fields", (hooks) => {
".o_state_selection_cell .o_field_state_selection span.o_status"
)[3]
);
assert.containsOnce(target, ".dropdown-menu", "there should be a dropdown");
assert.containsOnce(target, ".o_content .dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
".o_content .dropdown-menu .dropdown-item",
3,
"there should be three 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");
await click(target, ".o_content .dropdown-menu .dropdown-item:last-child");
assert.containsNone(
target,
".o_content .dropdown-menu",
"there should not be a dropdown anymore"
);
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
@ -406,10 +445,14 @@ QUnit.module("Fields", (hooks) => {
2,
"should now have two green status"
);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
assert.containsNone(target, ".o_content .dropdown-menu", "there should not be a dropdown");
// save
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
@ -427,11 +470,11 @@ QUnit.module("Fields", (hooks) => {
2,
"should have two green status"
);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
assert.containsNone(target, ".o_content .dropdown-menu", "there should not be a dropdown");
});
QUnit.test(
'StateSelectionField edited by the smart action "Set kanban state..."',
'StateSelectionField edited by the smart actions "Set kanban state as <state name>"',
async function (assert) {
await makeView({
type: "form",
@ -448,20 +491,23 @@ QUnit.module("Fields", (hooks) => {
triggerHotkey("control+k");
await nextTick();
const idx = [...target.querySelectorAll(".o_command")]
.map((el) => el.textContent)
.indexOf("Set kanban state...ALT + SHIFT + R");
var commandTexts = [...target.querySelectorAll(".o_command")].map(
(el) => el.textContent
);
assert.ok(commandTexts.includes("Set kanban state as NormalALT + D"));
const idx = commandTexts.indexOf("Set kanban state as DoneALT + G");
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");
triggerHotkey("control+k");
await nextTick();
commandTexts = [...target.querySelectorAll(".o_command")].map((el) => el.textContent);
assert.ok(commandTexts.includes("Set kanban state as NormalALT + D"));
assert.ok(commandTexts.includes("Set kanban state as BlockedALT + F"));
assert.notOk(commandTexts.includes("Set kanban state as DoneALT + G"));
}
);
@ -492,17 +538,17 @@ QUnit.module("Fields", (hooks) => {
});
await click(target, ".o_status");
let dropdownItemTexts = [...target.querySelectorAll(".dropdown-item")].map(
(el) => el.textContent
);
assert.deepEqual(dropdownItemTexts, ["Custom normal", "Custom done"]);
let dropdownItemTexts = [
...target.querySelectorAll(".o_field_state_selection .dropdown-item"),
].map((el) => el.textContent);
assert.deepEqual(dropdownItemTexts, ["Custom normal", "Custom blocked", "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"]);
dropdownItemTexts = [
...target.querySelectorAll(".o_field_state_selection .dropdown-item"),
].map((el) => el.textContent);
assert.deepEqual(dropdownItemTexts, ["Custom normal", "Custom blocked", "Custom done"]);
});
QUnit.test("works when required in a readonly view ", async function (assert) {
@ -523,18 +569,19 @@ QUnit.module("Fields", (hooks) => {
</templates>
</kanban>`,
mockRPC: (route, args, performRPC) => {
if (route === "/web/dataset/call_kw/partner/write") {
assert.step("write");
if (route === "/web/dataset/call_kw/partner/web_save") {
assert.step("web_save");
}
return performRPC(route, args);
},
});
assert.containsNone(target, ".o_status_label");
await click(target, ".o_field_state_selection button");
const doneItem = target.querySelectorAll(".dropdown-item")[1]; // item "done";
const doneItem = target.querySelectorAll(".dropdown-item")[2]; // item "done";
await click(doneItem);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
assert.hasClass(target.querySelector(".o_field_state_selection span"), "o_status_green");
});
@ -555,15 +602,15 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
if (method === "web_save") {
assert.step("web_save");
}
},
});
await click(target, ".o_field_widget.o_field_state_selection .o_status");
await click(target, ".dropdown-menu .dropdown-item:last-child");
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
}
);
@ -595,4 +642,58 @@ QUnit.module("Fields", (hooks) => {
assert.verifySteps([]);
}
);
QUnit.test(
"StateSelectionField - hotkey handling when there are more than 3 options available",
async function (assert) {
serverData.models.partner.fields.selection.selection.push(
["martin", "Martin"],
["martine", "Martine"]
);
serverData.models.partner.records[0].selection = null;
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,
});
await click(target, ".o_field_widget.o_field_state_selection .o_status");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
5,
"Five choices are displayed"
);
triggerHotkey("control+k");
await nextTick();
assert.strictEqual(
target.querySelector(".o_command#o_command_2").textContent,
"Set kanban state as DoneALT + G",
"hotkey and command are present"
);
assert.strictEqual(
target.querySelector(".o_command#o_command_4").textContent,
"Set kanban state as Martine",
"no hotkey is present, but the command exists"
);
await click(target.querySelector(".o_command#o_command_2"));
assert.hasClass(
target.querySelector(".o_status"),
"o_status_green",
"green color and Done state have been set"
);
}
);
});

View file

@ -1,18 +1,22 @@
/** @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,
clickSave,
editInput,
getFixture,
getNodesTextContent,
nextTick,
patchWithCleanup,
selectDropdownItem,
triggerHotkey,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { EventBus } from "@odoo/owl";
@ -247,10 +251,7 @@ QUnit.module("Fields", (hooks) => {
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"
);
assert.ok(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)"
@ -263,10 +264,7 @@ QUnit.module("Fields", (hooks) => {
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"
);
assert.ok(target.querySelector(".o_statusbar_status button[data-value='1']").disabled);
});
QUnit.test("statusbar with no status", async function (assert) {
@ -285,9 +283,9 @@ QUnit.module("Fields", (hooks) => {
});
assert.doesNotHaveClass(target.querySelector(".o_statusbar_status"), "o_field_empty");
assert.strictEqual(
target.querySelector(".o_statusbar_status").children.length,
0,
assert.containsNone(
target,
".o_statusbar_status > :not(.d-none)",
"statusbar widget should be empty"
);
});
@ -308,9 +306,8 @@ QUnit.module("Fields", (hooks) => {
});
assert.doesNotHaveClass(target.querySelector(".o_statusbar_status"), "o_field_empty");
const tooltipInfo = target.querySelector(".o_field_statusbar").attributes[
"data-tooltip-info"
];
const tooltipInfo =
target.querySelector(".o_field_statusbar").attributes["data-tooltip-info"];
assert.strictEqual(
JSON.parse(tooltipInfo.value).field.help,
"some info about the field",
@ -407,6 +404,7 @@ QUnit.module("Fields", (hooks) => {
".o_statusbar_status button:not(.dropdown-toggle)"
);
await click(buttons[buttons.length - 1]);
await nextTick();
assert.containsN(target, ".o_statusbar_status button:not(.dropdown-toggle)", 2);
}
);
@ -434,15 +432,15 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
await click(target, ".o_statusbar_status .dropdown-toggle");
await click(target, ".o_statusbar_status .dropdown-toggle:not(.d-none)");
const status = target.querySelectorAll(".o_statusbar_status");
assert.containsOnce(status[0], ".dropdown-item.disabled");
assert.containsOnce(status[status.length - 1], "button.disabled");
assert.containsOnce(status[status.length - 1], "button:disabled");
}
);
QUnit.test("statusbar: choose an item from the 'More' menu", async function (assert) {
QUnit.test("statusbar: choose an item from the folded menu", async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
@ -471,11 +469,11 @@ QUnit.module("Fields", (hooks) => {
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_statusbar_status .dropdown-toggle:not(.d-none)");
await click(target, ".o-dropdown .dropdown-item");
assert.strictEqual(
target.querySelector("[aria-checked='true']").textContent,
@ -509,10 +507,10 @@ QUnit.module("Fields", (hooks) => {
},
});
assert.containsN(target, ".o_statusbar_status button.disabled", 3);
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.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");
@ -551,35 +549,7 @@ QUnit.module("Fields", (hooks) => {
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) {
QUnit.test("smart actions are unavailable if readonly", async function (assert) {
await makeView({
serverData,
type: "form",
@ -594,7 +564,34 @@ QUnit.module("Fields", (hooks) => {
});
assert.containsOnce(target, ".o_field_widget");
triggerHotkey("alt+shift+x");
triggerHotkey("control+k");
await nextTick();
const moveStages = [...target.querySelectorAll(".o_command")].map((el) => el.textContent);
assert.notOk(moveStages.includes("Move to Trululu...ALT + SHIFT + X"));
assert.notOk(moveStages.includes("Move to next...ALT + X"));
});
QUnit.test("hotkeys are 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"); // Move to stage...
await nextTick();
assert.containsNone(target, ".modal", "command palette should not open");
triggerHotkey("alt+x"); // Move to next
await nextTick();
assert.containsNone(target, ".modal", "command palette should not open");
});
@ -612,8 +609,8 @@ QUnit.module("Fields", (hooks) => {
</header>
</form>`,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
if (method === "web_save") {
assert.step("web_save");
}
},
});
@ -621,9 +618,99 @@ QUnit.module("Fields", (hooks) => {
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current)"
);
await click(clickableButtons[clickableButtons.length - 1]);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
});
QUnit.test(
"For the same record, a single rpc is done to recover the specialData",
async function (assert) {
serverData.views = {
"partner,3,list": '<tree><field name="display_name"/></tree>',
"partner,9,search": `<search></search>`,
"partner,false,form": `<form>
<header>
<field name="trululu" widget="statusbar" readonly="1"/>
</header>
</form>`,
};
serverData.actions = {
1: {
id: 1,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
},
};
const mockRPC = (route, args) => {
if (args.method === "search_read") {
assert.step("search_read");
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1);
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.verifySteps(["search_read"]);
await click(target, ".o_back_button");
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.verifySteps([]);
}
);
QUnit.test(
"open form with statusbar, leave and come back to another one with other domain",
async function (assert) {
serverData.views = {
"partner,3,list": '<tree><field name="display_name"/></tree>',
"partner,9,search": `<search></search>`,
"partner,false,form": `<form>
<header>
<field name="trululu" widget="statusbar" domain="[['id', '>', id]]" readonly="1"/>
</header>
</form>`,
};
serverData.actions = {
1: {
id: 1,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
},
};
const mockRPC = (route, args) => {
if (args.method === "search_read") {
assert.step("search_read");
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1);
// open first record
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.verifySteps(["search_read"]);
// go back and open second record
await click(target, ".o_back_button");
await click(target.querySelectorAll(".o_data_row")[1].querySelector(".o_data_cell"));
assert.verifySteps(["search_read"]);
}
);
QUnit.test(
"clickable statusbar with readonly modifier set to false is editable",
async function (assert) {
@ -635,12 +722,15 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': true}" attrs="{'readonly': false}"/>
<field name="product_id" widget="statusbar" options="{'clickable': true}" readonly="False"/>
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button:visible", 2);
assert.containsNone(target, ".o_statusbar_status button.disabled[disabled]:visible");
assert.containsNone(
target,
".o_statusbar_status button[disabled][aria-checked='false']:visible"
);
}
);
@ -655,11 +745,11 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': true}" attrs="{'readonly': true}"/>
<field name="product_id" widget="statusbar" options="{'clickable': true}" readonly="True"/>
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button.disabled[disabled]:visible", 2);
assert.containsN(target, ".o_statusbar_status button[disabled]:visible", 2);
}
);
@ -674,11 +764,176 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': false}" attrs="{'readonly': false}"/>
<field name="product_id" widget="statusbar" options="{'clickable': false}" readonly="False"/>
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button.disabled[disabled]:visible", 2);
assert.containsN(target, ".o_statusbar_status button[disabled]:visible", 2);
}
);
QUnit.test(
"last status bar button have a border radius (no arrow shape) on the right side when a prior folded stage gets selected",
async function (assert) {
serverData.models = {
stage: {
fields: {
name: { string: "Name", type: "char" },
folded: { string: "Folded", type: "boolean", default: false },
},
records: [
{ id: 1, name: "New" },
{ id: 2, name: "In Progress", folded: true },
{ id: 3, name: "Done" },
],
},
task: {
fields: {
status: { string: "Status", type: "many2one", relation: "stage" },
},
records: [
{ id: 1, status: 1 },
{ id: 2, status: 2 },
{ id: 3, status: 3 },
],
},
};
await makeView({
type: "form",
resModel: "task",
resId: 3,
serverData,
arch: `
<form>
<header>
<field name="status" widget="statusbar" options="{'clickable': true, 'fold_field': 'folded'}" />
</header>
</form>`,
});
await click(target, ".o_statusbar_status .dropdown-toggle:not(.d-none)");
await click(target, ".o-dropdown .dropdown-item");
const button = target.querySelector(".o_statusbar_status button[data-value='3']");
assert.notEqual(button.style.borderTopRightRadius, "0px");
assert.hasClass(button, "o_first");
}
);
QUnit.test("correctly load statusbar when dynamic domain changes", async function (assert) {
serverData.models = {
stage: {
fields: {
name: { string: "Name", type: "char" },
folded: { string: "Folded", type: "boolean", default: false },
project_ids: { string: "Project", type: "many2many", relation: "project" },
},
records: [
{ id: 1, name: "Stage Project 1", project_ids: [1] },
{ id: 2, name: "Stage Project 2", project_ids: [2] },
],
},
project: {
fields: {
display_name: { string: "Name", type: "char" },
},
records: [
{ id: 1, display_name: "Project 1" },
{ id: 2, display_name: "Project 2" },
],
},
task: {
fields: {
status: { string: "Status", type: "many2one", relation: "stage" },
project_id: { string: "Project", type: "many2one", relation: "project" },
},
records: [{ id: 1, project_id: 1, status: 1 }],
},
};
serverData.models.task.onchanges = {
project_id: (obj) => {
obj.status = obj.project_id === 1 ? 1 : 2;
},
};
await makeView({
type: "form",
resModel: "task",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="status" widget="statusbar" domain="[('project_ids', 'in', project_id)]" />
</header>
<field name="project_id"/>
</form>`,
mockRPC(route, args) {
if (args.method === "search_read") {
assert.step(JSON.stringify(args.kwargs.domain));
}
},
});
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_statusbar_status button:not(.d-none)")),
["Stage Project 1"]
);
assert.verifySteps(['["|",["id","=",1],["project_ids","in",1]]']);
await selectDropdownItem(target, "project_id", "Project 2");
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_statusbar_status button:not(.d-none)")),
["Stage Project 2"]
);
assert.verifySteps(['["|",["id","=",2],["project_ids","in",2]]']);
await clickSave(target);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_statusbar_status button:not(.d-none)")),
["Stage Project 2"]
);
assert.verifySteps([]);
});
QUnit.test('"status" with no stages does not crash command palette', async function (assert) {
serverData.models = {
stage: {
fields: {
name: { string: "Stage Name", type: "char" },
},
records: [],
},
task: {
fields: {
status: { string: "Stage", type: "many2one", relation: "stage" },
},
records: [
{ id: 1, status: false }, // no stage set
],
},
};
await makeView({
serverData,
type: "form",
resModel: "task",
arch: `
<form>
<header>
<field name="status" widget="statusbar" options="{'withCommand': true, 'clickable': true}"/>
</header>
</form>`,
resId: 1,
});
// Open the command palette (Ctrl+K)
triggerHotkey("control+k");
await nextTick();
const commands = [...target.querySelectorAll(".o_command")].map((el) => el.textContent);
assert.notOk(
commands.some((txt) => txt.includes("Move to next Stage")),
"No 'Move to next stage' command available when no stages exist"
);
});
});

View file

@ -8,7 +8,9 @@ import {
clickSave,
editInput,
getFixture,
nextTick,
triggerEvent,
triggerHotkey,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
@ -20,7 +22,6 @@ let target;
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
@ -56,6 +57,17 @@ QUnit.module("Fields", (hooks) => {
},
],
},
partner_list: {
fields: {
partner_ids: {
string: "Partners",
type: "one2many",
relation: "partner",
relation_field: "id",
},
},
records: [{ id: 1, partner_ids: [1] }],
},
},
};
@ -159,7 +171,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="bar" />
<field name="txt" attrs="{'invisible': [('bar', '=', True)]}" />
<field name="txt" invisible="bar" />
</form>`,
});
@ -255,11 +267,7 @@ QUnit.module("Fields", (hooks) => {
});
const textarea = target.querySelector("textarea");
assert.strictEqual(
textarea.rows,
4,
"rowCount should be the one set on the field",
);
assert.strictEqual(textarea.rows, 4, "rowCount should be the one set on the field");
});
QUnit.test(
@ -282,8 +290,9 @@ QUnit.module("Fields", (hooks) => {
});
// ensure that autoresize is correctly done
let height = target.querySelector(".o_field_widget[name=text_field] textarea")
.offsetHeight;
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(
@ -352,8 +361,9 @@ QUnit.module("Fields", (hooks) => {
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;
height = target.querySelector(
".o_field_widget[name=text_field_empty] textarea"
).offsetHeight;
assert.strictEqual(height, 50, "empty textarea should have height of 50px");
});
@ -584,7 +594,11 @@ QUnit.module("Fields", (hooks) => {
arch: '<tree editable="top"><field name="foo"/></tree>',
});
await click(target.querySelector(".o_list_button_add"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_add"
)
);
assert.strictEqual(
target.querySelector("textarea"),
@ -592,4 +606,96 @@ QUnit.module("Fields", (hooks) => {
"text area should have the focus"
);
});
QUnit.test("field text with dynamic placeholder", async (assert) => {
serverData.models.partner.fields.model_reference_field = {
string: "Model Reference Field",
type: "char",
default: "partner",
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="model_reference_field" invisible="1"/>
<sheet>
<group>
<field
name="txt"
options="{
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'model_reference_field'
}"
/>
</group>
</sheet>
</form>`,
});
await click(target, "[name=txt] textarea");
assert.strictEqual(document.activeElement, target.querySelector("[name=txt] textarea"));
assert.containsNone(document.body, ".o_popover .o_model_field_selector_popover");
triggerHotkey("#");
await nextTick();
assert.containsOnce(document.body, ".o_popover .o_model_field_selector_popover");
});
QUnit.test("text field should vertical autoresize when saving", async function (assert) {
serverData.models.partner.fields.foo.type = "text";
serverData.models.partner.records[0].foo = "1";
await makeView({
type: "form",
resModel: "partner_list",
resId: 1,
serverData,
arch: `
<form>
<field name="partner_ids" widget="one2many">
<tree editable="bottom">
<field name="foo" widget="text"/>
</tree>
</field>
</form>`,
});
await click(target, "[name=foo] div");
let textarea = target.querySelector(".o_field_widget[name='foo'] textarea");
const initialHeight = textarea.offsetHeight;
await editInput(textarea, null, "1\n2\n3\n4\n5\n6\n7\n8");
await clickSave(target);
await click(target, "[name=foo] div");
textarea = target.querySelector(".o_field_widget[name='foo'] textarea");
const afterHeight = textarea.offsetHeight;
assert.ok(afterHeight > initialHeight, "Should be taller than one character");
});
QUnit.test("text field without line breaks", async function (assert) {
serverData.models.partner.fields.foo.type = "text";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form><field name="foo" options="{'line_breaks': False}"/></form>`,
});
assert.containsOnce(target, ".o_field_text textarea", "should have a text area");
const textarea = target.querySelector(".o_field_text textarea");
assert.strictEqual(textarea.value, "yop");
textarea.focus();
const keydownEvent = await triggerEvent(textarea, null, "keydown", { key: "Enter" });
assert.strictEqual(keydownEvent.defaultPrevented, true);
assert.strictEqual(textarea.value, "yop", "no line break should appear");
// Simulate a (very artificial) paste event
textarea.value = "text\nwith\nline\nbreaks\n";
await triggerEvent(textarea, null, "input", { inputType: "insertFromPaste" });
assert.strictEqual(textarea.value, "text with line breaks ", "no line break should appear");
});
});

View file

@ -55,7 +55,7 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
arch: /*xml*/ `
<tree string="Colors" editable="top">
<field name="tz_offset" invisible="True"/>
<field name="tz_offset" column_invisible="True"/>
<field name="color" widget="timezone_mismatch" />
</tree>
`,

View file

@ -226,7 +226,11 @@ QUnit.module("Fields", (hooks) => {
await editInput(cell, "input", "brolo");
// save
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
cell = target.querySelector("tbody td:not(.o_list_record_selector)");
assert.doesNotHaveClass(
cell.parentElement,
@ -272,7 +276,7 @@ QUnit.module("Fields", (hooks) => {
<form>
<sheet>
<group>
<field name="foo" widget="url" attrs="{'readonly': True}" />
<field name="foo" widget="url" readonly="True" />
<field name="foo2" />
</group>
</sheet>

View file

@ -6,6 +6,7 @@ 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";
import { Component, xml } from "@odoo/owl";
function compileTemplate(arch) {
const parser = new DOMParser();
@ -38,7 +39,7 @@ QUnit.module("Form Compiler", (hooks) => {
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 class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<div>lol</div>
</div>
</t>`;
@ -49,12 +50,12 @@ QUnit.module("Form Compiler", (hooks) => {
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 arch = /*xml*/ `<form><field field_id="test" 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 class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<Field id="'test'" name="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
<FormLabel id="'test'" fieldName="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" className="&quot;&quot;" string="\`\`" />
</div>
</t>`;
assert.areEquivalent(compileTemplate(arch), expected);
@ -62,13 +63,13 @@ QUnit.module("Form Compiler", (hooks) => {
);
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 arch = /*xml*/ `<form><div class="someClass">lol<field field_id="display_name" name="display_name"/></div></form>`;
const expected = /*xml*/ `
<t t-translation="off">
<div 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_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<div class="someClass">
lol
<Field id="'display_name'" name="'display_name'" record="props.record" fieldInfo="props.archInfo.fieldNodes['display_name']" readonly="props.archInfo.activeActions?.edit === false and !props.record.isNew" setDirty.alike="props.setFieldAsDirty"/>
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
</div>
</div>
</t>`;
@ -80,23 +81,23 @@ QUnit.module("Form Compiler", (hooks) => {
const arch = /*xml*/ `
<form>
<group>
<group><field name="display_name"/></group>
<group><field name="charfield"/></group>
<group><field field_id="display_name" name="display_name"/></group>
<group><field field_id="charfield" 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 t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'display_name',fieldName:'display_name',record:__comp__.props.record,string:__comp__.props.record.fields.display_name.string,fieldInfo:__comp__.props.archInfo.fieldNodes['display_name']}" Component="__comp__.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew" class="scope &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 t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'charfield',fieldName:'charfield',record:__comp__.props.record,string:__comp__.props.record.fields.charfield.string,fieldInfo:__comp__.props.archInfo.fieldNodes['charfield']}" Component="__comp__.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
<Field id="'charfield'" name="'charfield'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['charfield']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew" class="scope &amp;&amp; scope.className"/>
</t>
</InnerGroup>
</t>
@ -112,7 +113,7 @@ QUnit.module("Form Compiler", (hooks) => {
<group>
<form>
<div>
<field name="test"/>
<field field_id="test" name="test"/>
</div>
</form>
</group>
@ -120,13 +121,13 @@ QUnit.module("Form Compiler", (hooks) => {
</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_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<OuterGroup>
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
<InnerGroup class="scope &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 class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }} {{scope &amp;&amp; scope.className || &quot;&quot; }}">
<div><Field id="'test'" name="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/></div>
</div>
</t>
</InnerGroup>
@ -143,18 +144,18 @@ QUnit.module("Form Compiler", (hooks) => {
const arch = /*xml*/ `
<form>
<notebook>
<page name="p1" string="Page1"><field name="charfield"/></page>
<page name="p2" string="Page2"><field name="display_name"/></page>
<page name="p1" string="Page1"><field field_id="charfield" name="charfield"/></page>
<page name="p2" string="Page2"><field field_id="display_name" name="display_name"/></page>
</notebook>
</form>`;
const expected = /*xml*/ `
<Notebook defaultPage="props.record.isNew ? undefined : props.activeNotebookPages[0]" onPageUpdate="(page) =&gt; this.props.onNotebookPageChange(0, page)">
<Notebook defaultPage="__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[0]" onPageUpdate="(page) =&gt; __comp__.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"/>
<Field id="'charfield'" name="'charfield'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['charfield']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
</t>
<t t-set-slot="page_2" title="\`Page2\`" name="\`p2\`" isVisible="true">
<Field id="'display_name'" name="'display_name'" record="props.record" fieldInfo="props.archInfo.fieldNodes['display_name']" readonly="props.archInfo.activeActions?.edit === false and !props.record.isNew" setDirty.alike="props.setFieldAsDirty"/>
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
</t>
</Notebook>`;
@ -164,11 +165,11 @@ QUnit.module("Form Compiler", (hooks) => {
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..."/>
<field field_id="display_name" 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"/>
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
@ -183,8 +184,8 @@ QUnit.module("Form Compiler", (hooks) => {
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 class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<div class="o_form_statusbar position-relative d-flex justify-content-between mb-0 mb-md-2 pb-2 pb-md-0"><StatusBarButtons/></div>
<div>someDiv</div>
</div>
</t>`;
@ -205,11 +206,11 @@ QUnit.module("Form Compiler", (hooks) => {
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_renderer" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ __comp__.uiService.size &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }} {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<div class="o_form_sheet_bg">
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons readonly="!props.record.isInEdition"/></div>
<div class="o_form_statusbar position-relative d-flex justify-content-between mb-0 mb-md-2 pb-2 pb-md-0"><StatusBarButtons/></div>
<div>someDiv</div>
<div class="o_form_sheet position-relative clearfix">
<div class="o_form_sheet position-relative">
<div>inside sheet</div>
</div>
</div>
@ -221,6 +222,33 @@ QUnit.module("Form Compiler", (hooks) => {
assert.areEquivalent(compileTemplate(arch), expected);
});
QUnit.test("properly compile buttonBox invisible in sheet", async (assert) => {
const arch = /*xml*/ `
<form>
<sheet>
<div class="oe_button_box" name="button_box" invisible="'display_name' == 'plop'">
<div>Hello</div>
</div>
</sheet>
</form>`;
const expected = /*xml*/ `
<t t-translation="off">
<div class="o_form_renderer"
t-att-class="__comp__.props.class"
t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ __comp__.uiService.size &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }} {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}"
t-ref="compiled_view_root">
<div class="o_form_sheet_bg">
<div class="o_form_sheet position-relative">
</div>
</div>
</div>
</t>
`;
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
@ -228,18 +256,18 @@ QUnit.module("Form Compiler", (hooks) => {
// ```<form>
// <field name="display_name" invisible="1" />
// <div class="visible3" invisible="0"/>
// <div modifiers="{'invisible': [['display_name', '=', 'take']]}"/>
// <div 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;]]}"/>
<field field_id="display_name" name="display_name" invisible="True" />
<div class="visible3" invisible="False"/>
<div invisible="display_name == &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;]])" />
<div t-if="!__comp__.evaluateBooleanExpr(&quot;display_name == \\&quot;take\\&quot;&quot;,__comp__.props.record.evalContextWithVirtualIds)" />
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
@ -248,14 +276,14 @@ QUnit.module("Form Compiler", (hooks) => {
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;}"/>
<field name="display_name" invisible="True" />
<div class="visible3" invisible="False"/>
<div invisible="display_name == 'take'"/>
</form>`;
const expected = /*xml*/ `
<div class="visible3" />
<div t-if="!evalDomainFromRecord(props.record,&quot;[['display_name','=','take']]&quot;)" />
<div t-if="!__comp__.evaluateBooleanExpr(&quot;display_name == 'take'&quot;,__comp__.props.record.evalContextWithVirtualIds)" />
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
});
@ -267,8 +295,8 @@ QUnit.module("Form Compiler", (hooks) => {
</form>`;
const expected = /*xml*/ `
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom">
<StatusBarButtons readonly="!props.record.isInEdition">
<div class="o_form_statusbar position-relative d-flex justify-content-between mb-0 mb-md-2 pb-2 pb-md-0">
<StatusBarButtons>
<t t-set-slot="button_0" isVisible="true">
<div>someDiv</div>
</t>
@ -285,8 +313,64 @@ QUnit.module("Form Compiler", (hooks) => {
</form>`;
const expected = /*xml*/ `
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom">
<StatusBarButtons readonly="!props.record.isInEdition"/>
<div class="o_form_statusbar position-relative d-flex justify-content-between mb-0 mb-md-2 pb-2 pb-md-0">
<StatusBarButtons/>
</div>`;
assert.areContentEquivalent(compileTemplate(arch), expected);
});
QUnit.test("properly compile settings", (assert) => {
const arch = /*xml*/ `
<form>
<setting
help="this is bar"
documentation="/applications/technical/web/settings/this_is_a_test.html"
company_dependent="1">
<field field_id="bar" name="bar"/>
<label>label with content</label>
</setting>
</form>`;
const expected = /*xml*/ `
<Setting title="\`\`"
help="\`this is bar\`"
companyDependent="true"
documentation="\`/applications/technical/web/settings/this_is_a_test.html\`"
record="__comp__.props.record"
fieldInfo="__comp__.props.archInfo.fieldNodes['bar']"
fieldName="\`bar\`"
fieldId="\`bar\`"
string="\`\`"
addLabel="true">
<t t-set-slot="fieldSlot">
<Field id="'bar'"
name="'bar'"
record="__comp__.props.record"
fieldInfo="__comp__.props.archInfo.fieldNodes['bar']"
readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"/>
</t>
<label>label with content</label>
</Setting>`;
assert.areContentEquivalent(compileTemplate(arch), expected);
});
QUnit.test("properly compile empty ButtonBox", (assert) => {
const arch = /*xml*/ `
<form>
<sheet>
<div class="oe_button_box" name="button_box">
</div>
</sheet>
</form>`;
const expected = /*xml*/ `
<div class="o_form_sheet_bg">
<div class="o_form_sheet position-relative">
<div class="oe_button_box" name="button_box">
</div>
</div>
</div>`;
assert.areContentEquivalent(compileTemplate(arch), expected);
@ -314,14 +398,14 @@ QUnit.module("Form Renderer", (hooks) => {
};
});
QUnit.test("compile form with modifiers and attrs - string as domain", async (assert) => {
QUnit.test("compile form with modifiers", async (assert) => {
serverData.views = {
"partner,1,form": /*xml*/ `
<form>
<div modifiers="{&quot;invisible&quot;: &quot;[['display_name', '=', uid]]&quot;}">
<div invisible="display_name == uid">
<field name="charfield"/>
</div>
<field name="display_name" attrs="{'readonly': &quot;[['display_name', '=', uid]]&quot;}"/>
<field name="display_name" readonly="display_name == uid"/>
</form>`,
};
@ -343,7 +427,7 @@ QUnit.module("Form Renderer", (hooks) => {
<form>
<sheet>
<notebook>
<page name="p1" attrs="{'invisible': [['display_name', '=', 'lol']]}"><field name="charfield"/></page>
<page name="p1" invisible="display_name == 'lol'"><field name="charfield"/></page>
<page name="p2"><field name="display_name"/></page>
</notebook>
</sheet>
@ -381,15 +465,19 @@ QUnit.module("Form Renderer", (hooks) => {
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 });
const charField = {
component: class CharField extends Component {
static template = xml`<div/>`;
setup() {
assert.strictEqual(
this.props.placeholder,
"e.g. Contact's Name or //someinfo..."
);
}
},
extractProps: ({ attrs }) => ({ placeholder: attrs.placeholder }),
};
registry.category("fields").add("char", charField, { force: true });
serverData.views = {
"partner,1,form": /*xml*/ `
@ -448,7 +536,7 @@ QUnit.module("Form Renderer", (hooks) => {
QUnit.test("invisible is correctly computed with another t-if", (assert) => {
patchWithCleanup(FormCompiler.prototype, {
setup() {
this._super();
super.setup();
this.compilers.push({
selector: "myNode",
fn: () => {
@ -461,9 +549,39 @@ QUnit.module("Form Renderer", (hooks) => {
},
});
const arch = `<myNode modifiers="{&quot;invisible&quot;: [[&quot;field&quot;, &quot;=&quot;, &quot;value&quot;]]}" />`;
const arch = `<myNode invisible="field == 'value'" />`;
const expected = `<t t-translation="off"><div class="myNode" t-if="( myCondition or myOtherCondition ) and !__comp__.evaluateBooleanExpr(&quot;field == 'value'&quot;,__comp__.props.record.evalContextWithVirtualIds)" t-ref="compiled_view_root"/></t>`;
assert.areEquivalent(compileTemplate(arch), expected);
});
QUnit.test("keep nosheet style if a sheet is part of a nested form", (assert) => {
const arch = `
<form>
<field name="move_line_ids" field_id="move_line_ids">
<form>
<sheet/>
</form>
</field>
</form>`;
const expected = `<t t-translation="off">
<div
class="o_form_renderer o_form_nosheet"
t-att-class="__comp__.props.class"
t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}"
t-ref="compiled_view_root"
>
<Field
id="'move_line_ids'"
name="'move_line_ids'"
record="__comp__.props.record"
fieldInfo="__comp__.props.archInfo.fieldNodes['move_line_ids']"
readonly="__comp__.props.archInfo.activeActions?.edit === false and !__comp__.props.record.isNew"
/>
</div>
</t>`;
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);
});
});

View file

@ -1,31 +1,25 @@
/** @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 { createDebugContext } from "@web/core/debug/debug_context";
import { Dialog } from "@web/core/dialog/dialog";
import { MainComponentsContainer } from "@web/core/main_components_container";
import {
setupControlPanelFavoriteMenuRegistry,
setupControlPanelServiceRegistry,
} from "../search/helpers";
import { addLegacyMockEnvironment } from "../webclient/helpers";
import { registry } from "@web/core/registry";
import { View, getDefaultConfig } from "@web/views/view";
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 {
setupControlPanelFavoriteMenuRegistry,
setupControlPanelServiceRegistry,
} from "../search/helpers";
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>`;
@ -41,9 +35,10 @@ const rootDialogTemplate = xml`<Dialog><View t-props="props.viewProps"/></Dialog
*/
/**
* @template {Component} T
* @param {MakeViewParams} params
* @param {boolean} [inDialog=false]
* @returns {Component}
* @returns {Promise<T>}
*/
async function _makeView(params, inDialog = false) {
const props = { ...params };
@ -53,11 +48,9 @@ async function _makeView(params, inDialog = false) {
...getDefaultConfig(),
...props.config,
};
const legacyParams = props.legacyParams || {};
delete props.serverData;
delete props.mockRPC;
delete props.legacyParams;
delete props.config;
if (props.arch) {
@ -74,22 +67,6 @@ async function _makeView(params, inDialog = false) {
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 });
@ -124,7 +101,6 @@ async function _makeView(params, inDialog = false) {
/**
* @param {MakeViewParams} params
* @returns {Component}
*/
export function makeView(params) {
return _makeView(params);
@ -132,7 +108,6 @@ export function makeView(params) {
/**
* @param {MakeViewParams} params
* @returns {Component}
*/
export function makeViewInDialog(params) {
return _makeView(params, true);
@ -147,27 +122,6 @@ export function setupViewRegistries() {
{ force: true }
);
serviceRegistry.add("router", makeFakeRouterService(), { force: true });
serviceRegistry.add("localization", makeFakeLocalizationService()), { force: true };
serviceRegistry.add("popover", popoverService), { force: true };
serviceRegistry.add("localization", makeFakeLocalizationService());
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,113 @@
/** @odoo-module */
import { makeFakeDialogService } from "@web/../tests/helpers/mock_services";
import { click, editInput, nextTick } from "@web/../tests/helpers/utils";
import { registry } from "@web/core/registry";
export function patchDialog(addDialog) {
registry.category("services").add("dialog", makeFakeDialogService(addDialog), { force: true });
}
// Kanban
// WOWL remove this helper and use the control panel instead
export async function reload(kanban, params = {}) {
kanban.env.searchModel.reload(params);
kanban.env.searchModel.search();
await nextTick();
}
export function getCard(target, cardIndex = 0) {
return target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")[cardIndex];
}
export function getColumn(target, groupIndex = 0, ignoreFolded = false) {
let selector = ".o_kanban_group";
if (ignoreFolded) {
selector += ":not(.o_column_folded)";
}
return target.querySelectorAll(selector)[groupIndex];
}
export function getCardTexts(target, groupIndex) {
const root = groupIndex >= 0 ? getColumn(target, groupIndex) : target;
return [...root.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")]
.map((card) => card.innerText.trim())
.filter(Boolean);
}
export function getCounters(target) {
return [...target.querySelectorAll(".o_animated_number")].map((counter) => counter.innerText);
}
export function getProgressBars(target, columnIndex) {
const column = getColumn(target, columnIndex);
return [...column.querySelectorAll(".o_column_progress .progress-bar")];
}
export function getTooltips(target, groupIndex) {
const root = groupIndex >= 0 ? getColumn(target, groupIndex) : target;
return [...root.querySelectorAll(".o_column_progress .progress-bar")]
.map((card) => card.dataset.tooltip)
.filter(Boolean);
}
// Record
export async function createRecord(target) {
await click(target, ".o_control_panel_main_buttons .d-none button.o-kanban-button-new");
await nextTick();
}
export async function quickCreateRecord(target, groupIndex) {
await click(getColumn(target, groupIndex), ".o_kanban_quick_add");
await nextTick();
}
export async function editQuickCreateInput(target, field, value) {
await editInput(target, `.o_kanban_quick_create .o_field_widget[name=${field}] input`, value);
}
export async function validateRecord(target) {
await click(target, ".o_kanban_quick_create .o_kanban_add");
}
export async function editRecord(target) {
await click(target, ".o_kanban_quick_create .o_kanban_edit");
}
export async function discardRecord(target) {
await click(target, ".o_kanban_quick_create .o_kanban_cancel");
}
export async function toggleRecordDropdown(target, recordIndex) {
const group = target.querySelectorAll(`.o_kanban_record`)[recordIndex];
await click(group, ".o_dropdown_kanban .dropdown-toggle");
}
// Column
export async function createColumn(target) {
await click(target, ".o_column_quick_create > .o_quick_create_folded");
}
export async function editColumnName(target, value) {
await editInput(target, ".o_column_quick_create input", value);
}
export async function validateColumn(target) {
await click(target, ".o_column_quick_create .o_kanban_add");
}
export async function toggleColumnActions(target, columnIndex) {
const group = getColumn(target, columnIndex);
await click(group, ".o_kanban_config .dropdown-toggle");
const buttons = group.querySelectorAll(".o_kanban_config .dropdown-menu .dropdown-item");
return (buttonText) => {
const re = new RegExp(`\\b${buttonText}\\b`, "i");
const button = [...buttons].find((b) => re.test(b.innerText));
return click(button);
};
}
export async function loadMore(target, columnIndex) {
await click(getColumn(target, columnIndex), ".o_kanban_load_more button");
}

View file

@ -0,0 +1,36 @@
/** @odoo-module **/
import { KanbanArchParser } from "@web/views/kanban/kanban_arch_parser";
import { parseXML } from "@web/core/utils/xml";
function parseArch(arch, options = {}) {
const parser = new KanbanArchParser();
const xmlDoc = parseXML(arch);
return parser.parse(xmlDoc, { fake: {name: { string: "Name", type: "char" },} }, "fake");
}
QUnit.module("KanbanView - ArchParser");
QUnit.test("oe_kanban_colorpicker in kanban-menu and kanban-box", (assert) => {
const archInfo = parseArch(`
<kanban>
<templates>
<t t-name="kanban-menu">
<ul class="oe_kanban_colorpicker" data-field="kanban_menu_colorpicker" role="menu"/>
</t>
<t t-name="kanban-box"/>
</templates>
</kanban>
`);
assert.strictEqual(archInfo.colorField, "kanban_menu_colorpicker", "colorField should be 'kanban_menu_colorpicker'");
const archInfo_1 = parseArch(`
<kanban>
<templates>
<t t-name="kanban-menu"/>
<t t-name="kanban-box">
<ul class="oe_kanban_colorpicker" data-field="kanban_box_color" role="menu"/>
</t>
</templates>
</kanban>
`);
assert.strictEqual(archInfo_1.colorField, "kanban_box_color", "colorField should be 'kanban_box_color'");
});

View file

@ -5,8 +5,10 @@ import { makeWithSearch, setupControlPanelServiceRegistry } from "@web/../tests/
import { Layout } from "@web/search/layout";
import { getDefaultConfig } from "@web/views/view";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { SearchModel } from "@web/search/search_model";
import { Component, xml, useChildSubEnv } from "@odoo/owl";
import { Component, xml, onWillStart, useChildSubEnv, useSubEnv } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
let target;
let serverData;
@ -68,7 +70,7 @@ QUnit.module("Views", (hooks) => {
class ToyComponent extends Component {}
ToyComponent.template = xml`
<Layout display="props.display">
<t t-set-slot="control-panel-top-right">
<t t-set-slot="layout-actions">
<div class="toy_search_bar" />
</t>
<div class="toy_content" />
@ -82,12 +84,49 @@ QUnit.module("Views", (hooks) => {
searchViewId: false,
});
assert.containsOnce(target, ".o_control_panel .o_cp_top_right .toy_search_bar");
assert.containsOnce(target, ".o_control_panel .o_control_panel_actions .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("Rendering with default ControlPanel and SearchPanel", async (assert) => {
class ToyComponent extends Component {
setup() {
this.searchModel = new SearchModel(this.env, {
user: useService("user"),
orm: useService("orm"),
view: useService("view"),
});
useSubEnv({ searchModel: this.searchModel });
onWillStart(async () => {
await this.searchModel.load({ resModel: "foo" });
});
}
}
ToyComponent.template = xml`
<Layout className="'o_view_sample_data'" display="{
controlPanel: {},
searchPanel: true,
}">
<div class="toy_content" />
</Layout>`;
ToyComponent.components = { Layout };
const env = await makeTestEnv();
const toyEnv = Object.assign(Object.create(env), {
config: { breadcrumbs: getDefaultConfig().breadcrumbs },
});
await mount(ToyComponent, getFixture(), { env: toyEnv });
assert.containsOnce(target, ".o_search_panel");
assert.containsOnce(target, ".o_control_panel");
assert.containsOnce(target, ".o_breadcrumb");
assert.containsOnce(target, ".o_component_with_search_panel");
assert.containsOnce(target, ".o_content > .toy_content");
});
QUnit.test("Nested layouts", async (assert) => {
// Component C: bottom (no control panel)
class ToyC extends Component {
@ -120,7 +159,7 @@ QUnit.module("Views", (hooks) => {
}
ToyB.template = xml`
<Layout className="'toy_b'" display="props.display">
<t t-set-slot="control-panel-top-right">
<t t-set-slot="layout-actions">
<div class="toy_b_breadcrumbs" />
</t>
<ToyC/>
@ -131,7 +170,7 @@ QUnit.module("Views", (hooks) => {
class ToyA extends Component {}
ToyA.template = xml`
<Layout className="'toy_a'" display="props.display">
<t t-set-slot="control-panel-top-right">
<t t-set-slot="layout-actions">
<div class="toy_a_search" />
</t>
<ToyB display="props.display"/>
@ -252,21 +291,21 @@ QUnit.module("Views", (hooks) => {
});
QUnit.test("Simple rendering: with dynamically displayed search", async (assert) => {
let displayControlPanelTopRight = true;
let displayLayoutActions = true;
class ToyComponent extends Component {
get display() {
return {
...this.props.display,
controlPanel: {
...this.props.display.controlPanel,
"top-right": displayControlPanelTopRight,
layoutActions: displayLayoutActions,
},
};
}
}
ToyComponent.template = xml`
<Layout display="display">
<t t-set-slot="control-panel-top-right">
<t t-set-slot="layout-actions">
<div class="toy_search_bar" />
</t>
<div class="toy_content" />
@ -280,16 +319,16 @@ QUnit.module("Views", (hooks) => {
searchViewId: false,
});
assert.containsOnce(target, ".o_control_panel .o_cp_top_right .toy_search_bar");
assert.containsOnce(target, ".o_control_panel .o_control_panel_actions .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;
displayLayoutActions = false;
comp.render();
await nextTick();
assert.containsNone(target, ".o_control_panel .o_cp_top_right .toy_search_bar");
assert.containsNone(target, ".o_control_panel .o_control_panel_actions .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");

View file

@ -1,12 +1,24 @@
/** @odoo-module **/
import { Component, xml, useState, onError } from "@odoo/owl";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { browser } from "@web/core/browser/browser";
import { useRecordObserver } from "@web/model/relational_model/utils";
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";
import { CharField } from "@web/views/fields/char/char_field";
import { Many2OneField } from "@web/views/fields/many2one/many2one_field";
import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
import { Record } from "@web/model/record";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import {
click,
editInput,
getFixture,
getNodesTextContent,
mount,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
@ -111,7 +123,7 @@ QUnit.module("Record Component", (hooks) => {
);
assert.verifySteps([
"/web/dataset/call_kw/partner/fields_get",
"/web/dataset/call_kw/partner/read",
"/web/dataset/call_kw/partner/web_read",
]);
});
@ -127,7 +139,7 @@ QUnit.module("Record Component", (hooks) => {
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>
<button class="my-btn" t-on-click="() => this.state.resId++">Next</button>
</Record>`;
const env = await makeTestEnv({
serverData,
@ -138,12 +150,12 @@ QUnit.module("Record Component", (hooks) => {
await mount(Parent, target, { env, dev: true });
assert.verifySteps([
"/web/dataset/call_kw/partner/fields_get",
"/web/dataset/call_kw/partner/read",
"/web/dataset/call_kw/partner/web_read",
]);
assert.containsOnce(target, ".o_field_char:contains(yop)");
await click(target.querySelector("button"));
await click(target.querySelector("button.my-btn"));
assert.containsOnce(target, ".o_field_char:contains(blip)");
assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
assert.verifySteps(["/web/dataset/call_kw/partner/web_read"]);
});
QUnit.test("predefined fields and values", async function (assert) {
@ -167,7 +179,7 @@ QUnit.module("Record Component", (hooks) => {
}
Parent.components = { Record, Field };
Parent.template = xml`
<Record resModel="'partner'" fieldNames="['foo']" fields="fields" initialValues="values" t-slot-scope="data">
<Record resModel="'partner'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
<Field name="'foo'" record="data.record"/>
</Record>
`;
@ -183,4 +195,523 @@ QUnit.module("Record Component", (hooks) => {
assert.verifySteps([]);
assert.strictEqual(target.querySelector(".o_field_widget input").value, "abc");
});
QUnit.test("provides a way to handle changes in the record", 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,
};
}
onRecordChanged(record, changes) {
assert.step("record changed");
assert.strictEqual(record.model.constructor.name, "StandaloneRelationalModel");
assert.deepEqual(changes, { foo: "753" });
}
}
Parent.components = { Record, Field };
Parent.template = xml`
<Record resModel="'partner'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data" onRecordChanged.bind="onRecordChanged">
<Field name="'foo'" record="data.record"/>
</Record>
`;
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route) {
assert.step(route);
},
}),
});
assert.strictEqual(target.querySelector("[name='foo'] input").value, "abc");
await editInput(target, "[name='foo'] input", "753");
assert.verifySteps(["record changed"]);
assert.strictEqual(target.querySelector("[name='foo'] input").value, "753");
});
QUnit.test("provides a way to handle before/after saved the record", async function (assert) {
class Parent extends Component {
onRecordSaved(record) {
assert.step("onRecordSaved");
}
onWillSaveRecord(record) {
assert.step("onWillSaveRecord");
}
}
Parent.components = { Record, Field };
Parent.template = xml`
<Record resModel="'partner'" resId="1" fieldNames="['foo']" mode="'edit'" t-slot-scope="data" onRecordSaved="onRecordSaved" onWillSaveRecord="onWillSaveRecord">
<button class="save" t-on-click="() => data.record.save()">Save</button>
<Field name="'foo'" record="data.record"/>
</Record>`;
const env = await makeTestEnv({
serverData,
mockRPC(route, args) {
assert.step(args.method);
},
});
await mount(Parent, target, { env });
await editInput(target, "[name='foo'] input", "abc");
await click(target, "button.save");
assert.verifySteps([
"fields_get",
"web_read",
"onWillSaveRecord",
"web_save",
"onRecordSaved",
]);
});
QUnit.test("handles many2one fields", async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
serverData.models = {
bar: {
records: [
{ id: 1, display_name: "bar1" },
{ id: 3, display_name: "abc" },
],
},
};
class Parent extends Component {
setup() {
this.fields = {
foo: {
name: "foo",
type: "many2one",
relation: "bar",
},
};
this.values = {
foo: [1, "bar1"],
};
}
onRecordChanged(record, changes) {
assert.step("record changed");
assert.deepEqual(changes, { foo: 3 });
assert.deepEqual(record.data, { foo: [3, "abc"] });
}
}
Parent.components = { Record, Many2OneField };
Parent.template = xml`
<Record resModel="'partner'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data" onRecordChanged.bind="onRecordChanged">
<Many2OneField name="'foo'" record="data.record" relation="'bar'" value="data.record.data.foo"/>
</Record>
`;
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route, args) {
assert.step(route);
},
}),
});
assert.verifySteps([]);
assert.strictEqual(target.querySelector(".o_field_many2one_selection input").value, "bar1");
await editInput(target, ".o_field_many2one_selection input", "abc");
assert.verifySteps(["/web/dataset/call_kw/bar/name_search"]);
await click(target.querySelectorAll(".o-autocomplete--dropdown-item a")[0]);
assert.verifySteps(["record changed"]);
assert.strictEqual(target.querySelector(".o_field_many2one_selection input").value, "abc");
});
QUnit.test("handles many2one fields (2)", async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
serverData.models = {
bar: {
records: [
{ id: 1, display_name: "bar1" },
{ id: 3, display_name: "abc" },
],
},
};
class Parent extends Component {
setup() {
this.fields = {
foo: {
name: "foo",
type: "many2one",
relation: "bar",
},
};
this.values = {
foo: 1,
};
}
onRecordChanged(record, changes) {
assert.step("record changed");
assert.deepEqual(changes, { foo: 3 });
assert.deepEqual(record.data, { foo: [3, "abc"] });
}
}
Parent.components = { Record, Many2OneField };
Parent.template = xml`
<Record resModel="'partner'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
<Many2OneField name="'foo'" record="data.record" relation="'bar'" value="data.record.data.foo"/>
</Record>
`;
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route, args) {
assert.step(route);
},
}),
});
assert.verifySteps(["/web/dataset/call_kw/bar/web_read"]);
assert.strictEqual(target.querySelector(".o_field_many2one_selection input").value, "bar1");
});
QUnit.test("handles many2one fields (3)", async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
serverData.models = {
bar: {
records: [
{ id: 1, display_name: "bar1" },
{ id: 3, display_name: "abc" },
],
},
};
class Parent extends Component {
setup() {
this.fields = {
foo: {
name: "foo",
type: "many2one",
relation: "bar",
},
};
this.values = {
foo: [1],
};
}
onRecordChanged(record, changes) {
assert.step("record changed");
assert.deepEqual(changes, { foo: 3 });
assert.deepEqual(record.data, { foo: [3, "abc"] });
}
}
Parent.components = { Record, Many2OneField };
Parent.template = xml`
<Record resModel="'partner'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
<Many2OneField name="'foo'" record="data.record" relation="'bar'" value="data.record.data.foo"/>
</Record>
`;
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route, args) {
assert.step(route);
},
}),
});
assert.verifySteps(["/web/dataset/call_kw/bar/web_read"]);
assert.strictEqual(target.querySelector(".o_field_many2one_selection input").value, "bar1");
});
QUnit.test("handles x2many fields", async function (assert) {
serverData.models = {
tag: {
records: [
{ id: 1, display_name: "bug" },
{ id: 3, display_name: "ref" },
],
},
};
class Parent extends Component {
setup() {
this.activeFields = {
tags: {
related: {
activeFields: {
display_name: {},
},
fields: {
display_name: { name: "display_name", type: "string" },
},
},
},
};
this.fields = {
tags: {
name: "Tags",
type: "many2many",
relation: "tag",
},
};
this.values = {
tags: [1, 3],
};
}
}
Parent.components = { Record, Many2ManyTagsField };
Parent.template = xml`
<Record resModel="'partner'" fieldNames="['tags']" activeFields="activeFields" fields="fields" values="values" t-slot-scope="data">
<Many2ManyTagsField name="'tags'" record="data.record"/>
</Record>
`;
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route, args) {
assert.step(route);
},
}),
});
assert.verifySteps(["/web/dataset/call_kw/tag/web_read"]);
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_tag")), ["bug", "ref"]);
});
QUnit.test(
"supports passing dynamic values -- full control to the user of Record",
async (assert) => {
class Parent extends Component {
setup() {
this.fields = {
foo: {
name: "foo",
type: "char",
},
bar: {
name: "bar",
type: "boolean",
},
};
this.values = useState({
foo: "abc",
bar: true,
});
}
onRecordChanged(record, changes) {
assert.step("record changed");
assert.strictEqual(record.model.constructor.name, "StandaloneRelationalModel");
assert.deepEqual(changes, { foo: "753" });
this.values.foo = "357";
}
}
Parent.components = { Record, Field };
Parent.template = xml`
<Record resModel="'partner'" fieldNames="['foo']" fields="fields" values="{ foo: values.foo }" t-slot-scope="data" onRecordChanged.bind="onRecordChanged">
<Field name="'foo'" record="data.record"/>
</Record>
`;
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route) {
throw new Error("should not do any rpc");
},
}),
});
assert.strictEqual(target.querySelector("[name='foo'] input").value, "abc");
await editInput(target, "[name='foo'] input", "753");
assert.verifySteps(["record changed"]);
await nextTick();
assert.strictEqual(target.querySelector("[name='foo'] input").value, "357");
}
);
QUnit.test("can switch records", async (assert) => {
class Parent extends Component {
setup() {
this.fields = {
foo: {
name: "foo",
type: "char",
},
bar: {
name: "bar",
type: "boolean",
},
};
this.state = useState({ currentId: 1, num: 0 });
}
next() {
this.state.currentId = 5;
this.state.num++;
}
}
Parent.components = { Record, Field };
Parent.template = xml`
<a id="increment" t-on-click="() => state.num++" t-esc="state.num" />
<a id="next" t-on-click="next">NEXT</a>
<Record resId="state.currentId" resModel="'partner'" fieldNames="['foo']" fields="fields" t-slot-scope="data">
<Field name="'foo'" record="data.record"/>
</Record>
`;
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route, { method, args, kwargs }) {
assert.step(
`${method} : ${JSON.stringify(args[0])} - ${JSON.stringify(
kwargs.specification
)}`
);
},
}),
});
assert.verifySteps([`web_read : [1] - {"foo":{}}`]);
const increment = target.querySelector("#increment");
const field = target.querySelector("div[name='foo']");
assert.strictEqual(increment.textContent, "0");
assert.strictEqual(field.textContent, "yop");
await click(increment);
// No reload when a render from upstream comes
assert.verifySteps([]);
assert.strictEqual(increment.textContent, "1");
assert.strictEqual(field.textContent, "yop");
await click(target.querySelector("#next"));
assert.verifySteps([`web_read : [5] - {"foo":{}}`]);
assert.strictEqual(increment.textContent, "2");
assert.strictEqual(field.textContent, "blop");
});
QUnit.test("can switch records with values", async (assert) => {
class Parent extends Component {
setup() {
this.fields = {
foo: {
name: "foo",
type: "char",
},
bar: {
name: "bar",
type: "boolean",
},
};
this.values = {
foo: "abc",
bar: true,
};
this.state = useState({ currentId: 99 });
}
next() {
this.state.currentId = 100;
this.values = {
foo: "def",
bar: false,
};
}
}
Parent.components = { Record, Field };
Parent.template = xml`
<a id="next" t-on-click="next">NEXT</a>
<Record resId="state.currentId" resModel="'partner'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
<Field name="'foo'" record="data.record"/>
</Record>
`;
let _record;
patchWithCleanup(Record.components._Record.prototype, {
setup() {
super.setup();
_record = this;
},
});
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route) {
assert.step(route);
},
}),
});
// No load since the values are provided to the record
assert.verifySteps([]);
const field = target.querySelector("div[name='foo']");
// First values are loaded
assert.strictEqual(field.textContent, "abc");
// Verify that the underlying _Record Model root has the specified resId
assert.strictEqual(_record.model.root.resId, 99);
await click(target.querySelector("#next"));
// Still no load.
assert.verifySteps([]);
// Second values are loaded
assert.strictEqual(field.textContent, "def");
// Verify that the underlying _Record Model root has the updated resId
assert.strictEqual(_record.model.root.resId, 100);
});
QUnit.test("faulty useRecordObserver in widget", async (assert) => {
patchWithCleanup(CharField.prototype, {
setup() {
super.setup();
useRecordObserver((record, props) => {
throw new Error("faulty record observer");
});
},
});
class Parent extends Component {
static components = { Record, Field };
static template = xml`
<t t-if="!state.error">
<Record resId="1" resModel="'partner'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
<Field name="'foo'" record="data.record"/>
</Record>
</t>
<div t-else="" class="error" t-esc="state.error.message" />`;
setup() {
this.state = useState({ error: false });
onError((error) => {
this.state.error = error;
});
}
}
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
}),
});
assert.strictEqual(
target.querySelector(".error").textContent,
`The following error occurred in onWillStart: "faulty record observer"`
);
});
});

View file

@ -1,534 +0,0 @@
/** @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,135 @@
/** @odoo-module **/
import { Component, useState, xml } from "@odoo/owl";
import { ViewScaleSelector } from "@web/views/view_components/view_scale_selector";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { click, getFixture, mount } from "@web/../tests/helpers/utils";
import { registry } from "@web/core/registry";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { uiService } from "@web/core/ui/ui_service";
const serviceRegistry = registry.category("services");
let target;
QUnit.module("ViewComponents", (hooks) => {
hooks.beforeEach(async () => {
target = getFixture();
serviceRegistry.add("ui", uiService);
serviceRegistry.add("hotkey", hotkeyService);
});
QUnit.module("ViewScaleSelector");
QUnit.test("basic ViewScaleSelector component usage", async (assert) => {
const env = await makeTestEnv();
class Parent extends Component {
static components = { ViewScaleSelector };
static template = xml`<ViewScaleSelector t-props="compProps" />`;
setup() {
this.state = useState({
scale: "week",
});
}
get compProps() {
return {
scales: {
day: {
description: "Daily",
},
week: {
description: "Weekly",
hotkey: "o",
},
year: {
description: "Yearly",
},
},
isWeekendVisible: true,
setScale: (scale) => {
this.state.scale = scale;
assert.step(scale);
},
toggleWeekendVisibility: () => {
assert.step("toggleWeekendVisibility");
},
currentScale: this.state.scale,
};
}
}
await mount(Parent, target, { env });
assert.containsOnce(target, ".o_view_scale_selector");
assert.verifySteps([]);
assert.strictEqual(
target.querySelector(".o_view_scale_selector").textContent,
"Weekly",
"toggler displays the right text"
);
assert.strictEqual(
target.querySelector(".scale_button_selection").dataset.hotkey,
"v",
"toggler has the right hotkey"
);
await click(target, ".scale_button_selection");
assert.containsOnce(target, ".o_view_scale_selector .dropdown-menu", "a dropdown appeared");
assert.strictEqual(
target.querySelector(".o_view_scale_selector .dropdown-menu .active").textContent,
"Weekly",
"the active option is selected"
);
assert.strictEqual(
target.querySelector(".o_view_scale_selector .dropdown-menu span:nth-child(2)").dataset
.hotkey,
"o",
"'week' scale has the right hotkey"
);
await click(target, ".o_scale_button_day");
assert.verifySteps(["day"]);
assert.strictEqual(
target.querySelector(".o_view_scale_selector").textContent,
"Daily",
"the right text is displayed on the toggler"
);
await click(target, ".scale_button_selection");
await click(target, ".dropdown-item:last-child");
assert.verifySteps(["toggleWeekendVisibility"]);
});
QUnit.test("ViewScaleSelector with only one scale available", async (assert) => {
const env = await makeTestEnv();
class Parent extends Component {
static components = { ViewScaleSelector };
static template = xml`<ViewScaleSelector t-props="compProps" />`;
setup() {
this.state = useState({
scale: "day",
});
}
get compProps() {
return {
scales: {
day: {
description: "Daily",
},
},
setScale: () => {},
isWeekendVisible: false,
toggleWeekendVisibility: () => {},
currentScale: this.state.scale,
};
}
}
await mount(Parent, target, { env });
assert.containsNone(
target,
".o_view_scale_selector",
"toggler is not present as no other option is available"
);
});
});

View file

@ -20,6 +20,11 @@ import { makeFakeUserService } from "../../helpers/mock_services";
const serviceRegistry = registry.category("services");
async function exportAllAction(target) {
await click(target, ".o_cp_action_menus .dropdown-toggle");
await click(target, ".o_cp_action_menus .dropdown-item");
}
QUnit.module("ViewDialogs", (hooks) => {
let serverData;
let target;
@ -253,12 +258,13 @@ QUnit.module("ViewDialogs", (hooks) => {
mockRPC(route, args) {
if (args.method === "create") {
assert.strictEqual(args.model, "ir.exports");
const [values] = args.args[0];
assert.strictEqual(
args.args[0].name,
values.name,
"Export template",
"the template name is correctly sent"
);
return 2;
return [2];
}
if (args.method === "search_read") {
assert.deepEqual(
@ -774,6 +780,7 @@ QUnit.module("ViewDialogs", (hooks) => {
activateElement: () => {},
deactivateElement: () => {},
size: 4,
isSmall: true,
};
const fakeUIService = {
start(env) {
@ -987,7 +994,7 @@ QUnit.module("ViewDialogs", (hooks) => {
domain: [["bar", "!=", "glou"]],
});
await click(target.querySelector(".o_list_export_xlsx"));
await exportAllAction(target);
});
QUnit.test("Direct export grouped list ", async function (assert) {
@ -1039,7 +1046,7 @@ QUnit.module("ViewDialogs", (hooks) => {
domain: [["bar", "!=", "glou"]],
});
await click(target.querySelector(".o_list_export_xlsx"));
await exportAllAction(target);
});
QUnit.test("Export dialog with duplicated fields", async function (assert) {
@ -1087,6 +1094,40 @@ QUnit.module("ViewDialogs", (hooks) => {
);
});
QUnit.test("Export dialog: no column_invisible fields in default export list", async function (assert) {
await makeView({
serverData,
type: "list",
resModel: "partner",
arch: `
<tree>
<field name="foo"/>
<field name="bar" column_invisible="1"/>
</tree>`,
actionMenus: {},
mockRPC(route) {
if (route === "/web/export/formats") {
return Promise.resolve([{ tag: "csv", label: "CSV" }]);
}
if (route === "/web/export/get_fields") {
return Promise.resolve(fetchedFields.root);
}
}
});
await openExportDataDialog();
assert.containsOnce(
target,
".modal .o_export_field",
"there is only one field in export field list."
);
assert.strictEqual(
target.querySelector(".modal .o_export_field").textContent,
"Foo",
"the field to export corresponds to the visible one in the list view"
);
});
QUnit.test(
"Export dialog: export list contains field with 'default_export: true'",
async function (assert) {
@ -1291,7 +1332,7 @@ QUnit.module("ViewDialogs", (hooks) => {
"should have 3 th, 1 for selector, 1 for columns, 1 for optional columns"
);
await click(target.querySelector(".o_list_export_xlsx"));
await exportAllAction(target);
}
);

View file

@ -7,12 +7,12 @@ import {
nextTick,
patchWithCleanup,
triggerHotkey,
makeDeferred,
} from "@web/../tests/helpers/utils";
import { contains } from "@web/../tests/utils";
import { makeView } from "@web/../tests/views/helpers";
import { makeView, makeViewInDialog, setupViewRegistries } 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;
@ -67,7 +67,7 @@ QUnit.module("ViewDialogs", (hooks) => {
},
};
target = getFixture();
setupControlPanelServiceRegistry();
setupViewRegistries();
});
QUnit.module("FormViewDialog");
@ -108,11 +108,11 @@ QUnit.module("ViewDialogs", (hooks) => {
"partner,false,form": `
<form>
<field name="bar"/>
<footer attrs="{'invisible': [('bar','=',False)]}">
<footer invisible="not bar">
<button>Hello</button>
<button>World</button>
</footer>
<footer attrs="{'invisible': [('bar','!=',False)]}">
<footer invisible="bar">
<button>Foo</button>
</footer>
</form>`,
@ -205,15 +205,16 @@ QUnit.module("ViewDialogs", (hooks) => {
</tree>`,
};
await makeView({
await makeViewInDialog({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form>
arch: `
<form>
<field name="name"/>
<field name="instrument" context="{'tree_view_ref': 'some_tree_view'}" open_target="new"/>
</form>`,
<field name="instrument" context="{'tree_view_ref': 'some_tree_view'}"/>
</form>`,
mockRPC: function (route, args) {
if (args.method === "get_formview_id") {
return Promise.resolve(false);
@ -234,7 +235,6 @@ QUnit.module("ViewDialogs", (hooks) => {
assert.deepEqual(
args.kwargs.context,
{
base_model_name: "instrument",
lang: "en",
tree_view_ref: "some_other_tree_view",
tz: "taht",
@ -276,10 +276,10 @@ QUnit.module("ViewDialogs", (hooks) => {
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"]);
assert.verifySteps(["/web/webclient/load_menus", "get_views", "web_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
assert.verifySteps(["method1", "web_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
@ -299,7 +299,7 @@ QUnit.module("ViewDialogs", (hooks) => {
};
let reject = true;
function mockRPC(route, args) {
if (args.method === "create" && reject) {
if (args.method === "web_save" && reject) {
return Promise.reject();
}
}
@ -343,6 +343,82 @@ QUnit.module("ViewDialogs", (hooks) => {
assert.containsNone(target, ".o_dialog .o_form_view");
});
QUnit.test("Buttons are set as disabled on click", async function (assert) {
serverData.views = {
"partner,false,form": `
<form string="Partner">
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</form>
`,
};
const def = makeDeferred();
async function mockRPC(route, args) {
if (args.method === "web_save") {
await def;
}
}
const webClient = await createWebClient({ serverData, mockRPC });
webClient.env.services.dialog.add(FormViewDialog, {
resModel: "partner",
resId: 1,
});
await nextTick();
await editInput(
target.querySelector(".o_dialog .o_content .o_field_char .o_input"),
"",
"test"
);
await click(target.querySelector(".o_dialog .modal-footer .o_form_button_save"));
assert.strictEqual(
target
.querySelector(".o_dialog .modal-footer .o_form_button_save")
.getAttribute("disabled"),
"1"
);
def.resolve();
await nextTick();
assert.containsNone(target, ".o_dialog .o_form_view");
});
QUnit.test("FormViewDialog with discard 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,
onRecordDiscarded: () => assert.step("discard"),
});
await nextTick();
assert.containsOnce(target, ".o_dialog .o_form_view");
assert.containsOnce(target, ".o_dialog .modal-footer .o_form_button_cancel");
await click(target.querySelector(".o_dialog .modal-footer .o_form_button_cancel"));
assert.verifySteps(["discard"]);
assert.containsNone(target, ".o_dialog .o_form_view");
webClient.env.services.dialog.add(FormViewDialog, {
resModel: "partner",
resId: 1,
onRecordDiscarded: () => assert.step("discard"),
});
await nextTick();
assert.containsOnce(target, ".o_dialog .o_form_view");
await click(target.querySelector(".o_dialog .btn-close"));
assert.verifySteps(["discard"]);
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) {
@ -378,48 +454,56 @@ QUnit.module("ViewDialogs", (hooks) => {
}
);
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",
},
});
}
},
});
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="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({
value: {
name: false,
},
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 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(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"
);
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"])
});
assert.verifySteps(["onchange warning"]);
}
);
});

View file

@ -19,8 +19,7 @@ import {
editFavoriteName,
removeFacet,
saveFavorite,
toggleFavoriteMenu,
toggleFilterMenu,
toggleSearchBarMenu,
toggleMenuItem,
toggleSaveFavorite,
} from "@web/../tests/search/helpers";
@ -125,9 +124,6 @@ QUnit.module("ViewDialogs", (hooks) => {
fields: ["display_name", "foo", "bar"],
groupby: ["bar"],
orderby: "",
expand: false,
expand_orderby: null,
expand_limit: null,
lazy: true,
limit: 80,
offset: 0,
@ -152,7 +148,7 @@ QUnit.module("ViewDialogs", (hooks) => {
["display_name", "ilike", "piou"],
["foo", "ilike", "piou"],
],
fields: ["display_name", "foo"],
specification: { display_name: {}, foo: {} },
limit: 80,
offset: 0,
order: "",
@ -171,7 +167,7 @@ QUnit.module("ViewDialogs", (hooks) => {
uid: 7,
}, // not part of the test, may change
domain: [["display_name", "like", "a"]],
fields: ["display_name", "foo"],
specification: { display_name: {}, foo: {} },
limit: 80,
offset: 0,
order: "",
@ -228,7 +224,6 @@ QUnit.module("ViewDialogs", (hooks) => {
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]],
});
@ -315,13 +310,13 @@ QUnit.module("ViewDialogs", (hooks) => {
if (route === "/web/dataset/call_kw/instrument/get_formview_id") {
return Promise.resolve(false);
}
if (route === "/web/dataset/call_kw/instrument/create") {
if (route === "/web/dataset/call_kw/instrument/web_save") {
assert.deepEqual(
args.args,
[{ badassery: [[6, false, [1]]], name: "ABC" }],
args.args[1],
{ badassery: [[4, 1]], name: "ABC" },
"The method create should have been called with the right arguments"
);
return Promise.resolve(false);
return [{ id: 90 }];
}
},
});
@ -365,7 +360,7 @@ QUnit.module("ViewDialogs", (hooks) => {
patchWithCleanup(listView.Controller.prototype, {
setup() {
this._super(...arguments);
super.setup(...arguments);
useSetupAction({
getContext: () => ({ shouldBeInFilterContext: true }),
});
@ -407,13 +402,12 @@ QUnit.module("ViewDialogs", (hooks) => {
assert.containsN(target, ".o_data_row", 3, "should contain 3 records");
// filter on bar
await toggleFilterMenu(target);
await toggleSearchBarMenu(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);
@ -486,6 +480,27 @@ QUnit.module("ViewDialogs", (hooks) => {
}
);
QUnit.test("SelectCreateDialog: multiple clicks on record", async function (assert) {
serverData.views = {
"partner,false,list": `<tree><field name="display_name"/></tree>`,
"partner,false,search": `<search><field name="foo"/></search>`,
};
const webClient = await createWebClient({ serverData });
webClient.env.services.dialog.add(SelectCreateDialog, {
resModel: "partner",
onSelected: async function (records) {
assert.step(`select record ${records[0]}`);
},
});
await nextTick();
click(target.querySelector(".modal .o_data_row .o_data_cell"));
click(target.querySelector(".modal .o_data_row .o_data_cell"));
click(target.querySelector(".modal .o_data_row .o_data_cell"));
await nextTick();
assert.verifySteps(["select record 1"], "should have called onSelected only once");
});
QUnit.test("SelectCreateDialog: default props, create a record", async function (assert) {
serverData.views = {
"partner,false,list": `<tree><field name="display_name"/></tree>`,
@ -510,6 +525,7 @@ QUnit.module("ViewDialogs", (hooks) => {
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");
assert.containsNone(target, ".o_dialog .o_control_panel_main_buttons .o_list_button_add");
await click(target.querySelector(".o_dialog footer button.o_create_button"));

View file

@ -18,6 +18,10 @@ QUnit.module("View service", (hooks) => {
fields: {},
records: [],
},
"ir.ui.view": {
fields: {},
records: [],
},
};
const fakeUiService = {
@ -109,76 +113,34 @@ QUnit.module("View service", (hooks) => {
assert.verifySteps(["get_views", "get_views"]);
});
QUnit.test("loadViews stores fields in cache", async (assert) => {
assert.expect(2);
QUnit.test("clear cache when updating ir.ui.view", async (assert) => {
const mockRPC = (route, args) => {
if (route.includes("get_views")) {
assert.step("get_views");
}
if (route.includes("fields_get")) {
assert.step("fields_get");
}
};
const loadView = () =>
env.services.views.loadViews(
{
resModel: "take.five",
views: [[99, "list"]],
context: { default_field_value: 1 },
},
{}
);
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");
await loadView();
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"]);
await loadView();
assert.verifySteps([]); // cache works => no actual rpc
await env.services.orm.unlink("ir.ui.view", [3]);
await loadView();
assert.verifySteps(["get_views"]); // cache was invalidated
await env.services.orm.unlink("take.five", [3]);
await loadView();
assert.verifySteps([]); // cache was not invalidated
});
});

View file

@ -8,6 +8,7 @@ import {
mount,
nextTick,
patchWithCleanup,
mockTimeout,
} from "@web/../tests/helpers/utils";
import { setupViewRegistries } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
@ -16,6 +17,7 @@ import { View } from "@web/views/view";
import { actionService } from "@web/webclient/actions/action_service";
import { Component, onWillStart, onWillUpdateProps, useState, xml } from "@odoo/owl";
import { CallbackRecorder } from "@web/webclient/actions/action_hook";
const serviceRegistry = registry.category("services");
const viewRegistry = registry.category("views");
@ -74,7 +76,7 @@ QUnit.module("Views", (hooks) => {
class ToyController extends Component {
setup() {
this.class = "toy";
this.template = xml`${this.props.arch}`;
this.template = xml`${this.props.arch.outerHTML}`;
}
}
ToyController.template = xml`<div t-attf-class="{{class}} {{props.className}}"><t t-call="{{ template }}"/></div>`;
@ -121,9 +123,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,false,toy"]);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, false);
@ -159,9 +161,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,1,toy"]);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, 1);
@ -196,9 +198,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,1,toy"]);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, 1);
@ -237,9 +239,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,false,toy"]);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, false);
@ -280,9 +282,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,1,toy"]);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, 1);
@ -326,9 +328,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, `<toy>Specific arch content</toy>`);
assert.strictEqual(arch.outerHTML, `<toy>Specific arch content</toy>`);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, undefined);
@ -359,9 +361,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,false,toy"]);
assert.deepEqual(fields, {});
assert.deepEqual(info.actionMenus, {});
assert.strictEqual(this.env.config.viewId, false);
@ -399,9 +401,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, `<toy>Specific arch content</toy>`);
assert.strictEqual(arch.outerHTML, `<toy>Specific arch content</toy>`);
assert.deepEqual(fields, {});
assert.deepEqual(info.actionMenus, {});
assert.strictEqual(this.env.config.viewId, false);
@ -442,9 +444,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, `<toy>Specific arch content</toy>`);
assert.strictEqual(arch.outerHTML, `<toy>Specific arch content</toy>`);
assert.deepEqual(fields, {});
assert.deepEqual(info.actionMenus, {});
assert.strictEqual(this.env.config.viewId, undefined);
@ -478,13 +480,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const {
irFilters,
searchViewArch,
searchViewFields,
searchViewId,
} = this.props.info;
super.setup();
const { irFilters, searchViewArch, searchViewFields, searchViewId } =
this.props.info;
assert.strictEqual(searchViewArch, serverData.views["animal,false,search"]);
assert.deepEqual(searchViewFields, serverData.models.animal.fields);
assert.strictEqual(searchViewId, false);
@ -526,13 +524,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const {
irFilters,
searchViewArch,
searchViewFields,
searchViewId,
} = this.props.info;
super.setup();
const { irFilters, searchViewArch, searchViewFields, searchViewId } =
this.props.info;
assert.strictEqual(searchViewArch, `<search/>`);
assert.deepEqual(searchViewFields, {});
assert.strictEqual(searchViewId, false);
@ -570,13 +564,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const {
irFilters,
searchViewArch,
searchViewFields,
searchViewId,
} = this.props.info;
super.setup();
const { irFilters, searchViewArch, searchViewFields, searchViewId } =
this.props.info;
assert.strictEqual(searchViewArch, `<search/>`);
assert.deepEqual(searchViewFields, {});
assert.strictEqual(searchViewId, undefined);
@ -613,13 +603,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const {
irFilters,
searchViewArch,
searchViewFields,
searchViewId,
} = this.props.info;
super.setup();
const { irFilters, searchViewArch, searchViewFields, searchViewId } =
this.props.info;
assert.strictEqual(searchViewArch, `<search/>`);
assert.deepEqual(searchViewFields, {});
assert.strictEqual(searchViewId, false);
@ -678,13 +664,9 @@ QUnit.module("Views", (hooks) => {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const {
irFilters,
searchViewArch,
searchViewFields,
searchViewId,
} = this.props.info;
super.setup();
const { irFilters, searchViewArch, searchViewFields, searchViewId } =
this.props.info;
assert.strictEqual(searchViewArch, `<search/>`);
assert.deepEqual(searchViewFields, {});
assert.strictEqual(searchViewId, undefined);
@ -1126,7 +1108,7 @@ QUnit.module("Views", (hooks) => {
});
QUnit.test("real life banner", async (assert) => {
assert.expect(10);
assert.expect(8);
serverData.views["animal,1,toy"] = `
<toy banner_route="/mybody/isacage">
@ -1151,14 +1133,12 @@ QUnit.module("Views", (hooks) => {
</div>
</div>
</div>
<div class="o_onboarding_container collapse show">
<div class="o_onboarding" />
<div class="o_onboarding_wrap" />
<a href="#" data-bs-toggle="modal" data-bs-target=".o_onboarding_modal" class="float-end o_onboarding_btn_close">
<i class="fa fa-times" title="Close the onboarding panel" id="closeOnboarding"></i>
</a>
<div class="bannerContent">Content</div>
</div>
<div class="o_onboarding" />
<div class="o_onboarding_wrap" />
<a href="#" data-bs-toggle="modal" data-bs-target=".o_onboarding_modal" class="float-end o_onboarding_btn_close">
<i class="fa fa-times" title="Close the onboarding panel" id="closeOnboarding"></i>
</a>
<div class="bannerContent">Content</div>
</div>
</div>`;
@ -1172,6 +1152,7 @@ QUnit.module("Views", (hooks) => {
return true;
}
};
const { execRegisteredTimeouts } = mockTimeout();
const config = {
views: [[1, "toy"]],
};
@ -1182,31 +1163,17 @@ QUnit.module("Views", (hooks) => {
};
await mount(View, target, { env, props });
const prom = new Promise((resolve) => {
const complete = (ev) => {
if (ev.target.classList.contains("o_onboarding_container")) {
resolve();
}
};
// We need to handle both events, because the transition is not
// always executed
target.addEventListener("transitionend", complete);
target.addEventListener("transitioncancel", complete);
});
assert.verifySteps(["/mybody/isacage"]);
assert.isNotVisible(target.querySelector(".modal"));
assert.hasClass(target.querySelector(".o_onboarding_container"), "collapse show");
assert.hasClass(target.querySelector(".o_onboarding_container"), "o-vertical-slide");
await click(target.querySelector("#closeOnboarding"));
assert.isVisible(target.querySelector(".modal"));
await click(target.querySelector(".modal a[type='action']"));
assert.verifySteps(["mah_method"]);
await prom;
assert.doesNotHaveClass(target.querySelector(".o_onboarding_container"), "show");
assert.hasClass(target.querySelector(".o_onboarding_container"), "collapse");
assert.isNotVisible(target.querySelector(".modal"));
execRegisteredTimeouts();
assert.containsNone(target, ".o_onboarding_container");
});
////////////////////////////////////////////////////////////////////////////
@ -1425,7 +1392,7 @@ QUnit.module("Views", (hooks) => {
});
assert.deepEqual(domain, [[0, "=", 1]]);
assert.deepEqual(groupBy, ["birthday"]);
assert.deepEqual(orderBy, [{name: "bar", asc: true}]);
assert.deepEqual(orderBy, [{ name: "bar", asc: true }]);
}
}
ToyController.template = xml`<div/>`;
@ -1439,7 +1406,7 @@ QUnit.module("Views", (hooks) => {
domain: [[0, "=", 1]],
groupBy: ["birthday"],
context: { key: "val" },
orderBy: [{name: "bar", asc: true}],
orderBy: [{ name: "bar", asc: true }],
};
await mount(View, target, { env, props });
}
@ -1593,7 +1560,7 @@ QUnit.module("Views", (hooks) => {
});
assert.deepEqual(domain, ["&", [0, "=", 1], [1, "=", 1]]);
assert.deepEqual(groupBy, ["name"]);
assert.deepEqual(orderBy, [{name: "bar", asc: true}]);
assert.deepEqual(orderBy, [{ name: "bar", asc: true }]);
}
}
ToyController.template = xml`<div/>`;
@ -1607,12 +1574,60 @@ QUnit.module("Views", (hooks) => {
domain: [[0, "=", 1]],
groupBy: ["birthday"],
context: { search_default_filter: 1, search_default_group_by: 1 },
orderBy: [{name: "bar", asc: true}],
orderBy: [{ name: "bar", asc: true }],
};
await mount(View, target, { env, props });
}
);
QUnit.test("multiple ways to pass classes for styling", async (assert) => {
const env = await makeTestEnv({ serverData });
const props = {
resModel: "animal",
type: "toy",
className: "o_custom_class_from_props_1 o_custom_class_from_props_2",
arch: `
<toy
js_class="toy_imp"
class="o_custom_class_from_arch_1 o_custom_class_from_arch_2"
/>
`,
fields: {},
};
await mount(View, target, { env, props });
const view = target.querySelector(".o_toy_view");
assert.hasClass(view, "o_toy_imp_view", "should have the class from js_class attribute");
assert.hasClass(view, "o_custom_class_from_props_1", "should have the class from props");
assert.hasClass(view, "o_custom_class_from_props_2", "should have the class from props");
assert.hasClass(view, "o_custom_class_from_arch_1", "should have the class from arch");
assert.hasClass(view, "o_custom_class_from_arch_2", "should have the class from arch");
});
QUnit.test("callback recorders are moved from props to subenv", async (assert) => {
assert.expect(5);
class ToyController extends Component {
setup() {
assert.ok(this.env.__getGlobalState__ instanceof CallbackRecorder); // put in env by View
assert.ok(this.env.__getContext__ instanceof CallbackRecorder); // put in env by View
assert.strictEqual(this.env.__getLocalState__, null); // set by View
assert.strictEqual(this.env.__beforeLeave__, null); // set by View
assert.ok(this.env.__getOrderBy__ instanceof CallbackRecorder); // put in env by WithSearch
}
}
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
const props = {
type: "toy",
resModel: "animal",
__getGlobalState__: new CallbackRecorder(),
__getContext__: new CallbackRecorder(),
};
await mount(View, target, { env, props });
});
////////////////////////////////////////////////////////////////////////////
// update props
////////////////////////////////////////////////////////////////////////////

View file

@ -45,7 +45,7 @@ QUnit.module("Widgets", (hooks) => {
let fileInput;
patchWithCleanup(AttachDocumentWidget.prototype, {
setup() {
this._super();
super.setup();
fileInput = this.fileInput;
},
});
@ -75,10 +75,10 @@ QUnit.module("Widgets", (hooks) => {
assert.deepEqual(args.kwargs.attachment_ids, [5, 2]);
return true;
}
if (args.method === "write") {
if (args.method === "web_save") {
assert.deepEqual(args.args[1], { display_name: "yop" });
}
if (args.method === "read") {
if (args.method === "web_read") {
assert.deepEqual(args.args[0], [1]);
}
},
@ -88,13 +88,13 @@ QUnit.module("Widgets", (hooks) => {
<field name="display_name" required="1"/>
</form>`,
});
assert.verifySteps(["get_views", "read"]);
assert.verifySteps(["get_views", "web_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"]);
assert.verifySteps(["web_save", "post", "my_action", "web_read"]);
});
QUnit.test(
@ -103,7 +103,7 @@ QUnit.module("Widgets", (hooks) => {
let fileInput;
patchWithCleanup(AttachDocumentWidget.prototype, {
setup() {
this._super();
super.setup();
fileInput = this.fileInput;
},
});
@ -132,10 +132,10 @@ QUnit.module("Widgets", (hooks) => {
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 === "web_save") {
assert.deepEqual(args.args[1], { display_name: "yop" });
}
if (args.method === "read") {
if (args.method === "web_read") {
assert.deepEqual(args.args[0], [2]);
}
},
@ -151,7 +151,7 @@ QUnit.module("Widgets", (hooks) => {
await click(target, ".o_attach_document");
fileInput.dispatchEvent(new Event("change"));
await nextTick();
assert.verifySteps(["create", "read", "post", "my_action", "read"]);
assert.verifySteps(["web_save", "post", "my_action", "web_read"]);
}
);
});

View file

@ -0,0 +1,72 @@
/** @odoo-module **/
import { getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let target;
let serverData;
QUnit.module("Widgets", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
bar: { string: "Bar", type: "boolean" },
},
},
},
};
setupViewRegistries();
});
QUnit.module("DocumentationLink");
QUnit.test("documentation_link: relative path", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="bar"/>
<widget name="documentation_link" path="/applications/technical/web/settings/this_is_a_test.html"/>
</form>`,
});
assert.hasAttrValue(
target.querySelector(".o_doc_link"),
"href",
"https://www.odoo.com/documentation/1.0/applications/technical/web/settings/this_is_a_test.html"
);
});
QUnit.test("documentation_link: absoluth path (http)", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="bar"/>
<widget name="documentation_link" path="http://www.odoo.com/"/>
</form>`,
});
assert.hasAttrValue(target.querySelector(".o_doc_link"), "href", "http://www.odoo.com/");
});
QUnit.test("documentation_link: absoluth path (https)", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="bar"/>
<widget name="documentation_link" path="https://www.odoo.com/"/>
</form>`,
});
assert.hasAttrValue(target.querySelector(".o_doc_link"), "href", "https://www.odoo.com/");
});
});

View file

@ -0,0 +1,74 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
const viewData = {
type: "form",
resModel: "partner",
serverData: {
models: {
partner: {
records: [
{
id: 1,
},
],
},
},
},
resId: 1,
arch: `<form><widget name="notification_alert"/></form>`,
};
QUnit.module("Widgets", (hooks) => {
hooks.beforeEach(setupViewRegistries);
QUnit.module("NotificationAlert");
QUnit.test(
"notification alert should be displayed when notification denied",
async function (assert) {
assert.expect(1);
patchWithCleanup(browser, { Notification: { permission: "denied" } });
await makeView(viewData);
assert.containsOnce(
document.body,
".o_widget_notification_alert .alert",
"notification alert should be displayed when notification denied"
);
}
);
QUnit.test(
"notification alert should not be displayed when notification granted",
async function (assert) {
assert.expect(1);
patchWithCleanup(browser, { Notification: { permission: "granted" } });
await makeView(viewData);
assert.containsNone(
document.body,
".o_widget_notification_alert .alert",
"notification alert should not be displayed when notification granted"
);
}
);
QUnit.test(
"notification alert should not be displayed when notification default",
async function (assert) {
assert.expect(1);
patchWithCleanup(browser, { Notification: { permission: "default" } });
await makeView(viewData);
assert.containsNone(
document.body,
".o_widget_notification_alert .alert",
"notification alert should not be displayed when notification default"
);
}
);
});

View file

@ -19,7 +19,6 @@ QUnit.module("Widgets", (hooks) => {
type: "many2one",
relation: "product",
},
__last_update: { type: "datetime" },
sign: { string: "Signature", type: "binary" },
},
records: [
@ -55,7 +54,7 @@ QUnit.module("Widgets", (hooks) => {
patchWithCleanup(NameAndSignature.prototype, {
setup() {
this._super.apply(this, arguments);
super.setup(...arguments);
assert.strictEqual(this.props.signature.name, "");
},
});
@ -97,7 +96,7 @@ QUnit.module("Widgets", (hooks) => {
QUnit.test("Signature widget: full_name option", async function (assert) {
patchWithCleanup(NameAndSignature.prototype, {
setup() {
this._super.apply(this, arguments);
super.setup(...arguments);
assert.step(this.props.signature.name);
},
});

View file

@ -66,7 +66,7 @@ QUnit.module("Widgets", ({ beforeEach }) => {
</form>
`,
mockRPC(route, { args, method }) {
if (method === "write") {
if (method === "web_save") {
writeCall++;
if (writeCall === 1) {
assert.ok(args[1].sun, "value of sunday should be true");
@ -92,7 +92,7 @@ QUnit.module("Widgets", ({ beforeEach }) => {
assert.containsNone(
fixture,
".form-check input:disabled",
"all inputs should be enabled in readonly mode"
"all inputs should be enabled in edit mode"
);
await click(fixture.querySelector("td:nth-child(7) input"));
@ -136,6 +136,42 @@ QUnit.module("Widgets", ({ beforeEach }) => {
);
});
QUnit.test("week recurrence widget readonly modifiers", async (assert) => {
registry.category("services", makeFakeLocalizationService({ weekStart: 1 }));
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<widget name="week_days" readonly="1"/>
</group>
</sheet>
</form>
`,
});
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.containsN(
fixture,
".form-check input:disabled",
7,
"all inputs should be disabled in readonly mode"
);
});
QUnit.test(
"week recurrence widget show week start as per language configuration",
async (assert) => {