mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-19 10:51:58 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
|
|
@ -0,0 +1,632 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import core from "web.core";
|
||||
import AbstractAction from "web.AbstractAction";
|
||||
import testUtils from "web.test_utils";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
legacyExtraNextTick,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "../../helpers/utils";
|
||||
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
const actionRegistry = registry.category("actions");
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("Client Actions");
|
||||
|
||||
QUnit.test("can display client actions in Dialog", async function (assert) {
|
||||
assert.expect(2);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, {
|
||||
name: "Dialog Test",
|
||||
target: "new",
|
||||
tag: "__test__client__action__",
|
||||
type: "ir.actions.client",
|
||||
});
|
||||
assert.containsOnce(target, ".modal .test_client_action");
|
||||
assert.strictEqual(target.querySelector(".modal-title").textContent, "Dialog Test");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"can display client actions in Dialog and close the dialog",
|
||||
async function (assert) {
|
||||
assert.expect(3);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, {
|
||||
name: "Dialog Test",
|
||||
target: "new",
|
||||
tag: "__test__client__action__",
|
||||
type: "ir.actions.client",
|
||||
});
|
||||
assert.containsOnce(target, ".modal .test_client_action");
|
||||
assert.strictEqual(target.querySelector(".modal-title").textContent, "Dialog Test");
|
||||
target.querySelector(".modal footer .btn.btn-primary").click();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".modal .test_client_action");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("can display client actions as main, then in Dialog", async function (assert) {
|
||||
assert.expect(3);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, "__test__client__action__");
|
||||
assert.containsOnce(target, ".o_action_manager .test_client_action");
|
||||
await doAction(webClient, {
|
||||
target: "new",
|
||||
tag: "__test__client__action__",
|
||||
type: "ir.actions.client",
|
||||
});
|
||||
assert.containsOnce(target, ".o_action_manager .test_client_action");
|
||||
assert.containsOnce(target, ".modal .test_client_action");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"can display client actions in Dialog, then as main destroys Dialog",
|
||||
async function (assert) {
|
||||
assert.expect(4);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, {
|
||||
target: "new",
|
||||
tag: "__test__client__action__",
|
||||
type: "ir.actions.client",
|
||||
});
|
||||
assert.containsOnce(target, ".test_client_action");
|
||||
assert.containsOnce(target, ".modal .test_client_action");
|
||||
await doAction(webClient, "__test__client__action__");
|
||||
assert.containsOnce(target, ".test_client_action");
|
||||
assert.containsNone(target, ".modal .test_client_action");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("soft_reload will refresh data", async (assert) => {
|
||||
const mockRPC = async function (route, args) {
|
||||
if (route === "/web/dataset/call_kw/partner/web_search_read") {
|
||||
assert.step("web_search_read");
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1);
|
||||
assert.verifySteps(["web_search_read"]);
|
||||
await doAction(webClient, "soft_reload");
|
||||
assert.verifySteps(["web_search_read"]);
|
||||
});
|
||||
|
||||
QUnit.test("soft_reload when there is no controller", async (assert) => {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, "soft_reload");
|
||||
assert.ok(true, "No ControllerNotFoundError when there is no controller to restore");
|
||||
});
|
||||
|
||||
QUnit.test("can execute client actions from tag name (legacy)", async function (assert) {
|
||||
// remove this test as soon as legacy Widgets are no longer supported
|
||||
assert.expect(4);
|
||||
const ClientAction = AbstractAction.extend({
|
||||
start: function () {
|
||||
this.$el.text("Hello World");
|
||||
this.$el.addClass("o_client_action_test");
|
||||
},
|
||||
});
|
||||
const mockRPC = async function (route, args) {
|
||||
assert.step((args && args.method) || route);
|
||||
};
|
||||
core.action_registry.add("HelloWorldTestLeg", ClientAction);
|
||||
registerCleanup(() => delete core.action_registry.map.HelloWorldTestLeg);
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, "HelloWorldTestLeg");
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".o_control_panel",
|
||||
"shouldn't have rendered a control panel"
|
||||
);
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_client_action_test").text(),
|
||||
"Hello World",
|
||||
"should have correctly rendered the client action"
|
||||
);
|
||||
assert.verifySteps(["/web/webclient/load_menus"]);
|
||||
});
|
||||
|
||||
QUnit.test("can execute client actions from tag name", async function (assert) {
|
||||
assert.expect(4);
|
||||
class ClientAction extends Component {}
|
||||
ClientAction.template = xml`<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 webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, "HelloWorldTest");
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".o_control_panel",
|
||||
"shouldn't have rendered a control panel"
|
||||
);
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_client_action_test").text(),
|
||||
"Hello World",
|
||||
"should have correctly rendered the client action"
|
||||
);
|
||||
assert.verifySteps(["/web/webclient/load_menus"]);
|
||||
});
|
||||
|
||||
QUnit.test("async client action (function) returning another action", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
registry.category("actions").add("my_action", async () => {
|
||||
await Promise.resolve();
|
||||
return 1; // execute action 1
|
||||
});
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, "my_action");
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"'CLEAR-UNCOMMITTED-CHANGES' is not triggered for function client actions",
|
||||
async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
registry.category("actions").add("my_action", async () => {
|
||||
assert.step("my_action");
|
||||
});
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
webClient.env.bus.addEventListener("CLEAR-UNCOMMITTED-CHANGES", () => {
|
||||
assert.step("CLEAR-UNCOMMITTED-CHANGES");
|
||||
});
|
||||
|
||||
await doAction(webClient, "my_action");
|
||||
assert.verifySteps(["my_action"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("client action with control panel (legacy)", async function (assert) {
|
||||
assert.expect(4);
|
||||
// LPE Fixme: at this time we don't really know the API that wowl ClientActions implement
|
||||
const ClientAction = AbstractAction.extend({
|
||||
hasControlPanel: true,
|
||||
start() {
|
||||
this.$(".o_content").text("Hello World");
|
||||
this.$el.addClass("o_client_action_test");
|
||||
this.controlPanelProps.title = "Hello";
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
core.action_registry.add("HelloWorldTest", ClientAction);
|
||||
registerCleanup(() => delete core.action_registry.map.HelloWorldTest);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, "HelloWorldTest");
|
||||
assert.strictEqual(
|
||||
$(".o_control_panel:visible").length,
|
||||
1,
|
||||
"should have rendered a control panel"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_control_panel .breadcrumb-item",
|
||||
1,
|
||||
"there should be one controller in the breadcrumbs"
|
||||
);
|
||||
assert.strictEqual(
|
||||
$(".o_control_panel .breadcrumb-item").text(),
|
||||
"Hello",
|
||||
"breadcrumbs should still display the title of the controller"
|
||||
);
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_client_action_test .o_content").text(),
|
||||
"Hello World",
|
||||
"should have correctly rendered the client action"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("state is pushed for client action (legacy)", async function (assert) {
|
||||
assert.expect(6);
|
||||
const ClientAction = AbstractAction.extend({
|
||||
getTitle: function () {
|
||||
return "a title";
|
||||
},
|
||||
getState: function () {
|
||||
return { foo: "baz" };
|
||||
},
|
||||
});
|
||||
const pushState = browser.history.pushState;
|
||||
patchWithCleanup(browser, {
|
||||
history: Object.assign({}, browser.history, {
|
||||
pushState() {
|
||||
pushState(...arguments);
|
||||
assert.step("push_state");
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
core.action_registry.add("HelloWorldTest", ClientAction);
|
||||
registerCleanup(() => delete core.action_registry.map.HelloWorldTest);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
let currentTitle = webClient.env.services.title.current;
|
||||
assert.strictEqual(currentTitle, '{"zopenerp":"Odoo"}');
|
||||
let currentHash = webClient.env.services.router.current.hash;
|
||||
assert.deepEqual(currentHash, {});
|
||||
await doAction(webClient, "HelloWorldTest");
|
||||
currentTitle = webClient.env.services.title.current;
|
||||
assert.strictEqual(currentTitle, '{"zopenerp":"Odoo","action":"a title"}');
|
||||
currentHash = webClient.env.services.router.current.hash;
|
||||
assert.deepEqual(currentHash, {
|
||||
action: "HelloWorldTest",
|
||||
foo: "baz",
|
||||
});
|
||||
assert.verifySteps(["push_state"]);
|
||||
});
|
||||
|
||||
QUnit.test("action can use a custom control panel (legacy)", async function (assert) {
|
||||
assert.expect(1);
|
||||
class CustomControlPanel extends Component {}
|
||||
CustomControlPanel.template = xml`
|
||||
<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) => {
|
||||
assert.expect(4);
|
||||
class ClientAction extends Component {
|
||||
setup() {
|
||||
this.breadcrumbTitle = "myOwlAction";
|
||||
const { breadcrumbs } = this.env.config;
|
||||
assert.strictEqual(breadcrumbs.length, 2);
|
||||
assert.strictEqual(breadcrumbs[0].name, "Favorite Ponies");
|
||||
owl.onMounted(() => {
|
||||
this.env.config.setDisplayName(this.breadcrumbTitle);
|
||||
});
|
||||
}
|
||||
onClick() {
|
||||
this.breadcrumbTitle = "newOwlTitle";
|
||||
this.env.config.setDisplayName(this.breadcrumbTitle);
|
||||
}
|
||||
}
|
||||
ClientAction.template = xml`<div class="my_owl_action" t-on-click="onClick">owl client action</div>`;
|
||||
actionRegistry.add("OwlClientAction", ClientAction);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 8);
|
||||
await doAction(webClient, "OwlClientAction");
|
||||
assert.containsOnce(target, ".my_owl_action");
|
||||
await click(target, ".my_owl_action");
|
||||
await doAction(webClient, 3);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".breadcrumb").textContent,
|
||||
"Favorite PoniesnewOwlTitlePartners"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ClientAction receives arbitrary props from doAction (wowl)", async (assert) => {
|
||||
assert.expect(1);
|
||||
class ClientAction extends Component {
|
||||
setup() {
|
||||
assert.strictEqual(this.props.division, "bell");
|
||||
}
|
||||
}
|
||||
ClientAction.template = xml`<div class="my_owl_action"></div>`;
|
||||
actionRegistry.add("OwlClientAction", ClientAction);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, "OwlClientAction", {
|
||||
props: { division: "bell" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("ClientAction receives arbitrary props from doAction (legacy)", async (assert) => {
|
||||
assert.expect(1);
|
||||
const ClientAction = AbstractAction.extend({
|
||||
init(parent, action, options) {
|
||||
assert.strictEqual(options.division, "bell");
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
core.action_registry.add("ClientAction", ClientAction);
|
||||
registerCleanup(() => delete core.action_registry.map.ClientAction);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, "ClientAction", {
|
||||
props: { division: "bell" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("test display_notification client action", async function (assert) {
|
||||
assert.expect(6);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.client",
|
||||
tag: "display_notification",
|
||||
params: {
|
||||
title: "title",
|
||||
message: "message",
|
||||
sticky: true,
|
||||
},
|
||||
});
|
||||
const notificationSelector = ".o_notification_manager .o_notification";
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
notificationSelector,
|
||||
"a notification should be present"
|
||||
);
|
||||
const notificationElement = document.body.querySelector(notificationSelector);
|
||||
assert.strictEqual(
|
||||
notificationElement.querySelector(".o_notification_title").textContent,
|
||||
"title",
|
||||
"the notification should have the correct title"
|
||||
);
|
||||
assert.strictEqual(
|
||||
notificationElement.querySelector(".o_notification_content").textContent,
|
||||
"message",
|
||||
"the notification should have the correct message"
|
||||
);
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
await testUtils.dom.click(notificationElement.querySelector(".o_notification_close"));
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
notificationSelector,
|
||||
"the notification should be destroy "
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("test display_notification client action with links", async function (assert) {
|
||||
assert.expect(8);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.client",
|
||||
tag: "display_notification",
|
||||
params: {
|
||||
title: "title",
|
||||
message: "message %s <R&D>",
|
||||
sticky: true,
|
||||
links: [
|
||||
{
|
||||
label: "test <R&D>",
|
||||
url: "#action={action.id}&id={order.id}&model=purchase.order",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const notificationSelector = ".o_notification_manager .o_notification";
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
notificationSelector,
|
||||
"a notification should be present"
|
||||
);
|
||||
let notificationElement = document.body.querySelector(notificationSelector);
|
||||
assert.strictEqual(
|
||||
notificationElement.querySelector(".o_notification_title").textContent,
|
||||
"title",
|
||||
"the notification should have the correct title"
|
||||
);
|
||||
assert.strictEqual(
|
||||
notificationElement.querySelector(".o_notification_content").textContent,
|
||||
"message test <R&D> <R&D>",
|
||||
"the notification should have the correct message"
|
||||
);
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
await testUtils.dom.click(notificationElement.querySelector(".o_notification_close"));
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
notificationSelector,
|
||||
"the notification should be destroy "
|
||||
);
|
||||
|
||||
// display_notification without title
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.client",
|
||||
tag: "display_notification",
|
||||
params: {
|
||||
message: "message %s <R&D>",
|
||||
sticky: true,
|
||||
links: [
|
||||
{
|
||||
label: "test <R&D>",
|
||||
url: "#action={action.id}&id={order.id}&model=purchase.order",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
notificationSelector,
|
||||
"a notification should be present"
|
||||
);
|
||||
notificationElement = document.body.querySelector(notificationSelector);
|
||||
assert.containsNone(
|
||||
notificationElement,
|
||||
".o_notification_title",
|
||||
"the notification should not have title"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("test next action on display_notification client action", async function (assert) {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
const options = {
|
||||
onClose: function () {
|
||||
assert.step("onClose");
|
||||
},
|
||||
};
|
||||
await doAction(
|
||||
webClient,
|
||||
{
|
||||
type: "ir.actions.client",
|
||||
tag: "display_notification",
|
||||
params: {
|
||||
title: "title",
|
||||
message: "message",
|
||||
sticky: true,
|
||||
next: {
|
||||
type: "ir.actions.act_window_close",
|
||||
},
|
||||
},
|
||||
},
|
||||
options
|
||||
);
|
||||
const notificationSelector = ".o_notification_manager .o_notification";
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
notificationSelector,
|
||||
"a notification should be present"
|
||||
);
|
||||
assert.verifySteps(["onClose"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import testUtils from "web.test_utils";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
legacyExtraNextTick,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "../../helpers/utils";
|
||||
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { listView } from "../../../src/views/list/list_view";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module('"ir.actions.act_window_close" actions');
|
||||
|
||||
QUnit.test("close the currently opened dialog", async function (assert) {
|
||||
assert.expect(2);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
// execute an action in target="new"
|
||||
await doAction(webClient, 5);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".o_technical_modal .o_form_view",
|
||||
"should have rendered a form view in a modal"
|
||||
);
|
||||
// execute an 'ir.actions.act_window_close' action
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.act_window_close",
|
||||
});
|
||||
assert.containsNone(document.body, ".o_technical_modal", "should have closed the modal");
|
||||
});
|
||||
|
||||
QUnit.test("close dialog by clicking on the header button", async function (assert) {
|
||||
assert.expect(5);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
// execute an action in target="new"
|
||||
function onClose() {
|
||||
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.verifySteps(["on_close"]);
|
||||
|
||||
// execute an 'ir.actions.act_window_close' action
|
||||
// should not call 'on_close' as it was already called.
|
||||
await doAction(webClient, { type: "ir.actions.act_window_close" });
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test('execute "on_close" only if there is no dialog to close', async function (assert) {
|
||||
assert.expect(3);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
// execute an action in target="new"
|
||||
await doAction(webClient, 5);
|
||||
function onClose() {
|
||||
assert.step("on_close");
|
||||
}
|
||||
const options = { onClose };
|
||||
// execute an 'ir.actions.act_window_close' action
|
||||
// should not call 'on_close' as there is a dialog to close
|
||||
await doAction(webClient, { type: "ir.actions.act_window_close" }, options);
|
||||
assert.verifySteps([]);
|
||||
// execute again an 'ir.actions.act_window_close' action
|
||||
// should call 'on_close' as there is no dialog to close
|
||||
await doAction(webClient, { type: "ir.actions.act_window_close" }, options);
|
||||
assert.verifySteps(["on_close"]);
|
||||
});
|
||||
|
||||
QUnit.test("close action with provided infos", async function (assert) {
|
||||
assert.expect(1);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
const options = {
|
||||
onClose: function (infos) {
|
||||
assert.strictEqual(
|
||||
infos,
|
||||
"just for testing",
|
||||
"should have the correct close infos"
|
||||
);
|
||||
},
|
||||
};
|
||||
await doAction(
|
||||
webClient,
|
||||
{
|
||||
type: "ir.actions.act_window_close",
|
||||
infos: "just for testing",
|
||||
},
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("history back calls on_close handler of dialog action", async function (assert) {
|
||||
assert.expect(4);
|
||||
let form;
|
||||
patchWithCleanup(formView.Controller.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
form = this;
|
||||
},
|
||||
});
|
||||
const webClient = await createWebClient({ serverData });
|
||||
function onClose() {
|
||||
assert.step("on_close");
|
||||
}
|
||||
// open a new dialog form
|
||||
await doAction(webClient, 5, { onClose });
|
||||
assert.containsOnce(target, ".modal");
|
||||
form.env.config.historyBack();
|
||||
assert.verifySteps(["on_close"], "should have called the on_close handler");
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".modal");
|
||||
});
|
||||
|
||||
QUnit.test("history back called within on_close", async function (assert) {
|
||||
assert.expect(7);
|
||||
let list;
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
list = this;
|
||||
},
|
||||
});
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, 1);
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
|
||||
function onClose() {
|
||||
list.env.config.historyBack();
|
||||
assert.step("on_close");
|
||||
}
|
||||
// open a new dialog form
|
||||
await doAction(webClient, 5, { onClose });
|
||||
|
||||
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");
|
||||
assert.verifySteps(["on_close"]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"history back calls on_close handler of dialog action with 2 breadcrumbs",
|
||||
async function (assert) {
|
||||
assert.expect(7);
|
||||
let list;
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
list = this;
|
||||
},
|
||||
});
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1); // kanban
|
||||
await doAction(webClient, 3); // list
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
function onClose() {
|
||||
assert.step("on_close");
|
||||
}
|
||||
// open a new dialog form
|
||||
await doAction(webClient, 5, { onClose });
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
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: () => {},
|
||||
});
|
||||
|
||||
const readOnFirstRecordDef = testUtils.makeTestPromise();
|
||||
const mockRPC = (route, args) => {
|
||||
if (args.method === "read" && args.args[0][0] === 1) {
|
||||
return readOnFirstRecordDef;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 3);
|
||||
// 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.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");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,736 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
legacyExtraNextTick,
|
||||
makeDeferred,
|
||||
nextTick,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import {
|
||||
isItemSelected,
|
||||
toggleFilterMenu,
|
||||
toggleMenuItem,
|
||||
switchView,
|
||||
} from "@web/../tests/search/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useSetupView } from "@web/views/view_hook";
|
||||
import {
|
||||
createWebClient,
|
||||
doAction,
|
||||
getActionManagerServerData,
|
||||
loadState,
|
||||
} from "@web/../tests/webclient/helpers";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
const actionRegistry = registry.category("actions");
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("Concurrency management");
|
||||
|
||||
QUnit.test("drop previous actions if possible", async function (assert) {
|
||||
assert.expect(7);
|
||||
const def = makeDeferred();
|
||||
const mockRPC = async function (route) {
|
||||
assert.step(route);
|
||||
if (route === "/web/action/load") {
|
||||
await def;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
doAction(webClient, 4);
|
||||
doAction(webClient, 8);
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
// action 4 loads a kanban view first, 6 loads a list view. We want a list
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/web/action/load",
|
||||
"/web/dataset/call_kw/pony/get_views",
|
||||
"/web/dataset/call_kw/pony/web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("handle switching view and switching back on slow network", async function (assert) {
|
||||
assert.expect(9);
|
||||
const def = makeDeferred();
|
||||
const defs = [Promise.resolve(), def, Promise.resolve()];
|
||||
const mockRPC = async function (route, { method }) {
|
||||
assert.step(route);
|
||||
if (method === "web_search_read") {
|
||||
await defs.shift();
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 4);
|
||||
// kanban view is loaded, switch to list view
|
||||
await switchView(target, "list");
|
||||
// here, list view is not ready yet, because def is not resolved
|
||||
// switch back to kanban view
|
||||
await switchView(target, "kanban");
|
||||
// here, we want the kanban view to reload itself, regardless of list view
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/web/dataset/call_kw/partner/get_views",
|
||||
"/web/dataset/call_kw/partner/web_search_read",
|
||||
"/web/dataset/call_kw/partner/web_search_read",
|
||||
"/web/dataset/call_kw/partner/web_search_read",
|
||||
]);
|
||||
// we resolve def => list view is now ready (but we want to ignore it)
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_kanban_view", "there should be a kanban view in dom");
|
||||
assert.containsNone(target, ".o_list_view", "there should not be a list view in dom");
|
||||
});
|
||||
|
||||
QUnit.test("when an server action takes too much time...", async function (assert) {
|
||||
assert.expect(1);
|
||||
const def = makeDeferred();
|
||||
const mockRPC = async function (route, args) {
|
||||
if (route === "/web/action/run") {
|
||||
await def;
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
doAction(webClient, 2);
|
||||
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"
|
||||
);
|
||||
});
|
||||
|
||||
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") {
|
||||
await def;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
// create a situation with 3 breadcrumbs: kanban/form/list
|
||||
await doAction(webClient, 4);
|
||||
await click(target.querySelector(".o_kanban_record"));
|
||||
await doAction(webClient, 8);
|
||||
// now, the next read operations will be promise (this is the read
|
||||
// operation for the form view reload)
|
||||
def = makeDeferred();
|
||||
// click on the breadcrumbs for the form view, then on the kanban view
|
||||
// before the form view is fully reloaded
|
||||
await click(target.querySelectorAll(".o_control_panel .breadcrumb-item")[1]);
|
||||
await click(target.querySelector(".o_control_panel .breadcrumb-item"));
|
||||
// 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"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"execute a new action while loading a lazy-loaded controller",
|
||||
async function (assert) {
|
||||
assert.expect(16);
|
||||
let def;
|
||||
const mockRPC = async function (route, { method, model }) {
|
||||
assert.step(method || route);
|
||||
if (method === "web_search_read" && model === "partner") {
|
||||
await def;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await loadState(webClient, {
|
||||
action: 4,
|
||||
id: 2,
|
||||
view_type: "form",
|
||||
});
|
||||
assert.containsOnce(target, ".o_form_view", "should display the form view of action 4");
|
||||
// click to go back to Kanban (this request is blocked)
|
||||
def = makeDeferred();
|
||||
await click(target.querySelector(".o_control_panel .breadcrumb a"));
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_form_view",
|
||||
"should still display the form view of action 4"
|
||||
);
|
||||
// execute another action meanwhile (don't block this request)
|
||||
await doAction(webClient, 8, { clearBreadcrumbs: true });
|
||||
assert.containsOnce(target, ".o_list_view", "should display action 8");
|
||||
assert.containsNone(target, ".o_form_view", "should no longer display the form view");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"read",
|
||||
"web_search_read",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
// unblock the switch to Kanban in action 4
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_list_view", "should still display action 8");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_kanban_view",
|
||||
"should not display the kanban view of action 4"
|
||||
);
|
||||
assert.verifySteps([]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("execute a new action while handling a call_button", async function (assert) {
|
||||
assert.expect(17);
|
||||
const def = makeDeferred();
|
||||
const mockRPC = async function (route, args) {
|
||||
assert.step((args && args.method) || route);
|
||||
if (route === "/web/dataset/call_button") {
|
||||
await def;
|
||||
return serverData.actions[1];
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
// execute action 3 and open a record in form view
|
||||
await doAction(webClient, 3);
|
||||
await click(target.querySelector(".o_list_view .o_data_cell"));
|
||||
assert.containsOnce(target, ".o_form_view", "should display the form view of action 3");
|
||||
// click on 'Call method' button (this request is blocked)
|
||||
await click(target.querySelector('.o_form_view button[name="object"]'));
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_form_view",
|
||||
"should still display the form view of action 3"
|
||||
);
|
||||
// execute another action
|
||||
await doAction(webClient, 8, { clearBreadcrumbs: true });
|
||||
assert.containsOnce(target, ".o_list_view", "should display the list view of action 8");
|
||||
assert.containsNone(target, ".o_form_view", "should no longer display the form view");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"read",
|
||||
"object",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
// unblock the call_button request
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_list_view",
|
||||
"should still display the list view of action 8"
|
||||
);
|
||||
assert.containsNone(target, ".o_kanban_view", "should not display action 1");
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"execute a new action while switching to another controller",
|
||||
async function (assert) {
|
||||
assert.expect(16);
|
||||
// This test's bottom line is that a doAction always has priority
|
||||
// over a switch controller (clicking on a record row to go to form view).
|
||||
// In general, the last actionManager's operation has priority because we want
|
||||
// to allow the user to make mistakes, or to rapidly reconsider her next action.
|
||||
// Here we assert that the actionManager's RPC are in order, but a 'read' operation
|
||||
// is expected, with the current implementation, to take place when switching to the form view.
|
||||
// Ultimately the form view's 'read' is superfluous, but can happen at any point of the flow,
|
||||
// except at the very end, which should always be the final action's list's 'search_read'.
|
||||
let def;
|
||||
const mockRPC = async function (route, args) {
|
||||
assert.step((args && args.method) || route);
|
||||
if (args && args.method === "read") {
|
||||
await def;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view", "should display the list view of action 3");
|
||||
// switch to the form view (this request is blocked)
|
||||
def = makeDeferred();
|
||||
await click(target.querySelector(".o_list_view .o_data_cell"));
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_list_view",
|
||||
"should still display the list view of action 3"
|
||||
);
|
||||
// execute another action meanwhile (don't block this request)
|
||||
await doAction(webClient, 4, { clearBreadcrumbs: true });
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_view",
|
||||
"should display the kanban view of action 8"
|
||||
);
|
||||
assert.containsNone(target, ".o_list_view", "should no longer display the list view");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"read",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
// unblock the switch to the form view in action 3
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_view",
|
||||
"should still display the kanban view of action 8"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_form_view",
|
||||
"should not display the form view of action 3"
|
||||
);
|
||||
assert.verifySteps([]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("execute a new action while loading views", async function (assert) {
|
||||
assert.expect(11);
|
||||
const def = makeDeferred();
|
||||
const mockRPC = async function (route, args) {
|
||||
assert.step((args && args.method) || route);
|
||||
if (args && args.method === "get_views") {
|
||||
await def;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
// execute a first action (its 'get_views' RPC is blocked)
|
||||
doAction(webClient, 3);
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_list_view", "should not display the list view of action 3");
|
||||
// execute another action meanwhile (and unlock the RPC)
|
||||
doAction(webClient, 4);
|
||||
await nextTick();
|
||||
def.resolve();
|
||||
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.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("execute a new action while loading data of default view", async function (assert) {
|
||||
assert.expect(12);
|
||||
const def = makeDeferred();
|
||||
const mockRPC = async function (route, { method }) {
|
||||
assert.step(method || route);
|
||||
if (method === "web_search_read") {
|
||||
await def;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
// execute a first action (its 'search_read' RPC is blocked)
|
||||
doAction(webClient, 3);
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_list_view", "should not display the list view of action 3");
|
||||
// execute another action meanwhile (and unlock the RPC)
|
||||
doAction(webClient, 4);
|
||||
def.resolve();
|
||||
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.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
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") {
|
||||
await def;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
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");
|
||||
// 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");
|
||||
// open a record in form view
|
||||
await click(target.querySelector(".o_list_view .o_data_cell"));
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
assert.containsNone(target, ".o_control_panel .o_list_buttons");
|
||||
// unblock the search_read RPC
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
assert.containsNone(target, ".o_list_view");
|
||||
assert.containsNone(target, ".o_control_panel .o_list_buttons");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"properly drop client actions after new action is initiated",
|
||||
async function (assert) {
|
||||
assert.expect(3);
|
||||
const slowWillStartDef = makeDeferred();
|
||||
class ClientAction extends Component {
|
||||
setup() {
|
||||
owl.onWillStart(() => slowWillStartDef);
|
||||
}
|
||||
}
|
||||
ClientAction.template = xml`<div class="client_action">ClientAction</div>`;
|
||||
actionRegistry.add("slowAction", ClientAction);
|
||||
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");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"restoring a controller when doing an action -- load_action slow",
|
||||
async function (assert) {
|
||||
assert.expect(14);
|
||||
let def;
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step((args && args.method) || route);
|
||||
if (route === "/web/action/load") {
|
||||
return Promise.resolve(def);
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
await click(target.querySelector(".o_list_view .o_data_cell"));
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
def = makeDeferred();
|
||||
doAction(webClient, 4, { clearBreadcrumbs: true });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_form_view", "should still contain the form view");
|
||||
await click(target.querySelector(".o_control_panel .breadcrumb-item a"));
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_control_panel .breadcrumb-item").textContent,
|
||||
"Partners"
|
||||
);
|
||||
assert.containsNone(target, ".o_form_view");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"read",
|
||||
"/web/action/load",
|
||||
"web_search_read",
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("switching when doing an action -- load_action slow", async function (assert) {
|
||||
assert.expect(12);
|
||||
let def;
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step((args && args.method) || route);
|
||||
if (route === "/web/action/load") {
|
||||
return Promise.resolve(def);
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
def = makeDeferred();
|
||||
doAction(webClient, 4, { clearBreadcrumbs: true });
|
||||
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.containsNone(target, ".o_list_view");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"/web/action/load",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("switching when doing an action -- get_views slow", async function (assert) {
|
||||
assert.expect(13);
|
||||
let def;
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step((args && args.method) || route);
|
||||
if (args && args.method === "get_views") {
|
||||
return Promise.resolve(def);
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
def = makeDeferred();
|
||||
doAction(webClient, 4, { clearBreadcrumbs: true });
|
||||
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.containsNone(target, ".o_list_view");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("switching when doing an action -- search_read slow", async function (assert) {
|
||||
assert.expect(13);
|
||||
const def = makeDeferred();
|
||||
const defs = [null, def, null];
|
||||
const mockRPC = async (route, { method }) => {
|
||||
assert.step(method || route);
|
||||
if (method === "web_search_read") {
|
||||
await Promise.resolve(defs.shift());
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
doAction(webClient, 4, { clearBreadcrumbs: true });
|
||||
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.containsNone(target, ".o_list_view");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("click multiple times to open a record", async function (assert) {
|
||||
const def = makeDeferred();
|
||||
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_list_view");
|
||||
|
||||
await click(target.querySelector(".o_list_view .o_data_cell"));
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
|
||||
await click(target.querySelector(".o_back_button"));
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
|
||||
const row1 = target.querySelectorAll(".o_list_view .o_data_row")[0];
|
||||
const row2 = target.querySelectorAll(".o_list_view .o_data_row")[1];
|
||||
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"
|
||||
);
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".breadcrumb-item.active").innerText,
|
||||
"Second record"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"dialog will only open once for two rapid actions with the target new",
|
||||
async function (assert) {
|
||||
assert.expect(3)
|
||||
const def = makeDeferred();
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "onchange") {
|
||||
return def;
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
doAction(webClient, 5);
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_dialog .o_form_view");
|
||||
|
||||
doAction(webClient, 5);
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_dialog .o_form_view");
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_dialog .o_form_view", "dialog should open only once");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("local state, global state, and race conditions", async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,toy": `<toy/>`,
|
||||
"partner,false,list": `<list><field name="foo"/></list>`,
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<filter name="foo" string="Foo" domain="[]"/>
|
||||
</search>
|
||||
`,
|
||||
};
|
||||
|
||||
let def = Promise.resolve();
|
||||
|
||||
let id = 1;
|
||||
class ToyController extends Component {
|
||||
setup() {
|
||||
this.id = id++;
|
||||
assert.step(JSON.stringify(this.props.state || "no state"));
|
||||
useSetupView({
|
||||
getLocalState: () => {
|
||||
return { fromId: this.id };
|
||||
},
|
||||
});
|
||||
owl.onWillStart(() => def);
|
||||
}
|
||||
}
|
||||
ToyController.template = xml`
|
||||
<div class="o_toy_view">
|
||||
<ControlPanel />
|
||||
</div>`;
|
||||
ToyController.components = { ControlPanel };
|
||||
|
||||
registry.category("views").add("toy", {
|
||||
type: "toy",
|
||||
display_name: "Toy",
|
||||
icon: "fab fa-android",
|
||||
multiRecord: true,
|
||||
searchMenuTypes: ["filter"],
|
||||
Controller: ToyController,
|
||||
});
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, {
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
// list (or something else) must be added to have the view switcher displayed
|
||||
views: [
|
||||
[false, "toy"],
|
||||
[false, "list"],
|
||||
],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleMenuItem(target, "Foo");
|
||||
assert.ok(isItemSelected(target, "Foo"));
|
||||
|
||||
// reload twice by clicking on toy view switcher
|
||||
def = makeDeferred();
|
||||
await click(target.querySelector(".o_control_panel .o_switch_view.o_toy"));
|
||||
await click(target.querySelector(".o_control_panel .o_switch_view.o_toy"));
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
|
||||
await toggleFilterMenu(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
|
||||
// of the first instantiated toy view.
|
||||
|
||||
assert.verifySteps([
|
||||
`"no state"`, // setup first view instantiated
|
||||
`{"fromId":1}`, // setup second view instantiated
|
||||
`{"fromId":1}`, // setup third view instantiated
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import testUtils from "web.test_utils";
|
||||
import { clearRegistryWithCleanup } from "../../helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
legacyExtraNextTick,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "../../helpers/utils";
|
||||
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
|
||||
import { session } from "@web/session";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
const mainComponentRegistry = registry.category("main_components");
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("Effects");
|
||||
|
||||
QUnit.test("rainbowman integrated to webClient", async function (assert) {
|
||||
assert.expect(10);
|
||||
patchWithCleanup(session, { show_effect: true });
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
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
|
||||
// we let it die either after its animation or on user click
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_reward");
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
});
|
||||
|
||||
QUnit.test("on close with effect from server", async function (assert) {
|
||||
assert.expect(1);
|
||||
patchWithCleanup(session, { show_effect: true });
|
||||
const mockRPC = async (route) => {
|
||||
if (route === "/web/dataset/call_button") {
|
||||
return Promise.resolve({
|
||||
type: "ir.actions.act_window_close",
|
||||
effect: {
|
||||
type: "rainbow_man",
|
||||
message: "button called",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 6);
|
||||
await click(target.querySelector('button[name="object"]'));
|
||||
assert.containsOnce(target, ".o_reward");
|
||||
});
|
||||
|
||||
QUnit.test("on close with effect in xml", async function (assert) {
|
||||
assert.expect(2);
|
||||
serverData.views["partner,false,form"] = `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Call method" name="object" type="object"
|
||||
effect="{'type': 'rainbow_man', 'message': 'rainBowInXML'}"
|
||||
/>
|
||||
</header>
|
||||
<field name="display_name"/>
|
||||
</form>`;
|
||||
patchWithCleanup(session, { show_effect: true });
|
||||
const mockRPC = async (route) => {
|
||||
if (route === "/web/dataset/call_button") {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
};
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
|
||||
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,
|
||||
"rainBowInXML"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
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 { errorService } from "@web/core/errors/error_service";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
const actionRegistry = registry.category("actions");
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("Error handling");
|
||||
|
||||
QUnit.test("error in a client action (at rendering)", async function (assert) {
|
||||
assert.expect(4);
|
||||
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, "");
|
||||
await doAction(webClient, "1");
|
||||
const contents = target.querySelector(".o_action_manager").innerHTML;
|
||||
assert.ok(contents !== "");
|
||||
try {
|
||||
await doAction(webClient, "Boom");
|
||||
} catch (e) {
|
||||
assert.ok(e.cause instanceof TypeError);
|
||||
}
|
||||
assert.strictEqual(target.querySelector(".o_action_manager").innerHTML, contents);
|
||||
});
|
||||
|
||||
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: () => {},
|
||||
});
|
||||
|
||||
registry.category("services").add("error", errorService);
|
||||
|
||||
class Boom extends Component {
|
||||
setup() {
|
||||
this.boom = false;
|
||||
}
|
||||
onClick() {
|
||||
this.boom = true;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
Boom.template = xml`
|
||||
<div>
|
||||
<t t-if="boom" t-esc="a.b.c"/>
|
||||
<button t-else="" class="my_button" t-on-click="onClick">Click Me</button>
|
||||
</div>`;
|
||||
actionRegistry.add("Boom", Boom);
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, "Boom");
|
||||
assert.containsOnce(target, ".my_button");
|
||||
|
||||
await click(document.querySelector(".my_button"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".my_button");
|
||||
assert.containsOnce(target, ".o_dialog_error");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,776 @@
|
|||
/** @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);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,660 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
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 {
|
||||
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 { GraphModel } from "@web/views/graph/graph_model";
|
||||
import { fakeCookieService } from "../../helpers/mock_services";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
// legacy stuff
|
||||
const actionRegistry = registry.category("actions");
|
||||
const actionHandlersRegistry = registry.category("action_handlers");
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("Misc");
|
||||
|
||||
QUnit.test("can execute actions from id, xmlid and tag", async (assert) => {
|
||||
assert.expect(6);
|
||||
serverData.actions[1] = {
|
||||
tag: "client_action_by_db_id",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
};
|
||||
serverData.actions["wowl.some_action"] = {
|
||||
tag: "client_action_by_xml_id",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
};
|
||||
actionRegistry
|
||||
.add("client_action_by_db_id", () => assert.step("client_action_db_id"))
|
||||
.add("client_action_by_xml_id", () => assert.step("client_action_xml_id"))
|
||||
.add("client_action_by_object", () => assert.step("client_action_object"));
|
||||
setupWebClientRegistries();
|
||||
const env = await makeTestEnv({ serverData });
|
||||
await doAction(env, 1);
|
||||
assert.verifySteps(["client_action_db_id"]);
|
||||
await doAction(env, "wowl.some_action");
|
||||
assert.verifySteps(["client_action_xml_id"]);
|
||||
await doAction(env, {
|
||||
tag: "client_action_by_object",
|
||||
target: "current",
|
||||
type: "ir.actions.client",
|
||||
});
|
||||
assert.verifySteps(["client_action_object"]);
|
||||
});
|
||||
|
||||
QUnit.test("action doesn't exists", async (assert) => {
|
||||
assert.expect(1);
|
||||
setupWebClientRegistries();
|
||||
const env = await makeTestEnv({ serverData });
|
||||
try {
|
||||
await doAction(env, {
|
||||
tag: "this_is_a_tag",
|
||||
target: "current",
|
||||
type: "ir.not_action.error",
|
||||
});
|
||||
} catch (e) {
|
||||
assert.strictEqual(
|
||||
e.message,
|
||||
"The ActionManager service can't handle actions of type ir.not_action.error"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("action in handler registry", async (assert) => {
|
||||
assert.expect(2);
|
||||
setupWebClientRegistries();
|
||||
const env = await makeTestEnv({ serverData });
|
||||
actionHandlersRegistry.add("ir.action_in_handler_registry", ({ action }) =>
|
||||
assert.step(action.type)
|
||||
);
|
||||
await doAction(env, {
|
||||
tag: "this_is_a_tag",
|
||||
target: "current",
|
||||
type: "ir.action_in_handler_registry",
|
||||
});
|
||||
assert.verifySteps(["ir.action_in_handler_registry"]);
|
||||
});
|
||||
|
||||
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(webClient.env.services.notification, {
|
||||
add(message) {
|
||||
assert.strictEqual(message, "No action with id '4448' could be found");
|
||||
},
|
||||
});
|
||||
await doAction(webClient, 4448);
|
||||
assert.containsOnce(target, "div.o_invalid_action");
|
||||
});
|
||||
|
||||
QUnit.test("actions can be cached", async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (route === "/web/action/load") {
|
||||
assert.step(JSON.stringify(args));
|
||||
}
|
||||
};
|
||||
|
||||
setupWebClientRegistries();
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
|
||||
const loadAction = env.services.action.loadAction;
|
||||
|
||||
// With no additional params
|
||||
await loadAction(3);
|
||||
await loadAction(3);
|
||||
|
||||
// With specific additionalContext
|
||||
await loadAction(3, { additionalContext: { configuratorMode: "add" } });
|
||||
await loadAction(3, { additionalContext: { configuratorMode: "edit" } });
|
||||
|
||||
// With same active_id
|
||||
await loadAction(3, { active_id: 1 });
|
||||
await loadAction(3, { active_id: 1 });
|
||||
|
||||
// With active_id change
|
||||
await loadAction(3, { active_id: 2 });
|
||||
|
||||
// With same active_ids
|
||||
await loadAction(3, { active_ids: [1, 2] });
|
||||
await loadAction(3, { active_ids: [1, 2] });
|
||||
|
||||
// With active_ids change
|
||||
await loadAction(3, { active_ids: [1, 2, 3] });
|
||||
|
||||
// With same active_model
|
||||
await loadAction(3, { active_model: "a" });
|
||||
await loadAction(3, { active_model: "a" });
|
||||
|
||||
// With active_model change
|
||||
await loadAction(3, { active_model: "b" });
|
||||
|
||||
assert.verifySteps(
|
||||
[
|
||||
'{"action_id":3,"additional_context":{}}',
|
||||
'{"action_id":3,"additional_context":{"active_id":1}}',
|
||||
'{"action_id":3,"additional_context":{"active_id":2}}',
|
||||
'{"action_id":3,"additional_context":{"active_ids":[1,2]}}',
|
||||
'{"action_id":3,"additional_context":{"active_ids":[1,2,3]}}',
|
||||
'{"action_id":3,"additional_context":{"active_model":"a"}}',
|
||||
'{"action_id":3,"additional_context":{"active_model":"b"}}',
|
||||
],
|
||||
"should load from server once per active_id/active_ids/active_model change, nothing else"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("action cache: additionalContext is respected", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const mockRPC = async (route) => {
|
||||
if (route === "/web/action/load") {
|
||||
assert.step("server loaded");
|
||||
}
|
||||
};
|
||||
|
||||
setupWebClientRegistries();
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const { loadAction } = env.services.action;
|
||||
const actionParams = {
|
||||
additionalContext: {
|
||||
some: { deep: { nested: "Robert" } },
|
||||
},
|
||||
};
|
||||
|
||||
let action = await loadAction(3, actionParams);
|
||||
assert.verifySteps(["server loaded"]);
|
||||
assert.deepEqual(action.context, actionParams);
|
||||
|
||||
// Modify the action in place
|
||||
action.context.additionalContext.some.deep.nested = "Nesta";
|
||||
|
||||
// Change additionalContext and reload from cache
|
||||
actionParams.additionalContext.some.deep.nested = "Marley";
|
||||
action = await loadAction(3, actionParams);
|
||||
assert.verifySteps([], "loaded from cache");
|
||||
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");
|
||||
// push another action flagged with 'no_breadcrumbs=true'
|
||||
await doAction(webClient, 4);
|
||||
assert.containsNone(target, ".o_control_panel .breadcrumb-item");
|
||||
});
|
||||
|
||||
QUnit.test("document's title is updated when an action is executed", async function (assert) {
|
||||
const defaultTitle = { zopenerp: "Odoo" };
|
||||
const webClient = await createWebClient({ serverData });
|
||||
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);
|
||||
currentTitle = webClient.env.services.title.getParts();
|
||||
assert.deepEqual(currentTitle, {
|
||||
...defaultTitle,
|
||||
action: "Partners Action 4",
|
||||
});
|
||||
currentHash = webClient.env.services.router.current.hash;
|
||||
assert.deepEqual(currentHash, { action: 4, model: "partner", view_type: "kanban" });
|
||||
await doAction(webClient, 8);
|
||||
currentTitle = webClient.env.services.title.getParts();
|
||||
assert.deepEqual(currentTitle, {
|
||||
...defaultTitle,
|
||||
action: "Favorite Ponies",
|
||||
});
|
||||
currentHash = webClient.env.services.router.current.hash;
|
||||
assert.deepEqual(currentHash, { action: 8, model: "pony", view_type: "list" });
|
||||
await click(target.querySelector(".o_data_row .o_data_cell"));
|
||||
await nextTick();
|
||||
currentTitle = webClient.env.services.title.getParts();
|
||||
assert.deepEqual(currentTitle, {
|
||||
...defaultTitle,
|
||||
action: "Twilight Sparkle",
|
||||
});
|
||||
currentHash = webClient.env.services.router.current.hash;
|
||||
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);
|
||||
let list;
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
setup() {
|
||||
this._super(...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);
|
||||
list.env.config.historyBack();
|
||||
await testUtils.nextTick();
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_control_panel .breadcrumb-item");
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_control_panel .breadcrumb-item").text(),
|
||||
"Partners Action 4",
|
||||
"breadcrumbs should display the display_name of the action"
|
||||
);
|
||||
});
|
||||
|
||||
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}` });
|
||||
}
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("o_web_client");
|
||||
container.style.height = "250px";
|
||||
target.appendChild(container);
|
||||
const webClient = await createWebClient({ target: container, serverData });
|
||||
// execute a first action
|
||||
await doAction(webClient, 3);
|
||||
assert.strictEqual(target.querySelector(".o_content").scrollTop, 0);
|
||||
// simulate a scroll
|
||||
target.querySelector(".o_content").scrollTop = 100;
|
||||
// execute a second action (in which we don't scroll)
|
||||
await doAction(webClient, 4);
|
||||
assert.strictEqual(target.querySelector(".o_content").scrollTop, 0);
|
||||
// go back using the breadcrumbs
|
||||
await click(target.querySelector(".o_control_panel .breadcrumb a"));
|
||||
assert.strictEqual(target.querySelector(".o_content").scrollTop, 100);
|
||||
});
|
||||
|
||||
QUnit.test("stores and restores scroll position (in list)", async function (assert) {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
serverData.models.partner.records.push({ id: 100 + i, foo: `Record ${i}` });
|
||||
}
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("o_web_client");
|
||||
container.style.height = "250px";
|
||||
target.appendChild(container);
|
||||
const webClient = await createWebClient({ target: container, serverData });
|
||||
// execute a first action
|
||||
await doAction(webClient, 3);
|
||||
assert.strictEqual(target.querySelector(".o_content").scrollTop, 0);
|
||||
assert.strictEqual(target.querySelector(".o_list_renderer").scrollTop, 0);
|
||||
// simulate a scroll
|
||||
target.querySelector(".o_list_renderer").scrollTop = 100;
|
||||
await nextTick();
|
||||
// execute a second action (in which we don't scroll)
|
||||
await doAction(webClient, 4);
|
||||
assert.strictEqual(target.querySelector(".o_content").scrollTop, 0);
|
||||
// go back using the breadcrumbs
|
||||
await click(target.querySelector(".o_control_panel .breadcrumb a"));
|
||||
assert.strictEqual(target.querySelector(".o_content").scrollTop, 0);
|
||||
assert.strictEqual(target.querySelector(".o_list_renderer").scrollTop, 100);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
'executing an action with target != "new" closes all dialogs',
|
||||
async function (assert) {
|
||||
serverData.views["partner,false,form"] = `
|
||||
<form>
|
||||
<field name="o2m">
|
||||
<tree><field name="foo"/></tree>
|
||||
<form><field name="foo"/></form>
|
||||
</field>
|
||||
</form>
|
||||
`;
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
await click(target.querySelector(".o_list_view .o_data_row .o_list_char"));
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
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'
|
||||
assert.containsNone(document.body, ".modal");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
'executing an action with target "new" does not close dialogs',
|
||||
async function (assert) {
|
||||
assert.expect(4);
|
||||
serverData.views["partner,false,form"] = `
|
||||
<form>
|
||||
<field name="o2m">
|
||||
<tree><field name="foo"/></tree>
|
||||
<form><field name="foo"/></form>
|
||||
</field>
|
||||
</form>
|
||||
`;
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
await click(target.querySelector(".o_list_view .o_data_row .o_data_cell"));
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
await click(target.querySelector(".o_form_view .o_data_row .o_data_cell"));
|
||||
assert.containsOnce(document.body, ".modal .o_form_view");
|
||||
await doAction(webClient, 5); // target 'new'
|
||||
assert.containsN(document.body, ".modal .o_form_view", 2);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"search defaults are removed from context when switching view",
|
||||
async function (assert) {
|
||||
assert.expect(1);
|
||||
serverData.views["partner,false,graph"] = `<graph/>`;
|
||||
serverData.views["partner,false,list"] = `<list/>`;
|
||||
const context = {
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, {
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "graph"],
|
||||
],
|
||||
context,
|
||||
});
|
||||
// list view is loaded, switch to graph view
|
||||
await cpHelpers.switchView(target, "graph");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"retrieving a stored action should remove 'allowed_company_ids' from its context",
|
||||
async function (assert) {
|
||||
// Prepare a multi company scenario
|
||||
session.user_companies = {
|
||||
allowed_companies: {
|
||||
3: { id: 3, name: "Hermit", sequence: 1 },
|
||||
2: { id: 2, name: "Herman's", sequence: 2 },
|
||||
1: { id: 1, name: "Heroes TM", sequence: 3 },
|
||||
},
|
||||
current_company: 3,
|
||||
};
|
||||
registry.category("services").add("company", companyService);
|
||||
|
||||
// Prepare a stored action
|
||||
browser.sessionStorage.setItem(
|
||||
"current_action",
|
||||
JSON.stringify({
|
||||
...serverData.actions[1],
|
||||
context: {
|
||||
someKey: 44,
|
||||
allowed_company_ids: [1, 2],
|
||||
lang: "not_en",
|
||||
tz: "not_taht",
|
||||
uid: 42,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Prepare the URL hash to make sure the stored action will get executed.
|
||||
browser.location.hash = "#model=partner&view_type=kanban";
|
||||
|
||||
// Create the web client. It should execute the stored action.
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
// Check the current action context
|
||||
assert.deepEqual(webClient.env.services.action.currentController.action.context, {
|
||||
// action context
|
||||
someKey: 44,
|
||||
lang: "not_en",
|
||||
tz: "not_taht",
|
||||
uid: 42,
|
||||
// note there is no 'allowed_company_ids' in the action context
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
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 { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
const actionRegistry = registry.category("actions");
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("Push State");
|
||||
|
||||
QUnit.test("basic action as App", async (assert) => {
|
||||
assert.expect(5);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
let urlState = webClient.env.services.router.current;
|
||||
assert.deepEqual(urlState.hash, {});
|
||||
await click(target, ".o_navbar_apps_menu button");
|
||||
await click(target, ".o_navbar_apps_menu .dropdown-item:nth-child(3)");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
urlState = webClient.env.services.router.current;
|
||||
assert.strictEqual(urlState.hash.action, 1002);
|
||||
assert.strictEqual(urlState.hash.menu_id, 2);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".test_client_action").textContent.trim(),
|
||||
"ClientAction_Id 2"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "App2");
|
||||
});
|
||||
|
||||
QUnit.test("do action keeps menu in url", async (assert) => {
|
||||
assert.expect(9);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
let urlState = webClient.env.services.router.current;
|
||||
assert.deepEqual(urlState.hash, {});
|
||||
await click(target, ".o_navbar_apps_menu button");
|
||||
await click(target, ".o_navbar_apps_menu .dropdown-item:nth-child(3)");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
urlState = webClient.env.services.router.current;
|
||||
assert.strictEqual(urlState.hash.action, 1002);
|
||||
assert.strictEqual(urlState.hash.menu_id, 2);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".test_client_action").textContent.trim(),
|
||||
"ClientAction_Id 2"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "App2");
|
||||
await doAction(webClient, 1001, { clearBreadcrumbs: true });
|
||||
urlState = webClient.env.services.router.current;
|
||||
assert.strictEqual(urlState.hash.action, 1001);
|
||||
assert.strictEqual(urlState.hash.menu_id, 2);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".test_client_action").textContent.trim(),
|
||||
"ClientAction_Id 1"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "App2");
|
||||
});
|
||||
|
||||
QUnit.test("actions can push state", async (assert) => {
|
||||
assert.expect(5);
|
||||
class ClientActionPushes extends Component {
|
||||
setup() {
|
||||
this.router = useService("router");
|
||||
}
|
||||
_actionPushState() {
|
||||
this.router.pushState({ arbitrary: "actionPushed" });
|
||||
}
|
||||
}
|
||||
ClientActionPushes.template = xml`
|
||||
<div class="test_client_action" t-on-click="_actionPushState">
|
||||
ClientAction_<t t-esc="props.params and props.params.description" />
|
||||
</div>`;
|
||||
actionRegistry.add("client_action_pushes", ClientActionPushes);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
let urlState = webClient.env.services.router.current;
|
||||
assert.deepEqual(urlState.hash, {});
|
||||
await doAction(webClient, "client_action_pushes");
|
||||
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");
|
||||
urlState = webClient.env.services.router.current;
|
||||
assert.strictEqual(urlState.hash.action, "client_action_pushes");
|
||||
assert.strictEqual(urlState.hash.arbitrary, "actionPushed");
|
||||
});
|
||||
|
||||
QUnit.test("actions override previous state", async (assert) => {
|
||||
assert.expect(5);
|
||||
class ClientActionPushes extends Component {
|
||||
setup() {
|
||||
this.router = useService("router");
|
||||
}
|
||||
_actionPushState() {
|
||||
this.router.pushState({ arbitrary: "actionPushed" });
|
||||
}
|
||||
}
|
||||
ClientActionPushes.template = xml`
|
||||
<div class="test_client_action" t-on-click="_actionPushState">
|
||||
ClientAction_<t t-esc="props.params and props.params.description" />
|
||||
</div>`;
|
||||
actionRegistry.add("client_action_pushes", ClientActionPushes);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
let urlState = webClient.env.services.router.current;
|
||||
assert.deepEqual(urlState.hash, {});
|
||||
await doAction(webClient, "client_action_pushes");
|
||||
await click(target, ".test_client_action");
|
||||
urlState = webClient.env.services.router.current;
|
||||
assert.strictEqual(urlState.hash.action, "client_action_pushes");
|
||||
assert.strictEqual(urlState.hash.arbitrary, "actionPushed");
|
||||
await doAction(webClient, 1001);
|
||||
urlState = webClient.env.services.router.current;
|
||||
assert.strictEqual(urlState.hash.action, 1001);
|
||||
assert.strictEqual(urlState.hash.arbitrary, undefined);
|
||||
});
|
||||
|
||||
QUnit.test("actions override previous state from menu click", async (assert) => {
|
||||
assert.expect(3);
|
||||
class ClientActionPushes extends Component {
|
||||
setup() {
|
||||
this.router = useService("router");
|
||||
}
|
||||
_actionPushState() {
|
||||
this.router.pushState({ arbitrary: "actionPushed" });
|
||||
}
|
||||
}
|
||||
ClientActionPushes.template = xml`
|
||||
<div class="test_client_action" t-on-click="_actionPushState">
|
||||
ClientAction_<t t-esc="props.params and props.params.description" />
|
||||
</div>`;
|
||||
actionRegistry.add("client_action_pushes", ClientActionPushes);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
let urlState = webClient.env.services.router.current;
|
||||
assert.deepEqual(urlState.hash, {});
|
||||
await doAction(webClient, "client_action_pushes");
|
||||
await click(target, ".test_client_action");
|
||||
await click(target, ".o_navbar_apps_menu button");
|
||||
await click(target, ".o_navbar_apps_menu .dropdown-item:nth-child(3)");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
urlState = webClient.env.services.router.current;
|
||||
assert.strictEqual(urlState.hash.action, 1002);
|
||||
assert.strictEqual(urlState.hash.menu_id, 2);
|
||||
});
|
||||
|
||||
QUnit.test("action in target new do not push state", async (assert) => {
|
||||
assert.expect(1);
|
||||
serverData.actions[1001].target = "new";
|
||||
patchWithCleanup(browser, {
|
||||
history: Object.assign({}, browser.history, {
|
||||
pushState() {
|
||||
throw new Error("should not push state");
|
||||
},
|
||||
}),
|
||||
});
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1001);
|
||||
assert.containsOnce(target, ".modal .test_client_action");
|
||||
});
|
||||
|
||||
QUnit.test("properly push state", async function (assert) {
|
||||
assert.expect(3);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 4);
|
||||
assert.deepEqual(webClient.env.services.router.current.hash, {
|
||||
action: 4,
|
||||
model: "partner",
|
||||
view_type: "kanban",
|
||||
});
|
||||
await doAction(webClient, 8);
|
||||
assert.deepEqual(webClient.env.services.router.current.hash, {
|
||||
action: 8,
|
||||
model: "pony",
|
||||
view_type: "list",
|
||||
});
|
||||
await testUtils.dom.click($(target).find("tr .o_data_cell:first"));
|
||||
await nextTick();
|
||||
assert.deepEqual(webClient.env.services.router.current.hash, {
|
||||
action: 8,
|
||||
model: "pony",
|
||||
view_type: "form",
|
||||
id: 4,
|
||||
});
|
||||
});
|
||||
|
||||
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") {
|
||||
await def;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
doAction(webClient, 4);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.deepEqual(webClient.env.services.router.current.hash, {});
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.deepEqual(webClient.env.services.router.current.hash, {
|
||||
action: 4,
|
||||
model: "partner",
|
||||
view_type: "kanban",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("do not push state when action fails", async function (assert) {
|
||||
assert.expect(3);
|
||||
const mockRPC = async function (route, args) {
|
||||
if (args && args.method === "read") {
|
||||
return Promise.reject();
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 8);
|
||||
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");
|
||||
assert.deepEqual(webClient.env.services.router.current.hash, {
|
||||
action: 8,
|
||||
model: "pony",
|
||||
view_type: "list",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
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 {
|
||||
createWebClient,
|
||||
doAction,
|
||||
getActionManagerServerData,
|
||||
} from "@web/../tests/webclient/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
clearRegistryWithCleanup(registry.category("main_components"));
|
||||
});
|
||||
|
||||
QUnit.module("Report actions");
|
||||
|
||||
QUnit.test("can execute report actions from db ID", async function (assert) {
|
||||
assert.expect(6);
|
||||
mockDownload((options) => {
|
||||
assert.step(options.url);
|
||||
return Promise.resolve();
|
||||
});
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step((args && args.method) || route);
|
||||
if (route === "/report/check_wkhtmltopdf") {
|
||||
return Promise.resolve("ok");
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 7, { onClose: () => assert.step("on_close") });
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/report/check_wkhtmltopdf",
|
||||
"/report/download",
|
||||
"on_close",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("report actions can close modals and reload views", async function (assert) {
|
||||
assert.expect(8);
|
||||
mockDownload((options) => {
|
||||
assert.step(options.url);
|
||||
return Promise.resolve();
|
||||
});
|
||||
const mockRPC = async (route) => {
|
||||
if (route === "/report/check_wkhtmltopdf") {
|
||||
return Promise.resolve("ok");
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 5, { onClose: () => assert.step("on_close") });
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".o_technical_modal .o_form_view",
|
||||
"should have rendered a form view in a modal"
|
||||
);
|
||||
await doAction(webClient, 7, { onClose: () => assert.step("on_printed") });
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".o_technical_modal .o_form_view",
|
||||
"The modal should still exist"
|
||||
);
|
||||
await doAction(webClient, 11);
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".o_technical_modal .o_form_view",
|
||||
"the modal should have been closed after the action report"
|
||||
);
|
||||
assert.verifySteps(["/report/download", "on_printed", "/report/download", "on_close"]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"should trigger a notification if wkhtmltopdf is to upgrade",
|
||||
async function (assert) {
|
||||
serviceRegistry.add(
|
||||
"notification",
|
||||
makeFakeNotificationService(
|
||||
() => {
|
||||
assert.step("notify");
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
);
|
||||
mockDownload((options) => {
|
||||
assert.step(options.url);
|
||||
return Promise.resolve();
|
||||
});
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step((args && args.method) || route);
|
||||
if (route === "/report/check_wkhtmltopdf") {
|
||||
return Promise.resolve("upgrade");
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 7);
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/report/check_wkhtmltopdf",
|
||||
"notify",
|
||||
"/report/download",
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"should open the report client action if wkhtmltopdf is broken",
|
||||
async function (assert) {
|
||||
mockDownload(() => {
|
||||
assert.step("download"); // should not be called
|
||||
return Promise.resolve();
|
||||
});
|
||||
serviceRegistry.add(
|
||||
"notification",
|
||||
makeFakeNotificationService(
|
||||
() => {
|
||||
assert.step("notify");
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
);
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step(args.method || route);
|
||||
if (route === "/report/check_wkhtmltopdf") {
|
||||
return Promise.resolve("broken");
|
||||
}
|
||||
if (route.includes("/report/html/some_report")) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
// patch the report client action to override its iframe's url so that
|
||||
// it doesn't trigger an RPC when it is appended to the DOM (for this
|
||||
// usecase, using removeSRCAttribute doesn't work as the RPC is
|
||||
// triggered as soon as the iframe is in the DOM, even if its src
|
||||
// attribute is removed right after)
|
||||
patchWithCleanup(ReportAction.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
this.env.services.rpc(this.reportUrl);
|
||||
this.reportUrl = "about:blank";
|
||||
},
|
||||
});
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 7);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_content iframe",
|
||||
"should have opened the report client action"
|
||||
);
|
||||
assert.containsOnce(target, "button[title='Print']", "should have a print button");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/report/check_wkhtmltopdf",
|
||||
"notify",
|
||||
// context={"lang":'en',"uid":7,"tz":'taht'}
|
||||
"/report/html/some_report?context=%7B%22lang%22%3A%22en%22%2C%22uid%22%3A7%2C%22tz%22%3A%22taht%22%7D",
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("send context in case of html report", async function (assert) {
|
||||
assert.expect(5);
|
||||
mockDownload(() => {
|
||||
assert.step("download"); // should not be called
|
||||
return Promise.resolve();
|
||||
});
|
||||
serviceRegistry.add(
|
||||
"notification",
|
||||
makeFakeNotificationService(
|
||||
(message, options) => {
|
||||
assert.step(options.type || "notification");
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
);
|
||||
patchWithCleanup(session.user_context, { some_key: 2 });
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step(args.method || route);
|
||||
if (route.includes("/report/html/some_report")) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
// patch the report client action to override its iframe's url so that
|
||||
// it doesn't trigger an RPC when it is appended to the DOM (for this
|
||||
// usecase, using removeSRCAttribute doesn't work as the RPC is
|
||||
// triggered as soon as the iframe is in the DOM, even if its src
|
||||
// attribute is removed right after)
|
||||
patchWithCleanup(ReportAction.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
this.env.services.rpc(this.reportUrl);
|
||||
this.reportUrl = "about:blank";
|
||||
},
|
||||
});
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 12);
|
||||
assert.containsOnce(target, ".o_content iframe", "should have opened the client action");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
// context={"lang":'en',"uid":7,"tz":'taht',"some_key":2}
|
||||
"/report/html/some_report?context=%7B%22lang%22%3A%22en%22%2C%22uid%22%3A7%2C%22tz%22%3A%22taht%22%2C%22some_key%22%3A2%7D",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"UI unblocks after downloading the report even if it threw an error",
|
||||
async function (assert) {
|
||||
assert.expect(8);
|
||||
let timesDownloasServiceHasBeenCalled = 0;
|
||||
mockDownload(() => {
|
||||
if (timesDownloasServiceHasBeenCalled === 0) {
|
||||
assert.step("successful download");
|
||||
timesDownloasServiceHasBeenCalled++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (timesDownloasServiceHasBeenCalled === 1) {
|
||||
assert.step("failed download");
|
||||
return Promise.reject();
|
||||
}
|
||||
});
|
||||
serviceRegistry.add("ui", uiService);
|
||||
const mockRPC = async (route) => {
|
||||
if (route === "/report/check_wkhtmltopdf") {
|
||||
return Promise.resolve("ok");
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
const ui = webClient.env.services.ui;
|
||||
const onBlock = () => {
|
||||
assert.step("block");
|
||||
};
|
||||
const onUnblock = () => {
|
||||
assert.step("unblock");
|
||||
};
|
||||
ui.bus.addEventListener("BLOCK", onBlock);
|
||||
ui.bus.addEventListener("UNBLOCK", onUnblock);
|
||||
await doAction(webClient, 7);
|
||||
try {
|
||||
await doAction(webClient, 7);
|
||||
} catch (_e) {
|
||||
assert.step("error caught");
|
||||
}
|
||||
assert.verifySteps([
|
||||
"block",
|
||||
"successful download",
|
||||
"unblock",
|
||||
"block",
|
||||
"failed download",
|
||||
"unblock",
|
||||
"error caught",
|
||||
]);
|
||||
ui.bus.removeEventListener("BLOCK", onBlock);
|
||||
ui.bus.removeEventListener("UNBLOCK", onUnblock);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("can use custom handlers for report actions", async function (assert) {
|
||||
assert.expect(8);
|
||||
mockDownload((options) => {
|
||||
assert.step(options.url);
|
||||
return Promise.resolve();
|
||||
});
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step((args && args.method) || route);
|
||||
if (route === "/report/check_wkhtmltopdf") {
|
||||
return "ok";
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
let customHandlerCalled = false;
|
||||
registry.category("ir.actions.report handlers").add("custom_handler", async (action) => {
|
||||
if (action.id === 7 && !customHandlerCalled) {
|
||||
customHandlerCalled = true;
|
||||
assert.step("calling custom handler");
|
||||
return true;
|
||||
}
|
||||
assert.step("falling through to default handler");
|
||||
});
|
||||
await doAction(webClient, 7);
|
||||
assert.step("first doAction finished");
|
||||
await doAction(webClient, 7);
|
||||
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"calling custom handler",
|
||||
"first doAction finished",
|
||||
"falling through to default handler",
|
||||
"/report/check_wkhtmltopdf",
|
||||
"/report/download",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("context is correctly passed to the client action report", async (assert) => {
|
||||
assert.expect(8);
|
||||
|
||||
mockDownload((options) => {
|
||||
assert.step(options.url);
|
||||
assert.deepEqual(JSON.parse(options.data.data), [
|
||||
"/report/pdf/ennio.morricone/99",
|
||||
"qweb-pdf",
|
||||
]);
|
||||
return Promise.resolve();
|
||||
});
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step((args && args.method) || route);
|
||||
if (route === "/report/check_wkhtmltopdf") {
|
||||
return "ok";
|
||||
}
|
||||
if (route.includes("/report/html")) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
patchWithCleanup(ReportAction.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
this.env.services.rpc(this.reportUrl);
|
||||
this.reportUrl = "about:blank";
|
||||
},
|
||||
});
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
const action = {
|
||||
context: {
|
||||
rabbia: "E Tarantella",
|
||||
active_ids: [99],
|
||||
},
|
||||
data: null,
|
||||
name: "Ennio Morricone",
|
||||
report_name: "ennio.morricone",
|
||||
report_type: "qweb-html",
|
||||
type: "ir.actions.report",
|
||||
};
|
||||
assert.verifySteps(["/web/webclient/load_menus"]);
|
||||
|
||||
await doAction(webClient, action);
|
||||
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']"));
|
||||
assert.verifySteps(["/report/check_wkhtmltopdf", "/report/download"]);
|
||||
});
|
||||
|
||||
QUnit.test("url is valid", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
patchWithCleanup(ReportAction.prototype, {
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.reportUrl = "about:blank";
|
||||
},
|
||||
});
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, 12); // 12 is a html report action in serverData
|
||||
|
||||
const hash = webClient.router.current.hash;
|
||||
// used to put report.client_action in the url
|
||||
assert.strictEqual(hash.action === "report.client_action", false);
|
||||
assert.strictEqual(hash.action === 12, true);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
|
||||
import { getFixture } from "../../helpers/utils";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("Server actions");
|
||||
|
||||
QUnit.test("can execute server actions from db ID", async function (assert) {
|
||||
assert.expect(10);
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step((args && args.method) || route);
|
||||
if (route === "/web/action/run") {
|
||||
assert.strictEqual(args.action_id, 2, "should call the correct server action");
|
||||
return Promise.resolve(1); // execute action 1
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 2);
|
||||
assert.containsOnce(target, ".o_control_panel", "should have rendered a control panel");
|
||||
assert.containsOnce(target, ".o_kanban_view", "should have rendered a kanban view");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/web/action/run",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("handle server actions returning false", async function (assert) {
|
||||
assert.expect(10);
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step((args && args.method) || route);
|
||||
if (route === "/web/action/run") {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
// execute an action in target="new"
|
||||
function onClose() {
|
||||
assert.step("close handler");
|
||||
}
|
||||
await doAction(webClient, 5, { onClose });
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".o_technical_modal .o_form_view",
|
||||
"should have rendered a form view in a modal"
|
||||
);
|
||||
// execute a server action that returns false
|
||||
await doAction(webClient, 2);
|
||||
assert.containsNone(document.body, ".o_technical_modal", "should have closed the modal");
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"onchange",
|
||||
"/web/action/load",
|
||||
"/web/action/run",
|
||||
"close handler",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("send correct context when executing a server action", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
serverData.actions[2].context = { someKey: 44 };
|
||||
const mockRPC = async (route, args) => {
|
||||
if (route === "/web/action/run") {
|
||||
assert.deepEqual(args.context, {
|
||||
// user context
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
// action context
|
||||
someKey: 44,
|
||||
});
|
||||
return Promise.resolve(1); // execute action 1
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 2);
|
||||
});
|
||||
|
||||
QUnit.test("action with html help returned by a server action", async function (assert) {
|
||||
serverData.actions[2].context = { someKey: 44 };
|
||||
const mockRPC = async (route, args) => {
|
||||
if (route === "/web/action/run") {
|
||||
return Promise.resolve({
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
help: "<p>I am not a helper</p>",
|
||||
domain: [[0, "=", 1]],
|
||||
});
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 2);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_list_view .o_nocontent_help p").innerText,
|
||||
"I am not a helper"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,673 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import testUtils from "web.test_utils";
|
||||
import core from "web.core";
|
||||
import AbstractAction from "web.AbstractAction";
|
||||
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";
|
||||
|
||||
import { Component, onMounted, xml } from "@odoo/owl";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module('Actions in target="new"');
|
||||
|
||||
QUnit.test('can execute act_window actions in target="new"', async function (assert) {
|
||||
assert.expect(8);
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step((args && args.method) || route);
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 5);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".o_technical_modal .o_form_view",
|
||||
"should have rendered a form view in a modal"
|
||||
);
|
||||
assert.hasClass(
|
||||
$(".o_technical_modal .modal-body")[0],
|
||||
"o_act_window",
|
||||
"dialog main element should have classname 'o_act_window'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".o_technical_modal .o_form_view .o_form_editable",
|
||||
"form view should be in edit mode"
|
||||
);
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"onchange",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("chained action on_close", async function (assert) {
|
||||
assert.expect(4);
|
||||
function onClose(closeInfo) {
|
||||
assert.strictEqual(closeInfo, "smallCandle");
|
||||
assert.step("Close Action");
|
||||
}
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 5, { onClose });
|
||||
// a target=new action shouldn't activate the on_close
|
||||
await doAction(webClient, 5);
|
||||
assert.verifySteps([]);
|
||||
// An act_window_close should trigger the on_close
|
||||
await doAction(webClient, { type: "ir.actions.act_window_close", infos: "smallCandle" });
|
||||
assert.verifySteps(["Close Action"]);
|
||||
});
|
||||
|
||||
QUnit.test("footer buttons are moved to the dialog footer", async function (assert) {
|
||||
assert.expect(3);
|
||||
serverData.views["partner,false,form"] = `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<footer>
|
||||
<button string="Create" type="object" class="infooter"/>
|
||||
</footer>
|
||||
</form>
|
||||
`;
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 5);
|
||||
assert.containsNone(
|
||||
$(".o_technical_modal .modal-body")[0],
|
||||
"button.infooter",
|
||||
"the button should not be in the body"
|
||||
);
|
||||
assert.containsOnce(
|
||||
$(".o_technical_modal .modal-footer")[0],
|
||||
"button.infooter",
|
||||
"the button should be in the footer"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".modal-footer button:not(.d-none)",
|
||||
"the modal footer should only contain one visible button"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Button with `close` attribute closes dialog", async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,form": `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Open dialog" name="5" type="action"/>
|
||||
</header>
|
||||
</form>`,
|
||||
"partner,view_ref,form": `
|
||||
<form>
|
||||
<footer>
|
||||
<button string="I close the dialog" name="some_method" type="object" close="1"/>
|
||||
</footer>
|
||||
</form>`,
|
||||
"partner,false,search": "<search></search>",
|
||||
};
|
||||
serverData.actions[4] = {
|
||||
id: 4,
|
||||
name: "Partners Action 4",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
};
|
||||
serverData.actions[5] = {
|
||||
id: 5,
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [["view_ref", "form"]],
|
||||
};
|
||||
const mockRPC = async (route, args) => {
|
||||
assert.step(route);
|
||||
if (route === "/web/dataset/call_button" && args.method === "some_method") {
|
||||
return {
|
||||
tag: "display_notification",
|
||||
type: "ir.actions.client",
|
||||
};
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
assert.verifySteps(["/web/webclient/load_menus"]);
|
||||
await doAction(webClient, 4);
|
||||
assert.verifySteps([
|
||||
"/web/action/load",
|
||||
"/web/dataset/call_kw/partner/get_views",
|
||||
"/web/dataset/call_kw/partner/onchange",
|
||||
]);
|
||||
await testUtils.dom.click(`button[name="5"]`);
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/partner/create",
|
||||
"/web/dataset/call_kw/partner/read",
|
||||
"/web/action/load",
|
||||
"/web/dataset/call_kw/partner/get_views",
|
||||
"/web/dataset/call_kw/partner/onchange",
|
||||
]);
|
||||
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_button",
|
||||
"/web/dataset/call_kw/partner/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) {
|
||||
serverData.views["partner,false,form"] = `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<footer>
|
||||
<button string="Create" type="object" class="infooter"/>
|
||||
</footer>
|
||||
</form>
|
||||
`;
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 5);
|
||||
assert.containsNone(target, '.o_technical_modal .modal-body button[special="save"]');
|
||||
assert.containsNone(target, ".o_technical_modal .modal-body button.infooter");
|
||||
assert.containsOnce(target, ".o_technical_modal .modal-footer button.infooter");
|
||||
assert.containsOnce(target, ".o_technical_modal .modal-footer button:not(.d-none)");
|
||||
await doAction(webClient, 25);
|
||||
assert.containsNone(target, ".o_technical_modal .modal-body button.infooter");
|
||||
assert.containsNone(target, ".o_technical_modal .modal-footer button.infooter");
|
||||
assert.containsNone(target, '.o_technical_modal .modal-body button[special="save"]');
|
||||
assert.containsOnce(target, '.o_technical_modal .modal-footer button[special="save"]');
|
||||
assert.containsOnce(target, ".o_technical_modal .modal-footer button:not(.d-none)");
|
||||
}
|
||||
);
|
||||
|
||||
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) {
|
||||
serverData.actions[999] = {
|
||||
id: 999,
|
||||
name: "A window action",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[999, "form"]],
|
||||
};
|
||||
serverData.views["partner,999,form"] = `
|
||||
<form>
|
||||
<button name="method" string="Call method" type="object" confirm="Are you sure?"/>
|
||||
</form>`;
|
||||
serverData.views["partner,1000,form"] = `<form>Another action</form>`;
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (args.method === "method") {
|
||||
return Promise.resolve({
|
||||
id: 1000,
|
||||
name: "Another window action",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[1000, "form"]],
|
||||
});
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
await doAction(webClient, 999);
|
||||
|
||||
assert.containsOnce(document.body, ".modal button[name=method]");
|
||||
|
||||
await testUtils.dom.click($(".modal button[name=method]"));
|
||||
|
||||
assert.containsN(document.body, ".modal", 2);
|
||||
assert.strictEqual($(".modal:last .modal-body").text(), "Are you sure?");
|
||||
|
||||
await testUtils.dom.click($(".modal:last .modal-footer .btn-primary"));
|
||||
// needs two renderings to close the ConfirmationDialog:
|
||||
// - 1 to open the next dialog (the action in target="new")
|
||||
// - 1 to close the ConfirmationDialog, once the next action is executed
|
||||
await nextTick();
|
||||
assert.containsOnce(document.body, ".modal");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".modal main .o_content").innerText.trim(),
|
||||
"Another action"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test('actions in target="new" do not update page title', async function (assert) {
|
||||
const mockedTitleService = {
|
||||
start() {
|
||||
return {
|
||||
setParts({ action }) {
|
||||
if (action) {
|
||||
assert.step(action);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
registry.category("services").add("title", mockedTitleService);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
// sanity check: execute an action in target="current"
|
||||
await doAction(webClient, 1);
|
||||
assert.verifySteps(["Partners Action 1"]);
|
||||
|
||||
// execute an action in target="new"
|
||||
await doAction(webClient, 5);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
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: () => {},
|
||||
});
|
||||
|
||||
class ErrorClientAction extends Component {
|
||||
setup() {
|
||||
throw new Error("my error");
|
||||
}
|
||||
}
|
||||
ErrorClientAction.template = xml`<div/>`;
|
||||
registry.category("actions").add("failing", ErrorClientAction);
|
||||
|
||||
class ClientActionTargetNew extends Component {}
|
||||
ClientActionTargetNew.template = xml`<div class="my_action_new" />`;
|
||||
registry.category("actions").add("clientActionNew", ClientActionTargetNew);
|
||||
|
||||
class ClientAction extends Component {
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
}
|
||||
async onClick() {
|
||||
try {
|
||||
await this.action.doAction(
|
||||
{ type: "ir.actions.client", tag: "failing", target: "new" },
|
||||
{ onClose: () => assert.step("failing dialog closed") }
|
||||
);
|
||||
} catch (e) {
|
||||
assert.strictEqual(e.cause.message, "my error");
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientAction.template = xml`
|
||||
<div class="my_action" t-on-click="onClick">
|
||||
My Action
|
||||
</div>`;
|
||||
registry.category("actions").add("clientAction", ClientAction);
|
||||
|
||||
const errorDialogOpened = makeDeferred();
|
||||
patchWithCleanup(ClientErrorDialog.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
onMounted(() => errorDialogOpened.resolve());
|
||||
},
|
||||
});
|
||||
|
||||
registry.category("services").add("error", errorService);
|
||||
const webClient = await createWebClient({});
|
||||
|
||||
await doAction(webClient, { type: "ir.actions.client", tag: "clientAction" });
|
||||
await click(target, ".my_action");
|
||||
await errorDialogOpened;
|
||||
|
||||
assert.containsOnce(target, ".modal");
|
||||
await click(target, ".modal-body button.btn-link");
|
||||
assert.ok(
|
||||
target.querySelector(".modal-body .o_error_detail").textContent.includes("my error")
|
||||
);
|
||||
|
||||
await click(target, ".modal-footer button");
|
||||
assert.containsNone(target, ".modal");
|
||||
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.client",
|
||||
tag: "clientActionNew",
|
||||
target: "new",
|
||||
});
|
||||
assert.containsOnce(target, ".modal .my_action_new");
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test('breadcrumbs of actions in target="new"', async function (assert) {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
// execute an action in target="current"
|
||||
await doAction(webClient, 1);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".breadcrumb-item")].map((i) => i.innerText),
|
||||
["Partners Action 1"]
|
||||
);
|
||||
|
||||
// execute an action in target="new" and a list view (s.t. there is a control panel)
|
||||
await doAction(webClient, {
|
||||
xml_id: "action_5",
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
assert.containsNone(target, ".modal .breadcrumb");
|
||||
});
|
||||
|
||||
QUnit.test('call switchView in an action in target="new"', async function (assert) {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
// execute an action in target="current"
|
||||
await doAction(webClient, 4);
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
|
||||
// execute an action in target="new" and a list view (s.t. we can call switchView)
|
||||
await doAction(webClient, {
|
||||
xml_id: "action_5",
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
assert.containsOnce(target, ".modal .o_list_view");
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
|
||||
// click on a record in the dialog -> should do nothing as we can't switch view
|
||||
// in the dialog, and we don't want to switch view behind the dialog
|
||||
await click(target.querySelector(".modal .o_data_row .o_data_cell"));
|
||||
assert.containsOnce(target, ".modal .o_list_view");
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
});
|
||||
|
||||
QUnit.test("action with 'dialog_size' key in context", async function (assert) {
|
||||
const action = {
|
||||
name: "Some Action",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
target: "new",
|
||||
views: [[false, "form"]],
|
||||
};
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, action);
|
||||
assert.hasClass(target.querySelector(".o_dialog .modal-dialog"), "modal-lg");
|
||||
|
||||
await doAction(webClient, { ...action, context: { dialog_size: "small" } });
|
||||
assert.hasClass(target.querySelector(".o_dialog .modal-dialog"), "modal-sm");
|
||||
|
||||
await doAction(webClient, { ...action, context: { dialog_size: "medium" } });
|
||||
assert.hasClass(target.querySelector(".o_dialog .modal-dialog"), "modal-md");
|
||||
|
||||
await doAction(webClient, { ...action, context: { dialog_size: "large" } });
|
||||
assert.hasClass(target.querySelector(".o_dialog .modal-dialog"), "modal-lg");
|
||||
|
||||
await doAction(webClient, { ...action, context: { dialog_size: "extra-large" } });
|
||||
assert.hasClass(target.querySelector(".o_dialog .modal-dialog"), "modal-xl");
|
||||
});
|
||||
|
||||
QUnit.test('click on record in list view action in target="new"', async function (assert) {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1001);
|
||||
await doAction(webClient, {
|
||||
name: "Favorite Ponies",
|
||||
res_model: "pony",
|
||||
type: "ir.actions.act_window",
|
||||
target: "new",
|
||||
views: [[false, "list"], [false, "form"]],
|
||||
});
|
||||
|
||||
// The list view has been opened in a dialog
|
||||
assert.containsOnce(target, ".o_dialog .modal-dialog .o_list_view");
|
||||
|
||||
// click on a record in the dialog -> should do nothing as we can't switch view in the dialog
|
||||
await click(target.querySelector(".modal .o_data_row .o_data_cell"));
|
||||
assert.containsOnce(target, ".o_dialog .modal-dialog .o_list_view");
|
||||
assert.containsNone(target, ".o_form_view");
|
||||
});
|
||||
|
||||
QUnit.module('Actions in target="fullscreen"');
|
||||
|
||||
QUnit.test(
|
||||
'correctly execute act_window actions in target="fullscreen"',
|
||||
async function (assert) {
|
||||
serverData.actions[1].target = "fullscreen";
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
await nextTick(); // wait for the webclient template to be re-rendered
|
||||
assert.containsOnce(target, ".o_control_panel", "should have rendered a control panel");
|
||||
assert.containsOnce(target, ".o_kanban_view", "should have rendered a kanban view");
|
||||
assert.containsNone(target, ".o_main_navbar");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test('fullscreen on action change: back to a "current" action', async function (assert) {
|
||||
serverData.actions[1].target = "fullscreen";
|
||||
serverData.views[
|
||||
"partner,false,form"
|
||||
] = `<form><button name="1" type="action" class="oe_stat_button" /></form>`;
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 6);
|
||||
assert.containsOnce(target, ".o_main_navbar");
|
||||
await click(target.querySelector("button[name='1']"));
|
||||
await nextTick(); // wait for the webclient template to be re-rendered
|
||||
assert.containsNone(target, ".o_main_navbar");
|
||||
await click(target.querySelector(".breadcrumb li a"));
|
||||
await nextTick(); // wait for the webclient template to be re-rendered
|
||||
assert.containsOnce(target, ".o_main_navbar");
|
||||
});
|
||||
|
||||
QUnit.test('fullscreen on action change: all "fullscreen" actions', async function (assert) {
|
||||
serverData.actions[6].target = "fullscreen";
|
||||
serverData.views[
|
||||
"partner,false,form"
|
||||
] = `<form><button name="1" type="action" class="oe_stat_button" /></form>`;
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 6);
|
||||
assert.isNotVisible(target.querySelector(".o_main_navbar"));
|
||||
await click(target.querySelector("button[name='1']"));
|
||||
assert.isNotVisible(target.querySelector(".o_main_navbar"));
|
||||
await click(target.querySelector(".breadcrumb li a"));
|
||||
assert.isNotVisible(target.querySelector(".o_main_navbar"));
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
'fullscreen on action change: back to another "current" action',
|
||||
async function (assert) {
|
||||
serverData.menus = {
|
||||
root: { id: "root", children: [1], name: "root", appID: "root" },
|
||||
1: { id: 1, children: [], name: "MAIN APP", appID: 1, actionID: 6 },
|
||||
};
|
||||
serverData.actions[1].target = "fullscreen";
|
||||
serverData.views["partner,false,form"] =
|
||||
'<form><button name="24" type="action" class="oe_stat_button"/></form>';
|
||||
await createWebClient({ serverData });
|
||||
await nextTick(); // wait for the load state (default app)
|
||||
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']"));
|
||||
await nextTick(); // wait for the webclient template to be re-rendered
|
||||
assert.containsOnce(target, "nav .o_menu_brand");
|
||||
await click(target.querySelector("button[name='1']"));
|
||||
await nextTick(); // wait for the webclient template to be re-rendered
|
||||
assert.containsNone(target, "nav.o_main_navbar");
|
||||
await click(target.querySelectorAll(".breadcrumb li a")[1]);
|
||||
await nextTick(); // wait for the webclient template to be re-rendered
|
||||
assert.containsOnce(target, "nav .o_menu_brand");
|
||||
assert.strictEqual(target.querySelector("nav .o_menu_brand").innerText, "MAIN APP");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.module('Actions in target="main"');
|
||||
|
||||
QUnit.test('can execute act_window actions in target="main"', async function (assert) {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
assert.containsOnce(target, ".breadcrumb-item");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_control_panel .breadcrumb").textContent,
|
||||
"Partners Action 1"
|
||||
);
|
||||
|
||||
await doAction(webClient, {
|
||||
name: "Another Partner Action",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
target: "main",
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
assert.containsOnce(target, ".breadcrumb-item");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_control_panel .breadcrumb").textContent,
|
||||
"Another Partner Action"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('can switch view in an action in target="main"', async function (assert) {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, {
|
||||
name: "Partner Action",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
target: "main",
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
assert.containsOnce(target, ".breadcrumb-item");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_control_panel .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.strictEqual(
|
||||
target.querySelector(".o_control_panel .breadcrumb").textContent,
|
||||
"Partner ActionFirst record"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('can restore an action in target="main"', async function (assert) {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, {
|
||||
name: "Partner Action",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
target: "main",
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
assert.containsOnce(target, ".breadcrumb-item");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_control_panel .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.strictEqual(
|
||||
target.querySelector(".o_control_panel .breadcrumb").textContent,
|
||||
"Partner ActionFirst record"
|
||||
);
|
||||
|
||||
await doAction(webClient, 1);
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
assert.containsN(target, ".breadcrumb-item", 3);
|
||||
|
||||
// go back to form view
|
||||
await click(target.querySelectorAll(".breadcrumb-item")[1]);
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
assert.containsN(target, ".breadcrumb-item", 2);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_control_panel .breadcrumb").textContent,
|
||||
"Partner ActionFirst record"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/** @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 { browser } from "@web/core/browser/browser";
|
||||
|
||||
let serverData;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
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);
|
||||
},
|
||||
})
|
||||
);
|
||||
setupWebClientRegistries();
|
||||
const env = await makeTestEnv({ serverData });
|
||||
await doAction(env, {
|
||||
type: "ir.actions.act_url",
|
||||
target: "self",
|
||||
url: "/my/test/url",
|
||||
});
|
||||
assert.verifySteps(["/my/test/url"]);
|
||||
});
|
||||
|
||||
QUnit.test("execute an 'ir.actions.act_url' action with onClose option", async (assert) => {
|
||||
setupWebClientRegistries();
|
||||
patchWithCleanup(browser, {
|
||||
open: () => assert.step("browser open"),
|
||||
});
|
||||
const env = await makeTestEnv({ serverData });
|
||||
const options = {
|
||||
onClose: () => assert.step("onClose"),
|
||||
};
|
||||
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()");
|
||||
},
|
||||
})
|
||||
);
|
||||
setupWebClientRegistries();
|
||||
const env = await makeTestEnv({ serverData });
|
||||
await doAction(env, {
|
||||
type: "ir.actions.act_url",
|
||||
target: "self",
|
||||
url: "javascript:alert()",
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue