Odoo18-Base/addons/web/static/tests/webclient/actions/legacy_tests.js
2025-03-10 11:12:23 +07:00

777 lines
28 KiB
JavaScript

/** @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": '<list><field name="foo"/></list>',
"partner,2,kanban": "<kanban></kanban>",
"partner,3,form": `<form></form>`,
"partner,false,search": "<search></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*/ `
<form>
<field name="display_name" />
<footer>
<button name="echoes" type="object" string="Echoes" help="echoes"/>
</footer>
</form>
`;
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*/ `
<form>
<header>
<button name="echoes" type="object" string="Echoes" help="echoes"/>
</header>
<field name="display_name" />
</form>
`;
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"] = `<div js_class="test_jsClass"></div>`;
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": `<form>
<header>
<button name="object" string="Call method" type="object" help="need somebody"/>
</header>
<field name="display_name"/>
</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": `<search />`,
"partner,99,list": `<list limit="4"><field name="display_name" /></list>`,
"partner,100,form": `<form><field name="display_name" /></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);
});
});