/** @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 = $("