/** @odoo-module **/ import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; import core from "web.core"; import AbstractAction from "web.AbstractAction"; import testUtils from "web.test_utils"; import { registerCleanup } from "../../helpers/cleanup"; import { click, getFixture, legacyExtraNextTick, nextTick, patchWithCleanup, } from "../../helpers/utils"; import { createWebClient, doAction, getActionManagerServerData } from "./../helpers"; import { Component, xml } from "@odoo/owl"; let serverData; let target; const actionRegistry = registry.category("actions"); QUnit.module("ActionManager", (hooks) => { hooks.beforeEach(() => { serverData = getActionManagerServerData(); target = getFixture(); }); QUnit.module("Client Actions"); QUnit.test("can display client actions in Dialog", async function (assert) { assert.expect(2); const webClient = await createWebClient({ serverData }); await doAction(webClient, { name: "Dialog Test", target: "new", tag: "__test__client__action__", type: "ir.actions.client", }); assert.containsOnce(target, ".modal .test_client_action"); assert.strictEqual(target.querySelector(".modal-title").textContent, "Dialog Test"); }); QUnit.test( "can display client actions in Dialog and close the dialog", async function (assert) { assert.expect(3); const webClient = await createWebClient({ serverData }); await doAction(webClient, { name: "Dialog Test", target: "new", tag: "__test__client__action__", type: "ir.actions.client", }); assert.containsOnce(target, ".modal .test_client_action"); assert.strictEqual(target.querySelector(".modal-title").textContent, "Dialog Test"); target.querySelector(".modal footer .btn.btn-primary").click(); await nextTick(); assert.containsNone(target, ".modal .test_client_action"); } ); QUnit.test("can display client actions as main, then in Dialog", async function (assert) { assert.expect(3); const webClient = await createWebClient({ serverData }); await doAction(webClient, "__test__client__action__"); assert.containsOnce(target, ".o_action_manager .test_client_action"); await doAction(webClient, { target: "new", tag: "__test__client__action__", type: "ir.actions.client", }); assert.containsOnce(target, ".o_action_manager .test_client_action"); assert.containsOnce(target, ".modal .test_client_action"); }); QUnit.test( "can display client actions in Dialog, then as main destroys Dialog", async function (assert) { assert.expect(4); const webClient = await createWebClient({ serverData }); await doAction(webClient, { target: "new", tag: "__test__client__action__", type: "ir.actions.client", }); assert.containsOnce(target, ".test_client_action"); assert.containsOnce(target, ".modal .test_client_action"); await doAction(webClient, "__test__client__action__"); assert.containsOnce(target, ".test_client_action"); assert.containsNone(target, ".modal .test_client_action"); } ); QUnit.test("soft_reload will refresh data", async (assert) => { const mockRPC = async function (route, args) { if (route === "/web/dataset/call_kw/partner/web_search_read") { assert.step("web_search_read"); } }; const webClient = await createWebClient({ serverData, mockRPC }); await doAction(webClient, 1); assert.verifySteps(["web_search_read"]); await doAction(webClient, "soft_reload"); assert.verifySteps(["web_search_read"]); }); QUnit.test("soft_reload when there is no controller", async (assert) => { const webClient = await createWebClient({ serverData }); await doAction(webClient, "soft_reload"); assert.ok(true, "No ControllerNotFoundError when there is no controller to restore"); }); QUnit.test("can execute client actions from tag name (legacy)", async function (assert) { // remove this test as soon as legacy Widgets are no longer supported assert.expect(4); const ClientAction = AbstractAction.extend({ start: function () { this.$el.text("Hello World"); this.$el.addClass("o_client_action_test"); }, }); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; core.action_registry.add("HelloWorldTestLeg", ClientAction); registerCleanup(() => delete core.action_registry.map.HelloWorldTestLeg); const webClient = await createWebClient({ serverData, mockRPC }); await doAction(webClient, "HelloWorldTestLeg"); assert.containsNone( document.body, ".o_control_panel", "shouldn't have rendered a control panel" ); assert.strictEqual( $(target).find(".o_client_action_test").text(), "Hello World", "should have correctly rendered the client action" ); assert.verifySteps(["/web/webclient/load_menus"]); }); QUnit.test("can execute client actions from tag name", async function (assert) { assert.expect(4); class ClientAction extends Component {} ClientAction.template = xml`
Hello World
`; actionRegistry.add("HelloWorldTest", ClientAction); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; const webClient = await createWebClient({ serverData, mockRPC }); await doAction(webClient, "HelloWorldTest"); assert.containsNone( document.body, ".o_control_panel", "shouldn't have rendered a control panel" ); assert.strictEqual( $(target).find(".o_client_action_test").text(), "Hello World", "should have correctly rendered the client action" ); assert.verifySteps(["/web/webclient/load_menus"]); }); QUnit.test("async client action (function) returning another action", async function (assert) { assert.expect(1); registry.category("actions").add("my_action", async () => { await Promise.resolve(); return 1; // execute action 1 }); const webClient = await createWebClient({ serverData }); await doAction(webClient, "my_action"); assert.containsOnce(target, ".o_kanban_view"); }); QUnit.test( "'CLEAR-UNCOMMITTED-CHANGES' is not triggered for function client actions", async function (assert) { assert.expect(2); registry.category("actions").add("my_action", async () => { assert.step("my_action"); }); const webClient = await createWebClient({ serverData }); webClient.env.bus.addEventListener("CLEAR-UNCOMMITTED-CHANGES", () => { assert.step("CLEAR-UNCOMMITTED-CHANGES"); }); await doAction(webClient, "my_action"); assert.verifySteps(["my_action"]); } ); QUnit.test("client action with control panel (legacy)", async function (assert) { assert.expect(4); // LPE Fixme: at this time we don't really know the API that wowl ClientActions implement const ClientAction = AbstractAction.extend({ hasControlPanel: true, start() { this.$(".o_content").text("Hello World"); this.$el.addClass("o_client_action_test"); this.controlPanelProps.title = "Hello"; return this._super.apply(this, arguments); }, }); core.action_registry.add("HelloWorldTest", ClientAction); registerCleanup(() => delete core.action_registry.map.HelloWorldTest); const webClient = await createWebClient({ serverData }); await doAction(webClient, "HelloWorldTest"); assert.strictEqual( $(".o_control_panel:visible").length, 1, "should have rendered a control panel" ); assert.containsN( target, ".o_control_panel .breadcrumb-item", 1, "there should be one controller in the breadcrumbs" ); assert.strictEqual( $(".o_control_panel .breadcrumb-item").text(), "Hello", "breadcrumbs should still display the title of the controller" ); assert.strictEqual( $(target).find(".o_client_action_test .o_content").text(), "Hello World", "should have correctly rendered the client action" ); }); QUnit.test("state is pushed for client action (legacy)", async function (assert) { assert.expect(6); const ClientAction = AbstractAction.extend({ getTitle: function () { return "a title"; }, getState: function () { return { foo: "baz" }; }, }); const pushState = browser.history.pushState; patchWithCleanup(browser, { history: Object.assign({}, browser.history, { pushState() { pushState(...arguments); assert.step("push_state"); }, }), }); core.action_registry.add("HelloWorldTest", ClientAction); registerCleanup(() => delete core.action_registry.map.HelloWorldTest); const webClient = await createWebClient({ serverData }); let currentTitle = webClient.env.services.title.current; assert.strictEqual(currentTitle, '{"zopenerp":"Odoo"}'); let currentHash = webClient.env.services.router.current.hash; assert.deepEqual(currentHash, {}); await doAction(webClient, "HelloWorldTest"); currentTitle = webClient.env.services.title.current; assert.strictEqual(currentTitle, '{"zopenerp":"Odoo","action":"a title"}'); currentHash = webClient.env.services.router.current.hash; assert.deepEqual(currentHash, { action: "HelloWorldTest", foo: "baz", }); assert.verifySteps(["push_state"]); }); QUnit.test("action can use a custom control panel (legacy)", async function (assert) { assert.expect(1); class CustomControlPanel extends Component {} CustomControlPanel.template = xml`
My custom control panel
`; const ClientAction = AbstractAction.extend({ hasControlPanel: true, config: { ControlPanel: CustomControlPanel, }, }); core.action_registry.add("HelloWorldTest", ClientAction); registerCleanup(() => delete core.action_registry.map.HelloWorldTest); const webClient = await createWebClient({ serverData }); await doAction(webClient, "HelloWorldTest"); assert.containsOnce(target, ".custom-control-panel", "should have a custom control panel"); }); QUnit.test("breadcrumb is updated on title change (legacy)", async function (assert) { assert.expect(2); const ClientAction = AbstractAction.extend({ hasControlPanel: true, events: { click: function () { this.updateControlPanel({ title: "new title" }); }, }, start: async function () { this.$(".o_content").text("Hello World"); this.$el.addClass("o_client_action_test"); this.controlPanelProps.title = "initial title"; await this._super.apply(this, arguments); }, }); core.action_registry.add("HelloWorldTest", ClientAction); registerCleanup(() => delete core.action_registry.map.HelloWorldTest); const webClient = await createWebClient({ serverData }); await doAction(webClient, "HelloWorldTest"); assert.strictEqual( $("ol.breadcrumb").text(), "initial title", "should have initial title as breadcrumb content" ); await testUtils.dom.click($(target).find(".o_client_action_test")); await legacyExtraNextTick(); assert.strictEqual( $("ol.breadcrumb").text(), "new title", "should have updated title as breadcrumb content" ); }); QUnit.test("client actions can have breadcrumbs (legacy)", async function (assert) { assert.expect(4); const ClientAction = AbstractAction.extend({ hasControlPanel: true, init(parent, action) { action.display_name = "Goldeneye"; this._super.apply(this, arguments); }, start() { this.$el.addClass("o_client_action_test"); return this._super.apply(this, arguments); }, }); const ClientAction2 = AbstractAction.extend({ hasControlPanel: true, init(parent, action) { action.display_name = "No time for sweetness"; this._super.apply(this, arguments); }, start() { this.$el.addClass("o_client_action_test_2"); return this._super.apply(this, arguments); }, }); core.action_registry.add("ClientAction", ClientAction); core.action_registry.add("ClientAction2", ClientAction2); const webClient = await createWebClient({ serverData }); await doAction(webClient, "ClientAction"); assert.containsOnce(target, ".breadcrumb-item"); assert.strictEqual( target.querySelector(".breadcrumb-item.active").textContent, "Goldeneye" ); await doAction(webClient, "ClientAction2", { clearBreadcrumbs: false }); assert.containsN(target, ".breadcrumb-item", 2); assert.strictEqual( target.querySelector(".breadcrumb-item.active").textContent, "No time for sweetness" ); delete core.action_registry.map.ClientAction; delete core.action_registry.map.ClientAction2; }); QUnit.test("client action restore scrollbar (legacy)", async function (assert) { assert.expect(7); const ClientAction = AbstractAction.extend({ hasControlPanel: true, init(parent, action) { action.display_name = "Title1"; this._super.apply(this, arguments); }, async start() { for (let i = 0; i < 100; i++) { const content = document.createElement("div"); content.innerText = "Paper company"; content.className = "lorem"; this.el.querySelector(".o_content").appendChild(content); } await this._super(arguments); }, }); const ClientAction2 = AbstractAction.extend({ hasControlPanel: true, init(parent, action) { action.display_name = "Title2"; this._super.apply(this, arguments); }, start() { return this._super.apply(this, arguments); }, }); core.action_registry.add("ClientAction", ClientAction); core.action_registry.add("ClientAction2", ClientAction2); const webClient = await createWebClient({ serverData }); await doAction(webClient, "ClientAction"); assert.containsOnce(target, ".breadcrumb-item"); assert.strictEqual(target.querySelector(".breadcrumb-item.active").textContent, "Title1"); target.querySelector(".lorem:last-child").scrollIntoView(); const scrollPosition = target.querySelector(".o_content").scrollTop; assert.ok(scrollPosition > 0); await doAction(webClient, "ClientAction2", { clearBreadcrumbs: false }); assert.containsN(target, ".breadcrumb-item", 2); assert.strictEqual(target.querySelector(".breadcrumb-item.active").textContent, "Title2"); await click(target.querySelector(".breadcrumb-item:first-child")); assert.strictEqual(target.querySelector(".breadcrumb-item.active").textContent, "Title1"); assert.strictEqual( target.querySelector(".o_content").scrollTop, scrollPosition, "Should restore the scroll" ); delete core.action_registry.map.ClientAction; delete core.action_registry.map.ClientAction2; }); QUnit.test("ClientAction receives breadcrumbs and exports title (wowl)", async (assert) => { assert.expect(4); class ClientAction extends Component { setup() { this.breadcrumbTitle = "myOwlAction"; const { breadcrumbs } = this.env.config; assert.strictEqual(breadcrumbs.length, 2); assert.strictEqual(breadcrumbs[0].name, "Favorite Ponies"); owl.onMounted(() => { this.env.config.setDisplayName(this.breadcrumbTitle); }); } onClick() { this.breadcrumbTitle = "newOwlTitle"; this.env.config.setDisplayName(this.breadcrumbTitle); } } ClientAction.template = xml`
owl client action
`; actionRegistry.add("OwlClientAction", ClientAction); const webClient = await createWebClient({ serverData }); await doAction(webClient, 8); await doAction(webClient, "OwlClientAction"); assert.containsOnce(target, ".my_owl_action"); await click(target, ".my_owl_action"); await doAction(webClient, 3); assert.strictEqual( target.querySelector(".breadcrumb").textContent, "Favorite PoniesnewOwlTitlePartners" ); }); QUnit.test("ClientAction receives arbitrary props from doAction (wowl)", async (assert) => { assert.expect(1); class ClientAction extends Component { setup() { assert.strictEqual(this.props.division, "bell"); } } ClientAction.template = xml`
`; actionRegistry.add("OwlClientAction", ClientAction); const webClient = await createWebClient({ serverData }); await doAction(webClient, "OwlClientAction", { props: { division: "bell" }, }); }); QUnit.test("ClientAction receives arbitrary props from doAction (legacy)", async (assert) => { assert.expect(1); const ClientAction = AbstractAction.extend({ init(parent, action, options) { assert.strictEqual(options.division, "bell"); this._super.apply(this, arguments); }, }); core.action_registry.add("ClientAction", ClientAction); registerCleanup(() => delete core.action_registry.map.ClientAction); const webClient = await createWebClient({ serverData }); await doAction(webClient, "ClientAction", { props: { division: "bell" }, }); }); QUnit.test("test display_notification client action", async function (assert) { assert.expect(6); const webClient = await createWebClient({ serverData }); await doAction(webClient, 1); assert.containsOnce(target, ".o_kanban_view"); await doAction(webClient, { type: "ir.actions.client", tag: "display_notification", params: { title: "title", message: "message", sticky: true, }, }); const notificationSelector = ".o_notification_manager .o_notification"; assert.containsOnce( document.body, notificationSelector, "a notification should be present" ); const notificationElement = document.body.querySelector(notificationSelector); assert.strictEqual( notificationElement.querySelector(".o_notification_title").textContent, "title", "the notification should have the correct title" ); assert.strictEqual( notificationElement.querySelector(".o_notification_content").textContent, "message", "the notification should have the correct message" ); assert.containsOnce(target, ".o_kanban_view"); await testUtils.dom.click(notificationElement.querySelector(".o_notification_close")); assert.containsNone( document.body, notificationSelector, "the notification should be destroy " ); }); QUnit.test("test display_notification client action with links", async function (assert) { assert.expect(8); const webClient = await createWebClient({ serverData }); await doAction(webClient, 1); assert.containsOnce(target, ".o_kanban_view"); await doAction(webClient, { type: "ir.actions.client", tag: "display_notification", params: { title: "title", message: "message %s ", sticky: true, links: [ { label: "test ", url: "#action={action.id}&id={order.id}&model=purchase.order", }, ], }, }); const notificationSelector = ".o_notification_manager .o_notification"; assert.containsOnce( document.body, notificationSelector, "a notification should be present" ); let notificationElement = document.body.querySelector(notificationSelector); assert.strictEqual( notificationElement.querySelector(".o_notification_title").textContent, "title", "the notification should have the correct title" ); assert.strictEqual( notificationElement.querySelector(".o_notification_content").textContent, "message test ", "the notification should have the correct message" ); assert.containsOnce(target, ".o_kanban_view"); await testUtils.dom.click(notificationElement.querySelector(".o_notification_close")); assert.containsNone( document.body, notificationSelector, "the notification should be destroy " ); // display_notification without title await doAction(webClient, { type: "ir.actions.client", tag: "display_notification", params: { message: "message %s ", sticky: true, links: [ { label: "test ", url: "#action={action.id}&id={order.id}&model=purchase.order", }, ], }, }); assert.containsOnce( document.body, notificationSelector, "a notification should be present" ); notificationElement = document.body.querySelector(notificationSelector); assert.containsNone( notificationElement, ".o_notification_title", "the notification should not have title" ); }); QUnit.test("test next action on display_notification client action", async function (assert) { const webClient = await createWebClient({ serverData }); const options = { onClose: function () { assert.step("onClose"); }, }; await doAction( webClient, { type: "ir.actions.client", tag: "display_notification", params: { title: "title", message: "message", sticky: true, next: { type: "ir.actions.act_window_close", }, }, }, options ); const notificationSelector = ".o_notification_manager .o_notification"; assert.containsOnce( document.body, notificationSelector, "a notification should be present" ); assert.verifySteps(["onClose"]); }); });