vanilla 17.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:47:08 +02:00
parent d72e748793
commit a9bcec8e91
1986 changed files with 1613876 additions and 568976 deletions

View file

@ -2,20 +2,11 @@
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 testUtils from "@web/../tests/legacy/helpers/test_utils";
import { click, getFixture, nextTick, patchWithCleanup } from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
import { Component, xml } from "@odoo/owl";
import { Component, onMounted, xml } from "@odoo/owl";
let serverData;
let target;
@ -106,41 +97,39 @@ QUnit.module("ActionManager", (hooks) => {
assert.verifySteps(["web_search_read"]);
});
QUnit.test("soft_reload a form view", async (assert) => {
const mockRPC = async function (route, { args }) {
if (route === "/web/dataset/call_kw/partner/web_read") {
assert.step(`read ${args[0][0]}`);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, {
name: "Partners",
res_model: "partner",
views: [
[false, "list"],
[false, "form"],
],
type: "ir.actions.act_window",
});
await click(target.querySelector(".o_data_row .o_data_cell"));
await click(target, ".o_form_view .o_pager_next");
assert.verifySteps([
"read 1",
"read 2",
])
await doAction(webClient, "soft_reload");
assert.verifySteps(["read 2"]);
});
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 {}
@ -196,234 +185,7 @@ QUnit.module("ActionManager", (hooks) => {
}
);
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`
<div class="custom-control-panel">My custom control panel</div>
`;
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) => {
QUnit.test("ClientAction receives breadcrumbs and exports title", async (assert) => {
assert.expect(4);
class ClientAction extends Component {
setup() {
@ -431,7 +193,7 @@ QUnit.module("ActionManager", (hooks) => {
const { breadcrumbs } = this.env.config;
assert.strictEqual(breadcrumbs.length, 2);
assert.strictEqual(breadcrumbs[0].name, "Favorite Ponies");
owl.onMounted(() => {
onMounted(() => {
this.env.config.setDisplayName(this.breadcrumbTitle);
});
}
@ -449,12 +211,12 @@ QUnit.module("ActionManager", (hooks) => {
await click(target, ".my_owl_action");
await doAction(webClient, 3);
assert.strictEqual(
target.querySelector(".breadcrumb").textContent,
target.querySelector(".o_breadcrumb").textContent,
"Favorite PoniesnewOwlTitlePartners"
);
});
QUnit.test("ClientAction receives arbitrary props from doAction (wowl)", async (assert) => {
QUnit.test("ClientAction receives arbitrary props from doAction", async (assert) => {
assert.expect(1);
class ClientAction extends Component {
setup() {
@ -469,22 +231,6 @@ QUnit.module("ActionManager", (hooks) => {
});
});
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 });
@ -499,6 +245,7 @@ QUnit.module("ActionManager", (hooks) => {
sticky: true,
},
});
await nextTick(); // wait for the notification to be displayed
const notificationSelector = ".o_notification_manager .o_notification";
assert.containsOnce(
document.body,
@ -545,6 +292,7 @@ QUnit.module("ActionManager", (hooks) => {
],
},
});
await nextTick(); // wait for the notification to be displayed
const notificationSelector = ".o_notification_manager .o_notification";
assert.containsOnce(
document.body,
@ -585,6 +333,7 @@ QUnit.module("ActionManager", (hooks) => {
],
},
});
await nextTick(); // wait for the notification to be displayed
assert.containsOnce(
document.body,
notificationSelector,
@ -621,6 +370,7 @@ QUnit.module("ActionManager", (hooks) => {
},
options
);
await nextTick(); // wait for the notification to be displayed
const notificationSelector = ".o_notification_manager .o_notification";
assert.containsOnce(
document.body,
@ -629,4 +379,49 @@ QUnit.module("ActionManager", (hooks) => {
);
assert.verifySteps(["onClose"]);
});
QUnit.test("test reload client action", async function (assert) {
patchWithCleanup(browser.location, {
assign: (url) => {
assert.step(url);
},
origin: "",
hash: "#test=42",
});
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
type: "ir.actions.client",
tag: "reload",
});
await doAction(webClient, {
type: "ir.actions.client",
tag: "reload",
params: {
action_id: 2,
},
});
await doAction(webClient, {
type: "ir.actions.client",
tag: "reload",
params: {
menu_id: 1,
},
});
await doAction(webClient, {
type: "ir.actions.client",
tag: "reload",
params: {
action_id: 1,
menu_id: 2,
},
});
assert.verifySteps([
"/web/tests?reload=true#test=42",
"/web/tests?reload=true#action=2",
"/web/tests?reload=true#menu_id=1",
"/web/tests?reload=true#menu_id=2&action=1",
]);
});
});

View file

@ -1,17 +1,9 @@
/** @odoo-module **/
import testUtils from "web.test_utils";
import { registerCleanup } from "../../helpers/cleanup";
import {
click,
getFixture,
legacyExtraNextTick,
nextTick,
patchWithCleanup,
} from "../../helpers/utils";
import testUtils from "@web/../tests/legacy/helpers/test_utils";
import { click, getFixture, nextTick, patchWithCleanup } from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
import { registry } from "@web/core/registry";
import { formView } from "@web/views/form/form_view";
import { listView } from "../../../src/views/list/list_view";
@ -50,9 +42,9 @@ QUnit.module("ActionManager", (hooks) => {
assert.step("on_close");
}
await doAction(webClient, 5, { onClose });
assert.containsOnce(target, ".o_dialog_container .o_dialog");
await click(target.querySelector(".o_dialog_container .o_dialog .modal-header button"));
assert.containsNone(target, ".o_dialog_container .o_dialog");
assert.containsOnce(target, ".o_dialog");
await click(target.querySelector(".o_dialog .modal-header button"));
assert.containsNone(target, ".o_dialog");
assert.verifySteps(["on_close"]);
// execute an 'ir.actions.act_window_close' action
@ -107,7 +99,7 @@ QUnit.module("ActionManager", (hooks) => {
let form;
patchWithCleanup(formView.Controller.prototype, {
setup() {
this._super(...arguments);
super.setup(...arguments);
form = this;
},
});
@ -129,7 +121,7 @@ QUnit.module("ActionManager", (hooks) => {
let list;
patchWithCleanup(listView.Controller.prototype, {
setup() {
this._super(...arguments);
super.setup(...arguments);
list = this;
},
});
@ -149,7 +141,6 @@ QUnit.module("ActionManager", (hooks) => {
await click(target, ".modal-header button.btn-close");
await nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".modal");
assert.containsNone(target, ".o_list_view");
assert.containsOnce(target, ".o_kanban_view");
@ -163,7 +154,7 @@ QUnit.module("ActionManager", (hooks) => {
let list;
patchWithCleanup(listView.Controller.prototype, {
setup() {
this._super(...arguments);
super.setup(...arguments);
list = this;
},
});
@ -181,30 +172,18 @@ QUnit.module("ActionManager", (hooks) => {
list.env.config.historyBack();
assert.verifySteps(["on_close"], "should have called the on_close handler");
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_list_view");
assert.containsNone(target, ".modal");
}
);
QUnit.test("web client is not deadlocked when a view crashes", async function (assert) {
assert.expect(6);
const handler = (ev) => {
assert.step("error");
// need to preventDefault to remove error from console (so python test pass)
ev.preventDefault();
};
// fake error service so that the odoo qunit handlers don't think that they need to handle the error
registry.category("services").add("error", { start: () => {} });
window.addEventListener("unhandledrejection", handler);
registerCleanup(() => window.removeEventListener("unhandledrejection", handler));
patchWithCleanup(QUnit, {
onUnhandledRejection: () => {},
});
assert.expect(4);
assert.expectErrors();
const readOnFirstRecordDef = testUtils.makeTestPromise();
const mockRPC = (route, args) => {
if (args.method === "read" && args.args[0][0] === 1) {
const mockRPC = (route, { method, args, kwargs }) => {
if (method === "web_read" && args[0][0] === 1) {
return readOnFirstRecordDef;
}
};
@ -213,17 +192,15 @@ QUnit.module("ActionManager", (hooks) => {
// open first record in form view. this will crash and will not
// display a form view
await testUtils.dom.click($(target).find(".o_list_view .o_data_cell:first"));
assert.verifySteps([]);
await legacyExtraNextTick();
readOnFirstRecordDef.reject(new Error("not working as intended"));
await nextTick();
assert.verifySteps(["error"]);
assert.verifyErrors(["not working as intended"]);
assert.containsOnce(target, ".o_list_view", "there should still be a list view in dom");
// open another record, the read will not crash
await testUtils.dom.click(
$(target).find(".o_list_view .o_data_row:eq(2) .o_data_cell:first")
);
await legacyExtraNextTick();
assert.containsNone(target, ".o_list_view", "there should not be a list view in dom");
assert.containsOnce(target, ".o_form_view", "there should be a form view in dom");
});

View file

@ -3,14 +3,15 @@
import {
click,
getFixture,
legacyExtraNextTick,
getNodesTextContent,
makeDeferred,
nextTick,
} from "@web/../tests/helpers/utils";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { SearchBar } from "@web/search/search_bar/search_bar";
import {
isItemSelected,
toggleFilterMenu,
toggleSearchBarMenu,
toggleMenuItem,
switchView,
} from "@web/../tests/search/helpers";
@ -23,9 +24,13 @@ import {
loadState,
} from "@web/../tests/webclient/helpers";
import { Component, xml } from "@odoo/owl";
import { Component, onWillStart, xml } from "@odoo/owl";
const actionRegistry = registry.category("actions");
function getBreadCrumbTexts(target) {
return getNodesTextContent(target.querySelectorAll(".breadcrumb-item, .o_breadcrumb .active"));
}
let serverData;
let target;
@ -109,19 +114,14 @@ QUnit.module("ActionManager", (hooks) => {
doAction(webClient, 4);
def.resolve();
await nextTick();
await legacyExtraNextTick();
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item.active").text(),
"Partners Action 4",
"action 4 should be loaded"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 4"]);
});
QUnit.test("clicking quickly on breadcrumbs...", async function (assert) {
assert.expect(1);
let def;
const mockRPC = async function (route, args) {
if (args && args.method === "read") {
if (args && args.method === "web_read") {
await def;
}
};
@ -140,11 +140,7 @@ QUnit.module("ActionManager", (hooks) => {
// resolve the form view read
def.resolve();
await nextTick();
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item.active").text(),
"Partners Action 4",
"action 4 should be loaded and visible"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 4"]);
});
QUnit.test(
@ -181,7 +177,7 @@ QUnit.module("ActionManager", (hooks) => {
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"read",
"web_read",
"web_search_read",
"/web/action/load",
"get_views",
@ -231,7 +227,7 @@ QUnit.module("ActionManager", (hooks) => {
"/web/action/load",
"get_views",
"web_search_read",
"read",
"web_read",
"object",
"/web/action/load",
"get_views",
@ -264,7 +260,7 @@ QUnit.module("ActionManager", (hooks) => {
let def;
const mockRPC = async function (route, args) {
assert.step((args && args.method) || route);
if (args && args.method === "read") {
if (args && args.method === "web_read") {
await def;
}
};
@ -292,7 +288,7 @@ QUnit.module("ActionManager", (hooks) => {
"/web/action/load",
"get_views",
"web_search_read",
"read",
"web_read",
"/web/action/load",
"get_views",
"web_search_read",
@ -335,11 +331,7 @@ QUnit.module("ActionManager", (hooks) => {
await nextTick();
assert.containsOnce(target, ".o_kanban_view", "should display the kanban view of action 4");
assert.containsNone(target, ".o_list_view", "should not display the list view of action 3");
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should be one controller in the breadcrumbs"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 4"]);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
@ -370,11 +362,7 @@ QUnit.module("ActionManager", (hooks) => {
await nextTick();
assert.containsOnce(target, ".o_kanban_view", "should display the kanban view of action 4");
assert.containsNone(target, ".o_list_view", "should not display the list view of action 3");
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should be one controller in the breadcrumbs"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 4"]);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
@ -389,8 +377,8 @@ QUnit.module("ActionManager", (hooks) => {
QUnit.test("open a record while reloading the list view", async function (assert) {
assert.expect(10);
let def;
const mockRPC = async function (route) {
if (route === "/web/dataset/search_read") {
const mockRPC = async function (route, args) {
if (args.method === "web_search_read") {
await def;
}
};
@ -398,12 +386,12 @@ QUnit.module("ActionManager", (hooks) => {
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
assert.containsN(target, ".o_list_view .o_data_row", 5);
assert.containsOnce(target, ".o_control_panel .o_list_buttons");
assert.containsOnce(target, ".o_control_panel .d-none.d-xl-inline-flex .o_list_buttons");
// reload (the search_read RPC will be blocked)
def = makeDeferred();
await switchView(target, "list");
assert.containsN(target, ".o_list_view .o_data_row", 5);
assert.containsOnce(target, ".o_control_panel .o_list_buttons");
assert.containsOnce(target, ".o_control_panel .d-none.d-xl-inline-flex .o_list_buttons");
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
@ -423,7 +411,7 @@ QUnit.module("ActionManager", (hooks) => {
const slowWillStartDef = makeDeferred();
class ClientAction extends Component {
setup() {
owl.onWillStart(() => slowWillStartDef);
onWillStart(() => slowWillStartDef);
}
}
ClientAction.template = xml`<div class="client_action">ClientAction</div>`;
@ -431,15 +419,12 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData });
doAction(webClient, "slowAction");
await nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".client_action", "client action isn't ready yet");
doAction(webClient, 4);
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_kanban_view", "should have loaded a kanban view");
slowWillStartDef.resolve();
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_kanban_view", "should still display the kanban view");
}
);
@ -468,17 +453,14 @@ QUnit.module("ActionManager", (hooks) => {
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_list_view");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb-item").textContent,
"Partners"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners"]);
assert.containsNone(target, ".o_form_view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"read",
"web_read",
"/web/action/load",
"web_search_read",
]);
@ -505,10 +487,7 @@ QUnit.module("ActionManager", (hooks) => {
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_kanban_view");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb-item").textContent,
"Partners"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners"]);
assert.containsNone(target, ".o_list_view");
assert.verifySteps([
"/web/webclient/load_menus",
@ -533,17 +512,14 @@ QUnit.module("ActionManager", (hooks) => {
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
def = makeDeferred();
doAction(webClient, 4, { clearBreadcrumbs: true });
doAction(webClient, 4);
await nextTick();
assert.containsOnce(target, ".o_list_view", "should still contain the list view");
await switchView(target, "kanban");
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_kanban_view");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb-item").textContent,
"Partners"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners"]);
assert.containsNone(target, ".o_list_view");
assert.verifySteps([
"/web/webclient/load_menus",
@ -569,16 +545,13 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
doAction(webClient, 4, { clearBreadcrumbs: true });
doAction(webClient, 4);
await nextTick();
await switchView(target, "kanban");
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_kanban_view");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb-item").textContent,
"Partners"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners"]);
assert.containsNone(target, ".o_list_view");
assert.verifySteps([
"/web/webclient/load_menus",
@ -596,7 +569,7 @@ QUnit.module("ActionManager", (hooks) => {
const def = makeDeferred();
const defs = [null, def];
const mockRPC = async (route, args) => {
if (args.method === "read") {
if (args.method === "web_read") {
await Promise.resolve(defs.shift());
}
};
@ -616,18 +589,12 @@ QUnit.module("ActionManager", (hooks) => {
await click(row1.querySelector(".o_data_cell"));
await click(row2.querySelector(".o_data_cell"));
assert.containsOnce(target, ".o_form_view");
assert.strictEqual(
target.querySelector(".breadcrumb-item.active").innerText,
"Second record"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners", "Second record"]);
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_form_view");
assert.strictEqual(
target.querySelector(".breadcrumb-item.active").innerText,
"Second record"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners", "Second record"]);
});
QUnit.test(
@ -679,21 +646,21 @@ QUnit.module("ActionManager", (hooks) => {
return { fromId: this.id };
},
});
owl.onWillStart(() => def);
onWillStart(() => def);
}
}
ToyController.template = xml`
<div class="o_toy_view">
<ControlPanel />
<SearchBar />
</div>`;
ToyController.components = { ControlPanel };
ToyController.components = { ControlPanel, SearchBar };
registry.category("views").add("toy", {
type: "toy",
display_name: "Toy",
icon: "fab fa-android",
multiRecord: true,
searchMenuTypes: ["filter"],
Controller: ToyController,
});
@ -709,7 +676,7 @@ QUnit.module("ActionManager", (hooks) => {
],
});
await toggleFilterMenu(target);
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "Foo");
assert.ok(isItemSelected(target, "Foo"));
@ -721,7 +688,7 @@ QUnit.module("ActionManager", (hooks) => {
def.resolve();
await nextTick();
await toggleFilterMenu(target);
await toggleSearchBarMenu(target);
assert.ok(isItemSelected(target, "Foo"));
// this test is not able to detect that getGlobalState is put on the right place:
// currentController.action.globalState contains in any case the search state

View file

@ -1,15 +1,9 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import testUtils from "web.test_utils";
import testUtils from "@web/../tests/legacy/helpers/test_utils";
import { clearRegistryWithCleanup } from "../../helpers/mock_env";
import {
click,
getFixture,
legacyExtraNextTick,
nextTick,
patchWithCleanup,
} from "../../helpers/utils";
import { click, getFixture, nextTick, patchWithCleanup } from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
import { session } from "@web/session";
@ -37,16 +31,13 @@ QUnit.module("ActionManager", (hooks) => {
assert.containsNone(target, ".o_reward");
webClient.env.services.effect.add({ type: "rainbow_man", message: "", fadeout: "no" });
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_reward");
assert.containsOnce(target, ".o_kanban_view");
await testUtils.dom.click(target.querySelector(".o_kanban_record"));
await legacyExtraNextTick();
assert.containsNone(target, ".o_reward");
assert.containsOnce(target, ".o_kanban_view");
webClient.env.services.effect.add({ type: "rainbow_man", message: "", fadeout: "no" });
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_reward");
assert.containsOnce(target, ".o_kanban_view");
// Do not force rainbow man to destroy on doAction
@ -100,7 +91,6 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 6);
await click(target.querySelector('button[name="object"]'));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_reward");
assert.strictEqual(
target.querySelector(".o_reward .o_reward_msg_content").textContent,

View file

@ -2,9 +2,9 @@
import { registry } from "@web/core/registry";
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
import { registerCleanup } from "../../helpers/cleanup";
import { click, getFixture, nextTick, patchWithCleanup } from "../../helpers/utils";
import { click, getFixture, nextTick } from "../../helpers/utils";
import { errorService } from "@web/core/errors/error_service";
import { ConnectionLostError } from "@web/core/network/rpc_service";
import { Component, xml } from "@odoo/owl";
@ -21,42 +21,52 @@ QUnit.module("ActionManager", (hooks) => {
QUnit.module("Error handling");
QUnit.test("error in a client action (at rendering)", async function (assert) {
assert.expect(4);
assert.expect(11);
class Boom extends Component {}
Boom.template = xml`<div><t t-esc="a.b.c"/></div>`;
actionRegistry.add("Boom", Boom);
const webClient = await createWebClient({ serverData });
assert.strictEqual(target.querySelector(".o_action_manager").innerHTML, "");
const mockRPC = (route, args) => {
if (args.method === "web_search_read") {
assert.step("web_search_read");
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, "1");
const contents = target.querySelector(".o_action_manager").innerHTML;
assert.ok(contents !== "");
assert.containsOnce(target, ".o_kanban_view");
assert.strictEqual(target.querySelector(".o_breadcrumb").textContent, "Partners Action 1");
assert.deepEqual(
[...target.querySelectorAll(".o_kanban_record span")].map((el) => el.textContent),
["yop", "blip", "gnap", "plop", "zoup"]
);
assert.verifySteps(["web_search_read"]);
try {
await doAction(webClient, "Boom");
} catch (e) {
assert.ok(e.cause instanceof TypeError);
}
assert.strictEqual(target.querySelector(".o_action_manager").innerHTML, contents);
await nextTick();
assert.containsOnce(target, ".o_kanban_view");
assert.strictEqual(target.querySelector(".o_breadcrumb").textContent, "Partners Action 1");
assert.deepEqual(
[...target.querySelectorAll(".o_kanban_record span")].map((el) => el.textContent),
["yop", "blip", "gnap", "plop", "zoup"]
);
assert.verifySteps(["web_search_read"]);
});
QUnit.test("error in a client action (after the first rendering)", async function (assert) {
const handler = (ev) => {
// need to preventDefault to remove error from console (so python test pass)
ev.preventDefault();
};
window.addEventListener("unhandledrejection", handler);
registerCleanup(() => window.removeEventListener("unhandledrejection", handler));
patchWithCleanup(QUnit, {
onUnhandledRejection: () => {},
});
assert.expectErrors();
registry.category("services").add("error", errorService);
class Boom extends Component {
setup() {
this.boom = false;
}
get a() {
// a bit artificial, but makes the test firefox compliant
throw new Error("Cannot read properties of undefined (reading 'b')");
}
onClick() {
this.boom = true;
this.render();
@ -76,6 +86,42 @@ QUnit.module("ActionManager", (hooks) => {
await click(document.querySelector(".my_button"));
await nextTick();
assert.containsOnce(target, ".my_button");
assert.containsOnce(target, ".o_dialog_error");
assert.containsOnce(target, ".o_error_dialog");
assert.verifyErrors(["Cannot read properties of undefined (reading 'b')"]);
});
QUnit.test("connection lost when opening form view from kanban", async function (assert) {
assert.expectErrors();
registry.category("services").add("error", errorService);
let offline = false;
const mockRPC = (route, { method }) => {
assert.step(method || route);
if (offline) {
throw new ConnectionLostError(route);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
offline = true;
await click(target.querySelector(".o_data_cell"));
assert.containsOnce(target, ".o_list_view");
assert.containsOnce(target, ".o_notification");
assert.strictEqual(
target.querySelector(".o_notification").innerText,
"Connection lost. Trying to reconnect..."
);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"web_read",
"web_search_read",
]);
await nextTick();
assert.verifySteps([]); // doesn't indefinitely try to reload the list
});
});

View file

@ -1,776 +0,0 @@
/** @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);
});
});

View file

@ -3,22 +3,23 @@
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,
getNodesTextContent,
} from "../../helpers/utils";
import { pagerNext, toggleFilterMenu, toggleMenuItem } from "@web/../tests/search/helpers";
import {
pagerNext,
switchView,
toggleMenuItem,
toggleSearchBarMenu,
} from "@web/../tests/search/helpers";
import { session } from "@web/session";
import {
createWebClient,
@ -29,7 +30,11 @@ import {
} from "./../helpers";
import { errorService } from "@web/core/errors/error_service";
import { Component, xml } from "@odoo/owl";
import { Component, onMounted, xml } from "@odoo/owl";
function getBreadCrumbTexts(target) {
return getNodesTextContent(target.querySelectorAll(".breadcrumb-item, .o_breadcrumb .active"));
}
let serverData;
let target;
@ -118,7 +123,8 @@ QUnit.module("ActionManager", (hooks) => {
patchWithCleanup(session, { home_action_id: 1001 });
await createWebClient({ serverData });
await testUtils.nextTick(); // wait for the navbar to be updated
await nextTick(); // wait for the navbar to be updated
await nextTick(); // wait for the action to be displayed
assert.containsOnce(target, ".test_client_action");
assert.strictEqual(target.querySelector(".o_menu_brand").innerText, "App1");
@ -169,8 +175,8 @@ QUnit.module("ActionManager", (hooks) => {
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 mockRPC = async function (route, { method }) {
assert.step(method || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await loadState(webClient, {
@ -185,14 +191,17 @@ QUnit.module("ActionManager", (hooks) => {
class ClientAction extends Component {}
ClientAction.template = xml`<div class="o_client_action_test">Hello World</div>`;
actionRegistry.add("HelloWorldTest", ClientAction);
const mockRPC = async function (route, args) {
assert.step((args && args.method) || route);
const mockRPC = async function (route, { method }) {
assert.step(method || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
webClient.env.bus.trigger("test:hashchange", {
action: "HelloWorldTest",
});
await testUtils.nextTick();
await new Promise((r) => setTimeout(r)); // real "hashchange" event is triggered after a setTimeout [1]
// [1] https://github.com/odoo/odoo/blob/1882d8f89f760bd1ff8a2bf0ae798939402647a3/addons/web/static/tests/setup.js#L52
await nextTick();
await nextTick();
assert.strictEqual(
$(target).find(".o_client_action_test").text(),
"Hello World",
@ -203,15 +212,16 @@ QUnit.module("ActionManager", (hooks) => {
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 mockRPC = async function (route, { method }) {
assert.step(method || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
webClient.env.bus.trigger("test:hashchange", {
action: 1,
});
await testUtils.nextTick();
await legacyExtraNextTick();
await new Promise((r) => setTimeout(r)); // real "hashchange" event is triggered after a setTimeout [1]
await nextTick();
await nextTick();
assert.containsOnce(target, ".o_control_panel");
assert.containsOnce(target, ".o_kanban_view");
assert.verifySteps([
@ -224,29 +234,26 @@ QUnit.module("ActionManager", (hooks) => {
QUnit.test("properly load records", async function (assert) {
assert.expect(6);
const mockRPC = async function (route, args) {
assert.step((args && args.method) || route);
const mockRPC = async function (route, { method }) {
assert.step(method || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
webClient.env.bus.trigger("test:hashchange", {
id: 2,
model: "partner",
});
await testUtils.nextTick();
await legacyExtraNextTick();
await new Promise((r) => setTimeout(r)); // real "hashchange" event is triggered after a setTimeout [1]
await nextTick();
await 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.verifySteps(["/web/webclient/load_menus", "get_views", "read"]);
assert.deepEqual(getBreadCrumbTexts(target), ["Second record"]);
assert.verifySteps(["/web/webclient/load_menus", "get_views", "web_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);
const mockRPC = async function (route, { method }) {
assert.step(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
@ -259,21 +266,17 @@ QUnit.module("ActionManager", (hooks) => {
Object.assign(browser.location, { hash });
await createWebClient({ serverData, mockRPC });
await testUtils.nextTick();
await 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.deepEqual(getBreadCrumbTexts(target), ["Second record"]);
assert.containsNone(target, ".o_menu_brand");
assert.verifySteps(["/web/webclient/load_menus", "get_views", "read"]);
assert.verifySteps(["/web/webclient/load_menus", "get_views", "web_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 mockRPC = async function (route, { method }) {
assert.step(method || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
webClient.env.bus.trigger("test:hashchange", {
@ -282,8 +285,9 @@ QUnit.module("ActionManager", (hooks) => {
model: "partner",
view_type: "form",
});
await testUtils.nextTick();
await legacyExtraNextTick();
await new Promise((r) => setTimeout(r)); // real "hashchange" event is triggered after a setTimeout [1]
await nextTick();
await nextTick();
assert.containsOnce(target, ".o_form_view");
assert.verifySteps([
"/web/webclient/load_menus",
@ -295,16 +299,17 @@ QUnit.module("ActionManager", (hooks) => {
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 mockRPC = async function (route, { method }) {
assert.step(method || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
webClient.env.bus.trigger("test:hashchange", {
action: 3,
view_type: "kanban",
});
await testUtils.nextTick();
await legacyExtraNextTick();
await new Promise((r) => setTimeout(r)); // real "hashchange" event is triggered after a setTimeout [1]
await nextTick();
await nextTick();
assert.containsNone(target, ".o_list_view");
assert.containsOnce(target, ".o_kanban_view");
assert.verifySteps([
@ -318,9 +323,13 @@ QUnit.module("ActionManager", (hooks) => {
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);
assert.expect(11);
const mockRPC = async function (route, { method, kwargs }) {
if (method === "unity_read") {
assert.step(`unity_read ${kwargs.method}`);
} else {
assert.step(method || route);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
webClient.env.bus.trigger("test:hashchange", {
@ -328,82 +337,49 @@ QUnit.module("ActionManager", (hooks) => {
id: 2,
view_type: "form",
});
await testUtils.nextTick();
await legacyExtraNextTick();
await nextTick();
await nextTick();
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"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners", "Second record"]);
// go back to List
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
await click(target.querySelector(".o_control_panel .breadcrumb a"));
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_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"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 4"]);
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"
);
assert.deepEqual(getBreadCrumbTexts(target), [
"Partners Action 4",
"Partners",
"Second record",
]);
// 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"
);
await click(target.querySelector(".o_control_panel .breadcrumb .o_back_button a"));
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 4", "Partners"]);
});
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") {
const mockRPC = async function (route, { method, kwargs }) {
if (method === "web_read") {
return Promise.reject();
}
};
@ -421,9 +397,9 @@ QUnit.module("ActionManager", (hooks) => {
);
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);
assert.expect(13);
const mockRPC = async function (route, { method }) {
assert.step(method || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
@ -433,8 +409,9 @@ QUnit.module("ActionManager", (hooks) => {
action: 3,
view_type: "kanban",
});
await testUtils.nextTick();
await legacyExtraNextTick();
await new Promise((r) => setTimeout(r)); // real "hashchange" event is triggered after a setTimeout [1]
await nextTick();
await nextTick();
assert.containsNone(target, ".o_list_view");
assert.containsOnce(target, ".o_kanban_view");
// switch to form view, open record 4
@ -443,21 +420,12 @@ QUnit.module("ActionManager", (hooks) => {
id: 4,
view_type: "form",
});
await testUtils.nextTick();
await legacyExtraNextTick();
await new Promise((r) => setTimeout(r)); // real "hashchange" event is triggered after a setTimeout [1]
await nextTick();
await nextTick();
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"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners", "Fourth 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)
@ -467,46 +435,32 @@ QUnit.module("ActionManager", (hooks) => {
"get_views",
"web_search_read",
"web_search_read",
"read",
"web_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);
assert.expect(11);
const mockRPC = async function (route, { method }) {
assert.step(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();
await click(target.querySelector(".o_list_view .o_data_cell"));
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"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners", "First record"]);
// switch to record 4
webClient.env.bus.trigger("test:hashchange", {
action: 3,
id: 4,
view_type: "form",
});
await testUtils.nextTick();
await legacyExtraNextTick();
await new Promise((r) => setTimeout(r)); // real "hashchange" event is triggered after a setTimeout [1]
await nextTick();
await nextTick();
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"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners", "Fourth 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)
@ -515,8 +469,8 @@ QUnit.module("ActionManager", (hooks) => {
"/web/action/load",
"get_views",
"web_search_read",
"read",
"read",
"web_read",
"web_read",
]);
});
@ -543,8 +497,8 @@ QUnit.module("ActionManager", (hooks) => {
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();
await click(target.querySelector("tr .o_data_cell"));
await nextTick();
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: 3,
@ -555,164 +509,30 @@ QUnit.module("ActionManager", (hooks) => {
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 = $("<button id='client_action_button'>").text("Click Me!");
$button.on("click", () => {
this.trigger_up("push_state", {
controllerID: this.controllerID,
state: { someValue: "X" },
});
});
this.$el.append($button);
return this._super.apply(this, arguments);
},
});
const pushState = browser.history.pushState;
patchWithCleanup(browser, {
history: Object.assign({}, browser.history, {
pushState() {
pushState(...arguments);
assert.step("push_state");
},
}),
});
core.action_registry.add("ClientAction", ClientAction);
const webClient = await createWebClient({ serverData });
let currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {});
await loadState(webClient, { action: 9 });
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: 9,
});
assert.verifySteps([], "should not push the loaded state");
await testUtils.dom.click($(target).find("#client_action_button"));
await legacyExtraNextTick();
assert.verifySteps(["push_state"], "should push the state of it changes afterwards");
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: 9,
someValue: "X",
});
delete core.action_registry.map.ClientAction;
});
QUnit.test("change a param of an ir.actions.client in the url", async function (assert) {
assert.expect(12);
const ClientAction = AbstractAction.extend({
hasControlPanel: true,
init: function (parent, action) {
this._super.apply(this, arguments);
const context = action.context;
this.a = (context.params && context.params.a) || "default value";
},
start: function () {
assert.step("start");
this.$(".o_content").text(this.a);
this.$el.addClass("o_client_action");
this.trigger_up("push_state", {
controllerID: this.controllerID,
state: { a: this.a },
});
return this._super.apply(this, arguments);
},
});
const pushState = browser.history.pushState;
patchWithCleanup(browser, {
history: Object.assign({}, browser.history, {
pushState() {
pushState(...arguments);
assert.step("push_state");
},
}),
});
core.action_registry.add("ClientAction", ClientAction);
const webClient = await createWebClient({ serverData });
let currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {});
// execute the client action
await doAction(webClient, 9);
assert.verifySteps(["start", "push_state"]);
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: 9,
a: "default value",
});
assert.strictEqual(
$(target).find(".o_client_action .o_content").text(),
"default value",
"should have rendered the client action"
);
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
1,
"there should be one controller in the breadcrumbs"
);
// update param 'a' in the url
await loadState(webClient, {
action: 9,
a: "new value",
});
assert.verifySteps(["start"]); // No push state since the hash hasn't changed
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: 9,
a: "new value",
});
assert.strictEqual(
$(target).find(".o_client_action .o_content").text(),
"new value",
"should have rerendered the client action with the correct param"
);
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
1,
"there should still be one controller in the breadcrumbs"
);
delete core.action_registry.map.ClientAction;
});
QUnit.test("load a window action without id (in a multi-record view)", async function (assert) {
assert.expect(14);
patchWithCleanup(browser.sessionStorage, {
getItem(k) {
assert.step(`getItem session ${k}`);
return this._super(k);
return super.getItem(k);
},
setItem(k, v) {
assert.step(`setItem session ${k}`);
return this._super(k, v);
return super.setItem(k, v);
},
});
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
const mockRPC = async (route, { method, kwargs }) => {
assert.step(method || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 4);
assert.containsOnce(target, ".o_kanban_view", "should display a kanban view");
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners Action 4",
"breadcrumbs should display the display_name of the action"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 4"]);
await loadState(webClient, {
model: "partner",
view_type: "list",
});
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners Action 4",
"should still be in the same action"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 4"]);
assert.containsNone(target, ".o_kanban_view", "should no longer display a kanban view");
assert.containsOnce(target, ".o_list_view", "should display a list view");
assert.verifySteps([
@ -742,11 +562,7 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData, mockRPC });
await loadState(webClient, { menu_id: 666 });
assert.containsOnce(target, ".o_kanban_view", "should display a kanban view");
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners Action 1",
"breadcrumbs should display the display_name of the action"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 1"]);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
@ -762,21 +578,15 @@ QUnit.module("ActionManager", (hooks) => {
1: { id: 1, children: [], name: "App1", appID: 1, actionID: 1 },
};
const webClient = await createWebClient({ serverData });
await legacyExtraNextTick();
await nextTick();
assert.containsOnce(target, ".o_kanban_view"); // action 1 (default app)
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners Action 1"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 1"]);
await loadState(webClient, { action: 3 });
assert.containsOnce(target, ".o_list_view"); // action 3
assert.strictEqual($(target).find(".o_control_panel .breadcrumb-item").text(), "Partners");
assert.deepEqual(getBreadCrumbTexts(target), ["Partners"]);
await loadState(webClient, { home: 1 });
assert.containsOnce(target, ".o_kanban_view"); // action 1 (default app)
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners Action 1"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 1"]);
});
QUnit.test("load state supports #home as initial state", async function (assert) {
@ -791,12 +601,9 @@ QUnit.module("ActionManager", (hooks) => {
assert.step(route);
};
await createWebClient({ serverData, mockRPC });
await legacyExtraNextTick();
await nextTick();
assert.containsOnce(target, ".o_kanban_view", "should display a kanban view");
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners Action 1"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 1"]);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
@ -806,7 +613,7 @@ QUnit.module("ActionManager", (hooks) => {
});
QUnit.test("load state: in a form view, remove the id from the state", async function (assert) {
assert.expect(13);
assert.expect(11);
serverData.actions[999] = {
id: 999,
name: "Partner",
@ -823,16 +630,12 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 999, { viewType: "form", props: { resId: 2 } });
assert.containsOnce(target, ".o_form_view");
assert.containsN(target, ".breadcrumb-item", 2);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item.active").text(),
"Second record"
);
assert.deepEqual(getBreadCrumbTexts(target), ["Partner", "Second record"]);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/read",
"/web/dataset/call_kw/partner/web_read",
]);
await loadState(webClient, {
action: 999,
@ -841,57 +644,7 @@ QUnit.module("ActionManager", (hooks) => {
});
assert.verifySteps(["/web/dataset/call_kw/partner/onchange"]);
assert.containsOnce(target, ".o_form_view .o_form_editable");
assert.containsN(target, ".breadcrumb-item", 2);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item.active").text(),
"New"
);
});
QUnit.test("hashchange does not trigger canberemoved right away", async function (assert) {
assert.expect(9);
const ClientAction = AbstractAction.extend({
start() {
this.$el.text("Hello World");
this.$el.addClass("o_client_action_test");
},
canBeRemoved() {
assert.step("canBeRemoved");
return this._super.apply(this, arguments);
},
});
const ClientAction2 = AbstractAction.extend({
start() {
this.$el.text("Hello World");
this.$el.addClass("o_client_action_test_2");
},
canBeRemoved() {
assert.step("canBeRemoved_2");
return this._super.apply(this, arguments);
},
});
const pushState = browser.history.pushState;
patchWithCleanup(browser, {
history: Object.assign({}, browser.history, {
pushState() {
pushState(...arguments);
assert.step("hashSet");
},
}),
});
core.action_registry.add("ClientAction", ClientAction);
core.action_registry.add("ClientAction2", ClientAction2);
const webClient = await createWebClient({ serverData });
assert.verifySteps([]);
await doAction(webClient, 9);
assert.verifySteps(["hashSet"]);
assert.containsOnce(target, ".o_client_action_test");
assert.verifySteps([]);
await doAction(webClient, "ClientAction2");
assert.containsOnce(target, ".o_client_action_test_2");
assert.verifySteps(["canBeRemoved", "hashSet"]);
delete core.action_registry.map.ClientAction;
delete core.action_registry.map.ClientAction2;
assert.deepEqual(getBreadCrumbTexts(target), ["Partner", "New"]);
});
QUnit.test("state with integer active_ids should not crash", async function (assert) {
@ -919,8 +672,7 @@ QUnit.module("ActionManager", (hooks) => {
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view", "should now display the list view");
await testUtils.controlPanel.switchView(target, "kanban");
await legacyExtraNextTick();
await switchView(target, "kanban");
assert.containsOnce(target, ".o_kanban_view", "should now display the kanban view");
const hash = webClient.env.services.router.current.hash;
@ -957,11 +709,10 @@ QUnit.module("ActionManager", (hooks) => {
});
await click(target.querySelector(".o_control_panel .breadcrumb-item"));
await legacyExtraNextTick();
assert.containsN(target, ".o_list_view .o_data_row", 5);
await toggleFilterMenu(target);
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "Filter");
assert.containsN(target, ".o_list_view .o_data_row", 1);
@ -982,33 +733,23 @@ QUnit.module("ActionManager", (hooks) => {
},
{ props: { resIds: [1, 2] } }
);
assert.strictEqual(target.querySelector(".breadcrumb").textContent, "First record");
assert.deepEqual(getBreadCrumbTexts(target), ["First record"]);
await pagerNext(target);
assert.strictEqual(target.querySelector(".breadcrumb").textContent, "Second record");
assert.deepEqual(getBreadCrumbTexts(target), ["Second record"]);
await editInput(target, "[name=display_name] input", "new name");
// without saving we now make a loadState which should commit changes
await loadState(webClient, { action: 1337, id: 1, model: "partner", view_type: "form" });
assert.strictEqual(target.querySelector(".breadcrumb").textContent, "First record");
assert.deepEqual(getBreadCrumbTexts(target), ["First record"]);
// loadState again just to check if changes were commited
await loadState(webClient, { action: 1337, id: 2, model: "partner", view_type: "form" });
assert.strictEqual(target.querySelector(".breadcrumb").textContent, "new name");
assert.deepEqual(getBreadCrumbTexts(target), ["new name"]);
});
QUnit.test("initial action crashes", async (assert) => {
assert.expect(8);
const handler = (ev) => {
// need to preventDefault to remove error from console (so python test pass)
ev.preventDefault();
};
window.addEventListener("unhandledrejection", handler);
registerCleanup(() => window.removeEventListener("unhandledrejection", handler));
patchWithCleanup(QUnit, {
onUnhandledRejection: () => {},
});
assert.expectErrors();
browser.location.hash = "#action=__test__client__action__&menu_id=1";
const ClientAction = registry.category("actions").get("__test__client__action__");
@ -1026,9 +767,10 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData });
assert.verifySteps(["clientAction setup"]);
await nextTick();
assert.containsOnce(target, ".o_dialog_error");
assert.expectErrors(["my error"]);
assert.containsOnce(target, ".o_error_dialog");
await click(target, ".modal-header .btn-close");
assert.containsNone(target, ".o_dialog_error");
assert.containsNone(target, ".o_error_dialog");
await click(target, "nav .o_navbar_apps_menu .dropdown-toggle ");
assert.containsN(target, ".dropdown-item.o_app", 3);
assert.containsNone(target, ".o_menu_brand");
@ -1043,7 +785,7 @@ QUnit.module("ActionManager", (hooks) => {
const hashchangeDef = makeDeferred();
class MyAction extends Component {
setup() {
owl.onMounted(() => {
onMounted(() => {
assert.step("myAction mounted");
browser.addEventListener("hashchange", () => {
hashchangeDef.resolve();
@ -1058,6 +800,8 @@ QUnit.module("ActionManager", (hooks) => {
browser.location.hash = "#action=myAction";
const webClient = await createWebClient({ serverData });
assert.verifySteps([]);
await nextTick();
assert.verifySteps(["myAction mounted"]);
assert.containsOnce(target, ".not-here");
@ -1065,6 +809,9 @@ QUnit.module("ActionManager", (hooks) => {
await hashchangeDef;
await nextTick();
assert.containsNone(target, ".not-here");
assert.containsNone(target, ".test_client_action");
await nextTick();
assert.containsNone(target, ".not-here");
assert.containsOnce(target, ".test_client_action");
assert.deepEqual(webClient.env.services.router.current.hash, {
@ -1074,13 +821,13 @@ QUnit.module("ActionManager", (hooks) => {
});
QUnit.test("concurrent hashchange during action mounting -- 2", async (assert) => {
assert.expect(5);
assert.expect(6);
const baseURL = new URL(browser.location.href).toString();
class MyAction extends Component {
setup() {
owl.onMounted(() => {
onMounted(() => {
assert.step("myAction mounted");
const newURL = baseURL + "#action=__test__client__action__&menu_id=1";
// immediate triggering
@ -1093,8 +840,11 @@ QUnit.module("ActionManager", (hooks) => {
browser.location.hash = "#action=myAction";
const webClient = await createWebClient({ serverData });
assert.verifySteps([]);
await nextTick();
assert.verifySteps(["myAction mounted"]);
await nextTick();
await nextTick();
assert.containsNone(target, ".not-here");
assert.containsOnce(target, ".test_client_action");
@ -1115,7 +865,7 @@ QUnit.module("ActionManager", (hooks) => {
patchWithCleanup(browser.sessionStorage, {
getItem(k) {
assert.step(`getItem session ${k}`);
return this._super(k);
return super.getItem(k);
},
});

View file

@ -3,34 +3,23 @@
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import AbstractAction from "web.AbstractAction";
import core from "web.core";
import testUtils from "web.test_utils";
import Widget from "web.Widget";
import { makeTestEnv } from "../../helpers/mock_env";
import {
click,
getFixture,
hushConsole,
legacyExtraNextTick,
nextTick,
patchWithCleanup,
} from "../../helpers/utils";
import testUtils from "@web/../tests/legacy/helpers/test_utils";
import { click, getFixture, hushConsole, nextTick, patchWithCleanup } from "../../helpers/utils";
import {
createWebClient,
doAction,
getActionManagerServerData,
setupWebClientRegistries,
} from "./../helpers";
import * as cpHelpers from "@web/../tests/search/helpers";
import { listView } from "@web/views/list/list_view";
import { companyService } from "@web/webclient/company_service";
import { onWillStart } from "@odoo/owl";
import { GraphModel } from "@web/views/graph/graph_model";
import { fakeCookieService } from "../../helpers/mock_services";
import { switchView } from "../../search/helpers";
let serverData;
let target;
// legacy stuff
const actionRegistry = registry.category("actions");
const actionHandlersRegistry = registry.category("action_handlers");
@ -108,7 +97,7 @@ QUnit.module("ActionManager", (hooks) => {
QUnit.test("properly handle case when action id does not exist", async (assert) => {
assert.expect(2);
const webClient = await createWebClient({ serverData });
patchWithCleanup(window, { console: hushConsole }, { pure: true });
patchWithCleanup(window, { console: hushConsole });
patchWithCleanup(webClient.env.services.notification, {
add(message) {
assert.strictEqual(message, "No action with id '4448' could be found");
@ -207,195 +196,30 @@ QUnit.module("ActionManager", (hooks) => {
assert.deepEqual(action.context, actionParams);
});
QUnit.test("no widget memory leaks when doing some action stuff", async function (assert) {
assert.expect(1);
let delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta++;
this._super.apply(this, arguments);
},
destroy: function () {
delta--;
this._super.apply(this, arguments);
},
});
const webClient = await createWebClient({ serverData });
await doAction(webClient, 8);
const n = delta;
await doAction(webClient, 4);
// kanban view is loaded, switch to list view
await cpHelpers.switchView(target, "list");
await legacyExtraNextTick();
// open a record in form view
await testUtils.dom.click(target.querySelector(".o_list_view .o_data_row"));
await legacyExtraNextTick();
// go back to action 7 in breadcrumbs
await testUtils.dom.click(target.querySelector(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
assert.strictEqual(delta, n, "should have properly destroyed all other widgets");
testUtils.mock.unpatch(Widget);
});
QUnit.test("no widget memory leaks when executing actions in dialog", async function (assert) {
assert.expect(1);
let delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta++;
this._super.apply(this, arguments);
},
destroy: function () {
if (!this.isDestroyed()) {
delta--;
}
this._super.apply(this, arguments);
},
});
const webClient = await createWebClient({ serverData });
const n = delta;
await doAction(webClient, 5);
await doAction(webClient, { type: "ir.actions.act_window_close" });
assert.strictEqual(delta, n, "should have properly destroyed all widgets");
testUtils.mock.unpatch(Widget);
});
QUnit.test(
"no memory leaks when executing an action while switching view",
async function (assert) {
assert.expect(1);
let def;
let delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
const mockRPC = async function (route, args) {
if (args && args.method === "read") {
await Promise.resolve(def);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 4);
const n = delta;
await doAction(webClient, 3, { clearBreadcrumbs: true });
// switch to the form view (this request is blocked)
def = testUtils.makeTestPromise();
await testUtils.dom.click(target.querySelector(".o_list_view .o_data_row"));
// execute another action meanwhile (don't block this request)
await doAction(webClient, 4, { clearBreadcrumbs: true });
// unblock the switch to the form view in action 3
def.resolve();
await testUtils.nextTick();
assert.strictEqual(n, delta, "all widgets of action 3 should have been destroyed");
testUtils.mock.unpatch(Widget);
}
);
QUnit.test(
"no memory leaks when executing an action while loading views",
async function (assert) {
assert.expect(1);
let def;
let delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
const mockRPC = async function (route, args) {
if (args && args.method === "get_views") {
await Promise.resolve(def);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// execute action 4 to know the number of widgets it instantiates
await doAction(webClient, 4);
const n = delta;
// execute a first action (its 'get_views' RPC is blocked)
def = testUtils.makeTestPromise();
doAction(webClient, 3, { clearBreadcrumbs: true });
await testUtils.nextTick();
await legacyExtraNextTick();
// execute another action meanwhile (and unlock the RPC)
doAction(webClient, 4, { clearBreadcrumbs: true });
def.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.strictEqual(n, delta, "all widgets of action 3 should have been destroyed");
testUtils.mock.unpatch(Widget);
}
);
QUnit.test(
"no memory leaks when executing an action while loading data of default view",
async function (assert) {
assert.expect(1);
let def;
let delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
const mockRPC = async function (route) {
if (route === "/web/dataset/search_read") {
await Promise.resolve(def);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// execute action 4 to know the number of widgets it instantiates
await doAction(webClient, 4);
const n = delta;
// execute a first action (its 'search_read' RPC is blocked)
def = testUtils.makeTestPromise();
doAction(webClient, 3, { clearBreadcrumbs: true });
await testUtils.nextTick();
await legacyExtraNextTick();
// execute another action meanwhile (and unlock the RPC)
doAction(webClient, 4, { clearBreadcrumbs: true });
def.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.strictEqual(n, delta, "all widgets of action 3 should have been destroyed");
testUtils.mock.unpatch(Widget);
}
);
QUnit.test('action with "no_breadcrumbs" set to true', async function (assert) {
serverData.actions[4].context = { no_breadcrumbs: true };
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_control_panel .breadcrumb-item");
assert.containsOnce(target, ".o_control_panel .o_breadcrumb");
// push another action flagged with 'no_breadcrumbs=true'
await doAction(webClient, 4);
assert.containsNone(target, ".o_control_panel .breadcrumb-item");
assert.containsOnce(target, ".o_kanban_view");
assert.containsNone(target, ".o_control_panel .o_breadcrumb");
await click(target, ".o_switch_view.o_list");
assert.containsOnce(target, ".o_list_view");
assert.containsNone(target, ".o_control_panel .o_breadcrumb");
});
QUnit.test("document's title is updated when an action is executed", async function (assert) {
const defaultTitle = { zopenerp: "Odoo" };
const webClient = await createWebClient({ serverData });
await nextTick();
let currentTitle = webClient.env.services.title.getParts();
assert.deepEqual(currentTitle, defaultTitle);
let currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {});
await doAction(webClient, 4);
await nextTick();
currentTitle = webClient.env.services.title.getParts();
assert.deepEqual(currentTitle, {
...defaultTitle,
@ -404,6 +228,7 @@ QUnit.module("ActionManager", (hooks) => {
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, { action: 4, model: "partner", view_type: "kanban" });
await doAction(webClient, 8);
await nextTick();
currentTitle = webClient.env.services.title.getParts();
assert.deepEqual(currentTitle, {
...defaultTitle,
@ -422,61 +247,25 @@ QUnit.module("ActionManager", (hooks) => {
assert.deepEqual(currentHash, { action: 8, id: 4, model: "pony", view_type: "form" });
});
QUnit.test(
"on_reverse_breadcrumb handler is correctly called (legacy)",
async function (assert) {
// This test can be removed as soon as we no longer support legacy actions as the new
// ActionManager doesn't support this option. Indeed, it is used to reload the previous
// action when coming back, but we won't need such an artefact to that with Wowl, as the
// controller will be re-instantiated with an (exported) state given in props.
assert.expect(5);
const ClientAction = AbstractAction.extend({
events: {
"click button": "_onClick",
},
start() {
this.$el.html('<button class="my_button">Execute another action</button>');
},
_onClick() {
this.do_action(4, {
on_reverse_breadcrumb: () => assert.step("on_reverse_breadcrumb"),
});
},
});
core.action_registry.add("ClientAction", ClientAction);
const webClient = await createWebClient({ serverData });
await doAction(webClient, "ClientAction");
assert.containsOnce(target, ".my_button");
await testUtils.dom.click(target.querySelector(".my_button"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_kanban_view");
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a:first"));
await legacyExtraNextTick();
assert.containsOnce(target, ".my_button");
assert.verifySteps(["on_reverse_breadcrumb"]);
delete core.action_registry.map.ClientAction;
}
);
QUnit.test('handles "history_back" event', async function (assert) {
assert.expect(3);
assert.expect(4);
let list;
patchWithCleanup(listView.Controller.prototype, {
setup() {
this._super(...arguments);
super.setup(...arguments);
list = this;
},
});
const webClient = await createWebClient({ serverData });
await doAction(webClient, 4);
await doAction(webClient, 3);
assert.containsN(target, ".o_control_panel .breadcrumb-item", 2);
assert.containsOnce(target, "ol.breadcrumb");
assert.containsOnce(target, ".o_breadcrumb span");
list.env.config.historyBack();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_control_panel .breadcrumb-item");
await nextTick();
assert.containsOnce(target, ".o_breadcrumb span");
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
target.querySelector(".o_control_panel .o_breadcrumb").textContent,
"Partners Action 4",
"breadcrumbs should display the display_name of the action"
);
@ -484,7 +273,6 @@ QUnit.module("ActionManager", (hooks) => {
QUnit.test("stores and restores scroll position (in kanban)", async function (assert) {
serverData.actions[3].views = [[false, "kanban"]];
assert.expect(3);
for (let i = 0; i < 60; i++) {
serverData.models.partner.records.push({ id: 100 + i, foo: `Record ${i}` });
}
@ -550,6 +338,7 @@ QUnit.module("ActionManager", (hooks) => {
await click(target.querySelector(".o_form_view .o_data_row .o_data_cell"));
assert.containsOnce(document.body, ".modal .o_form_view");
await doAction(webClient, 1); // target != 'new'
await nextTick(); // wait for the dialog to be closed
assert.containsNone(document.body, ".modal");
}
);
@ -588,11 +377,10 @@ QUnit.module("ActionManager", (hooks) => {
search_default_x: true,
searchpanel_default_y: true,
};
registry.category("services").add("cookie", fakeCookieService);
patchWithCleanup(GraphModel.prototype, {
load(searchParams) {
assert.deepEqual(searchParams.context, { lang: "en", tz: "taht", uid: 7 });
return this._super.apply(this, arguments);
return super.load(...arguments);
},
});
@ -607,7 +395,7 @@ QUnit.module("ActionManager", (hooks) => {
context,
});
// list view is loaded, switch to graph view
await cpHelpers.switchView(target, "graph");
await switchView(target, "graph");
}
);
@ -645,6 +433,7 @@ QUnit.module("ActionManager", (hooks) => {
// Create the web client. It should execute the stored action.
const webClient = await createWebClient({ serverData });
await nextTick();
// Check the current action context
assert.deepEqual(webClient.env.services.action.currentController.action.context, {
@ -657,4 +446,44 @@ QUnit.module("ActionManager", (hooks) => {
});
}
);
QUnit.test(
"action is removed while waiting for another action with selectMenu",
async (assert) => {
let def;
const actionRegistry = registry.category("actions");
const ClientAction = actionRegistry.get("__test__client__action__");
patchWithCleanup(ClientAction.prototype, {
setup() {
super.setup();
onWillStart(() => {
return def;
});
},
});
const webClient = await createWebClient({ serverData });
// starting point: a kanban view
await doAction(webClient, 4);
const root = document.querySelector(".o_web_client");
assert.containsOnce(root, ".o_kanban_view");
// select one new app in navbar menu
def = testUtils.makeTestPromise();
const menus = webClient.env.services.menu.getApps();
webClient.env.services.menu.selectMenu(menus[1], { resetScreen: true });
await nextTick();
// check that the action manager is empty, even though client action is loading
assert.strictEqual(root.querySelector(".o_action_manager").textContent, "");
// resolve onwillstart so client action is ready
def.resolve();
await nextTick();
assert.strictEqual(
root.querySelector(".o_action_manager").textContent,
" ClientAction_Id 1"
);
}
);
});

View file

@ -3,15 +3,8 @@
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import testUtils from "web.test_utils";
import {
click,
getFixture,
legacyExtraNextTick,
makeDeferred,
nextTick,
patchWithCleanup,
} from "../../helpers/utils";
import testUtils from "@web/../tests/legacy/helpers/test_utils";
import { click, getFixture, makeDeferred, nextTick, patchWithCleanup } from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
import { Component, xml } from "@odoo/owl";
@ -65,6 +58,7 @@ QUnit.module("ActionManager", (hooks) => {
);
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "App2");
await doAction(webClient, 1001, { clearBreadcrumbs: true });
await nextTick();
urlState = webClient.env.services.router.current;
assert.strictEqual(urlState.hash.action, 1001);
assert.strictEqual(urlState.hash.menu_id, 2);
@ -94,10 +88,12 @@ QUnit.module("ActionManager", (hooks) => {
let urlState = webClient.env.services.router.current;
assert.deepEqual(urlState.hash, {});
await doAction(webClient, "client_action_pushes");
await nextTick();
urlState = webClient.env.services.router.current;
assert.strictEqual(urlState.hash.action, "client_action_pushes");
assert.strictEqual(urlState.hash.menu_id, undefined);
await click(target, ".test_client_action");
await nextTick();
urlState = webClient.env.services.router.current;
assert.strictEqual(urlState.hash.action, "client_action_pushes");
assert.strictEqual(urlState.hash.arbitrary, "actionPushed");
@ -123,10 +119,12 @@ QUnit.module("ActionManager", (hooks) => {
assert.deepEqual(urlState.hash, {});
await doAction(webClient, "client_action_pushes");
await click(target, ".test_client_action");
await nextTick();
urlState = webClient.env.services.router.current;
assert.strictEqual(urlState.hash.action, "client_action_pushes");
assert.strictEqual(urlState.hash.arbitrary, "actionPushed");
await doAction(webClient, 1001);
await nextTick();
urlState = webClient.env.services.router.current;
assert.strictEqual(urlState.hash.action, 1001);
assert.strictEqual(urlState.hash.arbitrary, undefined);
@ -174,18 +172,21 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData });
await doAction(webClient, 1001);
assert.containsOnce(target, ".modal .test_client_action");
await nextTick();
});
QUnit.test("properly push state", async function (assert) {
assert.expect(3);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 4);
await nextTick();
assert.deepEqual(webClient.env.services.router.current.hash, {
action: 4,
model: "partner",
view_type: "kanban",
});
await doAction(webClient, 8);
await nextTick();
assert.deepEqual(webClient.env.services.router.current.hash, {
action: 8,
model: "pony",
@ -202,7 +203,6 @@ QUnit.module("ActionManager", (hooks) => {
});
QUnit.test("push state after action is loaded, not before", async function (assert) {
assert.expect(2);
const def = makeDeferred();
const mockRPC = async function (route, args) {
if (args.method === "web_search_read") {
@ -233,15 +233,16 @@ QUnit.module("ActionManager", (hooks) => {
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 8);
await nextTick();
assert.deepEqual(webClient.env.services.router.current.hash, {
action: 8,
model: "pony",
view_type: "list",
});
await testUtils.dom.click($(target).find("tr.o_data_row:first"));
await legacyExtraNextTick();
// we make sure here that the list view is still in the dom
assert.containsOnce(target, ".o_list_view", "there should still be a list view in dom");
await nextTick();
assert.deepEqual(webClient.env.services.router.current.hash, {
action: 8,
model: "pony",

View file

@ -6,12 +6,20 @@ import { session } from "@web/session";
import { ReportAction } from "@web/webclient/actions/reports/report_action";
import { clearRegistryWithCleanup } from "@web/../tests/helpers/mock_env";
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
import { mockDownload, patchWithCleanup, getFixture, click } from "@web/../tests/helpers/utils";
import {
mockDownload,
nextTick,
patchWithCleanup,
getFixture,
click,
} from "@web/../tests/helpers/utils";
import {
createWebClient,
doAction,
getActionManagerServerData,
} from "@web/../tests/webclient/helpers";
import { downloadReport } from "@web/webclient/actions/reports/utils";
import { registerCleanup } from "../../../helpers/cleanup";
let serverData;
let target;
@ -23,6 +31,9 @@ QUnit.module("ActionManager", (hooks) => {
serverData = getActionManagerServerData();
target = getFixture();
clearRegistryWithCleanup(registry.category("main_components"));
registerCleanup(() => {
delete downloadReport.wkhtmltopdfStatusProm;
});
});
QUnit.module("Report actions");
@ -111,8 +122,8 @@ QUnit.module("ActionManager", (hooks) => {
"/web/webclient/load_menus",
"/web/action/load",
"/report/check_wkhtmltopdf",
"notify",
"/report/download",
"notify",
]);
}
);
@ -149,7 +160,7 @@ QUnit.module("ActionManager", (hooks) => {
// attribute is removed right after)
patchWithCleanup(ReportAction.prototype, {
setup() {
this._super(...arguments);
super.setup(...arguments);
this.env.services.rpc(this.reportUrl);
this.reportUrl = "about:blank";
},
@ -161,7 +172,12 @@ QUnit.module("ActionManager", (hooks) => {
".o_content iframe",
"should have opened the report client action"
);
assert.containsOnce(target, "button[title='Print']", "should have a print button");
// the control panel has the content twice and a d-none class is toggled depending the screen size
assert.containsOnce(
target,
":not(.d-none) > button[title='Print']",
"should have a print button"
);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
@ -202,7 +218,7 @@ QUnit.module("ActionManager", (hooks) => {
// attribute is removed right after)
patchWithCleanup(ReportAction.prototype, {
setup() {
this._super(...arguments);
super.setup(...arguments);
this.env.services.rpc(this.reportUrl);
this.reportUrl = "about:blank";
},
@ -253,7 +269,7 @@ QUnit.module("ActionManager", (hooks) => {
await doAction(webClient, 7);
try {
await doAction(webClient, 7);
} catch (_e) {
} catch {
assert.step("error caught");
}
assert.verifySteps([
@ -308,10 +324,14 @@ QUnit.module("ActionManager", (hooks) => {
});
QUnit.test("context is correctly passed to the client action report", async (assert) => {
assert.expect(8);
assert.expect(9);
mockDownload((options) => {
assert.step(options.url);
assert.deepEqual(
options.data.context,
`{"lang":"en","uid":7,"tz":"taht","rabbia":"E Tarantella","active_ids":[99]}`
);
assert.deepEqual(JSON.parse(options.data.data), [
"/report/pdf/ennio.morricone/99",
"qweb-pdf",
@ -330,7 +350,7 @@ QUnit.module("ActionManager", (hooks) => {
patchWithCleanup(ReportAction.prototype, {
setup() {
this._super(...arguments);
super.setup(...arguments);
this.env.services.rpc(this.reportUrl);
this.reportUrl = "about:blank";
},
@ -354,7 +374,11 @@ QUnit.module("ActionManager", (hooks) => {
assert.verifySteps([
"/report/html/ennio.morricone/99?context=%7B%22lang%22%3A%22en%22%2C%22uid%22%3A7%2C%22tz%22%3A%22taht%22%7D",
]);
await click(target.querySelector("button[title='Print']"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex button[title='Print']"
)
);
assert.verifySteps(["/report/check_wkhtmltopdf", "/report/download"]);
});
@ -363,7 +387,7 @@ QUnit.module("ActionManager", (hooks) => {
patchWithCleanup(ReportAction.prototype, {
init() {
this._super(...arguments);
super.init(...arguments);
this.reportUrl = "about:blank";
},
});
@ -371,7 +395,7 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData });
await doAction(webClient, 12); // 12 is a html report action in serverData
await nextTick();
const hash = webClient.router.current.hash;
// used to put report.client_action in the url
assert.strictEqual(hash.action === "report.client_action", false);

View file

@ -1,12 +1,9 @@
/** @odoo-module **/
import testUtils from "web.test_utils";
import core from "web.core";
import AbstractAction from "web.AbstractAction";
import testUtils from "@web/../tests/legacy/helpers/test_utils";
import { registry } from "@web/core/registry";
import { click, getFixture, patchWithCleanup, makeDeferred, nextTick } from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
import { registerCleanup } from "../../helpers/cleanup";
import { errorService } from "@web/core/errors/error_service";
import { useService } from "@web/core/utils/hooks";
import { ClientErrorDialog } from "@web/core/errors/error_dialogs";
@ -149,8 +146,7 @@ QUnit.module("ActionManager", (hooks) => {
]);
await testUtils.dom.click(`button[name="5"]`);
assert.verifySteps([
"/web/dataset/call_kw/partner/create",
"/web/dataset/call_kw/partner/read",
"/web/dataset/call_kw/partner/web_save",
"/web/action/load",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/onchange",
@ -158,40 +154,13 @@ QUnit.module("ActionManager", (hooks) => {
assert.containsOnce(document.body, ".modal");
await testUtils.dom.click(`button[name="some_method"]`);
assert.verifySteps([
"/web/dataset/call_kw/partner/create",
"/web/dataset/call_kw/partner/read",
"/web/dataset/call_kw/partner/web_save",
"/web/dataset/call_button",
"/web/dataset/call_kw/partner/read",
"/web/dataset/call_kw/partner/web_read",
]);
assert.containsNone(document.body, ".modal");
});
QUnit.test('on_attach_callback is called for actions in target="new"', async function (assert) {
assert.expect(3);
const ClientAction = AbstractAction.extend({
on_attach_callback: function () {
assert.step("on_attach_callback");
assert.containsOnce(
document.body,
".modal .o_test",
"should have rendered the client action in a dialog"
);
},
start: function () {
this.$el.addClass("o_test");
},
});
core.action_registry.add("test", ClientAction);
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
tag: "test",
target: "new",
type: "ir.actions.client",
});
assert.verifySteps(["on_attach_callback"]);
delete core.action_registry.map.test;
});
QUnit.test(
'footer buttons are updated when having another action in target "new"',
async function (assert) {
@ -218,32 +187,6 @@ QUnit.module("ActionManager", (hooks) => {
}
);
QUnit.test(
'buttons of client action in target="new" and transition to MVC action',
async function (assert) {
const ClientAction = AbstractAction.extend({
renderButtons($target) {
const button = document.createElement("button");
button.setAttribute("class", "o_stagger_lee");
$target[0].appendChild(button);
},
});
core.action_registry.add("test", ClientAction);
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
tag: "test",
target: "new",
type: "ir.actions.client",
});
assert.containsOnce(target, ".modal footer button.o_stagger_lee");
assert.containsNone(target, '.modal footer button[special="save"]');
await doAction(webClient, 25);
assert.containsNone(target, ".modal footer button.o_stagger_lee");
assert.containsOnce(target, '.modal footer button[special="save"]');
delete core.action_registry.map.test;
}
);
QUnit.test(
'button with confirm attribute in act_window action in target="new"',
async function (assert) {
@ -322,18 +265,8 @@ QUnit.module("ActionManager", (hooks) => {
});
QUnit.test("do not commit a dialog in error", async (assert) => {
assert.expect(6);
const handler = (ev) => {
// need to preventDefault to remove error from console (so python test pass)
ev.preventDefault();
};
window.addEventListener("unhandledrejection", handler);
registerCleanup(() => window.removeEventListener("unhandledrejection", handler));
patchWithCleanup(QUnit, {
onUnhandledRejection: () => {},
});
assert.expect(7);
assert.expectErrors();
class ErrorClientAction extends Component {
setup() {
@ -359,6 +292,7 @@ QUnit.module("ActionManager", (hooks) => {
);
} catch (e) {
assert.strictEqual(e.cause.message, "my error");
throw e;
}
}
}
@ -371,7 +305,7 @@ QUnit.module("ActionManager", (hooks) => {
const errorDialogOpened = makeDeferred();
patchWithCleanup(ClientErrorDialog.prototype, {
setup() {
this._super(...arguments);
super.setup(...arguments);
onMounted(() => errorDialogOpened.resolve());
},
});
@ -388,8 +322,9 @@ QUnit.module("ActionManager", (hooks) => {
assert.ok(
target.querySelector(".modal-body .o_error_detail").textContent.includes("my error")
);
assert.verifyErrors(["my error"]);
await click(target, ".modal-footer button");
await click(target, ".modal-footer .btn-primary");
assert.containsNone(target, ".modal");
await doAction(webClient, {
@ -408,7 +343,7 @@ QUnit.module("ActionManager", (hooks) => {
// execute an action in target="current"
await doAction(webClient, 1);
assert.deepEqual(
[...target.querySelectorAll(".breadcrumb-item")].map((i) => i.innerText),
[...target.querySelectorAll(".o_breadcrumb span")].map((i) => i.innerText),
["Partners Action 1"]
);
@ -421,7 +356,7 @@ QUnit.module("ActionManager", (hooks) => {
type: "ir.actions.act_window",
views: [[false, "list"]],
});
assert.containsNone(target, ".modal .breadcrumb");
assert.containsNone(target, ".modal .o_breadcrumb");
});
QUnit.test('call switchView in an action in target="new"', async function (assert) {
@ -484,7 +419,10 @@ QUnit.module("ActionManager", (hooks) => {
res_model: "pony",
type: "ir.actions.act_window",
target: "new",
views: [[false, "list"], [false, "form"]],
views: [
[false, "list"],
[false, "form"],
],
});
// The list view has been opened in a dialog
@ -534,10 +472,13 @@ QUnit.module("ActionManager", (hooks) => {
] = `<form><button name="1" type="action" class="oe_stat_button" /></form>`;
const webClient = await createWebClient({ serverData });
await doAction(webClient, 6);
await nextTick(); // for the webclient to react and remove the navbar
assert.isNotVisible(target.querySelector(".o_main_navbar"));
await click(target.querySelector("button[name='1']"));
await nextTick();
assert.isNotVisible(target.querySelector(".o_main_navbar"));
await click(target.querySelector(".breadcrumb li a"));
await nextTick();
assert.isNotVisible(target.querySelector(".o_main_navbar"));
});
@ -553,6 +494,7 @@ QUnit.module("ActionManager", (hooks) => {
'<form><button name="24" type="action" class="oe_stat_button"/></form>';
await createWebClient({ serverData });
await nextTick(); // wait for the load state (default app)
await nextTick(); // wait for the action to be mounted
assert.containsOnce(target, "nav .o_menu_brand");
assert.strictEqual(target.querySelector("nav .o_menu_brand").innerText, "MAIN APP");
await click(target.querySelector("button[name='24']"));
@ -575,9 +517,9 @@ QUnit.module("ActionManager", (hooks) => {
await doAction(webClient, 1);
assert.containsOnce(target, ".o_kanban_view");
assert.containsOnce(target, ".breadcrumb-item");
assert.containsOnce(target, ".o_breadcrumb span");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
target.querySelector(".o_control_panel .o_breadcrumb").textContent,
"Partners Action 1"
);
@ -590,9 +532,9 @@ QUnit.module("ActionManager", (hooks) => {
});
assert.containsOnce(target, ".o_list_view");
assert.containsOnce(target, ".breadcrumb-item");
assert.containsOnce(target, ".o_breadcrumb span");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
target.querySelector(".o_control_panel .o_breadcrumb").textContent,
"Another Partner Action"
);
});
@ -611,9 +553,9 @@ QUnit.module("ActionManager", (hooks) => {
});
assert.containsOnce(target, ".o_list_view");
assert.containsOnce(target, ".breadcrumb-item");
assert.containsOnce(target, ".o_breadcrumb span");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
target.querySelector(".o_control_panel .o_breadcrumb").textContent,
"Partner Action"
);
@ -621,9 +563,10 @@ QUnit.module("ActionManager", (hooks) => {
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
assert.containsN(target, ".breadcrumb-item", 2);
assert.containsOnce(target, "ol.breadcrumb");
assert.containsOnce(target, ".o_breadcrumb span");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
target.querySelector(".o_control_panel .o_breadcrumb").textContent,
"Partner ActionFirst record"
);
});
@ -642,31 +585,34 @@ QUnit.module("ActionManager", (hooks) => {
});
assert.containsOnce(target, ".o_list_view");
assert.containsOnce(target, ".breadcrumb-item");
assert.containsOnce(target, ".o_breadcrumb span");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
target.querySelector(".o_control_panel .o_breadcrumb").textContent,
"Partner Action"
);
// open first record
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
assert.containsN(target, ".breadcrumb-item", 2);
assert.containsOnce(target, "ol.breadcrumb");
assert.containsOnce(target, ".o_breadcrumb span");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
target.querySelector(".o_control_panel .o_breadcrumb").textContent,
"Partner ActionFirst record"
);
await doAction(webClient, 1);
assert.containsOnce(target, ".o_kanban_view");
assert.containsN(target, ".breadcrumb-item", 3);
assert.containsOnce(target, "ol.breadcrumb");
assert.containsOnce(target, ".o_breadcrumb span");
// go back to form view
await click(target.querySelectorAll(".breadcrumb-item")[1]);
await click(target.querySelector("ol.breadcrumb .o_back_button"));
assert.containsOnce(target, ".o_form_view");
assert.containsN(target, ".breadcrumb-item", 2);
assert.containsOnce(target, "ol.breadcrumb");
assert.containsOnce(target, ".o_breadcrumb span");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
target.querySelector(".o_control_panel .o_breadcrumb").textContent,
"Partner ActionFirst record"
);
});

View file

@ -1,31 +1,27 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { makeTestEnv } from "../../helpers/mock_env";
import { makeFakeRouterService } from "../../helpers/mock_services";
import { setupWebClientRegistries, doAction, getActionManagerServerData } from "./../helpers";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
let target;
let serverData;
const serviceRegistry = registry.category("services");
QUnit.module("ActionManager", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = getActionManagerServerData();
});
QUnit.module("URL actions");
QUnit.test("execute an 'ir.actions.act_url' action with target 'self'", async (assert) => {
serviceRegistry.add(
"router",
makeFakeRouterService({
onRedirect(url) {
assert.step(url);
},
})
);
patchWithCleanup(browser.location, {
assign: (url) => {
assert.step(url);
},
});
setupWebClientRegistries();
const env = await makeTestEnv({ serverData });
await doAction(env, {
@ -48,17 +44,14 @@ QUnit.module("ActionManager", (hooks) => {
await doAction(env, { type: "ir.actions.act_url" }, options);
assert.verifySteps(["browser open", "onClose"]);
});
QUnit.test("execute an 'ir.actions.act_url' action with url javascript:", async (assert) => {
assert.expect(1);
serviceRegistry.add(
"router",
makeFakeRouterService({
onRedirect(url) {
assert.strictEqual(url, "/javascript:alert()");
},
})
);
assert.expect(2);
patchWithCleanup(browser.location, {
assign: (url) => {
assert.step(url);
},
});
setupWebClientRegistries();
const env = await makeTestEnv({ serverData });
await doAction(env, {
@ -66,5 +59,23 @@ QUnit.module("ActionManager", (hooks) => {
target: "self",
url: "javascript:alert()",
});
assert.verifySteps(["/javascript:alert()"]);
});
QUnit.test("execute an 'ir.actions.act_url' action with target 'download'", async (assert) => {
patchWithCleanup(browser.location, {
assign: (url) => {
assert.step(url);
},
});
setupWebClientRegistries();
const env = await makeTestEnv({ serverData });
await doAction(env, {
type: "ir.actions.act_url",
target: "download",
url: "/my/test/url",
});
assert.containsNone(target, ".o_blockUI", "ui should not be blocked");
assert.verifySteps(["/my/test/url"]);
});
});