/** @odoo-module **/ import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; import { WebClient } from "@web/webclient/webclient"; import testUtils from "web.test_utils"; import core from "web.core"; import AbstractAction from "web.AbstractAction"; import { registerCleanup } from "../../helpers/cleanup"; import { makeTestEnv } from "../../helpers/mock_env"; import { click, getFixture, legacyExtraNextTick, patchWithCleanup, mount, nextTick, makeDeferred, editInput, } from "../../helpers/utils"; import { pagerNext, toggleFilterMenu, toggleMenuItem } from "@web/../tests/search/helpers"; import { session } from "@web/session"; import { createWebClient, doAction, getActionManagerServerData, loadState, setupWebClientRegistries, } from "./../helpers"; import { errorService } from "@web/core/errors/error_service"; 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("Load State"); QUnit.test("action loading", async (assert) => { assert.expect(2); const webClient = await createWebClient({ serverData }); await loadState(webClient, { action: 1001 }); assert.containsOnce(target, ".test_client_action"); assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "App1"); }); QUnit.test("menu loading", async (assert) => { assert.expect(2); const webClient = await createWebClient({ serverData }); await loadState(webClient, { menu_id: 2 }); assert.strictEqual( target.querySelector(".test_client_action").textContent.trim(), "ClientAction_Id 2" ); assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "App2"); }); QUnit.test("action and menu loading", async (assert) => { assert.expect(3); const webClient = await createWebClient({ serverData }); await loadState(webClient, { action: 1001, menu_id: 2, }); assert.strictEqual( target.querySelector(".test_client_action").textContent.trim(), "ClientAction_Id 1" ); assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "App2"); assert.deepEqual(webClient.env.services.router.current.hash, { action: 1001, menu_id: 2, }); }); QUnit.test("initial loading with action id", async (assert) => { assert.expect(4); const hash = "#action=1001"; Object.assign(browser.location, { hash }); setupWebClientRegistries(); const mockRPC = (route) => assert.step(route); const env = await makeTestEnv({ serverData, mockRPC }); assert.verifySteps(["/web/action/load", "/web/webclient/load_menus"]); await mount(WebClient, getFixture(), { env }); assert.verifySteps([]); }); QUnit.test("initial loading with action tag", async (assert) => { assert.expect(3); const hash = "#action=__test__client__action__"; Object.assign(browser.location, { hash }); setupWebClientRegistries(); const mockRPC = (route) => assert.step(route); const env = await makeTestEnv({ serverData, mockRPC }); assert.verifySteps(["/web/webclient/load_menus"]); await mount(WebClient, getFixture(), { env }); assert.verifySteps([]); }); QUnit.test("fallback on home action if no action found", async (assert) => { assert.expect(2); patchWithCleanup(session, { home_action_id: 1001 }); await createWebClient({ serverData }); await testUtils.nextTick(); // wait for the navbar to be updated assert.containsOnce(target, ".test_client_action"); assert.strictEqual(target.querySelector(".o_menu_brand").innerText, "App1"); }); QUnit.test("correctly sends additional context", async (assert) => { assert.expect(1); const hash = "#action=1001&active_id=4&active_ids=4,8"; Object.assign(browser.location, { hash }); function mockRPC(route, params) { if (route === "/web/action/load") { assert.deepEqual(params, { action_id: 1001, additional_context: { active_id: 4, active_ids: [4, 8], }, }); } } await createWebClient({ serverData, mockRPC }); }); QUnit.test("supports action as xmlId", async (assert) => { assert.expect(2); const webClient = await createWebClient({ serverData }); await loadState(webClient, { action: "wowl.client_action", }); assert.strictEqual( target.querySelector(".test_client_action").textContent.trim(), "ClientAction_xmlId" ); assert.containsNone(target, ".o_menu_brand"); }); QUnit.test("supports opening action in dialog", async (assert) => { assert.expect(3); serverData.actions["wowl.client_action"].target = "new"; const webClient = await createWebClient({ serverData }); await loadState(webClient, { action: "wowl.client_action", }); assert.containsOnce(target, ".test_client_action"); assert.containsOnce(target, ".modal .test_client_action"); assert.containsNone(target, ".o_menu_brand"); }); QUnit.test("should not crash on invalid state", async function (assert) { assert.expect(3); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; const webClient = await createWebClient({ serverData, mockRPC }); await loadState(webClient, { res_model: "partner", }); assert.strictEqual($(target).text(), "", "should display nothing"); assert.verifySteps(["/web/webclient/load_menus"]); }); QUnit.test("properly load client actions", async function (assert) { assert.expect(3); 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 }); webClient.env.bus.trigger("test:hashchange", { action: "HelloWorldTest", }); await testUtils.nextTick(); 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("properly load act window actions", async function (assert) { assert.expect(7); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; const webClient = await createWebClient({ serverData, mockRPC }); webClient.env.bus.trigger("test:hashchange", { action: 1, }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsOnce(target, ".o_control_panel"); assert.containsOnce(target, ".o_kanban_view"); assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", "get_views", "web_search_read", ]); }); QUnit.test("properly load records", async function (assert) { assert.expect(6); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; const webClient = await createWebClient({ serverData, mockRPC }); webClient.env.bus.trigger("test:hashchange", { id: 2, model: "partner", }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsOnce(target, ".o_form_view"); assert.strictEqual( $(target).find(".o_control_panel .breadcrumb-item").text(), "Second record", "should have opened the second record" ); assert.verifySteps(["/web/webclient/load_menus", "get_views", "read"]); }); QUnit.test("properly load records with existing first APP", async function (assert) { assert.expect(7); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; // simulate a real scenario with a first app (e.g. Discuss), to ensure that we don't // fallback on that first app when only a model and res_id are given in the url serverData.menus = { root: { id: "root", children: [1, 2], name: "root", appID: "root" }, 1: { id: 1, children: [], name: "App1", appID: 1, actionID: 1001, xmlid: "menu_1" }, 2: { id: 2, children: [], name: "App2", appID: 2, actionID: 1002, xmlid: "menu_2" }, }; const hash = "#id=2&model=partner"; Object.assign(browser.location, { hash }); await createWebClient({ serverData, mockRPC }); await testUtils.nextTick(); assert.containsOnce(target, ".o_form_view"); assert.strictEqual( $(target).find(".o_control_panel .breadcrumb-item").text(), "Second record", "should have opened the second record" ); assert.containsNone(target, ".o_menu_brand"); assert.verifySteps(["/web/webclient/load_menus", "get_views", "read"]); }); QUnit.test("properly load default record", async function (assert) { assert.expect(6); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; const webClient = await createWebClient({ serverData, mockRPC }); webClient.env.bus.trigger("test:hashchange", { action: 3, id: "", model: "partner", view_type: "form", }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsOnce(target, ".o_form_view"); assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", "get_views", "onchange", ]); }); QUnit.test("load requested view for act window actions", async function (assert) { assert.expect(7); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; const webClient = await createWebClient({ serverData, mockRPC }); webClient.env.bus.trigger("test:hashchange", { action: 3, view_type: "kanban", }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsNone(target, ".o_list_view"); assert.containsOnce(target, ".o_kanban_view"); assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", "get_views", "web_search_read", ]); }); QUnit.test( "lazy load multi record view if mono record one is requested", async function (assert) { assert.expect(12); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; const webClient = await createWebClient({ serverData, mockRPC }); webClient.env.bus.trigger("test:hashchange", { action: 3, id: 2, view_type: "form", }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsNone(target, ".o_list_view"); assert.containsOnce(target, ".o_form_view"); assert.containsN(target, ".o_control_panel .breadcrumb-item", 2); assert.strictEqual( $(target).find(".o_control_panel .breadcrumb-item:last").text(), "Second record", "breadcrumbs should contain the display_name of the opened record" ); // go back to List await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a")); await legacyExtraNextTick(); assert.containsOnce(target, ".o_list_view"); assert.containsNone(target, ".o_form_view"); assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", "get_views", "read", "web_search_read", ]); } ); QUnit.test("lazy load multi record view with previous action", async function (assert) { assert.expect(6); const webClient = await createWebClient({ serverData }); await doAction(webClient, 4); assert.containsOnce( target, ".o_control_panel .breadcrumb li", "there should be one controller in the breadcrumbs" ); assert.strictEqual( $(target).find(".o_control_panel .breadcrumb li").text(), "Partners Action 4", "breadcrumbs should contain the display_name of the opened record" ); await doAction(webClient, 3, { props: { resId: 2 }, viewType: "form", }); assert.containsN( target, ".o_control_panel .breadcrumb li", 3, "there should be three controllers in the breadcrumbs" ); assert.strictEqual( $(target).find(".o_control_panel .breadcrumb li").text(), "Partners Action 4PartnersSecond record", "the breadcrumb elements should be correctly ordered" ); // go back to List await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a:last")); await legacyExtraNextTick(); assert.containsN( target, ".o_control_panel .breadcrumb li", 2, "there should be two controllers in the breadcrumbs" ); assert.strictEqual( $(target).find(".o_control_panel .breadcrumb li").text(), "Partners Action 4Partners", "the breadcrumb elements should be correctly ordered" ); }); QUnit.test( "lazy loaded multi record view with failing mono record one", async function (assert) { assert.expect(3); const mockRPC = async function (route, args) { if (args && args.method === "read") { return Promise.reject(); } }; const webClient = await createWebClient({ serverData, mockRPC }); await loadState(webClient, { action: 3, id: 2, view_type: "form", }); assert.containsNone(target, ".o_form_view"); assert.containsNone(target, ".o_list_view"); await doAction(webClient, 1); assert.containsOnce(target, ".o_kanban_view"); } ); QUnit.test("change the viewType of the current action", async function (assert) { assert.expect(14); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; const webClient = await createWebClient({ serverData, mockRPC }); await doAction(webClient, 3); assert.containsOnce(target, ".o_list_view"); // switch to kanban view webClient.env.bus.trigger("test:hashchange", { action: 3, view_type: "kanban", }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsNone(target, ".o_list_view"); assert.containsOnce(target, ".o_kanban_view"); // switch to form view, open record 4 webClient.env.bus.trigger("test:hashchange", { action: 3, id: 4, view_type: "form", }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsNone(target, ".o_kanban_view"); assert.containsOnce(target, ".o_form_view"); assert.containsN( target, ".o_control_panel .breadcrumb-item", 2, "there should be two controllers in the breadcrumbs" ); assert.strictEqual( $(target).find(".o_control_panel .breadcrumb-item:last").text(), "Fourth record", "should have opened the requested record" ); // verify steps to ensure that the whole action hasn't been re-executed // (if it would have been, /web/action/load and get_views would appear // several times) assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", "get_views", "web_search_read", "web_search_read", "read", ]); }); QUnit.test("change the id of the current action", async function (assert) { assert.expect(12); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); }; const webClient = await createWebClient({ serverData, mockRPC }); // execute action 3 and open the first record in a form view await doAction(webClient, 3); await testUtils.dom.click($(target).find(".o_list_view .o_data_cell:first")); await legacyExtraNextTick(); assert.containsOnce(target, ".o_form_view"); assert.strictEqual( $(target).find(".o_control_panel .breadcrumb-item:last").text(), "First record", "should have opened the first record" ); // switch to record 4 webClient.env.bus.trigger("test:hashchange", { action: 3, id: 4, view_type: "form", }); await testUtils.nextTick(); await legacyExtraNextTick(); assert.containsOnce(target, ".o_form_view"); assert.containsN( target, ".o_control_panel .breadcrumb-item", 2, "there should be two controllers in the breadcrumbs" ); assert.strictEqual( $(target).find(".o_control_panel .breadcrumb-item:last").text(), "Fourth record", "should have switched to the requested record" ); // verify steps to ensure that the whole action hasn't been re-executed // (if it would have been, /web/action/load and get_views would appear // twice) assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", "get_views", "web_search_read", "read", "read", ]); }); QUnit.test("should push the correct state at the right time", async function (assert) { // formerly "should not push a loaded state" assert.expect(7); const pushState = browser.history.pushState; patchWithCleanup(browser, { history: Object.assign({}, browser.history, { pushState() { pushState(...arguments); assert.step("push_state"); }, }), }); const webClient = await createWebClient({ serverData }); let currentHash = webClient.env.services.router.current.hash; assert.deepEqual(currentHash, {}); await loadState(webClient, { action: 3 }); currentHash = webClient.env.services.router.current.hash; assert.deepEqual(currentHash, { action: 3, model: "partner", view_type: "list", }); assert.verifySteps(["push_state"], "should have pushed the final state"); await testUtils.dom.click($(target).find("tr .o_data_cell:first")); await legacyExtraNextTick(); currentHash = webClient.env.services.router.current.hash; assert.deepEqual(currentHash, { action: 3, id: 1, model: "partner", view_type: "form", }); assert.verifySteps(["push_state"], "should push the state of it changes afterwards"); }); QUnit.test("should not push a loaded state of a legacy client action", async function (assert) { assert.expect(6); const ClientAction = AbstractAction.extend({ init: function (parent, action, options) { this._super.apply(this, arguments); this.controllerID = options.controllerID; }, start: function () { const $button = $("