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

@ -931,7 +931,7 @@ QUnit.module("ActionSwiper", ({ beforeEach }) => {
const { execRegisteredTimeouts } = mockTimeout();
patchWithCleanup(ActionSwiper.prototype, {
setup() {
this._super(...arguments);
super.setup();
onPatched(() => {
if (executingAction) {
assert.step("ActionSwiper patched");

View file

@ -0,0 +1,91 @@
/** @odoo-module **/
import { Component, xml } from "@odoo/owl";
import { clearRegistryWithCleanup, makeTestEnv } from "@web/../tests/helpers/mock_env";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { click, getFixture, mount } from "@web/../tests/helpers/utils";
import { DateTimeInput } from "@web/core/datetime/datetime_input";
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { popoverService } from "@web/core/popover/popover_service";
import { registry } from "@web/core/registry";
import { uiService } from "@web/core/ui/ui_service";
const { DateTime } = luxon;
/**
* @typedef {import("@web/core/datetime/datetime_input").DateTimeInputProps} DateTimeInputProps
*/
/**
* @param {DateTimeInputProps} props
*/
const mountInput = async (props) => {
const env = await makeTestEnv();
await mount(Root, getFixture(), { env, props });
return getFixture().querySelector(".o_datetime_input");
};
class Root extends Component {
static components = { DateTimeInput };
static template = xml`
<div class="d-flex">
<DateTimeInput t-props="props" />
</div>
<t t-foreach="mainComponentEntries" t-as="comp" t-key="comp[0]">
<t t-component="comp[1].Component" t-props="comp[1].props" />
</t>
`;
setup() {
this.mainComponentEntries = mainComponentRegistry.getEntries();
}
}
const mainComponentRegistry = registry.category("main_components");
const serviceRegistry = registry.category("services");
QUnit.module("Components", ({ beforeEach }) => {
beforeEach(() => {
clearRegistryWithCleanup(mainComponentRegistry);
serviceRegistry
.add("datetime_picker", datetimePickerService)
.add("hotkey", hotkeyService)
.add(
"localization",
makeFakeLocalizationService({
dateFormat: "dd/MM/yyyy",
dateTimeFormat: "dd/MM/yyyy HH:mm:ss",
})
)
.add("popover", popoverService)
.add("ui", uiService);
});
QUnit.module("DateTimeInput (date)");
QUnit.test("popover should have enough space to be displayed", async (assert) => {
const { parentElement: parent } = await mountInput({
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
type: "date",
});
const initialParentRect = parent.getBoundingClientRect();
await click(parent, ".o_datetime_input");
const pickerRect = getFixture().querySelector(".o_datetime_picker").getBoundingClientRect();
const finalParentRect = parent.getBoundingClientRect();
assert.ok(
initialParentRect.height < pickerRect.height,
"initial height shouldn't be big enough to display the picker"
);
assert.ok(
finalParentRect.height > pickerRect.height,
"initial height should be big enough to display the picker"
);
});
});

View file

@ -0,0 +1,62 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { Dialog } from "@web/core/dialog/dialog";
import { makeTestEnv } from "../../helpers/mock_env";
import { getFixture, mount, dragAndDrop } from "../../helpers/utils";
import { makeFakeDialogService } from "../../helpers/mock_services";
import { Component, xml } from "@odoo/owl";
const serviceRegistry = registry.category("services");
let parent;
let target;
async function makeDialogTestEnv() {
const env = await makeTestEnv();
env.dialogData = {
isActive: true,
close: () => {},
scrollToOrigin: () => {},
};
return env;
}
QUnit.module("Components", (hooks) => {
hooks.beforeEach(async () => {
target = getFixture();
serviceRegistry.add("hotkey", hotkeyService);
serviceRegistry.add("dialog", makeFakeDialogService());
});
hooks.afterEach(() => {
if (parent) {
parent = undefined;
}
});
QUnit.module("Dialog");
QUnit.test("dialog can't be moved on small screen", async (assert) => {
class Parent extends Component {
static template = xml`<Dialog>content</Dialog>`;
static components = { Dialog };
}
await mount(Parent, target, { env: await makeDialogTestEnv() });
const content = target.querySelector(".modal-content");
assert.strictEqual(content.style.top, "0px");
assert.strictEqual(content.style.left, "0px");
const header = content.querySelector(".modal-header");
const headerRect = header.getBoundingClientRect();
// Even if the `dragAndDrop` is called, confirms that there are no effects
await dragAndDrop(header, document.body, {
// the util function sets the source coordinates at (x; y) + (w/2; h/2)
// so we need to move the dialog based on these coordinates.
x: headerRect.x + headerRect.width / 2 + 20,
y: headerRect.y + headerRect.height / 2 + 50,
});
assert.strictEqual(content.style.top, "0px");
assert.strictEqual(content.style.left, "0px");
});
});

View file

@ -1,12 +1,8 @@
/** @odoo-module **/
import { click, getFixture, triggerEvent, nextTick } from "@web/../tests/helpers/utils";
import { getFixture, nextTick } from "@web/../tests/helpers/utils";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import {
editSearch,
makeWithSearch,
setupControlPanelServiceRegistry,
} from "@web/../tests/search/helpers";
import { makeWithSearch, setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
import { registry } from "@web/core/registry";
import { uiService } from "@web/core/ui/ui_service";
@ -42,86 +38,6 @@ QUnit.module("Search", (hooks) => {
QUnit.module("Control Panel (mobile)");
QUnit.test("Display control panel mobile", async (assert) => {
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["filter"],
searchViewId: false,
});
assert.containsOnce(target, ".breadcrumb");
assert.containsOnce(target, ".o_enable_searchview");
assert.containsNone(target, ".o_searchview");
assert.containsNone(target, ".o_toggle_searchview_full");
await click(target, ".o_enable_searchview");
assert.containsNone(target, ".breadcrumb");
assert.containsOnce(target, ".o_enable_searchview");
assert.containsOnce(target, ".o_searchview");
assert.containsOnce(target, ".o_toggle_searchview_full");
await click(target, ".o_toggle_searchview_full");
assert.containsOnce(document.body, ".o_searchview.o_mobile_search");
assert.containsN(document.body, ".o_mobile_search .o_mobile_search_button", 2);
assert.strictEqual(
document.body.querySelector(".o_mobile_search_header").textContent.trim(),
"FILTER CLEAR"
);
assert.containsOnce(document.body, ".o_searchview.o_mobile_search .o_cp_searchview");
assert.containsOnce(document.body, ".o_searchview.o_mobile_search .o_mobile_search_footer");
await click(document.body.querySelector(".o_mobile_search_button"));
assert.containsNone(target, ".breadcrumb");
assert.containsOnce(target, ".o_enable_searchview");
assert.containsOnce(target, ".o_searchview");
assert.containsOnce(target, ".o_toggle_searchview_full");
await click(target, ".o_enable_searchview");
assert.containsOnce(target, ".breadcrumb");
assert.containsOnce(target, ".o_enable_searchview");
assert.containsNone(target, ".o_searchview");
assert.containsNone(target, ".o_toggle_searchview_full");
});
QUnit.test("Make a simple search in mobile mode", async (assert) => {
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["filter"],
searchViewFields: {
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
},
searchViewArch: `
<search>
<field name="birthday"/>
</search>
`,
});
assert.containsNone(target, ".o_searchview");
await click(target, ".o_enable_searchview");
assert.containsOnce(target, ".o_searchview");
const input = target.querySelector(".o_searchview input");
assert.containsNone(target, ".o_searchview_autocomplete");
await editSearch(target, "2022-02-14");
assert.strictEqual(input.value, "2022-02-14", "input value should be updated");
assert.containsOnce(target, ".o_searchview_autocomplete");
await triggerEvent(input, null, "keydown", { key: "Escape" });
assert.containsNone(target, ".o_searchview_autocomplete");
await click(target, ".o_enable_searchview");
assert.containsNone(target, ".o_searchview");
});
QUnit.test("Control panel is shown/hide on top when scrolling", async (assert) => {
await makeWithSearch({
serverData,

View file

@ -92,63 +92,55 @@ QUnit.module("Views", ({ beforeEach }) => {
assert.containsNone(target, ".o_calendar_button_prev", "prev button should be hidden");
assert.containsNone(target, ".o_calendar_button_next", "next button should be hidden");
await click(target, ".o_calendar_container .o_other_calendar_panel");
assert.isVisible(
target.querySelector(
".o_cp_bottom_left .o_calendar_buttons .o_calendar_scale_buttons + button.o_cp_today_button"
".o_calendar_container .o_calendar_header button.o_calendar_button_today"
),
"today button should be visible near the calendar buttons (bottom left corner)"
"today button should be visible"
);
// Test all views
// displays month mode by default
assert.containsOnce(
target,
".fc-view-container > .fc-timeGridWeek-view",
"should display the current week"
);
assert.equal(
target.querySelector(".breadcrumb-item").textContent,
"undefined (Dec 11 17, 2016)"
target.querySelector(".o_calendar_container .o_calendar_header .dropdown-toggle")
.textContent,
"Week",
"should display the current week"
);
// switch to day mode
await click(target, ".o_control_panel .scale_button_selection");
await click(target, ".o_control_panel .o_calendar_button_day");
await click(target, ".o_calendar_container .o_calendar_header .dropdown-toggle");
await click(target, ".o_calendar_container .o_calendar_header .o_scale_button_day");
await nextTick();
assert.containsOnce(
target,
".fc-view-container > .fc-timeGridDay-view",
"should display the current day"
);
assert.equal(
target.querySelector(".breadcrumb-item").textContent,
"undefined (December 12, 2016)"
target.querySelector(".o_calendar_container .o_calendar_header .dropdown-toggle")
.textContent,
"Day",
"should display the current day"
);
// switch to month mode
await click(target, ".o_control_panel .scale_button_selection");
await click(target, ".o_control_panel .o_calendar_button_month");
await click(target, ".o_calendar_container .o_calendar_header .dropdown-toggle");
await click(target, ".o_calendar_container .o_calendar_header .o_scale_button_month");
await nextTick();
assert.containsOnce(
target,
".fc-view-container > .fc-dayGridMonth-view",
"should display the current month"
);
assert.equal(
target.querySelector(".breadcrumb-item").textContent,
"undefined (December 2016)"
target.querySelector(".o_calendar_container .o_calendar_header .dropdown-toggle")
.textContent,
"Month",
"should display the current month"
);
// switch to year mode
await click(target, ".o_control_panel .scale_button_selection");
await click(target, ".o_control_panel .o_calendar_button_year");
await click(target, ".o_calendar_container .o_calendar_header .dropdown-toggle");
await click(target, ".o_calendar_container .o_calendar_header .o_scale_button_year");
await nextTick();
assert.containsOnce(
target,
".fc-view-container > .fc-dayGridYear-view",
assert.equal(
target.querySelector(".o_calendar_container .o_calendar_header .dropdown-toggle")
.textContent,
"Year",
"should display the current year"
);
assert.equal(target.querySelector(".breadcrumb-item").textContent, "undefined (2016)");
});
QUnit.test("calendar: popover is rendered as dialog in mobile", async function (assert) {
@ -186,7 +178,9 @@ QUnit.module("Views", ({ beforeEach }) => {
await swipeRight(target, ".o_calendar_widget");
assert.equal(target.querySelector(".fc-day-header[data-date]").dataset.date, "2016-12-11");
await click(target, ".o_other_calendar_panel");
await click(target, ".o_calendar_button_today");
await click(target, ".o_other_calendar_panel");
assert.equal(target.querySelector(".fc-day-header[data-date]").dataset.date, "2016-12-12");
});
@ -202,18 +196,21 @@ QUnit.module("Views", ({ beforeEach }) => {
</calendar>`,
});
assert.containsOnce(target, ".o_calendar_renderer");
assert.containsOnce(target, ".o_other_calendar_panel");
await click(target, ".o_other_calendar_panel");
assert.containsOnce(
target,
".o_calendar_filter_items_checkall",
"should contain one filter to check all"
);
assert.containsN(
target,
".o_other_calendar_panel .o_filter > *",
3,
"should contains 3 child nodes -> 1 label (USER) + 2 resources (user 1/2)"
".o_calendar_filter_item",
2,
"should contain 2 child nodes -> 2 resources"
);
assert.containsNone(target, ".o_calendar_sidebar");
assert.containsOnce(target, ".o_calendar_renderer");
// Toggle the other calendar panel should hide the calendar view and show the sidebar
await click(target, ".o_other_calendar_panel");
assert.containsOnce(target, ".o_calendar_sidebar");
assert.containsNone(target, ".o_calendar_renderer");
assert.containsOnce(target, ".o_calendar_filter");
@ -224,8 +221,8 @@ QUnit.module("Views", ({ beforeEach }) => {
assert.containsN(
target,
".o_other_calendar_panel .o_filter > *",
1,
"should contains 1 child node -> 1 label (USER)"
0,
"should contain 0 child nodes -> no filters selected"
);
// Toggle again the other calendar panel should hide the sidebar and show the calendar view
@ -238,11 +235,11 @@ QUnit.module("Views", ({ beforeEach }) => {
patchWithCleanup(CalendarCommonRenderer.prototype, {
onDateClick(...args) {
assert.step("dateClick");
return this._super(...args);
return super.onDateClick(...args);
},
onSelect(...args) {
assert.step("select");
return this._super(...args);
return super.onSelect(...args);
},
});
@ -268,20 +265,20 @@ QUnit.module("Views", ({ beforeEach }) => {
QUnit.test('calendar: select range on "Free Zone" opens quick create', async function (assert) {
patchWithCleanup(CalendarCommonRenderer.prototype, {
get options() {
return Object.assign({}, this._super(), {
return Object.assign({}, super.options, {
selectLongPressDelay: 0,
});
},
onDateClick(info) {
assert.step("dateClick");
return this._super(info);
return super.onDateClick(info);
},
onSelect(info) {
assert.step("select");
const { startStr, endStr } = info;
assert.equal(startStr, "2016-12-12T01:00:00+01:00");
assert.equal(endStr, "2016-12-12T02:00:00+01:00");
return this._super(info);
return super.onSelect(info);
},
});
@ -309,21 +306,21 @@ QUnit.module("Views", ({ beforeEach }) => {
QUnit.test("calendar (year): select date range opens quick create", async function (assert) {
patchWithCleanup(CalendarYearRenderer.prototype, {
get options() {
return Object.assign({}, this._super(), {
return Object.assign({}, super.options, {
longPressDelay: 0,
selectLongPressDelay: 0,
});
},
onDateClick(info) {
assert.step("dateClick");
return this._super(info);
return super.onDateClick(info);
},
onSelect(info) {
assert.step("select");
const { startStr, endStr } = info;
assert.equal(startStr, "2016-02-02");
assert.equal(endStr, "2016-02-06"); // end date is exclusive
return this._super(info);
return super.onSelect(info);
},
});
@ -358,34 +355,39 @@ QUnit.module("Views", ({ beforeEach }) => {
// Should display year view
assert.containsOnce(target, ".fc-dayGridYear-view");
assert.containsN(target, ".fc-month-container", 12);
assert.equal(target.querySelector(".breadcrumb-item").textContent, "undefined (2016)");
// Tap on a date
await tap(target, ".fc-day-top[data-date='2016-02-05']");
await nextTick(); // switch renderer
await nextTick(); // await breadcrumb update
assert.strictEqual(
document.querySelector(".o_calendar_container .o_calendar_header h5").textContent,
"5 February 2016"
);
// Should display day view
assert.containsNone(target, ".fc-dayGridYear-view");
assert.containsOnce(target, ".fc-timeGridDay-view");
assert.equal(
target.querySelector(".breadcrumb-item").textContent,
"undefined (February 5, 2016)"
);
assert.equal(target.querySelector(".fc-day-header[data-date]").dataset.date, "2016-02-05");
// Change scale to month
await changeScale(target, "month");
assert.containsOnce(target, ".o_calendar_container .o_calendar_header h5");
assert.strictEqual(
document.querySelector(".o_calendar_container .o_calendar_header h5").textContent,
"February 2016"
);
assert.containsNone(target, ".fc-timeGridDay-view");
assert.containsOnce(target, ".fc-dayGridMonth-view");
assert.equal(
target.querySelector(".breadcrumb-item").textContent,
"undefined (February 2016)"
);
// Tap on a date
await tap(target, ".fc-day-top[data-date='2016-02-10']");
await nextTick(); // await reload & render
await nextTick(); // await breadcrumb update
assert.strictEqual(
document.querySelector(".o_calendar_container .o_calendar_header h5").textContent,
"February 2016"
);
// should open a Quick create modal view in mobile on short tap on date in monthly view
assert.containsOnce(target, ".modal");

View file

@ -44,7 +44,7 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_tags" placeholder="foo"/>
</form>`,
});
assert.strictEqual(target.querySelector("#timmy").placeholder, "foo");
assert.strictEqual(target.querySelector("#timmy_0").placeholder, "foo");
});
QUnit.test("Many2ManyTagsField placeholder should be empty", async function (assert) {
@ -57,6 +57,6 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_tags"/>
</form>`,
});
assert.strictEqual(target.querySelector("#timmy").placeholder, "");
assert.strictEqual(target.querySelector("#timmy_0").placeholder, "");
});
});

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";
@ -123,8 +122,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

@ -2,6 +2,7 @@
import { click, getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
let fixture;
let serverData;
@ -10,6 +11,8 @@ QUnit.module("Mobile Fields", ({ beforeEach }) => {
beforeEach(() => {
setupViewRegistries();
fixture = getFixture();
fixture.setAttribute("style", "width:100vw; height:100vh;");
registerCleanup(() => fixture.removeAttribute("style"));
serverData = {
models: {
partner: {
@ -20,6 +23,7 @@ QUnit.module("Mobile Fields", ({ beforeEach }) => {
records: [
{ id: 1, display_name: "first record", trululu: 4 },
{ id: 2, display_name: "second record", trululu: 1 },
{ id: 3, display_name: "third record" },
{ id: 4, display_name: "aaa" },
],
},
@ -29,7 +33,7 @@ QUnit.module("Mobile Fields", ({ beforeEach }) => {
QUnit.module("StatusBarField");
QUnit.test("statusbar is rendered correclty on small devices", async (assert) => {
QUnit.test("statusbar is rendered correctly on small devices", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -45,16 +49,9 @@ QUnit.module("Mobile Fields", ({ beforeEach }) => {
`,
});
assert.containsOnce(
fixture,
".o_statusbar_status > button",
"should have only one visible status in mobile, the active one"
);
assert.containsOnce(
fixture,
".o_statusbar_status .dropdown",
"should have a dropdown containing all status"
);
assert.containsN(fixture, ".o_statusbar_status .o_arrow_button:visible", 4);
assert.containsOnce(fixture, ".o_statusbar_status .o_arrow_button.dropdown-toggle:visible");
assert.containsOnce(fixture, ".o_statusbar_status .o_arrow_button.o_arrow_button_current");
assert.containsNone(
fixture,
".o_statusbar_status .dropdown-menu",
@ -62,34 +59,18 @@ QUnit.module("Mobile Fields", ({ beforeEach }) => {
);
assert.strictEqual(
fixture.querySelector(".o_statusbar_status button.dropdown-toggle").textContent.trim(),
"aaa",
"statusbar button should display current field value"
"..."
);
// open the dropdown
await click(fixture, ".o_statusbar_status > button");
await click(fixture, ".o_statusbar_status .dropdown-toggle.o_last");
assert.containsOnce(
fixture,
".o_statusbar_status .dropdown-menu",
"dropdown should be visible"
);
assert.containsN(
fixture,
".o_statusbar_status .dropdown-menu .btn",
3,
"should have 3 status"
);
assert.containsN(
fixture,
".o_statusbar_status .btn.disabled",
3,
"all status should be disabled"
);
assert.hasClass(
fixture.querySelector(".o_statusbar_status .btn:nth-child(3)"),
"btn-primary",
"active status should be btn-primary"
);
assert.containsOnce(fixture, ".o_statusbar_status .dropdown-menu .dropdown-item.disabled");
});
QUnit.test("statusbar with no status on extra small screens", async (assert) => {
@ -112,49 +93,19 @@ QUnit.module("Mobile Fields", ({ beforeEach }) => {
"o_field_empty",
"statusbar widget should have class o_field_empty in edit"
);
assert.containsOnce(
fixture,
".o_statusbar_status button.dropdown-toggle",
"statusbar widget should have a button"
);
assert.strictEqual(
fixture.querySelector(".o_statusbar_status button.dropdown-toggle").textContent.trim(),
"",
"statusbar button shouldn't have text for null field value"
);
assert.containsOnce(fixture, ".o_statusbar_status button.dropdown-toggle:visible");
assert.strictEqual($(".o_statusbar_status button.dropdown-toggle:visible").text(), "...");
await click(fixture, ".o_statusbar_status button.dropdown-toggle");
assert.containsOnce(
fixture,
".o_statusbar_status .dropdown-menu",
"statusbar widget should have a dropdown menu"
);
assert.containsN(
fixture,
".o_statusbar_status .dropdown-menu .btn",
3,
"statusbar widget dropdown menu should have 3 buttons"
);
await click($(".o_statusbar_status button.dropdown-toggle:visible")[0]);
assert.containsOnce(fixture, ".o_statusbar_status .dropdown-menu");
assert.containsOnce(fixture, ".o_statusbar_status .dropdown-menu .dropdown-item");
assert.strictEqual(
fixture
.querySelectorAll(".o_statusbar_status .dropdown-menu .btn")[0]
.querySelector(".o_statusbar_status .dropdown-menu .dropdown-item")
.textContent.trim(),
"first record",
"statusbar widget dropdown first button should display the first record display_name"
);
assert.strictEqual(
fixture
.querySelectorAll(".o_statusbar_status .dropdown-menu .btn")[1]
.textContent.trim(),
"second record",
"statusbar widget dropdown second button should display the second record display_name"
);
assert.strictEqual(
fixture
.querySelectorAll(".o_statusbar_status .dropdown-menu .btn")[2]
.textContent.trim(),
"aaa",
"statusbar widget dropdown third button should display the third record display_name"
"statusbar widget dropdown first item should display the first record display_name"
);
});
@ -173,37 +124,20 @@ QUnit.module("Mobile Fields", ({ beforeEach }) => {
`,
});
await click(fixture, ".o_statusbar_status .dropdown-toggle");
assert.hasClass(
fixture.querySelector(".o_statusbar_status .dropdown-menu .btn:nth-child(3)"),
"btn-primary"
);
assert.hasClass(
fixture.querySelector(".o_statusbar_status .dropdown-menu .btn:nth-child(3)"),
"disabled"
);
// Open dropdown
await click($(".o_statusbar_status .dropdown-toggle:visible")[0]);
assert.containsN(
fixture,
".o_statusbar_status .btn-secondary:not(.dropdown-toggle):not(.disabled)",
2,
"other status should be btn-secondary and not disabled"
);
assert.containsOnce(fixture, ".o_statusbar_status .dropdown-item");
await click(
fixture.querySelector(
".o_statusbar_status .btn-secondary:not(.dropdown-toggle):not(.disabled)"
)
);
await click(fixture, ".o_statusbar_status .dropdown-item");
await click(fixture, ".o_statusbar_status .dropdown-toggle");
assert.hasClass(
fixture.querySelector(".o_statusbar_status .dropdown-menu .btn:nth-child(1)"),
"btn-primary"
);
assert.hasClass(
fixture.querySelector(".o_statusbar_status .dropdown-menu .btn:nth-child(1)"),
"disabled"
);
assert.strictEqual($(".o_arrow_button_current").text(), "first record");
assert.containsN(fixture, ".o_statusbar_status .o_arrow_button:visible", 3);
assert.containsOnce(fixture, ".o_statusbar_status .dropdown-toggle:visible");
// Open second dropdown
await click($(".o_statusbar_status .dropdown-toggle:visible")[0]);
assert.containsN(fixture, ".o_statusbar_status .dropdown-item", 2);
});
});

View file

@ -28,6 +28,7 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
fields: {
display_name: { type: "char", string: "Display Name" },
trululu: { type: "many2one", string: "Trululu", relation: "partner" },
boolean: { type: "boolean", string: "Bool" },
},
records: [
{ id: 1, display_name: "first record", trululu: 4 },
@ -104,8 +105,8 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
arch: `
<form>
<header>
<button string="Confirm" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
<button string="Do it" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
<button string="Confirm" invisible="display_name == 'first record'" />
<button string="Do it" invisible="display_name == 'first record'" />
</header>
<sheet>
<group>
@ -145,7 +146,7 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
arch: `
<form>
<header>
<button string="Hola" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
<button string="Hola" invisible="display_name == 'first record'" />
<button string="Ciao" />
</header>
<sheet>
@ -197,7 +198,7 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
<form>
<header>
<widget name="attach_document" string="Attach document" />
<button string="Ciao" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
<button string="Ciao" invisible="display_name == 'first record'" />
</header>
<sheet>
<group>
@ -251,8 +252,8 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
<form>
<header>
<button string="Just more than one" />
<button string="Confirm" attrs="{'invisible': [['display_name', '=', '']]}" />
<button string="Do it" attrs="{'invisible': [['display_name', '!=', '']]}" />
<button string="Confirm" invisible="display_name == ''" />
<button string="Do it" invisible="display_name != ''" />
</header>
<sheet>
<field name="display_name" />
@ -319,8 +320,8 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
</sheet>
</form>
`,
mockRPC(route, { method, args: [, , changedField] }) {
if (method === "onchange" && changedField === "display_name") {
mockRPC(route, { method, args }) {
if (method === "onchange" && args[2][0] === "display_name") {
return onchangeDef;
}
},
@ -422,7 +423,7 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
);
// click on back button
await click(fixture, ".modal .modal-header .fa-arrow-left");
await click(fixture, ".modal .modal-header .oi-arrow-left");
assert.strictEqual(
window.scrollY,
265,
@ -436,7 +437,7 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
let fileInput;
patchWithCleanup(AttachDocumentWidget.prototype, {
setup() {
this._super();
super.setup();
fileInput = this.fileInput;
},
});
@ -479,4 +480,38 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
await nextTick();
assert.verifySteps(["post"]);
});
QUnit.test("button box with 3/4 buttons (close to threshold)", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<div name="button_box">
<button>MyButton</button>
<button>MyButton2</button>
<button>MyButton3</button>
<button invisible="not boolean">MyButton4</button>
</div>
<field name="boolean"/>
</sheet>
</form>`,
resId: 2,
});
// 3 buttons to display -> no "More" dropdown
assert.containsNone(fixture, ".o_field_widget[name=boolean] input:checked");
assert.containsN(fixture, ".o-form-buttonbox > .oe_stat_button", 3);
assert.containsNone(fixture, ".o-form-buttonbox .o_button_more");
// 4 buttons to display -> 2 buttons visible + 2 inside the "More" dropdown
await click(fixture.querySelector(".o_field_widget[name=boolean] input"));
assert.containsN(fixture, ".o-form-buttonbox > .oe_stat_button", 3);
assert.containsOnce(fixture, ".o-form-buttonbox .oe_stat_button .o_button_more");
await click(fixture.querySelector(".o_button_more"));
assert.containsN(fixture, ".o_dropdown_more .oe_stat_button", 2);
});
});

View file

@ -0,0 +1,103 @@
/** @odoo-module **/
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import { AnimatedNumber } from "@web/views/view_components/animated_number";
let serverData;
let target;
QUnit.module("Views", (hooks) => {
hooks.beforeEach(() => {
patchWithCleanup(AnimatedNumber, { enableAnimations: false });
serverData = {
models: {
partner: {
fields: {
foo: { string: "Foo", type: "char" },
product_id: {
string: "something_id",
type: "many2one",
relation: "product",
},
},
records: [
{
id: 1,
foo: "yop",
product_id: 3,
},
{
id: 2,
foo: "blip",
product_id: 5,
},
{
id: 3,
foo: "gnap",
product_id: 3,
},
{
id: 4,
foo: "blip",
product_id: 5,
},
],
},
product: {
fields: {
id: { string: "ID", type: "integer" },
name: { string: "Display Name", type: "char" },
},
records: [
{ id: 3, name: "hello" },
{ id: 5, name: "xmo" },
],
},
},
views: {},
};
target = getFixture();
setupViewRegistries();
});
QUnit.module("KanbanView");
QUnit.test("Should load grouped kanban with folded column", async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<progressbar field="foo" colors='{"yop": "success", "blip": "danger"}'/>
<field name="product_id"/>
<templates>
<t t-name="kanban-box">
<div><field name="foo"/></div>
</t>
</templates>
</kanban>`,
groupBy: ["product_id"],
async mockRPC(route, args, performRPC) {
if (args.method === "web_read_group") {
const result = await performRPC(route, args);
result.groups[1].__fold = true;
return result;
}
},
});
assert.containsN(target, ".o_column_progress", 2, "Should have 2 progress bar");
assert.containsN(target, ".o_kanban_group", 2, "Should have 2 grouped column");
assert.containsN(target, ".o_kanban_record", 2, "Should have 2 loaded record");
assert.containsOnce(
target,
".o_kanban_load_more",
"Should have a folded column with a load more button"
);
await click(target, ".o_kanban_load_more button");
assert.containsNone(target, ".o_kanban_load_more", "Shouldn't have a load more button");
assert.containsN(target, ".o_kanban_record", 4, "Should have 4 loaded record");
});
});

View file

@ -61,13 +61,13 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
assert.containsN(fixture, ".o_data_row", 4);
assert.containsNone(fixture, ".o_list_selection_box");
assert.containsOnce(fixture, ".o_control_panel .o_cp_bottom_right");
assert.containsOnce(fixture, ".o_control_panel .fa-search");
// select a record
await triggerEvents(fixture, ".o_data_row:nth-child(1)", ["touchstart", "touchend"]);
assert.containsOnce(fixture, ".o_list_selection_box");
assert.containsNone(fixture, ".o_list_selection_box .o_list_select_domain");
assert.containsNone(fixture, ".o_control_panel .o_cp_bottom_right");
assert.containsNone(fixture, ".o_control_panel .o_cp_searchview");
assert.ok(
fixture.querySelector(".o_list_selection_box").textContent.includes("1 selected")
);
@ -87,14 +87,14 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
await toggleActionMenu(fixture);
assert.deepEqual(
getMenuItemTexts(fixture.querySelector(".o_cp_action_menus")),
["Delete"],
"action menu should contain the Delete action"
["Duplicate", "Delete"],
"action menu should contain the Duplicate and Delete actions"
);
// unselect all
await click(fixture, ".o_discard_selection");
await click(fixture, ".o_list_unselect_all");
assert.containsNone(fixture, ".o_list_selection_box");
assert.containsOnce(fixture, ".o_control_panel .o_cp_bottom_right");
assert.containsOnce(fixture, ".o_control_panel .fa-search");
});
QUnit.test("selection box is properly displayed (multi pages)", async function (assert) {
@ -134,8 +134,8 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
await toggleActionMenu(fixture);
assert.deepEqual(
getMenuItemTexts(fixture.querySelector(".o_cp_action_menus")),
["Delete"],
"action menu should contain the Delete action"
["Duplicate", "Delete"],
"action menu should contain the Duplicate and Delete actions"
);
// select all records of first page

View file

@ -1,6 +1,6 @@
/** @odoo-module */
import { click, getFixture } from "@web/../tests/helpers/utils";
import { click, getFixture, editInput } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
QUnit.module("ViewDialogs", (hooks) => {
@ -76,8 +76,8 @@ QUnit.module("ViewDialogs", (hooks) => {
<field name="linked_sale_order_line" widget="many2many_tags"/>
</form>`,
async mockRPC(route, args) {
if (args.method === "create" && args.model === "sale_order_line") {
const { product_id: selectedId } = args.args[0];
if (args.method === "web_save" && args.model === "sale_order_line") {
const { product_id: selectedId } = args.args[1];
assert.strictEqual(selectedId, false, `there should be no product selected`);
}
},
@ -132,8 +132,8 @@ QUnit.module("ViewDialogs", (hooks) => {
<field name="linked_sale_order_line" widget="many2many_tags"/>
</form>`,
async mockRPC(route, args) {
if (args.method === "create" && args.model === "sale_order_line") {
const { product_id: selectedId } = args.args[0];
if (args.method === "web_save" && args.model === "sale_order_line") {
const { product_id: selectedId } = args.args[1];
assert.strictEqual(selectedId, 111, `the product should be selected`);
}
if (args.method === "some_action") {
@ -148,4 +148,42 @@ QUnit.module("ViewDialogs", (hooks) => {
await click(target, ".o_form_button_save");
assert.verifySteps([]);
});
QUnit.test("SelectCreateDialog: default props, create a record", async function (assert) {
assert.expect(9);
serverData.views["product,false,form"] = `<form><field name="display_name"/></form>`;
await makeView({
type: "form",
resModel: "sale_order_line",
serverData,
arch: `
<form>
<field name="product_id"/>
<field name="linked_sale_order_line" widget="many2many_tags"/>
</form>`,
});
await click(target, '.o_field_widget[name="product_id"] input');
assert.containsOnce(target, ".o_dialog");
assert.containsOnce(
target,
".o_dialog .o_kanban_view .o_kanban_record:not(.o_kanban_ghost)"
);
assert.containsN(target, ".o_dialog footer button", 2);
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-kanban-button-new");
await click(target.querySelector(".o_dialog footer button.o_create_button"));
assert.containsN(target, ".o_dialog", 2);
assert.containsOnce(target, ".o_dialog .o_form_view");
await editInput(target, ".o_dialog .o_form_view .o_field_widget input", "hello");
await click(target.querySelector(".o_dialog .o_form_button_save"));
assert.containsNone(target, ".o_dialog");
});
});

View file

@ -53,11 +53,11 @@ QUnit.module("Widgets", (hooks) => {
assert.expect(7);
patchWithCleanup(SignatureWidget.prototype, {
async onClickSignature() {
await this._super.apply(this, arguments);
await super.onClickSignature(...arguments);
assert.step("onClickSignature");
},
async uploadSignature({signatureImage}) {
await this._super.apply(this, arguments);
await super.uploadSignature(...arguments);
assert.step("uploadSignature");
},
});

View file

@ -1,5 +1,5 @@
/** @odoo-module **/
import { click, legacyExtraNextTick } from "@web/../tests/helpers/utils";
import { click, nextTick } from "@web/../tests/helpers/utils";
import {
createWebClient,
doAction,
@ -17,6 +17,7 @@ import { companyService } from "@web/webclient/company_service";
let serverData;
const serviceRegistry = registry.category("services");
const userMenuRegistry = registry.category("user_menuitems");
QUnit.module("Burger Menu", {
beforeEach() {
@ -59,9 +60,7 @@ QUnit.test("Burger Menu on an App", async (assert) => {
await createWebClient({ serverData });
await click(document.body, ".o_navbar_apps_menu .dropdown-toggle");
await legacyExtraNextTick();
await click(document.body, ".o_app:nth-of-type(2)");
await legacyExtraNextTick();
assert.containsNone(document.body, ".o_burger_menu");
@ -72,7 +71,7 @@ QUnit.test("Burger Menu on an App", async (assert) => {
document.body.querySelector(".o_burger_menu nav.o_burger_menu_content li").textContent,
"SubMenu"
);
assert.hasClass(document.body.querySelector(".o_burger_menu_content"), "o_burger_menu_dark");
assert.hasClass(document.body.querySelector(".o_burger_menu_content"), "o_burger_menu_app");
await click(document.body, ".o_burger_menu_topbar");
assert.doesNotHaveClass(
@ -81,7 +80,7 @@ QUnit.test("Burger Menu on an App", async (assert) => {
);
await click(document.body, ".o_burger_menu_topbar");
assert.hasClass(document.body.querySelector(".o_burger_menu_content"), "o_burger_menu_dark");
assert.hasClass(document.body.querySelector(".o_burger_menu_content"), "o_burger_menu_app");
});
QUnit.test("Burger Menu on an App without SubMenu", async (assert) => {
@ -89,9 +88,7 @@ QUnit.test("Burger Menu on an App without SubMenu", async (assert) => {
await createWebClient({ serverData });
await click(document.body, ".o_navbar_apps_menu .dropdown-toggle");
await legacyExtraNextTick();
await click(document.body, ".o_app:nth-of-type(2)");
await legacyExtraNextTick();
assert.containsNone(document.body, ".o_burger_menu");
@ -111,7 +108,6 @@ QUnit.test("Burger menu closes when an action is requested", async (assert) => {
assert.containsOnce(document.body, ".o_burger_menu");
await doAction(wc, 1);
await legacyExtraNextTick();
assert.containsNone(document.body, ".o_burger_menu");
assert.containsOnce(document.body, ".o_kanban_view");
});
@ -131,9 +127,7 @@ QUnit.test("Burger menu closes when click on menu item", async (assert) => {
};
await createWebClient({ serverData });
await click(document.body, ".o_navbar_apps_menu .dropdown-toggle");
await legacyExtraNextTick();
await click(document.body, ".o_app:nth-of-type(2)");
await legacyExtraNextTick();
assert.containsNone(document.body, ".o_burger_menu");
@ -144,7 +138,30 @@ QUnit.test("Burger menu closes when click on menu item", async (assert) => {
"SubMenu"
);
await click(document.body, ".o_burger_menu nav.o_burger_menu_content li");
await legacyExtraNextTick();
await legacyExtraNextTick();
await nextTick();
assert.containsNone(document.body, ".o_burger_menu");
});
QUnit.test("Burger menu closes when click on user menu item", async (assert) => {
userMenuRegistry.add("ring_item", function () {
return {
type: "item",
id: "ring",
description: "Ring",
callback: () => {
assert.step("callback ring_item");
},
sequence: 5,
};
});
await createWebClient({ serverData });
assert.containsNone(document.body, ".o_burger_menu");
await click(document.body, ".o_mobile_menu_toggle");
assert.containsOnce(document.body, ".o_burger_menu");
await click(document.body, ".o_burger_menu .o_user_menu_mobile a");
assert.containsNone(document.body, ".o_burger_menu");
assert.verifySteps(["callback ring_item"]);
});

View file

@ -9,6 +9,7 @@ import { userService } from "@web/core/user_service";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { click, getFixture, mount } from "@web/../tests/helpers/utils";
import {markup} from "@odoo/owl";
const serviceRegistry = registry.category("services");
const userMenuRegistry = registry.category("user_menuitems");
@ -84,19 +85,30 @@ QUnit.test("can be rendered", async (assert) => {
},
};
});
userMenuRegistry.add("html_item", function () {
return {
type: "item",
id: "html",
description: markup(`<div>HTML<i class="fa fa-check px-2"></i></div>`),
callback: () => {
assert.step("callback html_item");
},
sequence: 20,
};
});
await mount(BurgerUserMenu, target, { env });
assert.containsN(target, ".o_user_menu_mobile .dropdown-item", 4);
assert.containsN(target, ".o_user_menu_mobile .dropdown-item", 5);
assert.containsOnce(target, ".o_user_menu_mobile .dropdown-item input.form-check-input");
assert.containsOnce(target, "div.dropdown-divider");
const children = [...(target.querySelector(".o_user_menu_mobile").children || [])];
assert.deepEqual(
children.map((el) => el.tagName),
["A", "A", "DIV", "DIV", "A"]
["A", "A", "DIV", "DIV", "A", "A"]
);
const items = [...target.querySelectorAll(".dropdown-item")] || [];
assert.deepEqual(
items.map((el) => el.textContent),
["Ring", "Bad", "Frodo", "Eye"]
["Ring", "Bad", "Frodo", "HTML", "Eye"]
);
for (const item of items) {
click(item);
@ -105,6 +117,7 @@ QUnit.test("can be rendered", async (assert) => {
"callback ring_item",
"callback bad_item",
"callback frodo_item",
"callback html_item",
"callback eye_item",
]);
});

View file

@ -49,36 +49,20 @@ QUnit.module("Mobile SettingsFormView", (hooks) => {
serverData,
arch: `
<form string="Settings" class="oe_form_configuration o_base_settings" js_class="base_settings">
<div class="o_setting_container">
<div class="settings">
<div class="app_settings_block" string="CRM" data-key="crm">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="bar"/>
</div>
<div class="o_setting_right_pane">
<label for="bar"/>
<div class="text-muted">this is bar</div>
</div>
</div>
</div>
</div>
<div class="app_settings_block" string="Project" data-key="project">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="foo"/>
</div>
<div class="o_setting_right_pane">
<label for="foo"/>
<div class="text-muted">this is foo</div>
</div>
</div>
</div>
</div>
</div>
</div>
<app string="CRM" name="crm">
<block>
<setting help="this is bar">
<field name="bar"/>
</setting>
</block>
</app>
<app string="Project" name="project">
<block>
<setting help="this is foo">
<field name="foo"/>
</setting>
</block>
</app>
</form>`,
});
@ -125,36 +109,20 @@ QUnit.module("Mobile SettingsFormView", (hooks) => {
serverData,
arch: `
<form string="Settings" class="oe_form_configuration o_base_settings" js_class="base_settings">
<div class="o_setting_container">
<div class="settings">
<div class="app_settings_block" string="CRM" data-key="crm">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="bar"/>
</div>
<div class="o_setting_right_pane">
<label for="bar"/>
<div class="text-muted">this is bar</div>
</div>
</div>
</div>
</div>
<div class="app_settings_block" string="Project" data-key="project">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="foo"/>
</div>
<div class="o_setting_right_pane">
<label for="foo"/>
<div class="text-muted">this is foo</div>
</div>
</div>
</div>
</div>
</div>
</div>
<app string="CRM" name="crm">
<block>
<setting help="this is bar">
<field name="bar"/>
</setting>
</block>
</app>
<app string="Project" name="project">
<block>
<setting help="this is foo">
<field name="foo"/>
</setting>
</block>
</app>
</form>`,
});

View file

@ -0,0 +1,67 @@
/** @odoo-module **/
import { getFixture } from "@web/../tests/helpers/utils";
import { setupViewRegistries } from "@web/../tests/views/helpers";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
let serverData, target;
QUnit.module("ActionManager", (hooks) => {
hooks.beforeEach(() => {
serverData = {
models: {
project: {
fields: {
foo: { string: "Foo", type: "boolean" },
},
records: [
{
id: 1,
foo: true,
},
{
id: 2,
foo: false,
},
],
},
},
views: {
"project,false,list": '<list><field name="foo"/></list>',
"project,false,kanban": `
<kanban>
<templates>
<t t-name='kanban-box'>
<div class='oe_kanban_card'>
<field name='foo' />
</div>
</t>
</templates>
</kanban>
`,
"project,false,search": "<search></search>",
},
};
target = getFixture();
setupViewRegistries();
});
QUnit.module("Window Actions");
QUnit.test("execute a window action with mobile_view_mode", async (assert) => {
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
xml_id: "project.action",
name: "Project Action",
res_model: "project",
type: "ir.actions.act_window",
view_mode: "list,kanban",
mobile_view_mode: "list",
views: [
[false, "kanban"],
[false, "list"],
],
});
assert.containsOnce(target, ".o_list_view");
});
});