/** @odoo-module **/ import { registry } from "@web/core/registry"; import testUtils from "web.test_utils"; import ListController from "web.ListController"; import FormView from "web.FormView"; import ListView from "web.ListView"; import { click, destroy, getFixture, makeDeferred, legacyExtraNextTick, patchWithCleanup, triggerEvents, } from "../../helpers/utils"; import KanbanView from "web.KanbanView"; import { registerCleanup } from "../../helpers/cleanup"; import { makeTestEnv } from "../../helpers/mock_env"; import { createWebClient, doAction, getActionManagerServerData } from "./../helpers"; import makeTestEnvironment from "web.test_env"; import { ClientActionAdapter, ViewAdapter } from "@web/legacy/action_adapters"; import { makeLegacyCrashManagerService } from "@web/legacy/utils"; import { useDebugCategory } from "@web/core/debug/debug_context"; import { ErrorDialog } from "@web/core/errors/error_dialogs"; import * as cpHelpers from "@web/../tests/search/helpers"; import AbstractView from "web.AbstractView"; import ControlPanel from "web.ControlPanel"; import core from "web.core"; import AbstractAction from "web.AbstractAction"; import Widget from "web.Widget"; import SystrayMenu from "web.SystrayMenu"; import legacyViewRegistry from "web.view_registry"; let serverData; let target; QUnit.module("ActionManager", (hooks) => { hooks.beforeEach(() => { registry.category("views").remove("form"); // remove new form from registry registry.category("views").remove("kanban"); // remove new kanban from registry registry.category("views").remove("list"); // remove new list from registry legacyViewRegistry.add("form", FormView); // add legacy form -> will be wrapped and added to new registry legacyViewRegistry.add("kanban", KanbanView); // add legacy kanban -> will be wrapped and added to new registry legacyViewRegistry.add("list", ListView); // add legacy list -> will be wrapped and added to new registry serverData = getActionManagerServerData(); target = getFixture(); }); QUnit.module("Legacy tests (to eventually drop)"); QUnit.test("display warning as notification", async function (assert) { // this test can be removed as soon as the legacy layer is dropped assert.expect(5); let list; patchWithCleanup(ListController.prototype, { init() { this._super(...arguments); list = this; }, }); const webClient = await createWebClient({ serverData }); await doAction(webClient, 3); assert.containsOnce(target, ".o_legacy_list_view"); list.trigger_up("warning", { title: "Warning!!!", message: "This is a warning...", }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsOnce(target, ".o_legacy_list_view"); assert.containsOnce(document.body, ".o_notification.border-warning"); assert.strictEqual($(".o_notification_title").text(), "Warning!!!"); assert.strictEqual($(".o_notification_content").text(), "This is a warning..."); }); QUnit.test("display warning as modal", async function (assert) { // this test can be removed as soon as the legacy layer is dropped assert.expect(5); let list; patchWithCleanup(ListController.prototype, { init() { this._super(...arguments); list = this; }, }); const webClient = await createWebClient({ serverData }); await doAction(webClient, 3); assert.containsOnce(target, ".o_legacy_list_view"); list.trigger_up("warning", { title: "Warning!!!", message: "This is a warning...", type: "dialog", }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsOnce(target, ".o_legacy_list_view"); assert.containsOnce(document.body, ".modal"); assert.strictEqual($(".modal-title").text(), "Warning!!!"); assert.strictEqual($(".modal-body").text(), "This is a warning..."); }); QUnit.test("display multiline warning as modal", async function (assert) { assert.expect(5); let list; patchWithCleanup(ListController.prototype, { init() { this._super(...arguments); list = this; }, }); const webClient = await createWebClient({ serverData }); await doAction(webClient, 3); assert.containsOnce(target, ".o_legacy_list_view"); list.trigger_up("warning", { title: "Warning!!!", message: "This is a warning...\nabc", type: "dialog", }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsOnce(target, ".o_legacy_list_view"); assert.containsOnce(document.body, ".modal"); assert.strictEqual($(".modal-title").text(), "Warning!!!"); assert.strictEqual($(".modal-body")[0].innerText, "This is a warning...\nabc"); }); QUnit.test( "legacy crash manager is still properly remapped to error service", async function (assert) { // this test can be removed as soon as the legacy layer is dropped assert.expect(2); const legacyEnv = makeTestEnvironment(); registry .category("services") .add("legacy_crash_manager", makeLegacyCrashManagerService(legacyEnv)) .add("dialog", { start() { return { add(dialogClass, props) { assert.strictEqual(dialogClass, ErrorDialog); assert.strictEqual(props.traceback, "BOOM"); }, }; }, }); await makeTestEnv(); legacyEnv.services.crash_manager.show_message("BOOM"); } ); QUnit.test("redraw a controller and open debugManager does not crash", async (assert) => { assert.expect(11); const LegacyAction = AbstractAction.extend({ start() { const ret = this._super(...arguments); const el = document.createElement("div"); el.classList.add("custom-action"); this.el.append(el); return ret; }, }); core.action_registry.add("customLegacy", LegacyAction); patchWithCleanup(ClientActionAdapter.prototype, { setup() { useDebugCategory("custom", { widget: this }); this._super(); }, }); registry .category("debug") .category("custom") .add("item1", ({ widget }) => { assert.step("debugItems executed"); assert.ok(widget); return {}; }); patchWithCleanup(odoo, { debug: true }); const mockRPC = (route) => { if (route.includes("check_access_rights")) { return true; } }; const webClient = await createWebClient({ serverData, mockRPC }); await doAction(webClient, "customLegacy"); assert.containsOnce(target, ".custom-action"); assert.verifySteps([]); await click(target, ".o_debug_manager button"); assert.verifySteps(["debugItems executed"]); await doAction(webClient, 5); // action in Dialog await click(target, ".modal .o_form_button_cancel"); assert.containsNone(target, ".modal"); assert.containsOnce(target, ".custom-action"); assert.verifySteps([]); // close debug menu await click(target, ".o_debug_manager button"); // open debug menu await click(target, ".o_debug_manager button"); assert.verifySteps(["debugItems executed"]); delete core.action_registry.map.customLegacy; }); QUnit.test("willUnmount is called down the legacy layers", async (assert) => { assert.expect(7); let mountCount = 0; patchWithCleanup(ControlPanel.prototype, { setup() { this._super(); owl.onMounted(() => { mountCount = mountCount + 1; this.__uniqueId = mountCount; assert.step(`mounted ${this.__uniqueId}`); }); owl.onWillUnmount(() => { assert.step(`willUnmount ${this.__uniqueId}`); }); }, }); const LegacyAction = AbstractAction.extend({ hasControlPanel: true, start() { const ret = this._super(...arguments); const el = document.createElement("div"); el.classList.add("custom-action"); this.el.append(el); return ret; }, }); core.action_registry.add("customLegacy", LegacyAction); const webClient = await createWebClient({ serverData }); await doAction(webClient, 1); await doAction(webClient, "customLegacy"); await click(target.querySelectorAll(".breadcrumb-item")[0]); await legacyExtraNextTick(); destroy(webClient); assert.verifySteps([ "mounted 1", "willUnmount 1", "mounted 2", "willUnmount 2", "mounted 3", "willUnmount 3", ]); delete core.action_registry.map.customLegacy; }); QUnit.test("Checks the availability of all views in the action", async (assert) => { assert.expect(2); patchWithCleanup(ListView.prototype, { init(viewInfo, params) { const action = params.action; const views = action.views.map((view) => [view.viewID, view.type]); assert.deepEqual(views, [ [1, "list"], [2, "kanban"], [3, "form"], ]); assert.deepEqual(action._views, [ [1, "list"], [2, "kanban"], [3, "form"], [false, "search"], ]); this._super(...arguments); }, }); const models = { partner: { fields: { display_name: { string: "Displayed name", type: "char", searchable: true }, foo: { string: "Foo", type: "char", default: "My little Foo Value", searchable: true, }, bar: { string: "Bar", type: "boolean" }, int_field: { string: "Integer field", type: "integer", group_operator: "sum" }, }, records: [ { id: 1, display_name: "first record", foo: "yop", int_field: 3, }, { id: 2, display_name: "second record", foo: "lalala", int_field: 5, }, { id: 4, display_name: "aaa", foo: "abc", int_field: 2, }, ], }, }; const views = { "partner,1,list": '', "partner,2,kanban": "", "partner,3,form": `
`, "partner,false,search": "", }; const serverData = { models, views }; const webClient = await createWebClient({ serverData }); await doAction(webClient, { id: 1, res_model: "partner", type: "ir.actions.act_window", views: [ [1, "list"], [2, "kanban"], [3, "form"], ], }); }); QUnit.test("client actions may take and push their params", async function (assert) { assert.expect(2); const ClientAction = AbstractAction.extend({ init(parent, action) { this._super(...arguments); assert.deepEqual(action.params, { active_id: 99, take: "five", active_ids: "1,2", list: [9, 10], }); }, }); core.action_registry.add("clientAction", ClientAction); registerCleanup(() => delete core.action_registry.map.clientAction); const webClient = await createWebClient({}); await doAction(webClient, { type: "ir.actions.client", tag: "clientAction", params: { active_id: 99, take: "five", active_ids: "1,2", list: [9, 10], }, }); assert.deepEqual(webClient.env.services.router.current.hash, { action: "clientAction", active_id: 99, take: "five", active_ids: "1,2", }); }); QUnit.test("client actions honour do_push_state", async function (assert) { assert.expect(2); const ClientAction = AbstractAction.extend({ init(parent) { this._super(...arguments); this.parent = parent; this.parent.do_push_state({ pinball: "wizard" }); }, async start() { await this._super(...arguments); const btn = document.createElement("button"); btn.classList.add("tommy"); btn.addEventListener("click", () => { this.parent.do_push_state({ gipsy: "the acid queen" }); }); this.el.append(btn); }, getState() { return { doctor: "quackson", }; }, }); core.action_registry.add("clientAction", ClientAction); registerCleanup(() => delete core.action_registry.map.clientAction); const webClient = await createWebClient({}); await doAction(webClient, { type: "ir.actions.client", tag: "clientAction", }); assert.deepEqual(webClient.env.services.router.current.hash, { action: "clientAction", pinball: "wizard", doctor: "quackson", }); await click(target, ".tommy"); assert.deepEqual(webClient.env.services.router.current.hash, { action: "clientAction", pinball: "wizard", gipsy: "the acid queen", doctor: "quackson", }); }); QUnit.test("Systray item triggers do action on legacy service provider", async (assert) => { assert.expect(3); function createMockActionService(assert) { return { dependencies: [], start() { return { doAction(params) { assert.step("do action"); assert.strictEqual(params, 128, "The doAction parameters are invalid."); }, loadState() {}, }; }, }; } registry.category("services").add("action", createMockActionService(assert)); const FakeSystrayItemWidget = Widget.extend({ on_attach_callback() { this.do_action(128); }, }); SystrayMenu.Items.push(FakeSystrayItemWidget); await createWebClient({ serverData }); assert.verifySteps(["do action"]); delete SystrayMenu.Items.FakeSystrayItemWidget; }); QUnit.test("usercontext always added to legacy actions", async (assert) => { assert.expect(8); core.action_registry.add("testClientAction", AbstractAction); registerCleanup(() => delete core.action_registry.map.testClientAction); patchWithCleanup(ClientActionAdapter.prototype, { setup() { assert.step("ClientActionAdapter"); const action = { ...this.props.widgetArgs[0] }; const originalAction = JSON.parse(action._originalAction); assert.deepEqual(originalAction.context, undefined); assert.deepEqual(action.context, this.env.services.user.context); this._super(); }, }); patchWithCleanup(ViewAdapter.prototype, { setup() { assert.step("ViewAdapter"); const action = { ...this.props.viewParams.action }; const originalAction = JSON.parse(action._originalAction); assert.deepEqual(originalAction.context, undefined); assert.deepEqual(action.context, this.env.services.user.context); this._super(); }, }); const webClient = await createWebClient({ serverData }); await doAction(webClient, "testClientAction"); assert.verifySteps(["ClientActionAdapter"]); await doAction(webClient, 1); assert.verifySteps(["ViewAdapter"]); }); QUnit.test("correctly transports legacy Props for doAction", async (assert) => { assert.expect(4); let ID = 0; const MyAction = AbstractAction.extend({ init() { this._super(...arguments); this.ID = ID++; assert.step(`id: ${this.ID} props: ${JSON.stringify(arguments[2])}`); }, async start() { const res = await this._super(...arguments); const link = document.createElement("a"); link.innerText = "some link"; link.setAttribute("id", `client_${this.ID}`); link.addEventListener("click", () => { this.do_action("testClientAction", { clear_breadcrumbs: true, props: { chain: "never break" }, }); }); this.el.appendChild(link); return res; }, }); core.action_registry.add("testClientAction", MyAction); registerCleanup(() => delete core.action_registry.map.testClientAction); const webClient = await createWebClient({ serverData }); await doAction(webClient, "testClientAction"); assert.verifySteps(['id: 0 props: {"className":"o_action","breadcrumbs":[]}']); await click(document.getElementById("client_0")); assert.verifySteps([ 'id: 1 props: {"chain":"never break","className":"o_action","breadcrumbs":[]}', ]); }); QUnit.test("bootstrap tooltip in dialog action auto destroy", async (assert) => { assert.expect(2); const mockRPC = (route) => { if (route === "/web/dataset/call_button") { return false; } }; serverData.views["partner,3,form"] = /*xml*/ `
`; const webClient = await createWebClient({ serverData, mockRPC }); await doAction(webClient, 25); const tooltipProm = makeDeferred(); $(target).one("shown.bs.tooltip", () => { tooltipProm.resolve(); }); triggerEvents(target, ".modal footer button", ["mouseover", "focusin"]); await tooltipProm; // check on webClient dom assert.containsOnce(document.body, ".tooltip"); await doAction(webClient, { type: "ir.actions.act_window_close", }); // check on the whole DOM assert.containsNone(document.body, ".tooltip"); }); QUnit.test("bootstrap tooltip destroyed on click", async (assert) => { assert.expect(2); const mockRPC = (route) => { if (route === "/web/dataset/call_button") { return false; } }; serverData.views["partner,666,form"] = /*xml*/ `
`; const webClient = await createWebClient({ serverData, mockRPC }); await doAction(webClient, 24); const tooltipProm = makeDeferred(); $(target).one("shown.bs.tooltip", () => { tooltipProm.resolve(); }); triggerEvents(target, ".o_form_statusbar button", ["mouseover", "focusin"]); await tooltipProm; // check on webClient DOM assert.containsOnce(document.body, ".tooltip"); await click(target, ".o_content"); // check on the whole DOM assert.containsNone(document.body, ".tooltip"); }); QUnit.test("breadcrumbs are correct in stacked legacy client actions", async function (assert) { const ClientAction = AbstractAction.extend({ hasControlPanel: true, async start() { this.$el.addClass("client_action"); return this._super(...arguments); }, getTitle() { return "Blabla"; }, }); core.action_registry.add("clientAction", ClientAction); registerCleanup(() => delete core.action_registry.map.clientAction); const webClient = await createWebClient({ serverData }); await doAction(webClient, 3); assert.containsOnce(target, ".o_legacy_list_view"); assert.strictEqual($(target).find(".breadcrumb-item").text(), "Partners"); await doAction(webClient, { type: "ir.actions.client", tag: "clientAction", }); assert.containsOnce(target, ".client_action"); assert.strictEqual($(target).find(".breadcrumb-item").text(), "PartnersBlabla"); }); QUnit.test("view with js_class attribute (legacy)", async function (assert) { assert.expect(2); const TestView = AbstractView.extend({ viewType: "test_view", }); const TestJsClassView = TestView.extend({ init() { this._super.call(this, ...arguments); assert.step("init js class"); }, }); serverData.views["partner,false,test_view"] = `
`; serverData.actions[9999] = { id: 1, name: "Partners Action 1", res_model: "partner", type: "ir.actions.act_window", views: [[false, "test_view"]], }; legacyViewRegistry.add("test_view", TestView); legacyViewRegistry.add("test_jsClass", TestJsClassView); const webClient = await createWebClient({ serverData }); await doAction(webClient, 9999); assert.verifySteps(["init js class"]); delete legacyViewRegistry.map.test_view; delete legacyViewRegistry.map.test_jsClass; }); QUnit.test( "execute action without modal closes bootstrap tooltips anyway", async function (assert) { assert.expect(12); Object.assign(serverData.views, { "partner,666,form": `
`, }); const mockRPC = async (route) => { assert.step(route); if (route === "/web/dataset/call_button") { // Some business stuff server side, then return an implicit close action return Promise.resolve(false); } }; const webClient = await createWebClient({ serverData, mockRPC }); await doAction(webClient, 24); assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", "/web/dataset/call_kw/partner/get_views", "/web/dataset/call_kw/partner/read", ]); assert.containsN(target, ".o_form_buttons_view button:not([disabled])", 2); const actionButton = target.querySelector("button[name=object]"); const tooltipProm = new Promise((resolve) => { document.body.addEventListener( "shown.bs.tooltip", () => { actionButton.dispatchEvent(new Event("mouseout")); resolve(); }, { once: true, } ); }); actionButton.dispatchEvent(new Event("mouseover")); await tooltipProm; assert.containsOnce(document.body, ".tooltip"); await click(actionButton); await legacyExtraNextTick(); assert.verifySteps(["/web/dataset/call_button", "/web/dataset/call_kw/partner/read"]); assert.containsNone(document.body, ".tooltip"); // body different from webClient in tests ! assert.containsN(target, ".o_form_buttons_view button:not([disabled])", 2); } ); QUnit.test("click multiple times to open a record", async function (assert) { assert.expect(5); const def = testUtils.makeTestPromise(); const defs = [null, def]; const mockRPC = async (route, args) => { if (args.method === "read") { await Promise.resolve(defs.shift()); } }; const webClient = await createWebClient({ serverData, mockRPC }); await doAction(webClient, 3); assert.containsOnce(target, ".o_legacy_list_view"); await testUtils.dom.click(target.querySelector(".o_legacy_list_view .o_data_row")); await legacyExtraNextTick(); assert.containsOnce(target, ".o_legacy_form_view"); await testUtils.dom.click(target.querySelector(".o_back_button")); await legacyExtraNextTick(); assert.containsOnce(target, ".o_legacy_list_view"); await testUtils.dom.click(target.querySelector(".o_legacy_list_view .o_data_row")); await testUtils.dom.click(target.querySelector(".o_legacy_list_view .o_data_row")); await legacyExtraNextTick(); assert.containsOnce(target, ".o_legacy_list_view"); def.resolve(); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsOnce(target, ".o_legacy_form_view"); }); QUnit.test("correct pager when coming from list (legacy)", async (assert) => { assert.expect(4); registry.category("views").remove("list"); legacyViewRegistry.add("list", ListView); serverData.views = { "partner,false,search": ``, "partner,99,list": ``, "partner,100,form": `
`, }; const wc = await createWebClient({ serverData }); await doAction(wc, { res_model: "partner", type: "ir.actions.act_window", views: [ [99, "list"], [100, "form"], ], }); assert.deepEqual(cpHelpers.getPagerValue(target), [1, 4]); assert.deepEqual(cpHelpers.getPagerLimit(target), 5); await click(target, ".o_data_row:nth-child(2) .o_data_cell"); await legacyExtraNextTick(); assert.deepEqual(cpHelpers.getPagerValue(target), [2]); assert.deepEqual(cpHelpers.getPagerLimit(target), 4); }); });