mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 07:32:08 +02:00
vanilla 19.0
This commit is contained in:
parent
991d2234ca
commit
d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions
|
|
@ -0,0 +1,500 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { Component, onMounted, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineModels,
|
||||
getService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
stepAllNetworkCalls,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { redirect } from "@web/core/utils/urls";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
const { ResCompany, ResPartner, ResUsers } = webModels;
|
||||
const actionRegistry = registry.category("actions");
|
||||
|
||||
class TestClientAction extends Component {
|
||||
static template = xml`
|
||||
<div class="test_client_action">
|
||||
ClientAction_<t t-esc="props.action.params?.description"/>
|
||||
</div>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
onMounted(() => this.env.config.setDisplayName(`Client action ${this.props.action.id}`));
|
||||
}
|
||||
}
|
||||
|
||||
class Partner extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record" },
|
||||
{ id: 2, display_name: "Second record" },
|
||||
];
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
"kanban,1": /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
list: /* xml */ `
|
||||
<list>
|
||||
<field name="display_name" />
|
||||
</list>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner, ResCompany, ResPartner, ResUsers]);
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[1, "kanban"]],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
xml_id: "action_3",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[1, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
actionRegistry.add("__test__client__action__", TestClientAction);
|
||||
});
|
||||
|
||||
test("can display client actions in Dialog", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Dialog Test",
|
||||
target: "new",
|
||||
tag: "__test__client__action__",
|
||||
type: "ir.actions.client",
|
||||
});
|
||||
|
||||
expect(".modal .test_client_action").toHaveCount(1);
|
||||
expect(".modal-title").toHaveText("Dialog Test");
|
||||
});
|
||||
|
||||
test("can display client actions in Dialog and close the dialog", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Dialog Test",
|
||||
target: "new",
|
||||
tag: "__test__client__action__",
|
||||
type: "ir.actions.client",
|
||||
});
|
||||
|
||||
expect(".modal .test_client_action").toHaveCount(1);
|
||||
expect(".modal-title").toHaveText("Dialog Test");
|
||||
await contains(".modal footer .btn.btn-primary").click();
|
||||
expect(".modal .test_client_action").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("can display client actions as main, then in Dialog", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction("__test__client__action__");
|
||||
expect(".o_action_manager .test_client_action").toHaveCount(1);
|
||||
|
||||
await getService("action").doAction({
|
||||
target: "new",
|
||||
tag: "__test__client__action__",
|
||||
type: "ir.actions.client",
|
||||
});
|
||||
expect(".o_action_manager .test_client_action").toHaveCount(1);
|
||||
expect(".modal .test_client_action").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("can display client actions in Dialog, then as main destroys Dialog", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
target: "new",
|
||||
tag: "__test__client__action__",
|
||||
type: "ir.actions.client",
|
||||
});
|
||||
expect(".test_client_action").toHaveCount(1);
|
||||
expect(".modal .test_client_action").toHaveCount(1);
|
||||
await getService("action").doAction("__test__client__action__");
|
||||
|
||||
expect(".test_client_action").toHaveCount(1);
|
||||
expect(".modal .test_client_action").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("dialog no header", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Dialog Test",
|
||||
target: "new",
|
||||
tag: "__test__client__action__",
|
||||
type: "ir.actions.client",
|
||||
context: { header: false },
|
||||
});
|
||||
|
||||
expect(".modal .test_client_action").toHaveCount(1);
|
||||
expect(".modal-title").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("soft_reload will refresh data", async () => {
|
||||
onRpc("web_search_read", () => {
|
||||
expect.step("web_search_read");
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect.verifySteps(["web_search_read"]);
|
||||
|
||||
await getService("action").doAction("soft_reload");
|
||||
expect.verifySteps(["web_search_read"]);
|
||||
});
|
||||
|
||||
test("soft_reload a form view", async () => {
|
||||
onRpc("web_read", ({ args }) => {
|
||||
expect.step(`read ${args[0][0]}`);
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
type: "ir.actions.act_window",
|
||||
});
|
||||
await contains(".o_data_row .o_data_cell").click();
|
||||
await contains(".o_form_view .o_pager_next").click();
|
||||
expect.verifySteps(["read 1", "read 2"]);
|
||||
|
||||
await getService("action").doAction("soft_reload");
|
||||
expect.verifySteps(["read 2"]);
|
||||
});
|
||||
|
||||
test("soft_reload when there is no controller", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction("soft_reload");
|
||||
expect(true).toBe(true, {
|
||||
message: "No ControllerNotFoundError when there is no controller to restore",
|
||||
});
|
||||
});
|
||||
|
||||
test("can execute client actions from tag name", async () => {
|
||||
class ClientAction extends Component {
|
||||
static template = xml`<div class="o_client_action_test">Hello World</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
actionRegistry.add("HelloWorldTest", ClientAction);
|
||||
|
||||
stepAllNetworkCalls();
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction("HelloWorldTest");
|
||||
expect(".o_control_panel").toHaveCount(0);
|
||||
expect(".o_client_action_test").toHaveText("Hello World");
|
||||
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
|
||||
});
|
||||
|
||||
test("async client action (function) returning another action", async () => {
|
||||
actionRegistry.add("my_action", async () => {
|
||||
await Promise.resolve();
|
||||
return 1; // execute action 1
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction("my_action");
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("'CLEAR-UNCOMMITTED-CHANGES' is not triggered for function client actions", async () => {
|
||||
actionRegistry.add("my_action", () => {
|
||||
expect.step("my_action");
|
||||
});
|
||||
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
webClient.env.bus.addEventListener("CLEAR-UNCOMMITTED-CHANGES", () => {
|
||||
expect.step("CLEAR-UNCOMMITTED-CHANGES");
|
||||
});
|
||||
|
||||
await getService("action").doAction("my_action");
|
||||
expect.verifySteps(["my_action"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("ClientAction receives breadcrumbs and exports title", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
class ClientAction extends Component {
|
||||
static template = xml`<div class="my_action" t-on-click="onClick">client action</div>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.breadcrumbTitle = "myAction";
|
||||
const { breadcrumbs } = this.env.config;
|
||||
expect(breadcrumbs).toHaveLength(2);
|
||||
expect(breadcrumbs[0].name).toBe("Partners Action 1");
|
||||
onMounted(() => {
|
||||
this.env.config.setDisplayName(this.breadcrumbTitle);
|
||||
});
|
||||
}
|
||||
onClick() {
|
||||
this.breadcrumbTitle = "newTitle";
|
||||
this.env.config.setDisplayName(this.breadcrumbTitle);
|
||||
}
|
||||
}
|
||||
actionRegistry.add("SomeClientAction", ClientAction);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
await getService("action").doAction("SomeClientAction");
|
||||
expect(".my_action").toHaveCount(1);
|
||||
await contains(".my_action").click();
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_breadcrumb").toHaveText("Partners Action 1\nnewTitle\nPartners");
|
||||
});
|
||||
|
||||
test("ClientAction receives arbitrary props from doAction", async () => {
|
||||
expect.assertions(1);
|
||||
class ClientAction extends Component {
|
||||
static template = xml`<div></div>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
expect(this.props.division).toBe("bell");
|
||||
}
|
||||
}
|
||||
actionRegistry.add("SomeClientAction", ClientAction);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction("SomeClientAction", {
|
||||
props: { division: "bell" },
|
||||
});
|
||||
});
|
||||
|
||||
test("ClientAction with extractProps", async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 128,
|
||||
name: "My Client Action",
|
||||
tag: "SomeClientAction",
|
||||
type: "ir.actions.client",
|
||||
params: {
|
||||
my_prop: "coucou",
|
||||
},
|
||||
},
|
||||
]);
|
||||
class ClientAction extends Component {
|
||||
static template = xml`<div class="my_client_action" t-esc="props.myProp"/>`;
|
||||
static props = ["*"];
|
||||
static extractProps(action) {
|
||||
return { myProp: action.params.my_prop };
|
||||
}
|
||||
}
|
||||
actionRegistry.add("SomeClientAction", ClientAction);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(128);
|
||||
expect(".my_client_action").toHaveText("coucou");
|
||||
});
|
||||
|
||||
test("test display_notification client action", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "display_notification",
|
||||
params: {
|
||||
message: "message",
|
||||
sticky: true,
|
||||
},
|
||||
});
|
||||
await animationFrame(); // wait for the notification to be displayed
|
||||
expect(".o_notification_manager .o_notification").toHaveCount(1);
|
||||
expect(".o_notification_manager .o_notification .o_notification_content").toHaveText("message");
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
await contains(".o_notification_close").click();
|
||||
expect(".o_notification_manager .o_notification").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("test display_notification client action with links", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
|
||||
await getService("action").doAction({
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await animationFrame(); // wait for the notification to be displayed
|
||||
expect(".o_notification_manager .o_notification").toHaveCount(1);
|
||||
expect(".o_notification_manager .o_notification .o_notification_content").toHaveText(
|
||||
"message test <R&D> <R&D>"
|
||||
);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
await contains(".o_notification_close").click();
|
||||
expect(".o_notification_manager .o_notification").toHaveCount(0);
|
||||
|
||||
// display_notification without title
|
||||
await getService("action").doAction({
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await animationFrame(); // wait for the notification to be displayed
|
||||
expect(".o_notification_manager .o_notification").toHaveCount(1);
|
||||
expect(".o_notification_manager .o_notification .o_notification_title").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("test next action on display_notification client action", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
const options = {
|
||||
onClose: function () {
|
||||
expect.step("onClose");
|
||||
},
|
||||
};
|
||||
await getService("action").doAction(
|
||||
{
|
||||
type: "ir.actions.client",
|
||||
tag: "display_notification",
|
||||
params: {
|
||||
title: "title",
|
||||
message: "message",
|
||||
sticky: true,
|
||||
next: {
|
||||
type: "ir.actions.act_window_close",
|
||||
},
|
||||
},
|
||||
},
|
||||
options
|
||||
);
|
||||
await animationFrame(); // wait for the notification to be displayed
|
||||
expect(".o_notification_manager .o_notification").toHaveCount(1);
|
||||
expect.verifySteps(["onClose"]);
|
||||
});
|
||||
|
||||
test("test reload client action", async () => {
|
||||
redirect("/odoo?test=42");
|
||||
browser.location.search = "?test=42";
|
||||
|
||||
patchWithCleanup(browser.history, {
|
||||
pushState: (_state, _unused, url) => {
|
||||
expect.step(`pushState ${url.replace(browser.location.origin, "")}`);
|
||||
},
|
||||
replaceState: (_state, _unused, url) => {
|
||||
expect.step(`replaceState ${url.replace(browser.location.origin, "")}`);
|
||||
},
|
||||
});
|
||||
patchWithCleanup(browser.location, {
|
||||
reload: function () {
|
||||
expect.step("window_reload");
|
||||
},
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await runAllTimers();
|
||||
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "reload",
|
||||
});
|
||||
await runAllTimers();
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "reload",
|
||||
params: {
|
||||
action_id: 2,
|
||||
},
|
||||
});
|
||||
await runAllTimers();
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "reload",
|
||||
params: {
|
||||
menu_id: 1,
|
||||
},
|
||||
});
|
||||
await runAllTimers();
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "reload",
|
||||
params: {
|
||||
action_id: 1,
|
||||
menu_id: 2,
|
||||
},
|
||||
});
|
||||
await runAllTimers();
|
||||
expect.verifySteps([
|
||||
"replaceState /odoo?test=42",
|
||||
"window_reload",
|
||||
"pushState /odoo/action-2",
|
||||
"window_reload",
|
||||
"pushState /odoo?menu_id=1",
|
||||
"window_reload",
|
||||
"pushState /odoo/action-1?menu_id=2",
|
||||
"window_reload",
|
||||
]);
|
||||
});
|
||||
|
||||
test("test home client action", async () => {
|
||||
redirect("/odoo");
|
||||
browser.location.search = "";
|
||||
|
||||
patchWithCleanup(browser.location, {
|
||||
assign: (url) => expect.step(`assign ${url}`),
|
||||
});
|
||||
|
||||
onRpc("/web/webclient/version_info", () => {
|
||||
expect.step("/web/webclient/version_info");
|
||||
return true;
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "home",
|
||||
});
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect.verifySteps(["/web/webclient/version_info", "assign /"]);
|
||||
});
|
||||
|
|
@ -1,632 +0,0 @@
|
|||
/** @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,202 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, Deferred } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineModels,
|
||||
getService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
const { ResCompany, ResPartner, ResUsers } = webModels;
|
||||
|
||||
class Partner extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record" },
|
||||
{ id: 2, display_name: "Second record" },
|
||||
];
|
||||
_views = {
|
||||
form: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</form>`,
|
||||
"kanban,1": `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: `<list><field name="display_name"/></list>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner, ResCompany, ResPartner, ResUsers]);
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[1, "kanban"]],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
xml_id: "action_3",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[1, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
xml_id: "action_5",
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
]);
|
||||
|
||||
test("close the currently opened dialog", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
// execute an action in target="new"
|
||||
await getService("action").doAction(5);
|
||||
expect(".o_technical_modal .o_form_view").toHaveCount(1);
|
||||
// execute an 'ir.actions.act_window_close' action
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.act_window_close",
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_technical_modal .o_form_view").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("close dialog by clicking on the header button", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
// execute an action in target="new"
|
||||
function onClose() {
|
||||
expect.step("on_close");
|
||||
}
|
||||
await getService("action").doAction(5, { onClose });
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
await contains(".o_dialog .modal-header button").click();
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
expect.verifySteps(["on_close"]);
|
||||
|
||||
// execute an 'ir.actions.act_window_close' action
|
||||
// should not call 'on_close' as it was already called.
|
||||
await getService("action").doAction({ type: "ir.actions.act_window_close" });
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test('execute "on_close" only if there is no dialog to close', async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
// execute an action in target="new"
|
||||
await getService("action").doAction(5);
|
||||
function onClose() {
|
||||
expect.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 getService("action").doAction({ type: "ir.actions.act_window_close" }, options);
|
||||
expect.verifySteps([]);
|
||||
// execute again an 'ir.actions.act_window_close' action
|
||||
// should call 'on_close' as there is no dialog to close
|
||||
await getService("action").doAction({ type: "ir.actions.act_window_close" }, options);
|
||||
expect.verifySteps(["on_close"]);
|
||||
});
|
||||
|
||||
test("close action with provided infos", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
const options = {
|
||||
onClose: function (infos) {
|
||||
expect(infos).toBe("just for testing", {
|
||||
message: "should have the correct close infos",
|
||||
});
|
||||
},
|
||||
};
|
||||
await getService("action").doAction(
|
||||
{
|
||||
type: "ir.actions.act_window_close",
|
||||
infos: "just for testing",
|
||||
},
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("history back called within on_close", async () => {
|
||||
let list;
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
list = this;
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
function onClose() {
|
||||
list.env.config.historyBack();
|
||||
expect.step("on_close");
|
||||
}
|
||||
// open a new dialog form
|
||||
await getService("action").doAction(5, { onClose });
|
||||
|
||||
await contains(".modal-header button.btn-close").click();
|
||||
// await nextTick();
|
||||
expect(".modal").toHaveCount(0);
|
||||
expect(".o_list_view").toHaveCount(0);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect.verifySteps(["on_close"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("web client is not deadlocked when a view crashes", async () => {
|
||||
expect.assertions(4);
|
||||
expect.errors(1);
|
||||
|
||||
const readOnFirstRecordDef = new Deferred();
|
||||
onRpc("web_read", ({ args }) => {
|
||||
if (args[0][0] === 1) {
|
||||
return readOnFirstRecordDef;
|
||||
}
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
// open first record in form view. this will crash and will not
|
||||
// display a form view
|
||||
await contains(".o_list_view .o_data_cell").click();
|
||||
readOnFirstRecordDef.reject(new Error("not working as intended"));
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["not working as intended"]);
|
||||
|
||||
expect(".o_list_view").toHaveCount(1, { message: "there should still be a list view in dom" });
|
||||
// open another record, the read will not crash
|
||||
await contains(".o_list_view .o_data_row:eq(1) .o_data_cell").click();
|
||||
expect(".o_list_view").toHaveCount(0, { message: "there should not be a list view in dom" });
|
||||
expect(".o_form_view").toHaveCount(1, { message: "there should be a form view in dom" });
|
||||
});
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
/** @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,799 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryAllTexts, runAllTimers } from "@odoo/hoot-dom";
|
||||
import { animationFrame, Deferred } from "@odoo/hoot-mock";
|
||||
import { Component, onWillStart, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineModels,
|
||||
fields,
|
||||
getService,
|
||||
isItemSelected,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
stepAllNetworkCalls,
|
||||
switchView,
|
||||
toggleMenuItem,
|
||||
toggleSearchBarMenu,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { redirect } from "@web/core/utils/urls";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { SearchBar } from "@web/search/search_bar/search_bar";
|
||||
import { useSetupAction } from "@web/search/action_hook";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { router } from "@web/core/browser/router";
|
||||
|
||||
const { ResCompany, ResPartner, ResUsers } = webModels;
|
||||
const actionRegistry = registry.category("actions");
|
||||
|
||||
class Partner extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
start = fields.Date();
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record" },
|
||||
{ id: 2, display_name: "Second record" },
|
||||
];
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<button name="object" string="Call method" type="object"/>
|
||||
</header>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
"kanban,1": /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: /* xml */ `<list><field name="display_name"/></list>`,
|
||||
calendar: /* xml */ `<calendar date_start="start"/>`,
|
||||
};
|
||||
}
|
||||
|
||||
class Pony extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 4, name: "Twilight Sparkle" },
|
||||
{ id: 6, name: "Applejack" },
|
||||
{ id: 9, name: "Fluttershy" },
|
||||
];
|
||||
_views = {
|
||||
list: /* xml */ `<list><field name="name"/></list>`,
|
||||
form: /* xml */ `<form><field name="name"/></form>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner, Pony, ResCompany, ResPartner, ResUsers]);
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 3,
|
||||
xml_id: "action_3",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[1, "kanban"],
|
||||
[false, "calendar"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
xml_id: "action_4",
|
||||
name: "Partners Action 4",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[1, "kanban"],
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
xml_id: "action_5",
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
xml_id: "action_8",
|
||||
name: "Favorite Ponies",
|
||||
res_model: "pony",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
test("drop previous actions if possible", async () => {
|
||||
const def = new Deferred();
|
||||
stepAllNetworkCalls();
|
||||
onRpc("/web/action/load", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("action").doAction(4);
|
||||
getService("action").doAction(8);
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
// action 4 loads a kanban view first, 6 loads a list view. We want a list
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("handle switching view and switching back on slow network", async () => {
|
||||
const def = new Deferred();
|
||||
const defs = [null, def, null];
|
||||
stepAllNetworkCalls();
|
||||
onRpc("web_search_read", () => defs.shift());
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(4);
|
||||
// kanban view is loaded, switch to list view
|
||||
await switchView("list");
|
||||
// here, list view is not ready yet, because def is not resolved
|
||||
// switch back to kanban view
|
||||
await switchView("kanban");
|
||||
// here, we want the kanban view to reload itself, regardless of list view
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
"web_search_read",
|
||||
]);
|
||||
|
||||
// we resolve def => list view is now ready (but we want to ignore it)
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1, { message: "there should be a kanban view in dom" });
|
||||
expect(".o_list_view").toHaveCount(0, { message: "there should not be a list view in dom" });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("clicking quickly on breadcrumbs...", async () => {
|
||||
let def;
|
||||
onRpc("web_read", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
// create a situation with 3 breadcrumbs: kanban/form/list
|
||||
await getService("action").doAction(4);
|
||||
await contains(".o_kanban_record").click();
|
||||
await getService("action").doAction(8);
|
||||
|
||||
// now, the next read operations will be promise (this is the read
|
||||
// operation for the form view reload)
|
||||
def = new Deferred();
|
||||
// click on the breadcrumbs for the form view, then on the kanban view
|
||||
// before the form view is fully reloaded
|
||||
await contains(queryAll(".o_control_panel .breadcrumb-item")[1]).click();
|
||||
await contains(".o_control_panel .breadcrumb-item").click();
|
||||
|
||||
// resolve the form view read
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners Action 4"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("execute a new action while loading a lazy-loaded controller", async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 77,
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "calendar"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
]);
|
||||
redirect("/odoo/action-77/2?cids=1");
|
||||
|
||||
let def;
|
||||
onRpc("partner", "search_read", () => def);
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await animationFrame(); // blank component
|
||||
expect(".o_form_view").toHaveCount(1, { message: "should display the form view of action 4" });
|
||||
|
||||
// click to go back to Kanban (this request is blocked)
|
||||
def = new Deferred();
|
||||
await contains(".o_control_panel .breadcrumb a").click();
|
||||
expect(".o_form_view").toHaveCount(1, {
|
||||
message: "should still display the form view of action 4",
|
||||
});
|
||||
|
||||
// execute another action meanwhile (don't block this request)
|
||||
await getService("action").doAction(8, { clearBreadcrumbs: true });
|
||||
expect(".o_list_view").toHaveCount(1, { message: "should display action 8" });
|
||||
expect(".o_form_view").toHaveCount(0, { message: "should no longer display the form view" });
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_read",
|
||||
"search_read",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
]);
|
||||
|
||||
// unblock the switch to Kanban in action 4
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_list_view").toHaveCount(1, { message: "should still display action 8" });
|
||||
expect(".o_kanban_view").toHaveCount(0, {
|
||||
message: "should not display the kanban view of action 4",
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("execute a new action while handling a call_button", async () => {
|
||||
const def = new Deferred();
|
||||
onRpc("/web/dataset/call_button/*", async () => {
|
||||
await def;
|
||||
return {
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[1, "kanban"]],
|
||||
};
|
||||
});
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
// execute action 3 and open a record in form view
|
||||
await getService("action").doAction(3);
|
||||
await contains(".o_list_view .o_data_cell").click();
|
||||
expect(".o_form_view").toHaveCount(1, { message: "should display the form view of action 3" });
|
||||
|
||||
// click on 'Call method' button (this request is blocked)
|
||||
await contains('.o_form_view button[name="object"]').click();
|
||||
expect(".o_form_view").toHaveCount(1, {
|
||||
message: "should still display the form view of action 3",
|
||||
});
|
||||
|
||||
// execute another action
|
||||
await getService("action").doAction(8, { clearBreadcrumbs: true });
|
||||
expect(".o_list_view").toHaveCount(1, { message: "should display the list view of action 8" });
|
||||
expect(".o_form_view").toHaveCount(0, { message: "should no longer display the form view" });
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
"web_read",
|
||||
"object",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
|
||||
// unblock the call_button request
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_list_view").toHaveCount(1, {
|
||||
message: "should still display the list view of action 8",
|
||||
});
|
||||
expect(".o_kanban_view").toHaveCount(0, { message: "should not display action 1" });
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("execute a new action while switching to another controller", async () => {
|
||||
// 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;
|
||||
stepAllNetworkCalls();
|
||||
onRpc("web_read", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1, { message: "should display the list view of action 3" });
|
||||
|
||||
// switch to the form view (this request is blocked)
|
||||
def = new Deferred();
|
||||
await contains(".o_list_view .o_data_cell").click();
|
||||
expect(".o_list_view").toHaveCount(1, {
|
||||
message: "should still display the list view of action 3",
|
||||
});
|
||||
|
||||
// execute another action meanwhile (don't block this request)
|
||||
await getService("action").doAction(4, { clearBreadcrumbs: true });
|
||||
expect(".o_kanban_view").toHaveCount(1, {
|
||||
message: "should display the kanban view of action 8",
|
||||
});
|
||||
expect(".o_list_view").toHaveCount(0, { message: "should no longer display the list view" });
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
"web_read",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
|
||||
// unblock the switch to the form view in action 3
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1, {
|
||||
message: "should still display the kanban view of action 8",
|
||||
});
|
||||
expect(".o_form_view").toHaveCount(0, {
|
||||
message: "should not display the form view of action 3",
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("execute a new action while loading views", async () => {
|
||||
const def = new Deferred();
|
||||
stepAllNetworkCalls();
|
||||
onRpc("get_views", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
// execute a first action (its 'get_views' RPC is blocked)
|
||||
getService("action").doAction(3);
|
||||
await animationFrame();
|
||||
expect(".o_list_view").toHaveCount(0, {
|
||||
message: "should not display the list view of action 3",
|
||||
});
|
||||
|
||||
// execute another action meanwhile (and unlock the RPC)
|
||||
getService("action").doAction(4);
|
||||
await animationFrame();
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1, {
|
||||
message: "should display the kanban view of action 4",
|
||||
});
|
||||
expect(".o_list_view").toHaveCount(0, {
|
||||
message: "should not display the list view of action 3",
|
||||
});
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners Action 4"]);
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("execute a new action while loading data of default view", async () => {
|
||||
const def = new Deferred();
|
||||
stepAllNetworkCalls();
|
||||
onRpc("web_read", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
// execute a first action (its 'web_read' RPC is blocked)
|
||||
getService("action").doAction({
|
||||
name: "A Partner",
|
||||
res_model: "partner",
|
||||
res_id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_form_view").toHaveCount(0, {
|
||||
message: "should not display the form view",
|
||||
});
|
||||
|
||||
// execute another action meanwhile (and unlock the RPC)
|
||||
getService("action").doAction(4);
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1, {
|
||||
message: "should display the kanban view of action 4",
|
||||
});
|
||||
expect(".o_form_view").toHaveCount(0, {
|
||||
message: "should not display the form view",
|
||||
});
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners Action 4"]);
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"get_views",
|
||||
"web_read",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("open a record while reloading the list view", async () => {
|
||||
let def;
|
||||
onRpc("search_read", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_calendar_view").toHaveCount(0);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
expect(".o_list_view .o_data_row").toHaveCount(2);
|
||||
expect(".o_control_panel .o_list_button_add").toHaveCount(1);
|
||||
|
||||
// reload (the search_read RPC will be blocked)
|
||||
def = new Deferred();
|
||||
await switchView("calendar");
|
||||
expect(".o_list_view .o_data_row").toHaveCount(2);
|
||||
expect(".o_control_panel .o_list_button_add").toHaveCount(1);
|
||||
|
||||
// open a record in form view
|
||||
await contains(".o_list_view .o_data_cell").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
expect(".o_control_panel .o_list_button_add").toHaveCount(0);
|
||||
|
||||
// unblock the search_read RPC
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
expect(".o_list_view").toHaveCount(0);
|
||||
expect(".o_calendar_view").toHaveCount(0);
|
||||
expect(".o_control_panel .o_list_button_add").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("properly drop client actions after new action is initiated", async () => {
|
||||
const slowWillStartDef = new Deferred();
|
||||
class ClientAction extends Component {
|
||||
static template = xml`<div class="client_action">ClientAction</div>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
onWillStart(() => slowWillStartDef);
|
||||
}
|
||||
}
|
||||
actionRegistry.add("slowAction", ClientAction);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("action").doAction("slowAction");
|
||||
await animationFrame();
|
||||
expect(".client_action").toHaveCount(0, { message: "client action isn't ready yet" });
|
||||
|
||||
getService("action").doAction(4);
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1, { message: "should have loaded a kanban view" });
|
||||
|
||||
slowWillStartDef.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1, { message: "should still display the kanban view" });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("restoring a controller when doing an action -- load_action slow", async () => {
|
||||
let def;
|
||||
onRpc("/web/action/load", () => def);
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
await contains(".o_list_view .o_data_cell").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
|
||||
def = new Deferred();
|
||||
getService("action").doAction(4, { clearBreadcrumbs: true });
|
||||
await animationFrame();
|
||||
expect(".o_form_view").toHaveCount(1, { message: "should still contain the form view" });
|
||||
|
||||
await contains(".o_control_panel .breadcrumb-item a").click();
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners"]);
|
||||
expect(".o_form_view").toHaveCount(0);
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
"web_read",
|
||||
"/web/action/load",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("switching when doing an action -- load_action slow", async () => {
|
||||
let def;
|
||||
onRpc("/web/action/load", () => def);
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
def = new Deferred();
|
||||
getService("action").doAction(4, { clearBreadcrumbs: true });
|
||||
await animationFrame();
|
||||
expect(".o_list_view").toHaveCount(1, { message: "should still contain the list view" });
|
||||
|
||||
await switchView("kanban");
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners"]);
|
||||
expect(".o_list_view").toHaveCount(0);
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
"/web/action/load",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("switching when doing an action -- get_views slow", async () => {
|
||||
let def;
|
||||
onRpc("get_views", () => def);
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
def = new Deferred();
|
||||
getService("action").doAction(4);
|
||||
await animationFrame();
|
||||
expect(".o_list_view").toHaveCount(1, { message: "should still contain the list view" });
|
||||
|
||||
await switchView("kanban");
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners"]);
|
||||
expect(".o_list_view").toHaveCount(0);
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("switching when doing an action -- search_read slow", async () => {
|
||||
const def = new Deferred();
|
||||
onRpc("search_read", () => def);
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
getService("action").doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "partner",
|
||||
views: [[false, "calendar"]],
|
||||
});
|
||||
await animationFrame();
|
||||
await switchView("kanban");
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners"]);
|
||||
expect(".o_list_view").toHaveCount(0);
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
"get_views",
|
||||
"search_read",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("click multiple times to open a record", async () => {
|
||||
const def = new Deferred();
|
||||
onRpc("web_read", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
const row1 = queryAll(".o_list_view .o_data_row")[0];
|
||||
const row2 = queryAll(".o_list_view .o_data_row")[1];
|
||||
await contains(row1.querySelector(".o_data_cell")).click();
|
||||
await contains(row2.querySelector(".o_data_cell")).click();
|
||||
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
|
||||
"Partners",
|
||||
"Second record",
|
||||
]);
|
||||
});
|
||||
|
||||
test("dialog will only open once for two rapid actions with the target new", async () => {
|
||||
const def = new Deferred();
|
||||
onRpc("onchange", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("action").doAction(5);
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_form_view").toHaveCount(0);
|
||||
|
||||
getService("action").doAction(5);
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_form_view").toHaveCount(0);
|
||||
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_dialog .o_form_view").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("local state, global state, and race conditions", async () => {
|
||||
patchWithCleanup(serverState.view_info, {
|
||||
toy: { multi_record: true, display_name: "Toy", icon: "fab fa-android" },
|
||||
});
|
||||
Partner._views = {
|
||||
toy: `<toy/>`,
|
||||
list: `<list><field name="display_name"/></list>`,
|
||||
search: `<search><filter name="display_name" string="Foo" domain="[]"/></search>`,
|
||||
};
|
||||
|
||||
let def = Promise.resolve();
|
||||
let id = 1;
|
||||
class ToyController extends Component {
|
||||
static template = xml`
|
||||
<div class="o_toy_view">
|
||||
<ControlPanel />
|
||||
<SearchBar />
|
||||
</div>`;
|
||||
static components = { ControlPanel, SearchBar };
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.id = id++;
|
||||
expect.step(this.props.state || "no state");
|
||||
useSetupAction({
|
||||
getLocalState: () => ({ fromId: this.id }),
|
||||
});
|
||||
onWillStart(() => def);
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("views").add("toy", {
|
||||
type: "toy",
|
||||
Controller: ToyController,
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
await getService("action").doAction({
|
||||
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 toggleSearchBarMenu();
|
||||
await toggleMenuItem("Foo");
|
||||
expect(isItemSelected("Foo")).toBe(true);
|
||||
|
||||
// reload twice by clicking on toy view switcher
|
||||
def = new Deferred();
|
||||
await contains(".o_control_panel .o_switch_view.o_toy").click();
|
||||
await contains(".o_control_panel .o_switch_view.o_toy").click();
|
||||
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
|
||||
await toggleSearchBarMenu();
|
||||
expect(isItemSelected("Foo")).toBe(true);
|
||||
// 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.
|
||||
|
||||
expect.verifySteps([
|
||||
"no state", // setup first view instantiated
|
||||
{ fromId: 1 }, // setup second view instantiated
|
||||
{ fromId: 1 }, // setup third view instantiated
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("doing browser back temporarily disables the UI", async () => {
|
||||
let def;
|
||||
onRpc("partner", "web_search_read", () => def);
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
await getService("action").doAction(4);
|
||||
await getService("action").doAction(8);
|
||||
await runAllTimers(); // wait for the update of the router
|
||||
expect(router.current).toEqual({
|
||||
action: 8,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
{
|
||||
action: 8,
|
||||
displayName: "Favorite Ponies",
|
||||
view_type: "list",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
def = new Deferred();
|
||||
browser.history.back();
|
||||
expect(document.body.style.pointerEvents).toBe("none");
|
||||
// await contains(".o_control_panel .breadcrumb-item").click(); todo JUM: click on breadcrumb
|
||||
def.resolve();
|
||||
|
||||
await animationFrame();
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners Action 4"]);
|
||||
expect(document.body.style.pointerEvents).toBe("auto");
|
||||
});
|
||||
|
|
@ -1,736 +0,0 @@
|
|||
/** @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,169 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineModels,
|
||||
getService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { user } from "@web/core/user";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
const { ResCompany, ResPartner, ResUsers } = webModels;
|
||||
|
||||
class Partner extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record" },
|
||||
{ id: 2, display_name: "Second record" },
|
||||
];
|
||||
_views = {
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<button name="object" string="Call method" type="object"/>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</header>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</form>`,
|
||||
"kanban,1": `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: `<list><field name="display_name"/></list>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner, ResCompany, ResPartner, ResUsers]);
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[1, "kanban"]],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
xml_id: "action_3",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[1, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
xml_id: "action_6",
|
||||
name: "Partner",
|
||||
res_id: 2,
|
||||
res_model: "partner",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
]);
|
||||
|
||||
test.tags("desktop");
|
||||
test("rainbowman integrated to webClient", async () => {
|
||||
patchWithCleanup(user, { showEffect: true });
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(".o_reward").toHaveCount(0);
|
||||
getService("effect").add({ type: "rainbow_man", message: "", fadeout: "no" });
|
||||
await animationFrame();
|
||||
expect(".o_reward").toHaveCount(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
await contains(".o_reward").click();
|
||||
expect(".o_reward").toHaveCount(0);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
getService("effect").add({ type: "rainbow_man", message: "", fadeout: "no" });
|
||||
await animationFrame();
|
||||
expect(".o_reward").toHaveCount(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
// Do not force rainbow man to destroy on doAction
|
||||
// we let it die either after its animation or on user click
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_reward").toHaveCount(1);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("on close with effect from server", async () => {
|
||||
patchWithCleanup(user, { showEffect: true });
|
||||
onRpc("/web/dataset/call_button/*", () => {
|
||||
return {
|
||||
type: "ir.actions.act_window_close",
|
||||
effect: {
|
||||
type: "rainbow_man",
|
||||
message: "button called",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(6);
|
||||
await contains("button[name=object]").click();
|
||||
expect(".o_reward").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("on close with effect in xml on desktop", async () => {
|
||||
patchWithCleanup(user, { showEffect: true });
|
||||
|
||||
Partner._views["form"] = `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Call method" name="object" type="object"
|
||||
effect="{'type': 'rainbow_man', 'message': 'rainBowInXML'}"
|
||||
/>
|
||||
</header>
|
||||
<field name="display_name"/>
|
||||
</form>`;
|
||||
onRpc("/web/dataset/call_button/*", () => false);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(6);
|
||||
await contains("button[name=object]").click();
|
||||
expect(".o_reward").toHaveCount(1);
|
||||
expect(".o_reward .o_reward_msg_content").toHaveText("rainBowInXML");
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("on close with effect in xml on mobile", async () => {
|
||||
patchWithCleanup(user, { showEffect: true });
|
||||
|
||||
Partner._views["form"] = `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Call method" name="object" type="object"
|
||||
effect="{'type': 'rainbow_man', 'message': 'rainBowInXML'}"
|
||||
/>
|
||||
</header>
|
||||
<field name="display_name"/>
|
||||
</form>`;
|
||||
onRpc("/web/dataset/call_button/*", () => false);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(6);
|
||||
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
|
||||
await contains("button[name=object]").click();
|
||||
expect(".o_reward").toHaveCount(1);
|
||||
expect(".o_reward .o_reward_msg_content").toHaveText("rainBowInXML");
|
||||
});
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
/** @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,638 @@
|
|||
import { describe, expect, test, beforeEach } from "@odoo/hoot";
|
||||
import { queryAllTexts, waitFor } from "@odoo/hoot-dom";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineModels,
|
||||
fields,
|
||||
getService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
toggleMenuItem,
|
||||
toggleSearchBarMenu,
|
||||
webModels,
|
||||
getKwArgs,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { mockTouch, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { router } from "@web/core/browser/router";
|
||||
import { user } from "@web/core/user";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
const { ResCompany, ResPartner, ResUsers, ResUsersSettings: WebResUsersSettings } = webModels;
|
||||
|
||||
class Partner extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
display_name = fields.Char();
|
||||
foo = fields.Char();
|
||||
m2o = fields.Many2one({ relation: "partner" });
|
||||
o2m = fields.One2many({ relation: "partner" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record", foo: "yop", m2o: 3, o2m: [2, 3] },
|
||||
{ id: 2, display_name: "Second record", foo: "blip", m2o: 3, o2m: [1, 4, 5] },
|
||||
{ id: 3, display_name: "Third record", foo: "gnap", m2o: 1, o2m: [] },
|
||||
{ id: 4, display_name: "Fourth record", foo: "plop", m2o: 1, o2m: [] },
|
||||
{ id: 5, display_name: "Fifth record", foo: "zoup", m2o: 1, o2m: [] },
|
||||
];
|
||||
_views = {
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<button name="object" string="Call method" type="object"/>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</header>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
<field name="foo"/>
|
||||
</group>
|
||||
</form>`,
|
||||
"form,74": `
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" type="action" name="1" icon="fa-star" context="{'default_partner': id}">
|
||||
<field string="Partners" name="o2m" widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
<field name="display_name"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
"kanban,1": `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: `<list><field name="foo"/></list>`,
|
||||
search: `<search><field name="foo" string="Foo"/></search>`,
|
||||
};
|
||||
}
|
||||
|
||||
class Pony extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 4, name: "Twilight Sparkle" },
|
||||
{ id: 6, name: "Applejack" },
|
||||
{ id: 9, name: "Fluttershy" },
|
||||
];
|
||||
_views = {
|
||||
list: `<list>
|
||||
<field name="name"/>
|
||||
<button name="action_test" type="object" string="Action Test" column_invisible="not context.get('display_button')"/>
|
||||
</list>`,
|
||||
kanban: `<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
form: `<form><field name="name"/></form>`,
|
||||
search: `<search>
|
||||
<filter name="my_filter" string="My filter" domain="[['name', '=', 'Applejack']]"/>
|
||||
</search>`,
|
||||
};
|
||||
}
|
||||
|
||||
class ResUsersSettings extends WebResUsersSettings {
|
||||
/** @param {number[]} id */
|
||||
get_embedded_actions_settings(id) {
|
||||
/** @type {import("mock_models").ResUsersSettingsEmbeddedAction} */
|
||||
const ResUsersSettingsEmbeddedAction = this.env["res.users.settings.embedded.action"];
|
||||
return ResUsersSettingsEmbeddedAction.embedded_action_settings_format(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} action_id
|
||||
* @param {number} res_id
|
||||
* @param {number} vals
|
||||
*/
|
||||
set_embedded_actions_setting(id, action_id, res_id, vals) {
|
||||
const kwargs = getKwArgs(arguments, "id", "action_id", "res_id", "vals");
|
||||
id = kwargs.id;
|
||||
action_id = kwargs.action_id;
|
||||
res_id = kwargs.res_id;
|
||||
vals = kwargs.vals;
|
||||
|
||||
/** @type {import("mock_models").ResUsersSettingsEmbeddedAction} */
|
||||
const ResUsersSettingsEmbeddedAction = this.env["res.users.settings.embedded.action"];
|
||||
|
||||
let [embeddedSettings] = ResUsersSettingsEmbeddedAction.search_read([
|
||||
["user_setting_id", "=", id],
|
||||
["action_id", "=", action_id],
|
||||
["res_id", "=", res_id],
|
||||
]);
|
||||
for (const [field, value] of Object.entries(vals)) {
|
||||
if (["embedded_actions_order", "embedded_actions_visibility"].includes(field)) {
|
||||
vals[field] = value
|
||||
.map((action_id) => (action_id === false ? "false" : String(action_id)))
|
||||
.join(",");
|
||||
}
|
||||
}
|
||||
if (!embeddedSettings) {
|
||||
embeddedSettings = ResUsersSettingsEmbeddedAction.create({
|
||||
action_id,
|
||||
res_id,
|
||||
...vals,
|
||||
});
|
||||
} else {
|
||||
ResUsersSettingsEmbeddedAction.write(embeddedSettings.id, vals);
|
||||
}
|
||||
return embeddedSettings;
|
||||
}
|
||||
}
|
||||
|
||||
class ResUsersSettingsEmbeddedAction extends models.ServerModel {
|
||||
_name = "res.users.settings.embedded.action";
|
||||
|
||||
/** @param {number[]} ids */
|
||||
embedded_action_settings_format(ids) {
|
||||
const embeddedSettings = {};
|
||||
for (const embeddedSettingsRecord of this.browse(ids)) {
|
||||
embeddedSettings[
|
||||
`${embeddedSettingsRecord.action_id}+${embeddedSettingsRecord.resId || ""}`
|
||||
] = {
|
||||
embedded_actions_order: embeddedSettingsRecord.embedded_actions_order
|
||||
? embeddedSettingsRecord.embedded_actions_order
|
||||
.split(",")
|
||||
.map((action_id) => (action_id === "false" ? false : parseInt(action_id)))
|
||||
: [],
|
||||
embedded_actions_visibility: embeddedSettingsRecord.embedded_actions_visibility
|
||||
? embeddedSettingsRecord.embedded_actions_visibility
|
||||
.split(",")
|
||||
.map((action_id) => (action_id === "false" ? false : parseInt(action_id)))
|
||||
: [],
|
||||
embedded_visibility: embeddedSettingsRecord.embedded_visibility,
|
||||
};
|
||||
}
|
||||
return embeddedSettings;
|
||||
}
|
||||
}
|
||||
|
||||
class IrActionsAct_Window extends models.ServerModel {
|
||||
_name = "ir.actions.act_window";
|
||||
|
||||
_records = [{ id: 1 }, { id: 4 }];
|
||||
}
|
||||
|
||||
defineModels([
|
||||
Partner,
|
||||
Pony,
|
||||
ResCompany,
|
||||
ResPartner,
|
||||
ResUsers,
|
||||
ResUsersSettings,
|
||||
ResUsersSettingsEmbeddedAction,
|
||||
IrActionsAct_Window,
|
||||
]);
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[1, "kanban"]],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
xml_id: "action_2",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[1, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
xml_id: "action_3",
|
||||
name: "Favorite Ponies",
|
||||
res_model: "pony",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
xml_id: "action_4",
|
||||
name: "Ponies",
|
||||
res_model: "pony",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
xml_id: "embedded_action_2",
|
||||
name: "Embedded Action 2",
|
||||
parent_res_model: "partner",
|
||||
type: "ir.embedded.actions",
|
||||
parent_action_id: 1,
|
||||
action_id: 3,
|
||||
context: {
|
||||
display_button: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
name: "Embedded Action 3",
|
||||
parent_res_model: "partner",
|
||||
type: "ir.embedded.actions",
|
||||
parent_action_id: 1,
|
||||
python_method: "do_python_method",
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
name: "Custom Embedded Action 4",
|
||||
type: "ir.embedded.actions",
|
||||
user_id: user.userId,
|
||||
parent_action_id: 4,
|
||||
action_id: 4,
|
||||
},
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
user.updateUserSettings("id", 1); // workaround to populate the user settings
|
||||
});
|
||||
|
||||
test("can display embedded actions linked to the current action", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_control_panel").toHaveCount(1, { message: "should have rendered a control panel" });
|
||||
expect(".o_kanban_view").toHaveCount(1, { message: "should have rendered a kanban view" });
|
||||
expect(".o_control_panel_navigation > button > i.fa-sliders").toHaveCount(1, {
|
||||
message: "should display the toggle embedded button",
|
||||
});
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
expect(".o_embedded_actions").toHaveCount(1, { message: "should display the embedded" });
|
||||
expect(".o_embedded_actions > button > span").toHaveText("Partners Action 1", {
|
||||
message:
|
||||
"The first embedded action should be the parent one and should be shown by default",
|
||||
});
|
||||
expect(user.settings.embedded_actions_config_ids).toEqual({
|
||||
"1+": {
|
||||
embedded_actions_order: [],
|
||||
embedded_actions_visibility: [false],
|
||||
embedded_visibility: true,
|
||||
res_model: "partner",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("can toggle visibility of embedded actions", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
await waitFor(".o_popover.dropdown-menu");
|
||||
expect(".o_popover.dropdown-menu .dropdown-item").toHaveCount(4, {
|
||||
message: "Three embedded actions should be displayed in the dropdown + button 'Save View'",
|
||||
});
|
||||
expect(".dropdown-menu .dropdown-item.selected").toHaveCount(1, {
|
||||
message: "only one embedded action should be selected",
|
||||
});
|
||||
await contains(
|
||||
".o_popover.dropdown-menu .dropdown-item > div > span:contains('Embedded Action 2')"
|
||||
).click();
|
||||
expect(".o_embedded_actions > button").toHaveCount(3, {
|
||||
message: "Should have 2 embedded actions in the embedded + the dropdown button",
|
||||
});
|
||||
});
|
||||
|
||||
test("can click on a embedded action and execute the corresponding action (with xml_id)", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
await waitFor(".o_popover.dropdown-menu");
|
||||
await contains(
|
||||
".o_popover.dropdown-menu .dropdown-item > div > span:contains('Embedded Action 2')"
|
||||
).click();
|
||||
await contains(".o_embedded_actions > button > span:contains('Embedded Action 2')").click();
|
||||
await runAllTimers();
|
||||
expect(router.current.action).toBe(3, {
|
||||
message: "the current action should be the one of the embedded action previously clicked",
|
||||
});
|
||||
expect(".o_list_view").toHaveCount(1, { message: "the view should be a list view" });
|
||||
expect(".o_embedded_actions").toHaveCount(1, { message: "the embedded should stay open" });
|
||||
expect(".o_embedded_actions > button.active").toHaveText("Embedded Action 2", {
|
||||
message: "The second embedded action should be active",
|
||||
});
|
||||
});
|
||||
|
||||
test("can click on a embedded action and execute the corresponding action (with python_method)", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
onRpc("do_python_method", () => ({
|
||||
id: 4,
|
||||
name: "Favorite Ponies from python action",
|
||||
res_model: "pony",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "kanban"]],
|
||||
}));
|
||||
await getService("action").doAction(1);
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
await waitFor(".o_popover.dropdown-menu");
|
||||
await contains(
|
||||
".o_popover.dropdown-menu .dropdown-item > div > span:contains('Embedded Action 3')"
|
||||
).click();
|
||||
await contains(".o_embedded_actions > button > span:contains('Embedded Action 3')").click();
|
||||
await runAllTimers();
|
||||
expect(router.current.action).toBe(4, {
|
||||
message: "the current action should be the one of the embedded action previously clicked",
|
||||
});
|
||||
expect(".o_kanban_view").toHaveCount(1, { message: "the view should be a kanban view" });
|
||||
expect(".o_embedded_actions").toHaveCount(1, { message: "the embedded should stay open" });
|
||||
expect(".o_embedded_actions > button.active").toHaveText("Embedded Action 3", {
|
||||
message: "The third embedded action should be active",
|
||||
});
|
||||
});
|
||||
|
||||
test("breadcrumbs are updated when clicking on embeddeds", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
onRpc("do_python_method", () => ({
|
||||
id: 4,
|
||||
name: "Favorite Ponies from python action",
|
||||
res_model: "pony",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "kanban"]],
|
||||
}));
|
||||
await getService("action").doAction(1);
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
await waitFor(".o_popover.dropdown-menu");
|
||||
await contains(
|
||||
".o_popover.dropdown-menu .dropdown-item > div > span:contains('Embedded Action 2')"
|
||||
).click();
|
||||
await contains(
|
||||
".o_popover.dropdown-menu .dropdown-item > div > span:contains('Embedded Action 3')"
|
||||
).click();
|
||||
expect(".o_control_panel .breadcrumb-item").toHaveCount(0);
|
||||
expect(".o_control_panel .o_breadcrumb .active").toHaveText("Partners Action 1");
|
||||
expect(browser.location.href).toBe("https://www.hoot.test/odoo/action-1");
|
||||
await contains(".o_embedded_actions > button > span:contains('Embedded Action 2')").click();
|
||||
await runAllTimers();
|
||||
expect(browser.location.href).toBe("https://www.hoot.test/odoo/action-3");
|
||||
expect(router.current.action).toBe(3, {
|
||||
message: "the current action should be the one of the embedded action previously clicked",
|
||||
});
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Favorite Ponies"]);
|
||||
await contains(".o_embedded_actions > button > span:contains('Embedded Action 3')").click();
|
||||
await runAllTimers();
|
||||
expect(browser.location.href).toBe("https://www.hoot.test/odoo/action-4");
|
||||
expect(router.current.action).toBe(4, {
|
||||
message: "the current action should be the one of the embedded action previously clicked",
|
||||
});
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
|
||||
"Favorite Ponies from python action",
|
||||
]);
|
||||
});
|
||||
|
||||
test("a view coming from a embedded can be saved in the embedded actions", async () => {
|
||||
onRpc("create", ({ args }) => {
|
||||
const values = args[0][0];
|
||||
expect(values.name).toBe("Custom Embedded Action 2");
|
||||
expect(values.action_id).toBe(3);
|
||||
expect(values).not.toInclude("python_method");
|
||||
return [4, values.name]; // Fake new embedded action id
|
||||
});
|
||||
onRpc("create_filter", ({ args }) => {
|
||||
expect(args[0].domain).toBe(`[["name", "=", "Applejack"]]`);
|
||||
expect(args[0].embedded_action_id).toBe(4);
|
||||
expect(args[0].user_ids).toEqual([]);
|
||||
return [5]; // Fake new filter id
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
await waitFor(".o_popover.dropdown-menu");
|
||||
await contains(
|
||||
".o_popover.dropdown-menu .dropdown-item > div > span:contains('Embedded Action 2')"
|
||||
).click();
|
||||
await contains(".o_embedded_actions > button > span:contains('Embedded Action 2')").click();
|
||||
await runAllTimers();
|
||||
expect(router.current.action).toBe(3, {
|
||||
message: "the current action should be the one of the embedded action previously clicked",
|
||||
});
|
||||
expect(".o_list_view").toHaveCount(1, { message: "the view should be a list view" });
|
||||
await contains("button.o_switch_view.o_kanban").click();
|
||||
expect(".o_kanban_view").toHaveCount(1, { message: "the view should be a kanban view" });
|
||||
await toggleSearchBarMenu();
|
||||
await toggleMenuItem("My filter");
|
||||
await toggleSearchBarMenu();
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1, {
|
||||
message: "There should be one record",
|
||||
});
|
||||
await contains(".o_embedded_actions .dropdown").click();
|
||||
await contains(".o_save_current_view ").click();
|
||||
await contains("input.form-check-input").click();
|
||||
await contains(".o_save_favorite ").click();
|
||||
expect(".o_embedded_actions > button").toHaveCount(4, {
|
||||
message: "Should have 2 embedded actions in the embedded + the dropdown button",
|
||||
});
|
||||
});
|
||||
|
||||
test("a view coming from a embedded with python_method can be saved in the embedded actions", async () => {
|
||||
onRpc(({ args, method }) => {
|
||||
let values;
|
||||
if (method === "create") {
|
||||
values = args[0][0];
|
||||
expect(values.name).toBe("Custom Embedded Action 3");
|
||||
expect(values.python_method).toBe("do_python_method");
|
||||
expect(values).not.toInclude("action_id");
|
||||
return [4, values.name]; // Fake new embedded action id
|
||||
} else if (method === "create_filter") {
|
||||
values = args[0][0];
|
||||
expect(args[0].domain).toBe(`[["name", "=", "Applejack"]]`);
|
||||
expect(args[0].embedded_action_id).toBe(4);
|
||||
expect(args[0].user_ids).toEqual([]);
|
||||
return 5; // Fake new filter id
|
||||
} else if (method === "do_python_method") {
|
||||
return {
|
||||
id: 4,
|
||||
name: "Favorite Ponies from python action",
|
||||
res_model: "pony",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "kanban"],
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
await waitFor(".o_popover.dropdown-menu");
|
||||
await contains(
|
||||
".o_popover.dropdown-menu .dropdown-item > div > span:contains('Embedded Action 3')"
|
||||
).click();
|
||||
await contains(".o_embedded_actions > button > span:contains('Embedded Action 3')").click();
|
||||
await runAllTimers();
|
||||
expect(router.current.action).toBe(4, {
|
||||
message: "the current action should be the one of the embedded action previously clicked",
|
||||
});
|
||||
expect(".o_list_view").toHaveCount(1, { message: "the view should be a list view" });
|
||||
await contains("button.o_switch_view.o_kanban").click();
|
||||
expect(".o_kanban_view").toHaveCount(1, { message: "the view should be a kanban view" });
|
||||
await toggleSearchBarMenu();
|
||||
await toggleMenuItem("My filter");
|
||||
await toggleSearchBarMenu();
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1, {
|
||||
message: "There should be one record",
|
||||
});
|
||||
await contains(".o_embedded_actions .dropdown").click();
|
||||
await contains(".o_save_current_view ").click();
|
||||
await contains("input.form-check-input").click();
|
||||
await contains(".o_save_favorite ").click();
|
||||
expect(".o_embedded_actions > button").toHaveCount(4, {
|
||||
message: "Should have 2 embedded actions in the embedded + the dropdown button",
|
||||
});
|
||||
});
|
||||
|
||||
test("the embedded actions should not be displayed when switching view", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
await waitFor(".o_popover.dropdown-menu");
|
||||
await contains(
|
||||
".o_popover.dropdown-menu .dropdown-item > div > span:contains('Embedded Action 2')"
|
||||
).click();
|
||||
await contains(".o_embedded_actions > button > span:contains('Embedded Action 2')").click();
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
await contains("button.o_switch_view.o_kanban").click();
|
||||
expect(".o_embedded_actions").toHaveCount(0, {
|
||||
message: "The embedded actions menu should not be displayed",
|
||||
});
|
||||
});
|
||||
|
||||
test("User can move the main (first) embedded action", async () => {
|
||||
mockTouch(true);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
await waitFor(".o_popover.dropdown-menu");
|
||||
await contains(
|
||||
".o_popover.dropdown-menu .dropdown-item > div > span:contains('Embedded Action 2')"
|
||||
).click();
|
||||
await contains(".o_embedded_actions > button:first-child").dragAndDrop(
|
||||
".o_embedded_actions > button:nth-child(2)"
|
||||
);
|
||||
expect(".o_embedded_actions > button:nth-child(2) > span").toHaveText("Partners Action 1", {
|
||||
message: "Main embedded action should've been moved to 2nd position",
|
||||
});
|
||||
});
|
||||
|
||||
test("User can unselect the main (first) embedded action", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
await waitFor(".o_popover.dropdown-menu");
|
||||
const dropdownItem =
|
||||
".o_popover.dropdown-menu .dropdown-item > div > span:contains('Partners Action 1')";
|
||||
expect(dropdownItem).not.toHaveClass("text-muted", {
|
||||
message: "Main embedded action should not be displayed in muted",
|
||||
});
|
||||
await contains(dropdownItem).click();
|
||||
expect(dropdownItem).not.toHaveClass("selected", {
|
||||
message: "Main embedded action should be unselected",
|
||||
});
|
||||
});
|
||||
|
||||
test("User should be redirected to the first embedded action set in user settings", async () => {
|
||||
// set embedded action 2 in first
|
||||
user.updateUserSettings("embedded_actions_config_ids", {
|
||||
"1+": {
|
||||
embedded_actions_visibility: [102],
|
||||
embedded_visibility: false,
|
||||
embedded_actions_order: [102, false, 103],
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doActionButton({
|
||||
name: 1,
|
||||
type: "action",
|
||||
});
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
expect(".o_embedded_actions > button:first-child").toHaveClass("active", {
|
||||
message: "First embedded action in order should have the 'active' class",
|
||||
});
|
||||
expect(".o_embedded_actions > button:first-child > span").toHaveText("Embedded Action 2", {
|
||||
message: "First embedded action in order should be 'Embedded Action 2'",
|
||||
});
|
||||
expect(".o_last_breadcrumb_item > span").toHaveText("Favorite Ponies", {
|
||||
message: "'Favorite Ponies' view should be loaded",
|
||||
});
|
||||
expect(".o_list_renderer .btn-link").toHaveCount(3, {
|
||||
message:
|
||||
"The button should be displayed since `display_button` is true in the context of the embedded action 2",
|
||||
});
|
||||
});
|
||||
|
||||
test("execute a regular action from an embedded action", async () => {
|
||||
Pony._views["form"] = `
|
||||
<form>
|
||||
<button type="action" name="2" string="Execute another action"/>
|
||||
<field name="name"/>
|
||||
</form>`;
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
|
||||
await contains(".o_control_panel_navigation button .fa-sliders").click();
|
||||
expect(".o_control_panel .o_embedded_actions button:not(.dropdown-toggle)").toHaveCount(1);
|
||||
|
||||
await waitFor(".o_popover.dropdown-menu");
|
||||
await contains(".dropdown-menu .dropdown-item span:contains('Embedded Action 2')").click();
|
||||
expect(".o_control_panel .o_embedded_actions button:not(.dropdown-toggle)").toHaveCount(2);
|
||||
|
||||
await contains(".o_control_panel .o_embedded_actions button:eq(1)").click();
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
await contains(".o_data_row .o_data_cell").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
|
||||
await contains(".o_form_view button[type=action]").click();
|
||||
expect(".o_control_panel .o_embedded_actions").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("custom embedded action loaded first", async () => {
|
||||
// set embedded action 4 in first
|
||||
user.updateUserSettings("embedded_actions_config_ids", {
|
||||
"4+": {
|
||||
embedded_actions_visibility: [104],
|
||||
embedded_visibility: false,
|
||||
embedded_actions_order: [104, false],
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doActionButton({
|
||||
name: 4,
|
||||
type: "action",
|
||||
});
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
|
||||
expect(".o_embedded_actions > button:first-child").toHaveClass("active", {
|
||||
message: "First embedded action in order should have the 'active' class",
|
||||
});
|
||||
expect(".o_embedded_actions > button:first-child > span").toHaveText(
|
||||
"Custom Embedded Action 4",
|
||||
{
|
||||
message: "First embedded action in order should be 'Embedded Action 4'",
|
||||
}
|
||||
);
|
||||
expect(".o_last_breadcrumb_item > span").toHaveText("Ponies", {
|
||||
message: "'Favorite Ponies' view should be loaded",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { animationFrame, mockFetch, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { Component, onMounted, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineModels,
|
||||
fields,
|
||||
getService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
stepAllNetworkCalls,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { BooleanField } from "@web/views/fields/boolean/boolean_field";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
const actionRegistry = registry.category("actions");
|
||||
|
||||
const { ResCompany, ResPartner, ResUsers } = webModels;
|
||||
|
||||
class Partner extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record" },
|
||||
{ id: 2, display_name: "Second record" },
|
||||
];
|
||||
_views = {
|
||||
"kanban,1": `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
form: `<form><field name="display_name"/></form>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner, ResCompany, ResPartner, ResUsers]);
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[1, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
test("error in a client action (at rendering)", async () => {
|
||||
expect.assertions(9);
|
||||
class Boom extends Component {
|
||||
static template = xml`<div><t t-esc="a.b.c"/></div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
actionRegistry.add("Boom", Boom);
|
||||
onRpc("web_search_read", () => {
|
||||
expect.step("web_search_read");
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(".o_breadcrumb").toHaveText("Partners Action 1");
|
||||
expect(queryAllTexts(".o_kanban_record span")).toEqual(["First record", "Second record"]);
|
||||
expect.verifySteps(["web_search_read"]);
|
||||
|
||||
try {
|
||||
await getService("action").doAction("Boom");
|
||||
} catch (e) {
|
||||
expect(e.cause).toBeInstanceOf(TypeError);
|
||||
}
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(".o_breadcrumb").toHaveText("Partners Action 1");
|
||||
expect(queryAllTexts(".o_kanban_record span")).toEqual(["First record", "Second record"]);
|
||||
expect.verifySteps(["web_search_read"]);
|
||||
});
|
||||
|
||||
test("error in a client action (after the first rendering)", async () => {
|
||||
expect.errors(1);
|
||||
|
||||
class Boom extends Component {
|
||||
static 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>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.boom = false;
|
||||
}
|
||||
get a() {
|
||||
// a bit artificial, but makes the test firefox compliant
|
||||
throw new Error("Cannot read properties of undefined (reading 'b')");
|
||||
}
|
||||
onClick() {
|
||||
this.boom = true;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
actionRegistry.add("Boom", Boom);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction("Boom");
|
||||
expect(".my_button").toHaveCount(1);
|
||||
|
||||
await contains(".my_button").click();
|
||||
await animationFrame();
|
||||
expect(".my_button").toHaveCount(1);
|
||||
expect(".o_error_dialog").toHaveCount(1);
|
||||
expect.verifyErrors(["Cannot read properties of undefined (reading 'b')"]);
|
||||
});
|
||||
|
||||
test("connection lost when opening form view from kanban", async () => {
|
||||
expect.errors(2);
|
||||
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
|
||||
mockFetch((input) => {
|
||||
expect.step(input);
|
||||
if (input === "/web/webclient/version_info") {
|
||||
// simulate a connection restore at the end of the test, to have no
|
||||
// impact on other tests (see connectionLostNotifRemove)
|
||||
return true;
|
||||
}
|
||||
throw new Error(); // simulate a ConnectionLost error
|
||||
});
|
||||
await contains(".o_kanban_record").click();
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification").toHaveText("Connection lost. Trying to reconnect...");
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
"/web/dataset/call_kw/partner/web_read", // from mockFetch
|
||||
"/web/dataset/call_kw/partner/web_search_read", // from mockFetch
|
||||
]);
|
||||
await animationFrame();
|
||||
expect.verifySteps([]); // doesn't indefinitely try to reload the list
|
||||
|
||||
// cleanup
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect.verifySteps(["/web/webclient/version_info"]);
|
||||
expect.verifyErrors([Error, Error]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("connection lost when coming back to kanban from form", async () => {
|
||||
expect.errors(1);
|
||||
|
||||
let offline = false;
|
||||
onRpc(
|
||||
"/*",
|
||||
(req) => {
|
||||
const url = new URL(req.url).pathname;
|
||||
if (!url.startsWith("/web/webclient/translations")) {
|
||||
expect.step(url);
|
||||
}
|
||||
if (url === "/web/webclient/version_info") {
|
||||
// simulate a connection restore
|
||||
return true;
|
||||
}
|
||||
if (offline) {
|
||||
throw new Error(); // simulate a ConnectionLost error
|
||||
}
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
|
||||
await contains(".o_kanban_record").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
|
||||
offline = true;
|
||||
await contains(".o_breadcrumb .o_back_button a").click();
|
||||
await animationFrame();
|
||||
expect(".o_form_view").toHaveCount(0);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(".o_kanban_view .o_kanban_renderer").toHaveCount(1);
|
||||
expect(".o_kanban_view .o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification").toHaveText("Connection lost. Trying to reconnect...");
|
||||
expect.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/res.users/has_group",
|
||||
"/web/dataset/call_kw/partner/web_read",
|
||||
"/web/dataset/call_kw/partner/web_search_read",
|
||||
]);
|
||||
await animationFrame();
|
||||
expect.verifySteps([]); // doesn't indefinitely try to reload the list
|
||||
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect.verifySteps(["/web/webclient/version_info"]);
|
||||
expect.verifyErrors([Error]);
|
||||
|
||||
offline = false;
|
||||
await contains(".o_searchview .o_searchview_icon").click();
|
||||
expect(".o_kanban_view .o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
|
||||
expect.verifySteps(["/web/dataset/call_kw/partner/web_search_read"]);
|
||||
});
|
||||
|
||||
test("error on onMounted", async () => {
|
||||
expect.errors(1);
|
||||
|
||||
Partner._fields.bar = fields.Boolean();
|
||||
Partner._views = {
|
||||
"kanban,1": `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
form: `<form><field name="display_name"/><field name="bar"/></form>`,
|
||||
};
|
||||
stepAllNetworkCalls();
|
||||
patchWithCleanup(BooleanField.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
onMounted(() => {
|
||||
throw new Error("faulty on mounted");
|
||||
});
|
||||
},
|
||||
});
|
||||
patchWithCleanup(FormController.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
onMounted(() => {
|
||||
// If a onMounted hook is faulty, the rest of the onMounted will not be executed
|
||||
// leading to inconsistent views.
|
||||
throw new Error("Never Executed code");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
await animationFrame();
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
]);
|
||||
|
||||
await contains(".o_kanban_record").click();
|
||||
await animationFrame();
|
||||
expect(".o_form_view").toHaveCount(0);
|
||||
// check that the action manager is empty
|
||||
expect(".o_action_manager").toHaveText("");
|
||||
expect(".o_error_dialog").toHaveCount(1);
|
||||
expect.verifySteps(["web_read"]);
|
||||
expect.verifyErrors(["Error: faulty on mounted"]);
|
||||
});
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
/** @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");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,776 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import testUtils from "web.test_utils";
|
||||
import ListController from "web.ListController";
|
||||
import FormView from "web.FormView";
|
||||
import ListView from "web.ListView";
|
||||
import {
|
||||
click,
|
||||
destroy,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
legacyExtraNextTick,
|
||||
patchWithCleanup,
|
||||
triggerEvents,
|
||||
} from "../../helpers/utils";
|
||||
import KanbanView from "web.KanbanView";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
|
||||
import makeTestEnvironment from "web.test_env";
|
||||
|
||||
import { ClientActionAdapter, ViewAdapter } from "@web/legacy/action_adapters";
|
||||
import { makeLegacyCrashManagerService } from "@web/legacy/utils";
|
||||
import { useDebugCategory } from "@web/core/debug/debug_context";
|
||||
import { ErrorDialog } from "@web/core/errors/error_dialogs";
|
||||
import * as cpHelpers from "@web/../tests/search/helpers";
|
||||
|
||||
import AbstractView from "web.AbstractView";
|
||||
import ControlPanel from "web.ControlPanel";
|
||||
import core from "web.core";
|
||||
import AbstractAction from "web.AbstractAction";
|
||||
import Widget from "web.Widget";
|
||||
import SystrayMenu from "web.SystrayMenu";
|
||||
import legacyViewRegistry from "web.view_registry";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("ActionManager", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
registry.category("views").remove("form"); // remove new form from registry
|
||||
registry.category("views").remove("kanban"); // remove new kanban from registry
|
||||
registry.category("views").remove("list"); // remove new list from registry
|
||||
legacyViewRegistry.add("form", FormView); // add legacy form -> will be wrapped and added to new registry
|
||||
legacyViewRegistry.add("kanban", KanbanView); // add legacy kanban -> will be wrapped and added to new registry
|
||||
legacyViewRegistry.add("list", ListView); // add legacy list -> will be wrapped and added to new registry
|
||||
|
||||
serverData = getActionManagerServerData();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("Legacy tests (to eventually drop)");
|
||||
|
||||
QUnit.test("display warning as notification", async function (assert) {
|
||||
// this test can be removed as soon as the legacy layer is dropped
|
||||
assert.expect(5);
|
||||
let list;
|
||||
patchWithCleanup(ListController.prototype, {
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
list = this;
|
||||
},
|
||||
});
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_legacy_list_view");
|
||||
list.trigger_up("warning", {
|
||||
title: "Warning!!!",
|
||||
message: "This is a warning...",
|
||||
});
|
||||
await testUtils.nextTick();
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_legacy_list_view");
|
||||
assert.containsOnce(document.body, ".o_notification.border-warning");
|
||||
assert.strictEqual($(".o_notification_title").text(), "Warning!!!");
|
||||
assert.strictEqual($(".o_notification_content").text(), "This is a warning...");
|
||||
});
|
||||
|
||||
QUnit.test("display warning as modal", async function (assert) {
|
||||
// this test can be removed as soon as the legacy layer is dropped
|
||||
assert.expect(5);
|
||||
let list;
|
||||
patchWithCleanup(ListController.prototype, {
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
list = this;
|
||||
},
|
||||
});
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_legacy_list_view");
|
||||
list.trigger_up("warning", {
|
||||
title: "Warning!!!",
|
||||
message: "This is a warning...",
|
||||
type: "dialog",
|
||||
});
|
||||
await testUtils.nextTick();
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_legacy_list_view");
|
||||
assert.containsOnce(document.body, ".modal");
|
||||
assert.strictEqual($(".modal-title").text(), "Warning!!!");
|
||||
assert.strictEqual($(".modal-body").text(), "This is a warning...");
|
||||
});
|
||||
|
||||
QUnit.test("display multiline warning as modal", async function (assert) {
|
||||
assert.expect(5);
|
||||
let list;
|
||||
patchWithCleanup(ListController.prototype, {
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
list = this;
|
||||
},
|
||||
});
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_legacy_list_view");
|
||||
list.trigger_up("warning", {
|
||||
title: "Warning!!!",
|
||||
message: "This is a warning...\nabc",
|
||||
type: "dialog",
|
||||
});
|
||||
await testUtils.nextTick();
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_legacy_list_view");
|
||||
assert.containsOnce(document.body, ".modal");
|
||||
assert.strictEqual($(".modal-title").text(), "Warning!!!");
|
||||
assert.strictEqual($(".modal-body")[0].innerText, "This is a warning...\nabc");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"legacy crash manager is still properly remapped to error service",
|
||||
async function (assert) {
|
||||
// this test can be removed as soon as the legacy layer is dropped
|
||||
assert.expect(2);
|
||||
|
||||
const legacyEnv = makeTestEnvironment();
|
||||
registry
|
||||
.category("services")
|
||||
.add("legacy_crash_manager", makeLegacyCrashManagerService(legacyEnv))
|
||||
.add("dialog", {
|
||||
start() {
|
||||
return {
|
||||
add(dialogClass, props) {
|
||||
assert.strictEqual(dialogClass, ErrorDialog);
|
||||
assert.strictEqual(props.traceback, "BOOM");
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await makeTestEnv();
|
||||
legacyEnv.services.crash_manager.show_message("BOOM");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("redraw a controller and open debugManager does not crash", async (assert) => {
|
||||
assert.expect(11);
|
||||
|
||||
const LegacyAction = AbstractAction.extend({
|
||||
start() {
|
||||
const ret = this._super(...arguments);
|
||||
const el = document.createElement("div");
|
||||
el.classList.add("custom-action");
|
||||
this.el.append(el);
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
core.action_registry.add("customLegacy", LegacyAction);
|
||||
|
||||
patchWithCleanup(ClientActionAdapter.prototype, {
|
||||
setup() {
|
||||
useDebugCategory("custom", { widget: this });
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
registry
|
||||
.category("debug")
|
||||
.category("custom")
|
||||
.add("item1", ({ widget }) => {
|
||||
assert.step("debugItems executed");
|
||||
assert.ok(widget);
|
||||
return {};
|
||||
});
|
||||
patchWithCleanup(odoo, { debug: true });
|
||||
|
||||
const mockRPC = (route) => {
|
||||
if (route.includes("check_access_rights")) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, "customLegacy");
|
||||
assert.containsOnce(target, ".custom-action");
|
||||
assert.verifySteps([]);
|
||||
|
||||
await click(target, ".o_debug_manager button");
|
||||
assert.verifySteps(["debugItems executed"]);
|
||||
|
||||
await doAction(webClient, 5); // action in Dialog
|
||||
await click(target, ".modal .o_form_button_cancel");
|
||||
assert.containsNone(target, ".modal");
|
||||
assert.containsOnce(target, ".custom-action");
|
||||
assert.verifySteps([]);
|
||||
|
||||
// close debug menu
|
||||
await click(target, ".o_debug_manager button");
|
||||
// open debug menu
|
||||
await click(target, ".o_debug_manager button");
|
||||
assert.verifySteps(["debugItems executed"]);
|
||||
delete core.action_registry.map.customLegacy;
|
||||
});
|
||||
|
||||
QUnit.test("willUnmount is called down the legacy layers", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
let mountCount = 0;
|
||||
patchWithCleanup(ControlPanel.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
owl.onMounted(() => {
|
||||
mountCount = mountCount + 1;
|
||||
this.__uniqueId = mountCount;
|
||||
assert.step(`mounted ${this.__uniqueId}`);
|
||||
});
|
||||
owl.onWillUnmount(() => {
|
||||
assert.step(`willUnmount ${this.__uniqueId}`);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const LegacyAction = AbstractAction.extend({
|
||||
hasControlPanel: true,
|
||||
start() {
|
||||
const ret = this._super(...arguments);
|
||||
const el = document.createElement("div");
|
||||
el.classList.add("custom-action");
|
||||
this.el.append(el);
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
core.action_registry.add("customLegacy", LegacyAction);
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
await doAction(webClient, "customLegacy");
|
||||
await click(target.querySelectorAll(".breadcrumb-item")[0]);
|
||||
await legacyExtraNextTick();
|
||||
|
||||
destroy(webClient);
|
||||
|
||||
assert.verifySteps([
|
||||
"mounted 1",
|
||||
"willUnmount 1",
|
||||
"mounted 2",
|
||||
"willUnmount 2",
|
||||
"mounted 3",
|
||||
"willUnmount 3",
|
||||
]);
|
||||
|
||||
delete core.action_registry.map.customLegacy;
|
||||
});
|
||||
|
||||
QUnit.test("Checks the availability of all views in the action", async (assert) => {
|
||||
assert.expect(2);
|
||||
patchWithCleanup(ListView.prototype, {
|
||||
init(viewInfo, params) {
|
||||
const action = params.action;
|
||||
const views = action.views.map((view) => [view.viewID, view.type]);
|
||||
assert.deepEqual(views, [
|
||||
[1, "list"],
|
||||
[2, "kanban"],
|
||||
[3, "form"],
|
||||
]);
|
||||
assert.deepEqual(action._views, [
|
||||
[1, "list"],
|
||||
[2, "kanban"],
|
||||
[3, "form"],
|
||||
[false, "search"],
|
||||
]);
|
||||
this._super(...arguments);
|
||||
},
|
||||
});
|
||||
const models = {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char", searchable: true },
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
},
|
||||
bar: { string: "Bar", type: "boolean" },
|
||||
int_field: { string: "Integer field", type: "integer", group_operator: "sum" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
foo: "yop",
|
||||
int_field: 3,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
foo: "lalala",
|
||||
int_field: 5,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
foo: "abc",
|
||||
int_field: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const views = {
|
||||
"partner,1,list": '<list><field name="foo"/></list>',
|
||||
"partner,2,kanban": "<kanban></kanban>",
|
||||
"partner,3,form": `<form></form>`,
|
||||
"partner,false,search": "<search></search>",
|
||||
};
|
||||
const serverData = { models, views };
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, {
|
||||
id: 1,
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[1, "list"],
|
||||
[2, "kanban"],
|
||||
[3, "form"],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("client actions may take and push their params", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const ClientAction = AbstractAction.extend({
|
||||
init(parent, action) {
|
||||
this._super(...arguments);
|
||||
assert.deepEqual(action.params, {
|
||||
active_id: 99,
|
||||
take: "five",
|
||||
active_ids: "1,2",
|
||||
list: [9, 10],
|
||||
});
|
||||
},
|
||||
});
|
||||
core.action_registry.add("clientAction", ClientAction);
|
||||
registerCleanup(() => delete core.action_registry.map.clientAction);
|
||||
const webClient = await createWebClient({});
|
||||
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.client",
|
||||
tag: "clientAction",
|
||||
params: {
|
||||
active_id: 99,
|
||||
take: "five",
|
||||
active_ids: "1,2",
|
||||
list: [9, 10],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(webClient.env.services.router.current.hash, {
|
||||
action: "clientAction",
|
||||
active_id: 99,
|
||||
take: "five",
|
||||
active_ids: "1,2",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("client actions honour do_push_state", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const ClientAction = AbstractAction.extend({
|
||||
init(parent) {
|
||||
this._super(...arguments);
|
||||
this.parent = parent;
|
||||
this.parent.do_push_state({ pinball: "wizard" });
|
||||
},
|
||||
|
||||
async start() {
|
||||
await this._super(...arguments);
|
||||
const btn = document.createElement("button");
|
||||
btn.classList.add("tommy");
|
||||
btn.addEventListener("click", () => {
|
||||
this.parent.do_push_state({ gipsy: "the acid queen" });
|
||||
});
|
||||
this.el.append(btn);
|
||||
},
|
||||
|
||||
getState() {
|
||||
return {
|
||||
doctor: "quackson",
|
||||
};
|
||||
},
|
||||
});
|
||||
core.action_registry.add("clientAction", ClientAction);
|
||||
registerCleanup(() => delete core.action_registry.map.clientAction);
|
||||
const webClient = await createWebClient({});
|
||||
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.client",
|
||||
tag: "clientAction",
|
||||
});
|
||||
|
||||
assert.deepEqual(webClient.env.services.router.current.hash, {
|
||||
action: "clientAction",
|
||||
pinball: "wizard",
|
||||
doctor: "quackson",
|
||||
});
|
||||
|
||||
await click(target, ".tommy");
|
||||
assert.deepEqual(webClient.env.services.router.current.hash, {
|
||||
action: "clientAction",
|
||||
pinball: "wizard",
|
||||
gipsy: "the acid queen",
|
||||
doctor: "quackson",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("Systray item triggers do action on legacy service provider", async (assert) => {
|
||||
assert.expect(3);
|
||||
function createMockActionService(assert) {
|
||||
return {
|
||||
dependencies: [],
|
||||
start() {
|
||||
return {
|
||||
doAction(params) {
|
||||
assert.step("do action");
|
||||
assert.strictEqual(params, 128, "The doAction parameters are invalid.");
|
||||
},
|
||||
loadState() {},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
registry.category("services").add("action", createMockActionService(assert));
|
||||
const FakeSystrayItemWidget = Widget.extend({
|
||||
on_attach_callback() {
|
||||
this.do_action(128);
|
||||
},
|
||||
});
|
||||
SystrayMenu.Items.push(FakeSystrayItemWidget);
|
||||
await createWebClient({ serverData });
|
||||
assert.verifySteps(["do action"]);
|
||||
delete SystrayMenu.Items.FakeSystrayItemWidget;
|
||||
});
|
||||
|
||||
QUnit.test("usercontext always added to legacy actions", async (assert) => {
|
||||
assert.expect(8);
|
||||
core.action_registry.add("testClientAction", AbstractAction);
|
||||
registerCleanup(() => delete core.action_registry.map.testClientAction);
|
||||
patchWithCleanup(ClientActionAdapter.prototype, {
|
||||
setup() {
|
||||
assert.step("ClientActionAdapter");
|
||||
const action = { ...this.props.widgetArgs[0] };
|
||||
const originalAction = JSON.parse(action._originalAction);
|
||||
assert.deepEqual(originalAction.context, undefined);
|
||||
assert.deepEqual(action.context, this.env.services.user.context);
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
patchWithCleanup(ViewAdapter.prototype, {
|
||||
setup() {
|
||||
assert.step("ViewAdapter");
|
||||
const action = { ...this.props.viewParams.action };
|
||||
const originalAction = JSON.parse(action._originalAction);
|
||||
assert.deepEqual(originalAction.context, undefined);
|
||||
assert.deepEqual(action.context, this.env.services.user.context);
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, "testClientAction");
|
||||
assert.verifySteps(["ClientActionAdapter"]);
|
||||
await doAction(webClient, 1);
|
||||
assert.verifySteps(["ViewAdapter"]);
|
||||
});
|
||||
|
||||
QUnit.test("correctly transports legacy Props for doAction", async (assert) => {
|
||||
assert.expect(4);
|
||||
|
||||
let ID = 0;
|
||||
const MyAction = AbstractAction.extend({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.ID = ID++;
|
||||
assert.step(`id: ${this.ID} props: ${JSON.stringify(arguments[2])}`);
|
||||
},
|
||||
async start() {
|
||||
const res = await this._super(...arguments);
|
||||
const link = document.createElement("a");
|
||||
link.innerText = "some link";
|
||||
link.setAttribute("id", `client_${this.ID}`);
|
||||
link.addEventListener("click", () => {
|
||||
this.do_action("testClientAction", {
|
||||
clear_breadcrumbs: true,
|
||||
props: { chain: "never break" },
|
||||
});
|
||||
});
|
||||
|
||||
this.el.appendChild(link);
|
||||
return res;
|
||||
},
|
||||
});
|
||||
core.action_registry.add("testClientAction", MyAction);
|
||||
registerCleanup(() => delete core.action_registry.map.testClientAction);
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, "testClientAction");
|
||||
assert.verifySteps(['id: 0 props: {"className":"o_action","breadcrumbs":[]}']);
|
||||
|
||||
await click(document.getElementById("client_0"));
|
||||
assert.verifySteps([
|
||||
'id: 1 props: {"chain":"never break","className":"o_action","breadcrumbs":[]}',
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("bootstrap tooltip in dialog action auto destroy", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
const mockRPC = (route) => {
|
||||
if (route === "/web/dataset/call_button") {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
serverData.views["partner,3,form"] = /*xml*/ `
|
||||
<form>
|
||||
<field name="display_name" />
|
||||
<footer>
|
||||
<button name="echoes" type="object" string="Echoes" help="echoes"/>
|
||||
</footer>
|
||||
</form>
|
||||
`;
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
await doAction(webClient, 25);
|
||||
|
||||
const tooltipProm = makeDeferred();
|
||||
$(target).one("shown.bs.tooltip", () => {
|
||||
tooltipProm.resolve();
|
||||
});
|
||||
|
||||
triggerEvents(target, ".modal footer button", ["mouseover", "focusin"]);
|
||||
await tooltipProm;
|
||||
// check on webClient dom
|
||||
assert.containsOnce(document.body, ".tooltip");
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.act_window_close",
|
||||
});
|
||||
// check on the whole DOM
|
||||
assert.containsNone(document.body, ".tooltip");
|
||||
});
|
||||
|
||||
QUnit.test("bootstrap tooltip destroyed on click", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
const mockRPC = (route) => {
|
||||
if (route === "/web/dataset/call_button") {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
serverData.views["partner,666,form"] = /*xml*/ `
|
||||
<form>
|
||||
<header>
|
||||
<button name="echoes" type="object" string="Echoes" help="echoes"/>
|
||||
</header>
|
||||
<field name="display_name" />
|
||||
</form>
|
||||
`;
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
await doAction(webClient, 24);
|
||||
|
||||
const tooltipProm = makeDeferred();
|
||||
$(target).one("shown.bs.tooltip", () => {
|
||||
tooltipProm.resolve();
|
||||
});
|
||||
|
||||
triggerEvents(target, ".o_form_statusbar button", ["mouseover", "focusin"]);
|
||||
await tooltipProm;
|
||||
// check on webClient DOM
|
||||
assert.containsOnce(document.body, ".tooltip");
|
||||
await click(target, ".o_content");
|
||||
// check on the whole DOM
|
||||
assert.containsNone(document.body, ".tooltip");
|
||||
});
|
||||
|
||||
QUnit.test("breadcrumbs are correct in stacked legacy client actions", async function (assert) {
|
||||
const ClientAction = AbstractAction.extend({
|
||||
hasControlPanel: true,
|
||||
async start() {
|
||||
this.$el.addClass("client_action");
|
||||
return this._super(...arguments);
|
||||
},
|
||||
getTitle() {
|
||||
return "Blabla";
|
||||
},
|
||||
});
|
||||
core.action_registry.add("clientAction", ClientAction);
|
||||
registerCleanup(() => delete core.action_registry.map.clientAction);
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_legacy_list_view");
|
||||
assert.strictEqual($(target).find(".breadcrumb-item").text(), "Partners");
|
||||
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.client",
|
||||
tag: "clientAction",
|
||||
});
|
||||
assert.containsOnce(target, ".client_action");
|
||||
assert.strictEqual($(target).find(".breadcrumb-item").text(), "PartnersBlabla");
|
||||
});
|
||||
|
||||
QUnit.test("view with js_class attribute (legacy)", async function (assert) {
|
||||
assert.expect(2);
|
||||
const TestView = AbstractView.extend({
|
||||
viewType: "test_view",
|
||||
});
|
||||
const TestJsClassView = TestView.extend({
|
||||
init() {
|
||||
this._super.call(this, ...arguments);
|
||||
assert.step("init js class");
|
||||
},
|
||||
});
|
||||
serverData.views["partner,false,test_view"] = `<div js_class="test_jsClass"></div>`;
|
||||
serverData.actions[9999] = {
|
||||
id: 1,
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "test_view"]],
|
||||
};
|
||||
legacyViewRegistry.add("test_view", TestView);
|
||||
legacyViewRegistry.add("test_jsClass", TestJsClassView);
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 9999);
|
||||
assert.verifySteps(["init js class"]);
|
||||
delete legacyViewRegistry.map.test_view;
|
||||
delete legacyViewRegistry.map.test_jsClass;
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"execute action without modal closes bootstrap tooltips anyway",
|
||||
async function (assert) {
|
||||
assert.expect(12);
|
||||
Object.assign(serverData.views, {
|
||||
"partner,666,form": `<form>
|
||||
<header>
|
||||
<button name="object" string="Call method" type="object" help="need somebody"/>
|
||||
</header>
|
||||
<field name="display_name"/>
|
||||
</form>`,
|
||||
});
|
||||
const mockRPC = async (route) => {
|
||||
assert.step(route);
|
||||
if (route === "/web/dataset/call_button") {
|
||||
// Some business stuff server side, then return an implicit close action
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 24);
|
||||
assert.verifySteps([
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/web/dataset/call_kw/partner/get_views",
|
||||
"/web/dataset/call_kw/partner/read",
|
||||
]);
|
||||
assert.containsN(target, ".o_form_buttons_view button:not([disabled])", 2);
|
||||
const actionButton = target.querySelector("button[name=object]");
|
||||
const tooltipProm = new Promise((resolve) => {
|
||||
document.body.addEventListener(
|
||||
"shown.bs.tooltip",
|
||||
() => {
|
||||
actionButton.dispatchEvent(new Event("mouseout"));
|
||||
resolve();
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
actionButton.dispatchEvent(new Event("mouseover"));
|
||||
await tooltipProm;
|
||||
assert.containsOnce(document.body, ".tooltip");
|
||||
await click(actionButton);
|
||||
await legacyExtraNextTick();
|
||||
assert.verifySteps(["/web/dataset/call_button", "/web/dataset/call_kw/partner/read"]);
|
||||
assert.containsNone(document.body, ".tooltip"); // body different from webClient in tests !
|
||||
assert.containsN(target, ".o_form_buttons_view button:not([disabled])", 2);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("click multiple times to open a record", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const def = testUtils.makeTestPromise();
|
||||
const defs = [null, def];
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "read") {
|
||||
await Promise.resolve(defs.shift());
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 3);
|
||||
|
||||
assert.containsOnce(target, ".o_legacy_list_view");
|
||||
|
||||
await testUtils.dom.click(target.querySelector(".o_legacy_list_view .o_data_row"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_legacy_form_view");
|
||||
|
||||
await testUtils.dom.click(target.querySelector(".o_back_button"));
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_legacy_list_view");
|
||||
|
||||
await testUtils.dom.click(target.querySelector(".o_legacy_list_view .o_data_row"));
|
||||
await testUtils.dom.click(target.querySelector(".o_legacy_list_view .o_data_row"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_legacy_list_view");
|
||||
|
||||
def.resolve();
|
||||
await testUtils.nextTick();
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_legacy_form_view");
|
||||
});
|
||||
|
||||
QUnit.test("correct pager when coming from list (legacy)", async (assert) => {
|
||||
assert.expect(4);
|
||||
|
||||
registry.category("views").remove("list");
|
||||
legacyViewRegistry.add("list", ListView);
|
||||
serverData.views = {
|
||||
"partner,false,search": `<search />`,
|
||||
"partner,99,list": `<list limit="4"><field name="display_name" /></list>`,
|
||||
"partner,100,form": `<form><field name="display_name" /></form>`,
|
||||
};
|
||||
|
||||
const wc = await createWebClient({ serverData });
|
||||
await doAction(wc, {
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[99, "list"],
|
||||
[100, "form"],
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(cpHelpers.getPagerValue(target), [1, 4]);
|
||||
assert.deepEqual(cpHelpers.getPagerLimit(target), 5);
|
||||
|
||||
await click(target, ".o_data_row:nth-child(2) .o_data_cell");
|
||||
await legacyExtraNextTick();
|
||||
assert.deepEqual(cpHelpers.getPagerValue(target), [2]);
|
||||
assert.deepEqual(cpHelpers.getPagerLimit(target), 4);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,794 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { queryOne, scroll, waitFor } from "@odoo/hoot-dom";
|
||||
import { animationFrame, Deferred } from "@odoo/hoot-mock";
|
||||
import { Component, onWillStart, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineMenus,
|
||||
defineModels,
|
||||
fields,
|
||||
getDropdownMenu,
|
||||
getService,
|
||||
makeMockEnv,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
stepAllNetworkCalls,
|
||||
switchView,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { router } from "@web/core/browser/router";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { PivotModel } from "@web/views/pivot/pivot_model";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
import { redirect } from "@web/core/utils/urls";
|
||||
|
||||
const { ResCompany, ResPartner, ResUsers } = webModels;
|
||||
|
||||
class Partner extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
o2m = fields.One2many({ relation: "partner", relation_field: "bar" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record", o2m: [2, 3] },
|
||||
{
|
||||
id: 2,
|
||||
display_name: "Second record",
|
||||
o2m: [1, 4, 5],
|
||||
},
|
||||
{ id: 3, display_name: "Third record", o2m: [] },
|
||||
{ id: 4, display_name: "Fourth record", o2m: [] },
|
||||
{ id: 5, display_name: "Fifth record", o2m: [] },
|
||||
];
|
||||
_views = {
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<button name="object" string="Call method" type="object"/>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</header>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</form>`,
|
||||
"kanban,1": `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: `<list><field name="display_name"/></list>`,
|
||||
"list,2": `<list limit="3"><field name="display_name"/></list>`,
|
||||
};
|
||||
}
|
||||
|
||||
class Pony extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 4, name: "Twilight Sparkle" },
|
||||
{ id: 6, name: "Applejack" },
|
||||
{ id: 9, name: "Fluttershy" },
|
||||
];
|
||||
_views = {
|
||||
list: '<list><field name="name"/></list>',
|
||||
form: `<form><field name="name"/></form>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner, Pony, ResCompany, ResPartner, ResUsers]);
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[1, "kanban"]],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
xml_id: "action_3",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[1, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
xml_id: "action_5",
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
xml_id: "action_4",
|
||||
name: "Partners Action 4",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[1, "kanban"],
|
||||
[2, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
xml_id: "action_8",
|
||||
name: "Favorite Ponies",
|
||||
res_model: "pony",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const actionRegistry = registry.category("actions");
|
||||
const actionHandlersRegistry = registry.category("action_handlers");
|
||||
|
||||
test("can execute actions from id, xmlid and tag", async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 10,
|
||||
tag: "client_action_by_db_id",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
xml_id: "some_action",
|
||||
tag: "client_action_by_xml_id",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
},
|
||||
{
|
||||
id: 30,
|
||||
path: "my_action",
|
||||
tag: "client_action_by_path",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
},
|
||||
]);
|
||||
actionRegistry
|
||||
.add("client_action_by_db_id", () => expect.step("client_action_db_id"))
|
||||
.add("client_action_by_xml_id", () => expect.step("client_action_xml_id"))
|
||||
.add("client_action_by_path", () => expect.step("client_action_path"))
|
||||
.add("client_action_by_tag", () => expect.step("client_action_tag"))
|
||||
.add("client_action_by_object", () => expect.step("client_action_object"));
|
||||
|
||||
await makeMockEnv();
|
||||
await getService("action").doAction(10);
|
||||
expect.verifySteps(["client_action_db_id"]);
|
||||
await getService("action").doAction("some_action");
|
||||
expect.verifySteps(["client_action_xml_id"]);
|
||||
await getService("action").doAction("my_action");
|
||||
expect.verifySteps(["client_action_path"]);
|
||||
await getService("action").doAction("client_action_by_tag");
|
||||
expect.verifySteps(["client_action_tag"]);
|
||||
await getService("action").doAction({
|
||||
tag: "client_action_by_object",
|
||||
target: "current",
|
||||
type: "ir.actions.client",
|
||||
});
|
||||
expect.verifySteps(["client_action_object"]);
|
||||
});
|
||||
|
||||
test("action doesn't exists", async () => {
|
||||
expect.assertions(1);
|
||||
await makeMockEnv();
|
||||
try {
|
||||
await getService("action").doAction({
|
||||
tag: "this_is_a_tag",
|
||||
target: "current",
|
||||
type: "ir.not_action.error",
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
"The ActionManager service can't handle actions of type ir.not_action.error"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("getCurrentAction", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
const currentAction = await getService("action").currentAction;
|
||||
expect(currentAction).toEqual({
|
||||
binding_type: "action",
|
||||
binding_view_types: "list,form",
|
||||
id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[1, "kanban"]],
|
||||
context: {},
|
||||
embedded_action_ids: [],
|
||||
group_ids: [],
|
||||
limit: 80,
|
||||
mobile_view_mode: "kanban",
|
||||
target: "current",
|
||||
view_ids: [],
|
||||
view_mode: "list,form",
|
||||
cache: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("getCurrentAction (virtual controller)", async () => {
|
||||
stepAllNetworkCalls();
|
||||
class ClientAction extends Component {
|
||||
static template = xml`<div class="o_client_action_test">Hello World</div>`;
|
||||
static props = ["*"];
|
||||
static path = "plop";
|
||||
setup() {
|
||||
onWillStart(async () => {
|
||||
const currentAction = await getService("action").currentAction;
|
||||
expect.step(currentAction);
|
||||
});
|
||||
}
|
||||
}
|
||||
actionRegistry.add("HelloWorldTest", ClientAction);
|
||||
|
||||
redirect("/odoo/action-1/plop");
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load_breadcrumbs",
|
||||
"/web/action/load",
|
||||
{
|
||||
binding_type: "action",
|
||||
binding_view_types: "list,form",
|
||||
id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[1, "kanban"]],
|
||||
context: {},
|
||||
embedded_action_ids: [],
|
||||
group_ids: [],
|
||||
limit: 80,
|
||||
mobile_view_mode: "kanban",
|
||||
target: "current",
|
||||
view_ids: [],
|
||||
view_mode: "list,form",
|
||||
cache: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("action in handler registry", async () => {
|
||||
await makeMockEnv();
|
||||
actionHandlersRegistry.add("ir.action_in_handler_registry", ({ action }) =>
|
||||
expect.step(action.type)
|
||||
);
|
||||
await getService("action").doAction({
|
||||
tag: "this_is_a_tag",
|
||||
target: "current",
|
||||
type: "ir.action_in_handler_registry",
|
||||
});
|
||||
expect.verifySteps(["ir.action_in_handler_registry"]);
|
||||
});
|
||||
|
||||
test("properly handle case when action id does not exist", async () => {
|
||||
expect.errors(1);
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("action").doAction(4448);
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["RPC_ERROR"]);
|
||||
expect(`.modal .o_error_dialog`).toHaveCount(1);
|
||||
expect(".o_error_dialog .modal-body").toHaveText("The action 4448 does not exist");
|
||||
});
|
||||
|
||||
test("properly handle case when action path does not exist", async () => {
|
||||
expect.errors(1);
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("action").doAction("plop");
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["RPC_ERROR"]);
|
||||
expect(`.modal .o_error_dialog`).toHaveCount(1);
|
||||
expect(".o_error_dialog .modal-body").toHaveText('The action "plop" does not exist');
|
||||
});
|
||||
|
||||
test("properly handle case when action xmlId does not exist", async () => {
|
||||
expect.errors(1);
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("action").doAction("not.found.action");
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["RPC_ERROR"]);
|
||||
expect(`.modal .o_error_dialog`).toHaveCount(1);
|
||||
expect(".o_error_dialog .modal-body").toHaveText(
|
||||
'The action "not.found.action" does not exist'
|
||||
);
|
||||
});
|
||||
|
||||
test("actions can be cached", async () => {
|
||||
onRpc("/web/action/load", async (request) => {
|
||||
const { params } = await request.json();
|
||||
expect.step(params.context);
|
||||
});
|
||||
|
||||
await makeMockEnv();
|
||||
|
||||
// With no additional params
|
||||
await getService("action").loadAction(3);
|
||||
await getService("action").loadAction(3);
|
||||
|
||||
// With specific context
|
||||
await getService("action").loadAction(3, { configuratorMode: "add" });
|
||||
await getService("action").loadAction(3, { configuratorMode: "edit" });
|
||||
|
||||
// With same active_id
|
||||
await getService("action").loadAction(3, { active_id: 1 });
|
||||
await getService("action").loadAction(3, { active_id: 1 });
|
||||
|
||||
// With active_id change
|
||||
await getService("action").loadAction(3, { active_id: 2 });
|
||||
|
||||
// With same active_ids
|
||||
await getService("action").loadAction(3, { active_ids: [1, 2] });
|
||||
await getService("action").loadAction(3, { active_ids: [1, 2] });
|
||||
|
||||
// With active_ids change
|
||||
await getService("action").loadAction(3, { active_ids: [1, 2, 3] });
|
||||
|
||||
// With same active_model
|
||||
await getService("action").loadAction(3, { active_model: "a" });
|
||||
await getService("action").loadAction(3, { active_model: "a" });
|
||||
|
||||
// With active_model change
|
||||
await getService("action").loadAction(3, { active_model: "b" });
|
||||
|
||||
// should load from server once per active_id/active_ids/active_model change, nothing else
|
||||
const baseCtx = {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
allowed_company_ids: [1],
|
||||
};
|
||||
expect.verifySteps([
|
||||
{ ...baseCtx },
|
||||
{ ...baseCtx, configuratorMode: "add" },
|
||||
{ ...baseCtx, configuratorMode: "edit" },
|
||||
{ ...baseCtx, active_id: 1 },
|
||||
{ ...baseCtx, active_id: 2 },
|
||||
{ ...baseCtx, active_ids: [1, 2] },
|
||||
{ ...baseCtx, active_ids: [1, 2, 3] },
|
||||
{ ...baseCtx, active_model: "a" },
|
||||
{ ...baseCtx, active_model: "b" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("action cache: additionalContext is used on the key", async () => {
|
||||
onRpc("/web/action/load", () => {
|
||||
expect.step("server loaded");
|
||||
});
|
||||
|
||||
await makeMockEnv();
|
||||
const actionParams = {
|
||||
additionalContext: {
|
||||
some: { deep: { nested: "Robert" } },
|
||||
},
|
||||
};
|
||||
|
||||
let action = await getService("action").loadAction(3, actionParams);
|
||||
expect.verifySteps(["server loaded"]);
|
||||
expect(action.context).toEqual(actionParams);
|
||||
|
||||
// Modify the action in place
|
||||
action.context.additionalContext.some.deep.nested = "Nesta";
|
||||
|
||||
// Change additionalContext and reload
|
||||
actionParams.additionalContext.some.deep.nested = "Marley";
|
||||
action = await getService("action").loadAction(3, actionParams);
|
||||
expect.verifySteps(["server loaded"]);
|
||||
expect(action.context).toEqual(actionParams);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test('action with "no_breadcrumbs" set to true', async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 42,
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[1, "kanban"],
|
||||
[false, "list"],
|
||||
],
|
||||
context: { no_breadcrumbs: true },
|
||||
},
|
||||
]);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_breadcrumb").toHaveCount(1);
|
||||
// push another action flagged with 'no_breadcrumbs=true'
|
||||
await getService("action").doAction(42);
|
||||
await waitFor(".o_kanban_view");
|
||||
expect(".o_breadcrumb").toHaveCount(0);
|
||||
await contains(".o_switch_view.o_list").click();
|
||||
await waitFor(".o_list_view");
|
||||
expect(".o_breadcrumb").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("document's title is updated when an action is executed", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await animationFrame();
|
||||
let currentTitle = getService("title").getParts();
|
||||
expect(currentTitle).toEqual({});
|
||||
let currentState = router.current;
|
||||
await getService("action").doAction(4);
|
||||
await animationFrame();
|
||||
currentTitle = getService("title").getParts();
|
||||
expect(currentTitle).toEqual({ action: "Partners Action 4" });
|
||||
currentState = router.current;
|
||||
expect(currentState).toEqual({
|
||||
action: 4,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await getService("action").doAction(8);
|
||||
await animationFrame();
|
||||
currentTitle = getService("title").getParts();
|
||||
expect(currentTitle).toEqual({ action: "Favorite Ponies" });
|
||||
currentState = router.current;
|
||||
expect(currentState).toEqual({
|
||||
action: 8,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
{
|
||||
action: 8,
|
||||
displayName: "Favorite Ponies",
|
||||
view_type: "list",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await contains(".o_data_row .o_data_cell").click();
|
||||
await animationFrame();
|
||||
currentTitle = getService("title").getParts();
|
||||
expect(currentTitle).toEqual({ action: "Twilight Sparkle" });
|
||||
currentState = router.current;
|
||||
expect(currentState).toEqual({
|
||||
action: 8,
|
||||
resId: 4,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
{
|
||||
action: 8,
|
||||
displayName: "Favorite Ponies",
|
||||
view_type: "list",
|
||||
},
|
||||
{
|
||||
action: 8,
|
||||
resId: 4,
|
||||
displayName: "Twilight Sparkle",
|
||||
view_type: "form",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test('handles "history_back" event', async () => {
|
||||
let list;
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
list = this;
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(4);
|
||||
await getService("action").doAction(3);
|
||||
expect("ol.breadcrumb").toHaveCount(1);
|
||||
expect(".o_breadcrumb span").toHaveCount(1);
|
||||
list.env.config.historyBack();
|
||||
await animationFrame();
|
||||
expect(".o_breadcrumb span").toHaveCount(1);
|
||||
expect(".o_breadcrumb").toHaveText("Partners Action 4", {
|
||||
message: "breadcrumbs should display the display_name of the action",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("stores and restores scroll position (in kanban)", async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 10,
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [[false, "kanban"]],
|
||||
},
|
||||
]);
|
||||
for (let i = 0; i < 60; i++) {
|
||||
Partner._records.push({ id: 100 + i, display_name: `Record ${i}` });
|
||||
}
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("o_web_client");
|
||||
container.style.height = "250px";
|
||||
getFixture().appendChild(container);
|
||||
await mountWithCleanup(WebClient, { target: container });
|
||||
// execute a first action
|
||||
await getService("action").doAction(10);
|
||||
expect(".o_content").toHaveProperty("scrollTop", 0);
|
||||
// simulate a scroll
|
||||
await scroll(".o_content", { top: 100 });
|
||||
// execute a second action (in which we don't scroll)
|
||||
await getService("action").doAction(4);
|
||||
expect(".o_content").toHaveProperty("scrollTop", 0);
|
||||
// go back using the breadcrumbs
|
||||
await contains(".o_control_panel .breadcrumb a").click();
|
||||
expect(".o_content").toHaveProperty("scrollTop", 100);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("stores and restores scroll position (in list)", async () => {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
Partner._records.push({ id: 100 + i, display_name: `Record ${i}` });
|
||||
}
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("o_web_client");
|
||||
container.style.height = "250px";
|
||||
getFixture().appendChild(container);
|
||||
await mountWithCleanup(WebClient, { target: container });
|
||||
// execute a first action
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_content").toHaveProperty("scrollTop", 0);
|
||||
expect(queryOne(".o_list_renderer").scrollTop).toBe(0);
|
||||
// simulate a scroll
|
||||
queryOne(".o_list_renderer").scrollTop = 100;
|
||||
// execute a second action (in which we don't scroll)
|
||||
await getService("action").doAction(4);
|
||||
expect(".o_content").toHaveProperty("scrollTop", 0);
|
||||
// go back using the breadcrumbs
|
||||
await contains(".o_control_panel .breadcrumb a").click();
|
||||
expect(".o_content").toHaveProperty("scrollTop", 0);
|
||||
expect(queryOne(".o_list_renderer").scrollTop).toBe(100);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test('executing an action with target != "new" closes all dialogs', async () => {
|
||||
Partner._views["form"] = `
|
||||
<form>
|
||||
<field name="o2m">
|
||||
<list><field name="display_name"/></list>
|
||||
<form><field name="display_name"/></form>
|
||||
</field>
|
||||
</form>`;
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
await contains(".o_list_view .o_data_row .o_list_char").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
await contains(".o_form_view .o_data_row .o_data_cell").click();
|
||||
expect(".modal .o_form_view").toHaveCount(1);
|
||||
await getService("action").doAction(1); // target != 'new'
|
||||
await animationFrame(); // wait for the dialog to be closed
|
||||
expect(".modal").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test('executing an action with target "new" does not close dialogs', async () => {
|
||||
Partner._views["form"] = `
|
||||
<form>
|
||||
<field name="o2m">
|
||||
<list><field name="display_name"/></list>
|
||||
<form><field name="display_name"/></form>
|
||||
</field>
|
||||
</form>`;
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
await contains(".o_list_view .o_data_row .o_data_cell").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
await contains(".o_form_view .o_data_row .o_data_cell").click();
|
||||
expect(".modal .o_form_view").toHaveCount(1);
|
||||
await getService("action").doAction(5); // target 'new'
|
||||
expect(".modal .o_form_view").toHaveCount(2);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("search defaults are removed from context when switching view", async () => {
|
||||
expect.assertions(1);
|
||||
const context = {
|
||||
search_default_x: true,
|
||||
searchpanel_default_y: true,
|
||||
};
|
||||
patchWithCleanup(PivotModel.prototype, {
|
||||
load(searchParams) {
|
||||
expect(searchParams.context).toEqual({
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
});
|
||||
return super.load(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "pivot"],
|
||||
],
|
||||
context,
|
||||
});
|
||||
// list view is loaded, switch to pivot view
|
||||
await switchView("pivot");
|
||||
});
|
||||
|
||||
test("retrieving a stored action should remove 'allowed_company_ids' from its context (model)", async () => {
|
||||
// Prepare a multi company scenario
|
||||
serverState.companies = [
|
||||
{ id: 3, name: "Hermit", sequence: 1 },
|
||||
{ id: 2, name: "Herman's", sequence: 2 },
|
||||
{ id: 1, name: "Heroes TM", sequence: 3 },
|
||||
];
|
||||
|
||||
// Prepare a stored action
|
||||
browser.sessionStorage.setItem(
|
||||
"current_action",
|
||||
JSON.stringify({
|
||||
id: 1,
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[1, "kanban"]],
|
||||
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.
|
||||
Object.assign(browser.location, { search: "?model=partner&view_type=kanban" });
|
||||
|
||||
// Create the web client. It should execute the stored action.
|
||||
await mountWithCleanup(WebClient);
|
||||
await animationFrame(); // blank action
|
||||
|
||||
// Check the current action context
|
||||
expect(getService("action").currentController.action.context).toEqual({
|
||||
// action context
|
||||
someKey: 44,
|
||||
lang: "not_en",
|
||||
tz: "not_taht",
|
||||
uid: 42,
|
||||
// note there is no 'allowed_company_ids' in the action context
|
||||
});
|
||||
});
|
||||
|
||||
test("retrieving a stored action should remove 'allowed_company_ids' from its context (action)", async () => {
|
||||
// Prepare a multi company scenario
|
||||
serverState.companies = [
|
||||
{ id: 3, name: "Hermit", sequence: 1 },
|
||||
{ id: 2, name: "Herman's", sequence: 2 },
|
||||
{ id: 1, name: "Heroes TM", sequence: 3 },
|
||||
];
|
||||
|
||||
// Prepare a stored action
|
||||
browser.sessionStorage.setItem(
|
||||
"current_action",
|
||||
JSON.stringify({
|
||||
id: 1,
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[1, "kanban"]],
|
||||
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.
|
||||
// Object.assign(browser.location, { search: "?model=partner&view_type=kanban" });
|
||||
redirect("/odoo/action-1?view_type=kanban");
|
||||
|
||||
// Create the web client. It should execute the stored action.
|
||||
await mountWithCleanup(WebClient);
|
||||
await animationFrame(); // blank action
|
||||
|
||||
// Check the current action context
|
||||
expect(getService("action").currentController.action.context).toEqual({
|
||||
// action context
|
||||
someKey: 44,
|
||||
lang: "not_en",
|
||||
tz: "not_taht",
|
||||
uid: 42,
|
||||
// note there is no 'allowed_company_ids' in the action context
|
||||
});
|
||||
});
|
||||
test.tags("desktop");
|
||||
test("action is removed while waiting for another action with selectMenu", async () => {
|
||||
let def;
|
||||
class SlowClientAction extends Component {
|
||||
static template = xml`<div>My client action</div>`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
onWillStart(() => def);
|
||||
}
|
||||
}
|
||||
actionRegistry.add("slow_client_action", SlowClientAction);
|
||||
defineActions([
|
||||
{
|
||||
id: 1001,
|
||||
tag: "slow_client_action",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "Id 1" },
|
||||
},
|
||||
]);
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
name: "App1",
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
]);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
// starting point: a kanban view
|
||||
await getService("action").doAction(4);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
|
||||
// select app in navbar menu
|
||||
def = new Deferred();
|
||||
await contains(".o_navbar_apps_menu .dropdown-toggle").click();
|
||||
const appsMenu = getDropdownMenu(".o_navbar_apps_menu");
|
||||
await contains(".o_app:contains(App1)", { root: appsMenu }).click();
|
||||
|
||||
// check that the action manager is empty, even though client action is loading
|
||||
expect(".o_action_manager").toHaveText("");
|
||||
|
||||
// resolve onwillstart so client action is ready
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_action_manager").toHaveText("My client action");
|
||||
});
|
||||
|
|
@ -1,660 +0,0 @@
|
|||
/** @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,158 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { cookie } from "@web/core/browser/cookie";
|
||||
import { redirect } from "@web/core/utils/urls";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
getService,
|
||||
makeServerError,
|
||||
models,
|
||||
mountWebClient,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { animationFrame } from "@odoo/hoot-dom";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
name = fields.Char();
|
||||
|
||||
_records = [{ id: 1, name: "First record" }];
|
||||
_views = {
|
||||
form: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
beforeEach(() => {
|
||||
serverState.companies = [
|
||||
{ id: 1, name: "Company 1", sequence: 1, parent_id: false, child_ids: [] },
|
||||
{ id: 2, name: "Company 2", sequence: 2, parent_id: false, child_ids: [] },
|
||||
{ id: 3, name: "Company 3", sequence: 3, parent_id: false, child_ids: [] },
|
||||
];
|
||||
patchWithCleanup(browser.location, {
|
||||
reload() {
|
||||
expect.step("reload");
|
||||
},
|
||||
});
|
||||
patchWithCleanup(browser.location, {
|
||||
origin: "http://example.com",
|
||||
});
|
||||
});
|
||||
|
||||
test("open record withtout the correct company (load state)", async () => {
|
||||
cookie.set("cids", "1");
|
||||
onRpc("web_read", () => {
|
||||
throw makeServerError({
|
||||
type: "AccessError",
|
||||
message: "Wrong Company",
|
||||
context: { suggested_company: { id: 2, display_name: "Company 2" } },
|
||||
});
|
||||
});
|
||||
|
||||
redirect("/odoo/res.partner/1");
|
||||
await mountWebClient();
|
||||
expect(cookie.get("cids")).toBe("1-2");
|
||||
expect.verifySteps(["reload"]);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/res.partner/1", {
|
||||
message: "url did not change",
|
||||
});
|
||||
});
|
||||
|
||||
test("open record withtout the correct company (doAction)", async () => {
|
||||
cookie.set("cids", "1");
|
||||
onRpc("web_read", () => {
|
||||
throw makeServerError({
|
||||
type: "AccessError",
|
||||
message: "Wrong Company",
|
||||
context: { suggested_company: { id: 2, display_name: "Company 2" } },
|
||||
});
|
||||
});
|
||||
|
||||
await mountWebClient();
|
||||
getService("action").doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_id: 1,
|
||||
res_model: "res.partner",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
await animationFrame();
|
||||
expect(cookie.get("cids")).toBe("1-2");
|
||||
expect.verifySteps(["reload"]);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/res.partner/1", {
|
||||
message: "url should contain the information of the doAction",
|
||||
});
|
||||
});
|
||||
|
||||
test("create/modify a record with a non-connected company", async () => {
|
||||
cookie.set("cids", "1");
|
||||
onRpc("web_save", ({ kwargs }) => {
|
||||
expect.step(kwargs.context.allowed_company_ids);
|
||||
if (
|
||||
kwargs.context.allowed_company_ids.length === 1 &&
|
||||
kwargs.context.allowed_company_ids[0] === 1
|
||||
) {
|
||||
throw makeServerError({
|
||||
type: "AccessError",
|
||||
message: "Wrong Company",
|
||||
context: { suggested_company: { id: 2, display_name: "Company 2" } },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await mountWebClient();
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "res.partner",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
await contains(`.o_field_widget[name=name] input`).edit("some foo value");
|
||||
await contains(`.o_form_button_save`).click();
|
||||
await animationFrame();
|
||||
expect.verifySteps([[1], [1, 2]]);
|
||||
expect(cookie.get("cids")).toBe("1-2");
|
||||
expect(`.o_field_widget[name=name] input`).toHaveValue("some foo value");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("form view in dialog shows wrong company error", async () => {
|
||||
expect.errors(1);
|
||||
cookie.set("cids", "1");
|
||||
|
||||
onRpc("web_read", () => {
|
||||
throw makeServerError({
|
||||
type: "AccessError",
|
||||
message: "Wrong Company",
|
||||
context: { suggested_company: { id: 2, display_name: "Company 2" } },
|
||||
});
|
||||
});
|
||||
onRpc("has_group", () => true);
|
||||
Partner._views.list = /* xml */ `
|
||||
<list>
|
||||
<field name="display_name" />
|
||||
</list>
|
||||
`;
|
||||
|
||||
await mountWebClient();
|
||||
|
||||
getService("dialog").add(FormViewDialog, {
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
});
|
||||
await animationFrame();
|
||||
expect.verifyErrors(['Error: The following error occurred in onWillStart: "Wrong Company"']);
|
||||
expect(cookie.get("cids")).toBe("1"); // cookies were not modified
|
||||
expect.verifySteps([]); // don't reload
|
||||
});
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
import {
|
||||
defineActions,
|
||||
defineModels,
|
||||
fields,
|
||||
getService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
class TestClientAction extends Component {
|
||||
static template = xml`
|
||||
<div class="test_client_action">
|
||||
ClientAction
|
||||
</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
class Partner extends models.Model {
|
||||
display_name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record" },
|
||||
{ id: 2, display_name: "Second record" },
|
||||
{ id: 3, display_name: "Third record" },
|
||||
{ id: 4, display_name: "Fourth record" },
|
||||
{ id: 5, display_name: "Fifth record" },
|
||||
];
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</form>`,
|
||||
kanban: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
};
|
||||
}
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Partner, User]);
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "kanban"]],
|
||||
},
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(browser, {
|
||||
open: (url) => expect.step("open: " + url),
|
||||
});
|
||||
});
|
||||
|
||||
test("can execute act_window actions from db ID in a new window", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1, { newWindow: true });
|
||||
expect.verifySteps(["open: /odoo/action-1"]);
|
||||
});
|
||||
|
||||
test("'CLEAR-UNCOMMITTED-CHANGES' is not triggered for window action", async () => {
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
webClient.env.bus.addEventListener("CLEAR-UNCOMMITTED-CHANGES", () => {
|
||||
expect.step("CLEAR-UNCOMMITTED-CHANGES");
|
||||
});
|
||||
|
||||
await getService("action").doAction(1, { newWindow: true });
|
||||
expect.verifySteps(["open: /odoo/action-1"]);
|
||||
});
|
||||
|
||||
test("'CLEAR-UNCOMMITTED-CHANGES' is not triggered for client actions", async () => {
|
||||
class ClientAction extends Component {
|
||||
static template = xml`<div class="o_client_action_test">Hello World</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
registry.category("actions").add("my_action", ClientAction);
|
||||
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
webClient.env.bus.addEventListener("CLEAR-UNCOMMITTED-CHANGES", () => {
|
||||
expect.step("CLEAR-UNCOMMITTED-CHANGES");
|
||||
});
|
||||
|
||||
await getService("action").doAction("my_action", { newWindow: true });
|
||||
expect.verifySteps(["open: /odoo/my_action"]);
|
||||
});
|
||||
|
||||
test("'CLEAR-UNCOMMITTED-CHANGES' is not triggered for switchView", async () => {
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
webClient.env.bus.addEventListener("CLEAR-UNCOMMITTED-CHANGES", () => {
|
||||
expect.step("CLEAR-UNCOMMITTED-CHANGES");
|
||||
});
|
||||
|
||||
await getService("action").doAction(1);
|
||||
await getService("action").switchView("kanban", {}, { newWindow: true });
|
||||
expect.verifySteps([
|
||||
"CLEAR-UNCOMMITTED-CHANGES", // The first do action clear uncommitted changes as expected. The second one doesn't
|
||||
"open: /odoo/action-1",
|
||||
]);
|
||||
});
|
||||
|
||||
test("can execute dynamic act_window actions in a new window", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(
|
||||
{
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
res_id: 22,
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
{
|
||||
newWindow: true,
|
||||
}
|
||||
);
|
||||
expect.verifySteps(["open: /odoo/m-partner/22"]);
|
||||
});
|
||||
|
||||
test("can execute an actions in a new window and preserve the breadcrumb", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
await getService("action").doAction(
|
||||
{
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
res_id: 22,
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
{
|
||||
newWindow: true,
|
||||
}
|
||||
);
|
||||
expect.verifySteps(["open: /odoo/action-1/m-partner/22"]);
|
||||
});
|
||||
|
||||
test("can execute client actions in a new window", async () => {
|
||||
registry.category("actions").add("__test__client__action__", TestClientAction);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(
|
||||
{
|
||||
name: "Dialog Test",
|
||||
target: "current",
|
||||
tag: "__test__client__action__",
|
||||
type: "ir.actions.client",
|
||||
},
|
||||
{
|
||||
newWindow: true,
|
||||
}
|
||||
);
|
||||
expect.verifySteps(["open: /odoo/__test__client__action__"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,673 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, onMounted, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineMenus,
|
||||
defineModels,
|
||||
editSearch,
|
||||
fields,
|
||||
getService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
validateSearch,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { router } from "@web/core/browser/router";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { redirect } from "@web/core/utils/urls";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
const actionRegistry = registry.category("actions");
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 3,
|
||||
xml_id: "action_3",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[1, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
xml_id: "action_4",
|
||||
name: "Partners Action 4",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[1, "kanban"],
|
||||
[2, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
xml_id: "action_8",
|
||||
name: "Favorite Ponies",
|
||||
res_model: "pony",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1001,
|
||||
tag: "__test__client__action__",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "Id 1" },
|
||||
},
|
||||
{
|
||||
id: 1002,
|
||||
tag: "__test__client__action__",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "Id 2" },
|
||||
},
|
||||
]);
|
||||
|
||||
defineMenus([
|
||||
{ id: 0 }, // prevents auto-loading the first action
|
||||
{ id: 1, actionID: 1001 },
|
||||
{ id: 2, actionID: 1002 },
|
||||
]);
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
foo = fields.Char();
|
||||
parent_id = fields.Many2one({ relation: "partner" });
|
||||
child_ids = fields.One2many({ relation: "partner", relation_field: "parent_id" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "First record", foo: "yop" },
|
||||
{ id: 2, name: "Second record", foo: "blip" },
|
||||
{ id: 3, name: "Third record", foo: "gnap" },
|
||||
{ id: 4, name: "Fourth record", foo: "plop" },
|
||||
{ id: 5, name: "Fifth record", foo: "zoup" },
|
||||
];
|
||||
_views = {
|
||||
"kanban,1": /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
"list,2": /* xml */ `
|
||||
<list>
|
||||
<field name="foo" />
|
||||
</list>
|
||||
`,
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<button name="object" string="Call method" type="object"/>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</header>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
<field name="foo"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
search: /* xml */ `
|
||||
<search>
|
||||
<field name="foo" string="Foo" />
|
||||
</search>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
class Pony extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 4, name: "Twilight Sparkle" },
|
||||
{ id: 6, name: "Applejack" },
|
||||
{ id: 9, name: "Fluttershy" },
|
||||
];
|
||||
_views = {
|
||||
list: `<list><field name="name"/></list>`,
|
||||
form: `<form><field name="name"/></form>`,
|
||||
};
|
||||
}
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Partner, Pony, User]);
|
||||
|
||||
class TestClientAction extends Component {
|
||||
static template = xml`
|
||||
<div class="test_client_action">
|
||||
ClientAction_<t t-esc="props.action.params?.description"/>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
this.env.config.setDisplayName(`Client action ${this.props.action.id}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
beforeEach(() => {
|
||||
actionRegistry.add("__test__client__action__", TestClientAction);
|
||||
patchWithCleanup(browser.location, {
|
||||
origin: "http://example.com",
|
||||
});
|
||||
redirect("/odoo");
|
||||
});
|
||||
|
||||
test(`basic action as App`, async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(router.current).toEqual({});
|
||||
|
||||
await contains(`.o_navbar_apps_menu button`).click();
|
||||
await contains(`.o-dropdown-item:eq(2)`).click();
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(router.current.action).toBe(1002);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-1002");
|
||||
expect(`.test_client_action`).toHaveText("ClientAction_Id 2");
|
||||
expect(`.o_menu_brand`).toHaveText("App2");
|
||||
});
|
||||
|
||||
test(`do action keeps menu in url`, async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(router.current).toEqual({});
|
||||
|
||||
await contains(`.o_navbar_apps_menu button`).click();
|
||||
await contains(`.o-dropdown-item:eq(2)`).click();
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-1002");
|
||||
expect(router.current.action).toBe(1002);
|
||||
expect(`.test_client_action`).toHaveText("ClientAction_Id 2");
|
||||
expect(`.o_menu_brand`).toHaveText("App2");
|
||||
|
||||
await getService("action").doAction(1001, { clearBreadcrumbs: true });
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-1001");
|
||||
expect(router.current.action).toBe(1001);
|
||||
expect(`.test_client_action`).toHaveText("ClientAction_Id 1");
|
||||
expect(`.o_menu_brand`).toHaveText("App2");
|
||||
});
|
||||
|
||||
test(`actions can push state`, async () => {
|
||||
class ClientActionPushes extends Component {
|
||||
static template = xml`
|
||||
<div class="test_client_action" t-on-click="_actionPushState">
|
||||
ClientAction_<t t-esc="props.params and props.params.description"/>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
|
||||
_actionPushState() {
|
||||
router.pushState({ arbitrary: "actionPushed" });
|
||||
}
|
||||
}
|
||||
actionRegistry.add("client_action_pushes", ClientActionPushes);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(browser.history.length).toBe(1);
|
||||
expect(router.current).toEqual({});
|
||||
|
||||
await getService("action").doAction("client_action_pushes");
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/client_action_pushes");
|
||||
expect(browser.history.length).toBe(2);
|
||||
expect(router.current.action).toBe("client_action_pushes");
|
||||
expect(router.current.menu_id).toBe(undefined);
|
||||
|
||||
await contains(`.test_client_action`).click();
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe(
|
||||
"http://example.com/odoo/client_action_pushes?arbitrary=actionPushed"
|
||||
);
|
||||
expect(browser.history.length).toBe(3);
|
||||
expect(router.current.action).toBe("client_action_pushes");
|
||||
expect(router.current.arbitrary).toBe("actionPushed");
|
||||
});
|
||||
|
||||
test(`actions override previous state`, async () => {
|
||||
class ClientActionPushes extends Component {
|
||||
static template = xml`
|
||||
<div class="test_client_action" t-on-click="_actionPushState">
|
||||
ClientAction_<t t-esc="props.params and props.params.description"/>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
|
||||
_actionPushState() {
|
||||
router.pushState({ arbitrary: "actionPushed" });
|
||||
}
|
||||
}
|
||||
actionRegistry.add("client_action_pushes", ClientActionPushes);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(browser.history.length).toBe(1);
|
||||
expect(router.current).toEqual({});
|
||||
|
||||
await getService("action").doAction("client_action_pushes");
|
||||
await animationFrame(); // wait for pushState because it's unrealistic to click before it
|
||||
await contains(`.test_client_action`).click();
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe(
|
||||
"http://example.com/odoo/client_action_pushes?arbitrary=actionPushed"
|
||||
);
|
||||
expect(browser.history.length).toBe(3); // Two history entries
|
||||
expect(router.current.action).toBe("client_action_pushes");
|
||||
expect(router.current.arbitrary).toBe("actionPushed");
|
||||
|
||||
await getService("action").doAction(1001);
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-1001", {
|
||||
message: "client_action_pushes removed from url because action 1001 is in target main",
|
||||
});
|
||||
expect(browser.history.length).toBe(4);
|
||||
expect(router.current.action).toBe(1001);
|
||||
expect(router.current.arbitrary).toBe(undefined);
|
||||
});
|
||||
|
||||
test(`actions override previous state from menu click`, async () => {
|
||||
class ClientActionPushes extends Component {
|
||||
static template = xml`
|
||||
<div class="test_client_action" t-on-click="_actionPushState">
|
||||
ClientAction_<t t-esc="props.params and props.params.description"/>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
|
||||
_actionPushState() {
|
||||
router.pushState({ arbitrary: "actionPushed" });
|
||||
}
|
||||
}
|
||||
actionRegistry.add("client_action_pushes", ClientActionPushes);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(router.current).toEqual({});
|
||||
|
||||
await getService("action").doAction("client_action_pushes");
|
||||
await contains(`.test_client_action`).click();
|
||||
await contains(`.o_navbar_apps_menu button`).click();
|
||||
await contains(`.o-dropdown-item:eq(2)`).click();
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-1002");
|
||||
expect(router.current.action).toBe(1002);
|
||||
});
|
||||
|
||||
test(`action in target new do not push state`, async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 2001,
|
||||
tag: "__test__client__action__",
|
||||
target: "new",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "Id 1" },
|
||||
},
|
||||
]);
|
||||
|
||||
patchWithCleanup(browser.history, {
|
||||
pushState() {
|
||||
throw new Error("should not push state");
|
||||
},
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(browser.history.length).toBe(1);
|
||||
|
||||
await getService("action").doAction(2001);
|
||||
expect(`.modal .test_client_action`).toHaveCount(1);
|
||||
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo", {
|
||||
message: "url did not change",
|
||||
});
|
||||
expect(browser.history.length).toBe(1, { message: "did not create a history entry" });
|
||||
expect(router.current).toEqual({});
|
||||
});
|
||||
|
||||
test(`properly push state`, async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(browser.history.length).toBe(1);
|
||||
|
||||
await getService("action").doAction(4);
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-4");
|
||||
expect(browser.history.length).toBe(2);
|
||||
expect(router.current).toEqual({
|
||||
action: 4,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await getService("action").doAction(8);
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-4/action-8");
|
||||
expect(browser.history.length).toBe(3);
|
||||
expect(router.current).toEqual({
|
||||
action: 8,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
{
|
||||
action: 8,
|
||||
displayName: "Favorite Ponies",
|
||||
view_type: "list",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await contains(`tr .o_data_cell:first`).click();
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-4/action-8/4");
|
||||
expect(browser.history.length).toBe(4);
|
||||
expect(router.current).toEqual({
|
||||
action: 8,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
{
|
||||
action: 8,
|
||||
displayName: "Favorite Ponies",
|
||||
view_type: "list",
|
||||
},
|
||||
{
|
||||
action: 8,
|
||||
displayName: "Twilight Sparkle",
|
||||
resId: 4,
|
||||
view_type: "form",
|
||||
},
|
||||
],
|
||||
resId: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test(`push state after action is loaded, not before`, async () => {
|
||||
const def = new Deferred();
|
||||
onRpc("get_views", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(browser.history.length).toBe(1);
|
||||
|
||||
getService("action").doAction(4);
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(browser.history.length).toBe(1);
|
||||
expect(router.current).toEqual({});
|
||||
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-4");
|
||||
expect(browser.history.length).toBe(2);
|
||||
expect(router.current).toEqual({
|
||||
action: 4,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test(`do not push state when action fails`, async () => {
|
||||
onRpc("read", () => Promise.reject());
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(browser.history.length).toBe(1);
|
||||
|
||||
await getService("action").doAction(8);
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-8");
|
||||
expect(browser.history.length).toBe(2);
|
||||
expect(router.current).toEqual({
|
||||
action: 8,
|
||||
actionStack: [
|
||||
{
|
||||
action: 8,
|
||||
displayName: "Favorite Ponies",
|
||||
view_type: "list",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await contains(`tr.o_data_row:first`).click();
|
||||
// we make sure here that the list view is still in the dom
|
||||
expect(`.o_list_view`).toHaveCount(1, {
|
||||
message: "there should still be a list view in dom",
|
||||
});
|
||||
|
||||
await animationFrame(); // wait for possible debounced pushState
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-8");
|
||||
expect(browser.history.length).toBe(2);
|
||||
expect(router.current).toEqual({
|
||||
action: 8,
|
||||
actionStack: [
|
||||
{
|
||||
action: 8,
|
||||
displayName: "Favorite Ponies",
|
||||
view_type: "list",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test(`view_type is in url when not the default one`, async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(browser.history.length).toBe(1);
|
||||
|
||||
await getService("action").doAction(3);
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-3");
|
||||
expect(browser.history.length).toBe(2);
|
||||
expect(router.current).toEqual({
|
||||
action: 3,
|
||||
actionStack: [
|
||||
{
|
||||
action: 3,
|
||||
displayName: "Partners",
|
||||
view_type: "list",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(`.breadcrumb`).toHaveCount(0);
|
||||
|
||||
await getService("action").doAction(3, { viewType: "kanban" });
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-3?view_type=kanban");
|
||||
expect(browser.history.length).toBe(3, { message: "created a history entry" });
|
||||
expect(`.breadcrumb`).toHaveCount(1, {
|
||||
message: "created a breadcrumb entry",
|
||||
});
|
||||
expect(router.current).toEqual({
|
||||
action: 3,
|
||||
view_type: "kanban", // view_type is on the state when it's not the default one
|
||||
actionStack: [
|
||||
{
|
||||
action: 3,
|
||||
displayName: "Partners",
|
||||
view_type: "list",
|
||||
},
|
||||
{
|
||||
action: 3,
|
||||
displayName: "Partners",
|
||||
view_type: "kanban",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test(`switchView pushes the stat but doesn't add to the breadcrumbs`, async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(browser.history.length).toBe(1);
|
||||
|
||||
await getService("action").doAction(3);
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-3");
|
||||
expect(browser.history.length).toBe(2);
|
||||
expect(router.current).toEqual({
|
||||
action: 3,
|
||||
actionStack: [
|
||||
{
|
||||
action: 3,
|
||||
displayName: "Partners",
|
||||
view_type: "list",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(`.breadcrumb`).toHaveCount(0);
|
||||
|
||||
await getService("action").switchView("kanban");
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-3?view_type=kanban");
|
||||
expect(browser.history.length).toBe(3, { message: "created a history entry" });
|
||||
expect(`.breadcrumb`).toHaveCount(0, { message: "didn't create a breadcrumb entry" });
|
||||
expect(router.current).toEqual({
|
||||
action: 3,
|
||||
view_type: "kanban", // view_type is on the state when it's not the default one
|
||||
actionStack: [
|
||||
{
|
||||
action: 3,
|
||||
displayName: "Partners",
|
||||
view_type: "kanban",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test(`properly push globalState`, async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo");
|
||||
expect(browser.history.length).toBe(1);
|
||||
|
||||
await getService("action").doAction(4);
|
||||
await animationFrame();
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-4");
|
||||
expect(browser.history.length).toBe(2);
|
||||
expect(router.current).toEqual({
|
||||
action: 4,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// add element on the search Model
|
||||
await editSearch("blip");
|
||||
await validateSearch();
|
||||
expect(queryAllTexts(".o_facet_value")).toEqual(["blip"]);
|
||||
|
||||
//open record
|
||||
await contains(".o_kanban_record").click();
|
||||
|
||||
// Add the globalState on the state before leaving the kanban
|
||||
expect(router.current).toEqual({
|
||||
action: 4,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
],
|
||||
globalState: {
|
||||
searchModel: `{"nextGroupId":2,"nextGroupNumber":1,"nextId":2,"query":[{"searchItemId":1,"autocompleteValue":{"label":"blip","operator":"ilike","value":"blip"}}],"searchItems":{"1":{"type":"field","fieldName":"foo","fieldType":"char","description":"Foo","groupId":1,"id":1}},"searchPanelInfo":{"className":"","viewTypes":["kanban","list"],"loaded":false,"shouldReload":true},"sections":[]}`,
|
||||
},
|
||||
});
|
||||
|
||||
// pushState is defered
|
||||
await animationFrame();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-4/2");
|
||||
expect(router.current).toEqual({
|
||||
action: 4,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Second record",
|
||||
resId: 2,
|
||||
view_type: "form",
|
||||
},
|
||||
],
|
||||
resId: 2,
|
||||
});
|
||||
|
||||
// came back using the browser
|
||||
browser.history.back(); // Click on back button
|
||||
await animationFrame();
|
||||
|
||||
// The search Model should be restored
|
||||
expect(queryAllTexts(".o_facet_value")).toEqual(["blip"]);
|
||||
expect(browser.location.href).toBe("http://example.com/odoo/action-4");
|
||||
|
||||
// The global state is restored on the state
|
||||
expect(router.current).toEqual({
|
||||
action: 4,
|
||||
actionStack: [
|
||||
{
|
||||
action: 4,
|
||||
displayName: "Partners Action 4",
|
||||
view_type: "kanban",
|
||||
},
|
||||
],
|
||||
globalState: {
|
||||
searchModel: `{"nextGroupId":2,"nextGroupNumber":1,"nextId":2,"query":[{"searchItemId":1,"autocompleteValue":{"label":"blip","operator":"ilike","value":"blip"}}],"searchItems":{"1":{"type":"field","fieldName":"foo","fieldType":"char","description":"Foo","groupId":1,"id":1}},"searchPanelInfo":{"className":"","viewTypes":["kanban","list"],"loaded":false,"shouldReload":true},"sections":[]}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -1,251 +0,0 @@
|
|||
/** @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,420 @@
|
|||
import { afterEach, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineModels,
|
||||
getService,
|
||||
mockService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
stepAllNetworkCalls,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { router } from "@web/core/browser/router";
|
||||
import { download } from "@web/core/network/download";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ReportAction } from "@web/webclient/actions/reports/report_action";
|
||||
import { downloadReport } from "@web/webclient/actions/reports/utils";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record" },
|
||||
{ id: 2, display_name: "Second record" },
|
||||
];
|
||||
_views = {
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<button name="object" string="Call method" type="object"/>
|
||||
</header>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</form>`,
|
||||
"kanban,1": `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: `<list><field name="display_name"/></list>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 7,
|
||||
xml_id: "action_7",
|
||||
name: "Some Report",
|
||||
report_name: "some_report",
|
||||
report_type: "qweb-pdf",
|
||||
type: "ir.actions.report",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
xml_id: "action_11",
|
||||
name: "Another Report",
|
||||
report_name: "another_report",
|
||||
report_type: "qweb-pdf",
|
||||
type: "ir.actions.report",
|
||||
close_on_report_download: true,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
xml_id: "action_12",
|
||||
name: "Some HTML Report",
|
||||
report_name: "some_report",
|
||||
report_type: "qweb-html",
|
||||
type: "ir.actions.report",
|
||||
},
|
||||
]);
|
||||
|
||||
afterEach(() => {
|
||||
// In the prod environment, we keep a promise with the wkhtmlstatus (e.g. broken, upgrade...).
|
||||
// This ensures the request to be done only once. In the test environment, we mock this request
|
||||
// to simulate the different status, so we want to erase the promise at the end of each test,
|
||||
// otherwise all tests but the first one would use the status in cache.
|
||||
delete downloadReport.wkhtmltopdfStatusProm;
|
||||
});
|
||||
|
||||
test("can execute report actions from db ID", async () => {
|
||||
patchWithCleanup(download, {
|
||||
_download: (options) => {
|
||||
expect.step(options.url);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
onRpc("/report/check_wkhtmltopdf", () => "ok");
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(7, { onClose: () => expect.step("on_close") });
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/report/check_wkhtmltopdf",
|
||||
"/report/download",
|
||||
"on_close",
|
||||
]);
|
||||
});
|
||||
|
||||
test("report actions can close modals and reload views", async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 5,
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
]);
|
||||
patchWithCleanup(download, {
|
||||
_download: (options) => {
|
||||
expect.step(options.url);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("/report/check_wkhtmltopdf", () => "ok");
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(5, { onClose: () => expect.step("on_close") });
|
||||
expect(".o_technical_modal .o_form_view").toHaveCount(1, {
|
||||
message: "should have rendered a form view in a modal",
|
||||
});
|
||||
|
||||
await getService("action").doAction(7, { onClose: () => expect.step("on_printed") });
|
||||
expect(".o_technical_modal .o_form_view").toHaveCount(1, {
|
||||
message: "The modal should still exist",
|
||||
});
|
||||
|
||||
await getService("action").doAction(11);
|
||||
await animationFrame();
|
||||
expect(".o_technical_modal .o_form_view").toHaveCount(0, {
|
||||
message: "the modal should have been closed after the action report",
|
||||
});
|
||||
expect.verifySteps(["/report/download", "on_printed", "/report/download", "on_close"]);
|
||||
});
|
||||
|
||||
test("should trigger a notification if wkhtmltopdf is to upgrade", async () => {
|
||||
patchWithCleanup(download, {
|
||||
_download: (options) => {
|
||||
expect.step(options.url);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
mockService("notification", {
|
||||
add: () => expect.step("notify"),
|
||||
});
|
||||
|
||||
onRpc("/report/check_wkhtmltopdf", () => "upgrade");
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(7);
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/report/check_wkhtmltopdf",
|
||||
"/report/download",
|
||||
"notify",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should open the report client action if wkhtmltopdf is broken", async () => {
|
||||
// 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
|
||||
patchWithCleanup(ReportAction.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
rpc(this.reportUrl);
|
||||
this.reportUrl = "about:blank";
|
||||
},
|
||||
});
|
||||
patchWithCleanup(download, {
|
||||
_download: () => {
|
||||
expect.step("download"); // should not be called
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
mockService("notification", {
|
||||
add: () => expect.step("notify"),
|
||||
});
|
||||
|
||||
onRpc("/report/check_wkhtmltopdf", () => "broken");
|
||||
onRpc("/report/html/some_report", async (request) => {
|
||||
const search = decodeURIComponent(new URL(request.url).search);
|
||||
expect(search).toBe(`?context={"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1]}`);
|
||||
return true;
|
||||
});
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(7);
|
||||
expect(".o_content iframe").toHaveCount(1, {
|
||||
message: "should have opened the report client action",
|
||||
});
|
||||
// the control panel has the content twice and a d-none class is toggled depending the screen size
|
||||
expect(":not(.d-none) > button[title='Print']").toHaveCount(1, {
|
||||
message: "should have a print button",
|
||||
});
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/report/check_wkhtmltopdf",
|
||||
"notify",
|
||||
"/report/html/some_report",
|
||||
]);
|
||||
});
|
||||
|
||||
test("send context in case of html report", async () => {
|
||||
serverState.userContext = { some_key: 2 };
|
||||
// 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
|
||||
patchWithCleanup(ReportAction.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
rpc(this.reportUrl);
|
||||
this.reportUrl = "about:blank";
|
||||
},
|
||||
});
|
||||
patchWithCleanup(download, {
|
||||
_download: () => {
|
||||
expect.step("download"); // should not be called
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
mockService("notification", {
|
||||
add(message, options) {
|
||||
expect.step(options.type || "notification");
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("/report/html/some_report", async (request) => {
|
||||
const search = decodeURIComponent(new URL(request.url).search);
|
||||
expect(search).toBe(
|
||||
`?context={"some_key":2,"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1]}`
|
||||
);
|
||||
return true;
|
||||
});
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(12);
|
||||
expect(".o_content iframe").toHaveCount(1, { message: "should have opened the client action" });
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/report/html/some_report",
|
||||
]);
|
||||
});
|
||||
|
||||
test("UI unblocks after downloading the report even if it threw an error", async () => {
|
||||
let timesDownloasServiceHasBeenCalled = 0;
|
||||
patchWithCleanup(download, {
|
||||
_download: () => {
|
||||
if (timesDownloasServiceHasBeenCalled === 0) {
|
||||
expect.step("successful download");
|
||||
timesDownloasServiceHasBeenCalled++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (timesDownloasServiceHasBeenCalled === 1) {
|
||||
expect.step("failed download");
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("/report/check_wkhtmltopdf", () => "ok");
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
const onBlock = () => {
|
||||
expect.step("block");
|
||||
};
|
||||
const onUnblock = () => {
|
||||
expect.step("unblock");
|
||||
};
|
||||
getService("ui").bus.addEventListener("BLOCK", onBlock);
|
||||
getService("ui").bus.addEventListener("UNBLOCK", onUnblock);
|
||||
|
||||
await getService("action").doAction(7);
|
||||
try {
|
||||
await getService("action").doAction(7);
|
||||
} catch {
|
||||
expect.step("error caught");
|
||||
}
|
||||
expect.verifySteps([
|
||||
"block",
|
||||
"successful download",
|
||||
"unblock",
|
||||
"block",
|
||||
"failed download",
|
||||
"unblock",
|
||||
"error caught",
|
||||
]);
|
||||
getService("ui").bus.removeEventListener("BLOCK", onBlock);
|
||||
getService("ui").bus.removeEventListener("UNBLOCK", onUnblock);
|
||||
});
|
||||
|
||||
test("can use custom handlers for report actions", async () => {
|
||||
patchWithCleanup(download, {
|
||||
_download: (options) => {
|
||||
expect.step(options.url);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("/report/check_wkhtmltopdf", () => "ok");
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
let customHandlerCalled = false;
|
||||
registry.category("ir.actions.report handlers").add("custom_handler", async (action) => {
|
||||
if (action.id === 7 && !customHandlerCalled) {
|
||||
customHandlerCalled = true;
|
||||
expect.step("calling custom handler");
|
||||
return true;
|
||||
}
|
||||
expect.step("falling through to default handler");
|
||||
});
|
||||
await getService("action").doAction(7);
|
||||
expect.step("first doAction finished");
|
||||
|
||||
await getService("action").doAction(7);
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"calling custom handler",
|
||||
"first doAction finished",
|
||||
"falling through to default handler",
|
||||
"/report/check_wkhtmltopdf",
|
||||
"/report/download",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("context is correctly passed to the client action report", async (assert) => {
|
||||
patchWithCleanup(download, {
|
||||
_download: (options) => {
|
||||
expect.step(options.url);
|
||||
expect(options.data.context).toBe(
|
||||
`{"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1],"rabbia":"E Tarantella","active_ids":[99]}`
|
||||
);
|
||||
expect(JSON.parse(options.data.data)).toEqual([
|
||||
"/report/pdf/ennio.morricone/99",
|
||||
"qweb-pdf",
|
||||
]);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
patchWithCleanup(ReportAction.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
rpc(this.reportUrl);
|
||||
this.reportUrl = "about:blank";
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("/report/check_wkhtmltopdf", () => "ok");
|
||||
onRpc("/report/html", async (request) => {
|
||||
const search = decodeURIComponent(new URL(request.url).search);
|
||||
expect(search).toBe(`?context={"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1]}`);
|
||||
return true;
|
||||
});
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
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",
|
||||
};
|
||||
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
|
||||
|
||||
await getService("action").doAction(action);
|
||||
expect.verifySteps(["/report/html/ennio.morricone/99"]);
|
||||
|
||||
await contains(".o_control_panel_main_buttons button[title='Print']").click();
|
||||
expect.verifySteps(["/report/check_wkhtmltopdf", "/report/download"]);
|
||||
});
|
||||
|
||||
test("url is valid", async (assert) => {
|
||||
patchWithCleanup(ReportAction.prototype, {
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
this.reportUrl = "about:blank";
|
||||
},
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(12); // 12 is a html report action
|
||||
await runAllTimers();
|
||||
const urlState = router.current;
|
||||
// used to put report.client_action in the url
|
||||
expect(urlState.action === "report.client_action").toBe(false);
|
||||
expect(urlState.action).toBe(12);
|
||||
});
|
||||
|
|
@ -1,380 +0,0 @@
|
|||
/** @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,151 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineActions,
|
||||
defineModels,
|
||||
getService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
stepAllNetworkCalls,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record" },
|
||||
{ id: 2, display_name: "Second record" },
|
||||
];
|
||||
_views = {
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<button name="object" string="Call method" type="object"/>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</header>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</form>`,
|
||||
"kanban,1": `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
};
|
||||
}
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Partner, User]);
|
||||
|
||||
test("can execute server actions from db ID", async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 2,
|
||||
type: "ir.actions.server",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[1, "kanban"]],
|
||||
},
|
||||
]);
|
||||
onRpc(
|
||||
"/web/action/run",
|
||||
async () => 1 // execute action 1
|
||||
);
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(2, { additionalContext: { someKey: 44 } });
|
||||
expect(".o_control_panel").toHaveCount(1, { message: "should have rendered a control panel" });
|
||||
expect(".o_kanban_view").toHaveCount(1, { message: "should have rendered a kanban view" });
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/web/action/run",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
]);
|
||||
});
|
||||
|
||||
test("handle server actions returning false", async function (assert) {
|
||||
defineActions([
|
||||
{
|
||||
id: 2,
|
||||
type: "ir.actions.server",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
xml_id: "action_5",
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
]);
|
||||
onRpc("/web/action/run", async () => false);
|
||||
stepAllNetworkCalls();
|
||||
await mountWithCleanup(WebClient);
|
||||
// execute an action in target="new"
|
||||
function onClose() {
|
||||
expect.step("close handler");
|
||||
}
|
||||
await getService("action").doAction(5, { onClose });
|
||||
expect(".o_technical_modal .o_form_view").toHaveCount(1, {
|
||||
message: "should have rendered a form view in a modal",
|
||||
});
|
||||
|
||||
// execute a server action that returns false
|
||||
await getService("action").doAction(2);
|
||||
await animationFrame();
|
||||
expect(".o_technical_modal").toHaveCount(0, { message: "should have closed the modal" });
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"onchange",
|
||||
"/web/action/load",
|
||||
"/web/action/run",
|
||||
"close handler",
|
||||
]);
|
||||
});
|
||||
|
||||
test("action with html help returned by a server action", async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 2,
|
||||
type: "ir.actions.server",
|
||||
},
|
||||
]);
|
||||
onRpc("/web/action/run", async () => ({
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "kanban"]],
|
||||
help: "<p>I am not a helper</p>",
|
||||
domain: [[0, "=", 1]],
|
||||
}));
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(2);
|
||||
|
||||
expect(".o_kanban_view .o_nocontent_help p").toHaveText("I am not a helper");
|
||||
});
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
/** @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,770 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryAllTexts, queryText } from "@odoo/hoot-dom";
|
||||
import { animationFrame, Deferred } from "@odoo/hoot-mock";
|
||||
import { Component, onMounted, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineMenus,
|
||||
defineModels,
|
||||
getService,
|
||||
mockService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
stepAllNetworkCalls,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { ClientErrorDialog } from "@web/core/errors/error_dialogs";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
const { ResCompany, ResPartner, ResUsers } = webModels;
|
||||
|
||||
class Partner extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "First record" },
|
||||
{ id: 2, display_name: "Second record" },
|
||||
];
|
||||
_views = {
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<button name="object" string="Call method" type="object"/>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</header>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</form>`,
|
||||
"kanban,1": `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: `<list><field name="display_name"/></list>`,
|
||||
"list,2": `<list limit="3"><field name="display_name"/></list>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner, ResCompany, ResPartner, ResUsers]);
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[1, "kanban"]],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
xml_id: "action_4",
|
||||
name: "Partners Action 4",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[1, "kanban"],
|
||||
[2, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
xml_id: "action_5",
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Partners Action Fullscreen",
|
||||
res_model: "partner",
|
||||
target: "fullscreen",
|
||||
views: [[1, "kanban"]],
|
||||
},
|
||||
]);
|
||||
|
||||
describe("new", () => {
|
||||
test('can execute act_window actions in target="new"', async () => {
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(5);
|
||||
expect(".o_technical_modal .o_form_view").toHaveCount(1, {
|
||||
message: "should have rendered a form view in a modal",
|
||||
});
|
||||
expect(".o_technical_modal .modal-body").toHaveClass("o_act_window", {
|
||||
message: "dialog main element should have classname 'o_act_window'",
|
||||
});
|
||||
expect(".o_technical_modal .o_form_view .o_form_editable").toHaveCount(1, {
|
||||
message: "form view should be in edit mode",
|
||||
});
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"onchange",
|
||||
]);
|
||||
});
|
||||
|
||||
test("chained action on_close", async () => {
|
||||
function onClose(closeInfo) {
|
||||
expect(closeInfo).toBe("smallCandle");
|
||||
expect.step("Close Action");
|
||||
}
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(5, { onClose });
|
||||
// a target=new action shouldn't activate the on_close
|
||||
await getService("action").doAction(5);
|
||||
expect.verifySteps([]);
|
||||
// An act_window_close should trigger the on_close
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.act_window_close",
|
||||
infos: "smallCandle",
|
||||
});
|
||||
expect.verifySteps(["Close Action"]);
|
||||
});
|
||||
|
||||
test("footer buttons are moved to the dialog footer", async () => {
|
||||
Partner._views["form"] = `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<footer>
|
||||
<button string="Create" type="object" class="infooter"/>
|
||||
</footer>
|
||||
</form>`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(5);
|
||||
expect(".o_technical_modal .modal-body button.infooter").toHaveCount(0, {
|
||||
message: "the button should not be in the body",
|
||||
});
|
||||
expect(".o_technical_modal .modal-footer button.infooter").toHaveCount(1, {
|
||||
message: "the button should be in the footer",
|
||||
});
|
||||
expect(".modal-footer button:visible").toHaveCount(1, {
|
||||
message: "the modal footer should only contain one visible button",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Button with `close` attribute closes dialog on desktop", async () => {
|
||||
Partner._views = {
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Open dialog" name="5" type="action"/>
|
||||
</header>
|
||||
</form>`,
|
||||
"form,17": `
|
||||
<form>
|
||||
<footer>
|
||||
<button string="I close the dialog" name="some_method" type="object" close="1"/>
|
||||
</footer>
|
||||
</form>`,
|
||||
};
|
||||
defineActions(
|
||||
[
|
||||
{
|
||||
id: 4,
|
||||
name: "Partners Action 4",
|
||||
res_model: "partner",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
views: [[17, "form"]],
|
||||
},
|
||||
],
|
||||
{ mode: "replace" }
|
||||
);
|
||||
|
||||
onRpc("/web/dataset/call_button/*", async (request) => {
|
||||
const { params } = await request.json();
|
||||
if (params.method === "some_method") {
|
||||
return {
|
||||
tag: "display_notification",
|
||||
type: "ir.actions.client",
|
||||
};
|
||||
}
|
||||
});
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
|
||||
await getService("action").doAction(4);
|
||||
expect.verifySteps(["/web/action/load", "get_views", "onchange"]);
|
||||
await contains(`button[name="5"]`).click();
|
||||
expect.verifySteps(["web_save", "/web/action/load", "get_views", "onchange"]);
|
||||
expect(".modal").toHaveCount(1);
|
||||
await contains(`button[name=some_method]`).click();
|
||||
expect.verifySteps(["web_save", "some_method", "web_read"]);
|
||||
expect(".modal").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("Button with `close` attribute closes dialog on mobile", async () => {
|
||||
Partner._views = {
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Open dialog" name="5" type="action"/>
|
||||
</header>
|
||||
</form>`,
|
||||
"form,17": `
|
||||
<form>
|
||||
<footer>
|
||||
<button string="I close the dialog" name="some_method" type="object" close="1"/>
|
||||
</footer>
|
||||
</form>`,
|
||||
};
|
||||
defineActions(
|
||||
[
|
||||
{
|
||||
id: 4,
|
||||
name: "Partners Action 4",
|
||||
res_model: "partner",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
views: [[17, "form"]],
|
||||
},
|
||||
],
|
||||
{ mode: "replace" }
|
||||
);
|
||||
|
||||
onRpc("/web/dataset/call_button/*", async (request) => {
|
||||
const { params } = await request.json();
|
||||
if (params.method === "some_method") {
|
||||
return {
|
||||
tag: "display_notification",
|
||||
type: "ir.actions.client",
|
||||
};
|
||||
}
|
||||
});
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
|
||||
await getService("action").doAction(4);
|
||||
expect.verifySteps(["/web/action/load", "get_views", "onchange"]);
|
||||
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
|
||||
await contains(`button[name="5"]`).click();
|
||||
expect.verifySteps(["web_save", "/web/action/load", "get_views", "onchange"]);
|
||||
expect(".modal").toHaveCount(1);
|
||||
await contains(`button[name=some_method]`).click();
|
||||
expect.verifySteps(["web_save", "some_method", "web_read"]);
|
||||
expect(".modal").toHaveCount(0);
|
||||
});
|
||||
|
||||
test('footer buttons are updated when having another action in target "new"', async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 25,
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
views: [[3, "form"]],
|
||||
},
|
||||
]);
|
||||
Partner._views = {
|
||||
form: `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<footer>
|
||||
<button string="Create" type="object" class="infooter"/>
|
||||
</footer>
|
||||
</form>`,
|
||||
"form,3": `
|
||||
<form>
|
||||
<footer>
|
||||
<button class="btn-primary" string="Save" special="save"/>
|
||||
</footer>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(5);
|
||||
expect('.o_technical_modal .modal-body button[special="save"]').toHaveCount(0);
|
||||
expect(".o_technical_modal .modal-body button.infooter").toHaveCount(0);
|
||||
expect(".o_technical_modal .modal-footer button.infooter").toHaveCount(1);
|
||||
expect(".o_technical_modal .modal-footer button:visible").toHaveCount(1);
|
||||
await getService("action").doAction(25);
|
||||
expect(".o_technical_modal .modal-body button.infooter").toHaveCount(0);
|
||||
expect(".o_technical_modal .modal-footer button.infooter").toHaveCount(0);
|
||||
expect('.o_technical_modal .modal-body button[special="save"]').toHaveCount(0);
|
||||
expect('.o_technical_modal .modal-footer button[special="save"]').toHaveCount(1);
|
||||
expect(".o_technical_modal .modal-footer button:visible").toHaveCount(1);
|
||||
});
|
||||
|
||||
test('button with confirm attribute in act_window action in target="new"', async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 999,
|
||||
name: "A window action",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
views: [[999, "form"]],
|
||||
},
|
||||
]);
|
||||
Partner._views["form,999"] = `
|
||||
<form>
|
||||
<button name="method" string="Call method" type="object" confirm="Are you sure?"/>
|
||||
</form>`;
|
||||
Partner._views["form,1000"] = `<form>Another action</form>`;
|
||||
|
||||
onRpc("method", () => ({
|
||||
id: 1000,
|
||||
name: "Another window action",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[1000, "form"]],
|
||||
}));
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(999);
|
||||
expect(".modal button[name=method]").toHaveCount(1);
|
||||
|
||||
await contains(".modal button[name=method]").click();
|
||||
expect(".modal").toHaveCount(2);
|
||||
expect(".modal:last .modal-body").toHaveText("Are you sure?");
|
||||
|
||||
await contains(".modal:last .modal-footer .btn-primary").click();
|
||||
// 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 animationFrame();
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(".modal main .o_content").toHaveText("Another action");
|
||||
});
|
||||
|
||||
test('actions in target="new" do not update page title', async () => {
|
||||
mockService("title", {
|
||||
setParts({ action }) {
|
||||
if (action) {
|
||||
expect.step(action);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
// sanity check: execute an action in target="current"
|
||||
await getService("action").doAction(1);
|
||||
expect.verifySteps(["Partners Action 1"]);
|
||||
|
||||
// execute an action in target="new"
|
||||
await getService("action").doAction(5);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("do not commit a dialog in error", async () => {
|
||||
expect.assertions(7);
|
||||
expect.errors(1);
|
||||
|
||||
class ErrorClientAction extends Component {
|
||||
static template = xml`<div/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
throw new Error("my error");
|
||||
}
|
||||
}
|
||||
registry.category("actions").add("failing", ErrorClientAction);
|
||||
|
||||
class ClientActionTargetNew extends Component {
|
||||
static template = xml`<div class="my_action_new" />`;
|
||||
static props = ["*"];
|
||||
}
|
||||
registry.category("actions").add("clientActionNew", ClientActionTargetNew);
|
||||
|
||||
class ClientAction extends Component {
|
||||
static template = xml`
|
||||
<div class="my_action" t-on-click="onClick">
|
||||
My Action
|
||||
</div>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
}
|
||||
async onClick() {
|
||||
try {
|
||||
await this.action.doAction(
|
||||
{ type: "ir.actions.client", tag: "failing", target: "new" },
|
||||
{ onClose: () => expect.step("failing dialog closed") }
|
||||
);
|
||||
} catch (e) {
|
||||
expect(e.cause.message).toBe("my error");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
registry.category("actions").add("clientAction", ClientAction);
|
||||
|
||||
const errorDialogOpened = new Deferred();
|
||||
patchWithCleanup(ClientErrorDialog.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
onMounted(() => errorDialogOpened.resolve());
|
||||
},
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({ type: "ir.actions.client", tag: "clientAction" });
|
||||
await contains(".my_action").click();
|
||||
await errorDialogOpened;
|
||||
expect(".modal").toHaveCount(1);
|
||||
|
||||
await contains(".modal-body button.btn-link").click();
|
||||
expect(queryText(".modal-body .o_error_detail")).toInclude("my error");
|
||||
expect.verifyErrors(["my error"]);
|
||||
|
||||
await contains(".modal-footer .btn-primary").click();
|
||||
expect(".modal").toHaveCount(0);
|
||||
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "clientActionNew",
|
||||
target: "new",
|
||||
});
|
||||
expect(".modal .my_action_new").toHaveCount(1);
|
||||
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test('breadcrumbs of actions in target="new"', async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
// execute an action in target="current"
|
||||
await getService("action").doAction(1);
|
||||
expect(queryAllTexts(".o_breadcrumb span")).toEqual(["Partners Action 1"]);
|
||||
|
||||
// execute an action in target="new" and a list view (s.t. there is a control panel)
|
||||
await getService("action").doAction({
|
||||
xml_id: "action_5",
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
expect(".modal .o_breadcrumb").toHaveCount(0);
|
||||
});
|
||||
|
||||
test('call switchView in an action in target="new"', async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
// execute an action in target="current"
|
||||
await getService("action").doAction(4);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
|
||||
// execute an action in target="new" and a list view (s.t. we can call switchView)
|
||||
await getService("action").doAction({
|
||||
xml_id: "action_5",
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
expect(".modal .o_list_view").toHaveCount(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
|
||||
// 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 contains(".modal .o_data_row .o_data_cell").click();
|
||||
expect(".modal .o_list_view").toHaveCount(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("action with 'dialog_size' key in context", async () => {
|
||||
const action = {
|
||||
name: "Some Action",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
target: "new",
|
||||
views: [[false, "form"]],
|
||||
};
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
await getService("action").doAction(action);
|
||||
expect(".o_dialog .modal-dialog").toHaveClass("modal-lg");
|
||||
|
||||
await getService("action").doAction({ ...action, context: { dialog_size: "small" } });
|
||||
expect(".o_dialog .modal-dialog").toHaveClass("modal-sm");
|
||||
|
||||
await getService("action").doAction({ ...action, context: { dialog_size: "medium" } });
|
||||
expect(".o_dialog .modal-dialog").toHaveClass("modal-md");
|
||||
|
||||
await getService("action").doAction({ ...action, context: { dialog_size: "large" } });
|
||||
expect(".o_dialog .modal-dialog").toHaveClass("modal-lg");
|
||||
|
||||
await getService("action").doAction({ ...action, context: { dialog_size: "extra-large" } });
|
||||
expect(".o_dialog .modal-dialog").toHaveClass("modal-xl");
|
||||
});
|
||||
|
||||
test('click on record in list view action in target="new"', async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "My Partners",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
target: "new",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
});
|
||||
|
||||
// The list view has been opened in a dialog
|
||||
expect(".o_dialog .modal-dialog .o_list_view").toHaveCount(1);
|
||||
|
||||
// click on a record in the dialog -> should do nothing as we can't switch view in the dialog
|
||||
await contains(".modal .o_data_row .o_data_cell").click();
|
||||
expect(".o_dialog .modal-dialog .o_list_view").toHaveCount(1);
|
||||
expect(".o_form_view").toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fullscreen", () => {
|
||||
test('correctly execute act_window actions in target="fullscreen"', async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(15);
|
||||
await animationFrame(); // wait for the webclient template to be re-rendered
|
||||
expect(".o_control_panel").toHaveCount(1, {
|
||||
message: "should have rendered a control panel",
|
||||
});
|
||||
expect(".o_kanban_view").toHaveCount(1, { message: "should have rendered a kanban view" });
|
||||
expect(".o_main_navbar").toHaveCount(0);
|
||||
});
|
||||
|
||||
test('action after another in target="fullscreen" is not displayed in fullscreen mode', async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(15);
|
||||
await animationFrame(); // wait for the webclient template to be re-rendered
|
||||
expect(".o_main_navbar").toHaveCount(0);
|
||||
await getService("action").doAction(1);
|
||||
await animationFrame(); // wait for the webclient template to be re-rendered
|
||||
// The navbar should be displayed again
|
||||
expect(".o_main_navbar").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test('fullscreen on action change: back to a "current" action', async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 6,
|
||||
xml_id: "action_6",
|
||||
name: "Partner",
|
||||
res_id: 2,
|
||||
res_model: "partner",
|
||||
target: "current",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
]);
|
||||
Partner._views["form"] = `
|
||||
<form>
|
||||
<button name="15" type="action" class="oe_stat_button" />
|
||||
</form>`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(6);
|
||||
expect(".o_main_navbar").toHaveCount(1);
|
||||
|
||||
await contains("button[name='15']").click();
|
||||
await animationFrame(); // wait for the webclient template to be re-rendered
|
||||
expect(".o_main_navbar").toHaveCount(0);
|
||||
|
||||
await contains(".breadcrumb li a").click();
|
||||
await animationFrame(); // wait for the webclient template to be re-rendered
|
||||
expect(".o_main_navbar").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test('fullscreen on action change: all "fullscreen" actions', async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 6,
|
||||
xml_id: "action_6",
|
||||
name: "Partner",
|
||||
res_id: 2,
|
||||
res_model: "partner",
|
||||
target: "fullscreen",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
]);
|
||||
Partner._views["form"] = `
|
||||
<form>
|
||||
<button name="15" type="action" class="oe_stat_button" />
|
||||
</form>`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(6);
|
||||
await animationFrame(); // for the webclient to react and remove the navbar
|
||||
expect(".o_main_navbar").not.toHaveCount();
|
||||
|
||||
await contains("button[name='15']").click();
|
||||
await animationFrame();
|
||||
expect(".o_main_navbar").not.toHaveCount();
|
||||
|
||||
await contains(".breadcrumb li a").click();
|
||||
await animationFrame();
|
||||
expect(".o_main_navbar").not.toHaveCount();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test('fullscreen on action change: back to another "current" action', async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 6,
|
||||
name: "Partner",
|
||||
res_id: 2,
|
||||
res_model: "partner",
|
||||
target: "current",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: "Partner",
|
||||
res_id: 2,
|
||||
res_model: "partner",
|
||||
views: [[666, "form"]],
|
||||
},
|
||||
]);
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
name: "MAIN APP",
|
||||
actionID: 6,
|
||||
},
|
||||
]);
|
||||
Partner._views["form"] = `
|
||||
<form>
|
||||
<button name="24" type="action" string="Execute action 24" class="oe_stat_button"/>
|
||||
</form>`;
|
||||
Partner._views["form,666"] = `
|
||||
<form>
|
||||
<button type="action" name="15" icon="fa-star" context="{'default_partner': id}" class="oe_stat_button"/>
|
||||
</form>`;
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await animationFrame(); // wait for the load state (default app)
|
||||
await animationFrame(); // wait for the action to be mounted
|
||||
expect("nav .o_menu_brand").toHaveCount(1);
|
||||
expect("nav .o_menu_brand").toHaveText("MAIN APP");
|
||||
|
||||
await contains("button[name='24']").click();
|
||||
await animationFrame(); // wait for the webclient template to be re-rendered
|
||||
expect("nav .o_menu_brand").toHaveCount(1);
|
||||
|
||||
await contains("button[name='15']").click();
|
||||
await animationFrame(); // wait for the webclient template to be re-rendered
|
||||
expect("nav.o_main_navbar").toHaveCount(0);
|
||||
|
||||
await contains(queryAll(".breadcrumb li a")[1]).click();
|
||||
await animationFrame(); // wait for the webclient template to be re-rendered
|
||||
expect("nav .o_menu_brand").toHaveCount(1);
|
||||
expect("nav .o_menu_brand").toHaveText("MAIN APP");
|
||||
});
|
||||
});
|
||||
|
||||
describe("main", () => {
|
||||
test.tags("desktop");
|
||||
test('can execute act_window actions in target="main"', async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(".o_breadcrumb span").toHaveCount(1);
|
||||
expect(".o_control_panel .o_breadcrumb").toHaveText("Partners Action 1");
|
||||
|
||||
await getService("action").doAction({
|
||||
name: "Another Partner Action",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
target: "main",
|
||||
});
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
expect(".o_breadcrumb span").toHaveCount(1);
|
||||
expect(".o_control_panel .o_breadcrumb").toHaveText("Another Partner Action");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test('can switch view in an action in target="main"', async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partner Action",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
target: "main",
|
||||
});
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
expect(".o_breadcrumb span").toHaveCount(1);
|
||||
expect(".o_control_panel .o_breadcrumb").toHaveText("Partner Action");
|
||||
|
||||
// open first record
|
||||
await contains(".o_data_row .o_data_cell").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
expect("ol.breadcrumb").toHaveCount(1);
|
||||
expect(".o_breadcrumb span").toHaveCount(1);
|
||||
expect(".o_control_panel .o_breadcrumb").toHaveText("Partner Action\nFirst record");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test('can restore an action in target="main"', async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partner Action",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
target: "main",
|
||||
});
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
expect(".o_breadcrumb span").toHaveCount(1);
|
||||
expect(".o_control_panel .o_breadcrumb").toHaveText("Partner Action");
|
||||
|
||||
// open first record
|
||||
await contains(".o_data_row .o_data_cell").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
expect("ol.breadcrumb").toHaveCount(1);
|
||||
expect(".o_breadcrumb span").toHaveCount(1);
|
||||
expect(".o_control_panel .o_breadcrumb").toHaveText("Partner Action\nFirst record");
|
||||
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect("ol.breadcrumb").toHaveCount(1);
|
||||
expect(".o_breadcrumb span").toHaveCount(1);
|
||||
|
||||
// go back to form view
|
||||
await contains("ol.breadcrumb .o_back_button").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
expect("ol.breadcrumb").toHaveCount(1);
|
||||
expect(".o_breadcrumb span").toHaveCount(1);
|
||||
expect(".o_control_panel .o_breadcrumb").toHaveText("Partner Action\nFirst record");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,673 +0,0 @@
|
|||
/** @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,61 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { getService, makeMockEnv, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
test("execute an 'ir.actions.act_url' action with target 'self'", async () => {
|
||||
patchWithCleanup(browser.location, {
|
||||
assign: (url) => {
|
||||
expect.step(url);
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.act_url",
|
||||
target: "self",
|
||||
url: "/my/test/url",
|
||||
});
|
||||
expect.verifySteps(["/my/test/url"]);
|
||||
});
|
||||
|
||||
test("execute an 'ir.actions.act_url' action with onClose option", async () => {
|
||||
patchWithCleanup(browser, {
|
||||
open: () => expect.step("browser open"),
|
||||
});
|
||||
await makeMockEnv();
|
||||
const options = {
|
||||
onClose: () => expect.step("onClose"),
|
||||
};
|
||||
await getService("action").doAction({ type: "ir.actions.act_url" }, options);
|
||||
expect.verifySteps(["browser open", "onClose"]);
|
||||
});
|
||||
|
||||
test("execute an 'ir.actions.act_url' action with url javascript:", async () => {
|
||||
patchWithCleanup(browser.location, {
|
||||
assign: (url) => {
|
||||
expect.step(url);
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.act_url",
|
||||
target: "self",
|
||||
url: "javascript:alert()",
|
||||
});
|
||||
expect.verifySteps(["/javascript:alert()"]);
|
||||
});
|
||||
|
||||
test("execute an 'ir.actions.act_url' action with target 'download'", async () => {
|
||||
patchWithCleanup(browser, {
|
||||
open: (url) => {
|
||||
expect.step(url);
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.act_url",
|
||||
target: "download",
|
||||
url: "/my/test/url",
|
||||
});
|
||||
expect(".o_blockUI").toHaveCount(0);
|
||||
expect.verifySteps(["/my/test/url"]);
|
||||
});
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
/** @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
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,137 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { Deferred } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
makeMockEnv,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { scanBarcode } from "@web/core/barcode/barcode_dialog";
|
||||
import { BarcodeVideoScanner } from "@web/core/barcode/barcode_video_scanner";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
/* global ZXing */
|
||||
|
||||
test("Barcode scanner crop overlay", async () => {
|
||||
const env = await makeMockEnv();
|
||||
await mountWithCleanup(WebClient, { env });
|
||||
|
||||
const firstBarcodeValue = "Odoo";
|
||||
const secondBarcodeValue = "OCDTEST";
|
||||
|
||||
let barcodeToGenerate = firstBarcodeValue;
|
||||
let videoReady = new Deferred();
|
||||
|
||||
function mockUserMedia() {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const stream = canvas.captureStream();
|
||||
|
||||
const multiFormatWriter = new ZXing.MultiFormatWriter();
|
||||
const bitMatrix = multiFormatWriter.encode(
|
||||
barcodeToGenerate,
|
||||
ZXing.BarcodeFormat.QR_CODE,
|
||||
250,
|
||||
250,
|
||||
null
|
||||
);
|
||||
canvas.width = bitMatrix.width;
|
||||
canvas.height = bitMatrix.height;
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
for (let x = 0; x < bitMatrix.width; x++) {
|
||||
for (let y = 0; y < bitMatrix.height; y++) {
|
||||
if (bitMatrix.get(x, y)) {
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, 1, 1);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
// simulate an environment with a camera/webcam
|
||||
patchWithCleanup(browser.navigator, {
|
||||
mediaDevices: {
|
||||
getUserMedia: mockUserMedia,
|
||||
},
|
||||
});
|
||||
|
||||
patchWithCleanup(BarcodeVideoScanner.prototype, {
|
||||
async isVideoReady() {
|
||||
await super.isVideoReady(...arguments);
|
||||
videoReady.resolve();
|
||||
},
|
||||
onResize(overlayInfo) {
|
||||
expect.step(overlayInfo);
|
||||
return super.onResize(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
const firstBarcodeFound = scanBarcode(env);
|
||||
await videoReady;
|
||||
await contains(".o_crop_icon").dragAndDrop(".o_crop_container", {
|
||||
relative: true,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const firstValueScanned = await firstBarcodeFound;
|
||||
expect(firstValueScanned).toBe(firstBarcodeValue, {
|
||||
message: `The detected barcode (${firstValueScanned}) should be the same as generated (${firstBarcodeValue})`,
|
||||
});
|
||||
|
||||
// Do another scan barcode to the test position of the overlay saved in the locale storage
|
||||
// Reset all values for the second test
|
||||
barcodeToGenerate = secondBarcodeValue;
|
||||
videoReady = new Deferred();
|
||||
|
||||
const secondBarcodeFound = scanBarcode(env);
|
||||
await videoReady;
|
||||
const secondValueScanned = await secondBarcodeFound;
|
||||
expect(secondValueScanned).toBe(secondBarcodeValue, {
|
||||
message: `The detected barcode (${secondValueScanned}) should be the same as generated (${secondBarcodeValue})`,
|
||||
});
|
||||
|
||||
expect.verifySteps([
|
||||
{ x: 25, y: 100, width: 200, height: 50 },
|
||||
{ x: 0, y: 0, width: 250, height: 250 },
|
||||
{ x: 0, y: 0, width: 250, height: 250 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("BarcodeVideoScanner onReady props", async () => {
|
||||
function mockUserMedia() {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const stream = canvas.captureStream();
|
||||
canvas.width = 250;
|
||||
canvas.height = 250;
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
return stream;
|
||||
}
|
||||
// Simulate an environment with a camera/webcam.
|
||||
patchWithCleanup(browser.navigator, {
|
||||
mediaDevices: {
|
||||
getUserMedia: mockUserMedia,
|
||||
},
|
||||
});
|
||||
const resolvedOnReadyPromise = new Promise((resolve) => {
|
||||
mountWithCleanup(BarcodeVideoScanner, {
|
||||
props: {
|
||||
facingMode: "environment",
|
||||
onReady: () => resolve(true),
|
||||
onResult: () => {},
|
||||
onError: () => {},
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(await resolvedOnReadyPromise).toBe(true);
|
||||
});
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
/* global ZXing */
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import {
|
||||
makeDeferred,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
|
||||
import { scanBarcode, BarcodeDialog } from "@web/webclient/barcode/barcode_scanner";
|
||||
|
||||
QUnit.module("Barcode scanner", {});
|
||||
|
||||
QUnit.test("Barcode scanner crop overlay", async (assert) => {
|
||||
const firstBarcodeValue = "Odoo";
|
||||
const secondBarcodeValue = "O-CMD-TEST";
|
||||
|
||||
let barcodeToGenerate = firstBarcodeValue;
|
||||
let videoReady = makeDeferred();
|
||||
|
||||
function mockUserMedia() {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const stream = canvas.captureStream();
|
||||
|
||||
const multiFormatWriter = new ZXing.MultiFormatWriter();
|
||||
const bitMatrix = multiFormatWriter.encode(
|
||||
barcodeToGenerate,
|
||||
ZXing.BarcodeFormat.QR_CODE,
|
||||
250,
|
||||
250,
|
||||
null
|
||||
);
|
||||
canvas.width = bitMatrix.width;
|
||||
canvas.height = bitMatrix.height;
|
||||
for (let x = 0; x < bitMatrix.width; x++) {
|
||||
for (let y = 0; y < bitMatrix.height; y++) {
|
||||
if (bitMatrix.get(x, y)) {
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, 1, 1);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
// simulate an environment with a camera/webcam
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
Object.assign({}, browser, {
|
||||
navigator: {
|
||||
mediaDevices: {
|
||||
getUserMedia: mockUserMedia,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
patchWithCleanup(BarcodeDialog.prototype, {
|
||||
async isVideoReady() {
|
||||
return this._super(...arguments).then(() => {
|
||||
videoReady.resolve();
|
||||
});
|
||||
},
|
||||
onResize(overlayInfo) {
|
||||
assert.step(JSON.stringify(overlayInfo));
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
const firstBarcodeFound = scanBarcode();
|
||||
await videoReady;
|
||||
// Needed due to the change on the props in the Crop component
|
||||
await nextTick();
|
||||
const cropIconSelector = ".o_crop_icon";
|
||||
const cropIcon = document.querySelector(cropIconSelector);
|
||||
const cropOverlay = document.querySelector(".o_crop_overlay");
|
||||
const cropContainer = document.querySelector(".o_crop_container");
|
||||
const cropIconPosition = cropIcon.getBoundingClientRect();
|
||||
const cropOverlayPosition = cropOverlay.getBoundingClientRect();
|
||||
await triggerEvent(cropContainer, cropIconSelector, "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: cropIconPosition.x + cropIconPosition.width / 2,
|
||||
clientY: cropIconPosition.y + cropIconPosition.height / 2,
|
||||
target: cropIcon,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(cropContainer, cropIconSelector, "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: cropOverlayPosition.right,
|
||||
clientY: cropOverlayPosition.bottom,
|
||||
target: cropIcon,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(cropContainer, cropIconSelector, "touchend", {});
|
||||
const firstValueScanned = await firstBarcodeFound;
|
||||
assert.strictEqual(
|
||||
firstValueScanned,
|
||||
firstBarcodeValue,
|
||||
`The detected barcode should be the same as generated (${firstBarcodeValue})`
|
||||
);
|
||||
|
||||
// Do another scan barcode to the test position of the overlay saved in the locale storage
|
||||
// Reset all values for the second test
|
||||
barcodeToGenerate = secondBarcodeValue;
|
||||
videoReady = makeDeferred();
|
||||
|
||||
const secondBarcodeFound = scanBarcode();
|
||||
await videoReady;
|
||||
const secondValueScanned = await secondBarcodeFound;
|
||||
assert.strictEqual(
|
||||
secondValueScanned,
|
||||
secondBarcodeValue,
|
||||
`The detected barcode should be the same as generated (${secondBarcodeValue})`
|
||||
);
|
||||
|
||||
assert.verifySteps(
|
||||
[
|
||||
JSON.stringify({ x: 25, y: 100, width: 200, height: 50 }),
|
||||
JSON.stringify({ x: 0, y: 0, width: 250, height: 250 }),
|
||||
JSON.stringify({ x: 0, y: 0, width: 250, height: 250 }),
|
||||
],
|
||||
"We should haves three resize event; one for the default position, another one for the all frame and the last one must be the same as the saved second position"
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,624 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, Deferred, mockDate, runAllTimers, tick } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineActions,
|
||||
defineMenus,
|
||||
defineModels,
|
||||
fields,
|
||||
makeServerError,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { onWillStart, onWillUpdateProps } from "@odoo/owl";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { SUCCESS_SIGNAL } from "@web/webclient/clickbot/clickbot";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
class Foo extends models.Model {
|
||||
foo = fields.Char();
|
||||
bar = fields.Boolean();
|
||||
date = fields.Date();
|
||||
|
||||
_records = [
|
||||
{ id: 1, bar: true, foo: "yop", date: "2017-01-25" },
|
||||
{ id: 2, bar: true, foo: "blip" },
|
||||
{ id: 3, bar: true, foo: "gnap" },
|
||||
{ id: 4, bar: false, foo: "blip" },
|
||||
];
|
||||
|
||||
_views = {
|
||||
search: /* xml */ `
|
||||
<search>
|
||||
<filter string="Not Bar" name="not bar" domain="[['bar','=',False]]"/>
|
||||
<filter string="Date" name="date" date="date"/>
|
||||
</search>
|
||||
`,
|
||||
list: /* xml */ `
|
||||
<list>
|
||||
<field name="foo" />
|
||||
</list>
|
||||
`,
|
||||
kanban: /* xml */ `
|
||||
<kanban class="o_kanban_test">
|
||||
<templates><t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t></templates>
|
||||
</kanban>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
defineModels([Foo]);
|
||||
|
||||
beforeEach(() => {
|
||||
onRpc("has_group", () => true);
|
||||
defineActions([
|
||||
{
|
||||
id: 1001,
|
||||
name: "App1",
|
||||
res_model: "foo",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "kanban"],
|
||||
],
|
||||
xml_id: "app1",
|
||||
},
|
||||
{
|
||||
id: 1002,
|
||||
name: "App2 Menu 1",
|
||||
res_model: "foo",
|
||||
views: [[false, "kanban"]],
|
||||
xml_id: "app2_menu1",
|
||||
},
|
||||
{
|
||||
id: 1022,
|
||||
name: "App2 Menu 2",
|
||||
res_model: "foo",
|
||||
views: [[false, "list"]],
|
||||
xml_id: "app2_menu2",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("clickbot clickeverywhere test", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
mockDate("2017-10-08T15:35:11.000");
|
||||
const clickEverywhereDef = new Deferred();
|
||||
patchWithCleanup(browser, {
|
||||
console: {
|
||||
log: (msg) => {
|
||||
expect.step(msg);
|
||||
if (msg === SUCCESS_SIGNAL) {
|
||||
clickEverywhereDef.resolve();
|
||||
}
|
||||
},
|
||||
error: (msg) => {
|
||||
expect.step(msg);
|
||||
clickEverywhereDef.resolve();
|
||||
},
|
||||
},
|
||||
});
|
||||
defineMenus([
|
||||
{ id: 1, name: "App1", appID: 1, actionID: 1001, xmlid: "app1" },
|
||||
{
|
||||
id: 2,
|
||||
children: [
|
||||
{
|
||||
id: 3,
|
||||
name: "menu 1",
|
||||
appID: 2,
|
||||
actionID: 1002,
|
||||
xmlid: "app2_menu1",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "menu 2",
|
||||
appID: 2,
|
||||
actionID: 1022,
|
||||
xmlid: "app2_menu2",
|
||||
},
|
||||
],
|
||||
name: "App2",
|
||||
appID: 2,
|
||||
actionID: 1002,
|
||||
xmlid: "app2",
|
||||
},
|
||||
]);
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
patchWithCleanup(odoo, {
|
||||
__WOWL_DEBUG__: { root: webClient },
|
||||
});
|
||||
window.clickEverywhere();
|
||||
await clickEverywhereDef;
|
||||
expect.verifySteps([
|
||||
"Clicking on: apps menu toggle button",
|
||||
"Testing app menu: app1",
|
||||
"Testing menu App1 app1",
|
||||
'Clicking on: menu item "App1"',
|
||||
"Testing 2 filters",
|
||||
'Clicking on: filter "Not Bar"',
|
||||
'Clicking on: filter "Date"',
|
||||
'Clicking on: filter option "October"',
|
||||
"Testing view switch: kanban",
|
||||
"Clicking on: kanban view switcher",
|
||||
"Testing 2 filters",
|
||||
'Clicking on: filter "Not Bar"',
|
||||
'Clicking on: filter "Date"',
|
||||
'Clicking on: filter option "October"',
|
||||
"Clicking on: apps menu toggle button",
|
||||
"Testing app menu: app2",
|
||||
"Testing menu App2 app2",
|
||||
'Clicking on: menu item "App2"',
|
||||
"Testing 2 filters",
|
||||
'Clicking on: filter "Not Bar"',
|
||||
'Clicking on: filter "Date"',
|
||||
'Clicking on: filter option "October"',
|
||||
"Testing menu menu 1 app2_menu1",
|
||||
'Clicking on: menu item "menu 1"',
|
||||
"Testing 2 filters",
|
||||
'Clicking on: filter "Not Bar"',
|
||||
'Clicking on: filter "Date"',
|
||||
'Clicking on: filter option "October"',
|
||||
"Testing menu menu 2 app2_menu2",
|
||||
'Clicking on: menu item "menu 2"',
|
||||
"Testing 2 filters",
|
||||
'Clicking on: filter "Not Bar"',
|
||||
'Clicking on: filter "Date"',
|
||||
'Clicking on: filter option "October"',
|
||||
"Successfully tested 2 apps",
|
||||
"Successfully tested 2 menus",
|
||||
"Successfully tested 0 modals",
|
||||
"Successfully tested 10 filters",
|
||||
SUCCESS_SIGNAL,
|
||||
]);
|
||||
});
|
||||
|
||||
test("clickbot clickeverywhere test (with dropdown menu)", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
mockDate("2017-10-08T15:35:11.000");
|
||||
const clickEverywhereDef = new Deferred();
|
||||
patchWithCleanup(browser, {
|
||||
console: {
|
||||
log: (msg) => {
|
||||
expect.step(msg);
|
||||
if (msg === SUCCESS_SIGNAL) {
|
||||
clickEverywhereDef.resolve();
|
||||
}
|
||||
},
|
||||
error: (msg) => {
|
||||
expect.step(msg);
|
||||
clickEverywhereDef.resolve();
|
||||
},
|
||||
},
|
||||
});
|
||||
defineMenus(
|
||||
[
|
||||
{
|
||||
id: 2,
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
children: [
|
||||
{
|
||||
id: 3,
|
||||
name: "menu 1",
|
||||
appID: 2,
|
||||
actionID: 1002,
|
||||
xmlid: "app2_menu1",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "menu 2",
|
||||
appID: 2,
|
||||
actionID: 1022,
|
||||
xmlid: "app2_menu2",
|
||||
},
|
||||
],
|
||||
name: "a dropdown",
|
||||
appID: 2,
|
||||
xmlid: "app2_dropdown_menu",
|
||||
},
|
||||
],
|
||||
name: "App2",
|
||||
appID: 2,
|
||||
actionID: 1002,
|
||||
xmlid: "app2",
|
||||
},
|
||||
],
|
||||
{ mode: "replace" }
|
||||
);
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
patchWithCleanup(odoo, {
|
||||
__WOWL_DEBUG__: { root: webClient },
|
||||
});
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_menu_sections .dropdown-toggle").toHaveText("a dropdown");
|
||||
window.clickEverywhere();
|
||||
await clickEverywhereDef;
|
||||
expect.verifySteps([
|
||||
"Clicking on: apps menu toggle button",
|
||||
"Testing app menu: app2",
|
||||
"Testing menu App2 app2",
|
||||
'Clicking on: menu item "App2"',
|
||||
"Testing 2 filters",
|
||||
'Clicking on: filter "Not Bar"',
|
||||
'Clicking on: filter "Date"',
|
||||
'Clicking on: filter option "October"',
|
||||
"Clicking on: menu toggler",
|
||||
"Testing menu menu 1 app2_menu1",
|
||||
'Clicking on: menu item "menu 1"',
|
||||
"Testing 2 filters",
|
||||
'Clicking on: filter "Not Bar"',
|
||||
'Clicking on: filter "Date"',
|
||||
'Clicking on: filter option "October"',
|
||||
"Clicking on: menu toggler",
|
||||
"Testing menu menu 2 app2_menu2",
|
||||
'Clicking on: menu item "menu 2"',
|
||||
"Testing 2 filters",
|
||||
'Clicking on: filter "Not Bar"',
|
||||
'Clicking on: filter "Date"',
|
||||
'Clicking on: filter option "October"',
|
||||
"Successfully tested 1 apps",
|
||||
"Successfully tested 2 menus",
|
||||
"Successfully tested 0 modals",
|
||||
"Successfully tested 6 filters",
|
||||
SUCCESS_SIGNAL,
|
||||
]);
|
||||
});
|
||||
|
||||
test("clickbot test waiting rpc after clicking filter", async () => {
|
||||
const clickEverywhereDef = new Deferred();
|
||||
let clickBotStarted = false;
|
||||
patchWithCleanup(browser, {
|
||||
console: {
|
||||
log: (msg) => {
|
||||
if (msg === SUCCESS_SIGNAL) {
|
||||
expect.step(msg);
|
||||
clickEverywhereDef.resolve();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
clickEverywhereDef.resolve();
|
||||
},
|
||||
},
|
||||
});
|
||||
onRpc("web_search_read", async () => {
|
||||
if (clickBotStarted) {
|
||||
expect.step("web_search_read called");
|
||||
await tick();
|
||||
expect.step("response");
|
||||
}
|
||||
});
|
||||
defineActions(
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
res_model: "foo",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
],
|
||||
{ mode: "replace" }
|
||||
);
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
actionID: 1,
|
||||
xmlid: "app1",
|
||||
},
|
||||
]);
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
patchWithCleanup(odoo, {
|
||||
__WOWL_DEBUG__: { root: webClient },
|
||||
});
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
clickBotStarted = true;
|
||||
window.clickEverywhere();
|
||||
await clickEverywhereDef;
|
||||
expect.verifySteps([
|
||||
"web_search_read called", // click on the App
|
||||
"response",
|
||||
"web_search_read called", // click on the Filter
|
||||
"response",
|
||||
"web_search_read called", // click on the Second Filter
|
||||
"response",
|
||||
SUCCESS_SIGNAL,
|
||||
]);
|
||||
});
|
||||
|
||||
test("clickbot show rpc error when an error dialog is detected", async () => {
|
||||
expect.errors(1);
|
||||
onRpc("has_group", () => true);
|
||||
mockDate("2024-04-10T00:00:00.000");
|
||||
const clickEverywhereDef = new Deferred();
|
||||
let clickBotStarted = false;
|
||||
let id = 1;
|
||||
patchWithCleanup(browser, {
|
||||
console: {
|
||||
log: (msg) => {
|
||||
if (msg === "test successful") {
|
||||
expect.step(msg);
|
||||
clickEverywhereDef.resolve();
|
||||
}
|
||||
},
|
||||
error: (msg) => {
|
||||
// Replace msg with null id as JSON-RPC ids are not reset between two tests
|
||||
expect.step(msg.toString().replaceAll(/"id":\d+,/g, `"id":null,`));
|
||||
clickEverywhereDef.resolve();
|
||||
},
|
||||
},
|
||||
});
|
||||
onRpc("web_search_read", () => {
|
||||
if (clickBotStarted) {
|
||||
if (id === 3) {
|
||||
// click on the Second Filter
|
||||
throw makeServerError({
|
||||
message: "This is a server Error, it should be displayed in an error dialog",
|
||||
type: "Programming error",
|
||||
});
|
||||
}
|
||||
id++;
|
||||
}
|
||||
});
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
name: "App1",
|
||||
res_model: "foo",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
]);
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
name: "App1",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "app1",
|
||||
},
|
||||
]);
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
patchWithCleanup(odoo, {
|
||||
__WOWL_DEBUG__: { root: webClient },
|
||||
});
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
clickBotStarted = true;
|
||||
window.clickEverywhere();
|
||||
await clickEverywhereDef;
|
||||
await tick();
|
||||
|
||||
const expectedRpcData = JSON.stringify({
|
||||
data: {
|
||||
id: null,
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
model: "foo",
|
||||
method: "web_search_read",
|
||||
args: [],
|
||||
kwargs: {
|
||||
specification: { foo: {} },
|
||||
offset: 0,
|
||||
order: "",
|
||||
limit: 80,
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
allowed_company_ids: [1],
|
||||
bin_size: true,
|
||||
},
|
||||
count_limit: 10001,
|
||||
domain: [
|
||||
"|",
|
||||
["bar", "=", false],
|
||||
"&",
|
||||
["date", ">=", "2024-04-01"],
|
||||
["date", "<=", "2024-04-30"],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: { silent: false, cache: false },
|
||||
error: {
|
||||
name: "RPC_ERROR",
|
||||
type: "server",
|
||||
code: 0,
|
||||
data: {
|
||||
name: "odoo.exceptions.Programming error",
|
||||
debug: "traceback",
|
||||
arguments: [],
|
||||
context: {},
|
||||
},
|
||||
exceptionName: "odoo.exceptions.Programming error",
|
||||
subType: "server",
|
||||
message: "This is a server Error, it should be displayed in an error dialog",
|
||||
model: "foo",
|
||||
errorEvent: { isTrusted: true },
|
||||
},
|
||||
});
|
||||
const expectedModalHtml = /* xml */ `
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title text-break flex-grow-1">Oops!</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" tabindex="-1"></button>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<div role="alert">
|
||||
<p class="text-prewrap"> Something went wrong... If you really are stuck, share the report with your friendly support service </p>
|
||||
<button class="btn btn-link p-0">See technical details</button>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="modal-footer justify-content-around justify-content-md-start flex-wrap gap-1 w-100">
|
||||
<button class="btn btn-primary o-default-button">Close</button>
|
||||
</footer>`
|
||||
.trim()
|
||||
.replaceAll(/>[\n\s]+</gm, "><");
|
||||
|
||||
expect.verifyErrors(["This is a server Error"]);
|
||||
expect.verifySteps([
|
||||
`A RPC in error was detected, maybe it's related to the error dialog : ${expectedRpcData}`,
|
||||
"Error while testing App1 app1",
|
||||
`Error: Error dialog detected${expectedModalHtml}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("clickbot test waiting render after clicking filter", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
const clickEverywhereDef = new Deferred();
|
||||
let clickBotStarted = false;
|
||||
patchWithCleanup(browser, {
|
||||
console: {
|
||||
log: (msg) => {
|
||||
if (msg === SUCCESS_SIGNAL) {
|
||||
expect.step(msg);
|
||||
clickEverywhereDef.resolve();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
clickEverywhereDef.resolve();
|
||||
},
|
||||
},
|
||||
});
|
||||
patchWithCleanup(ListRenderer.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
onWillStart(async () => {
|
||||
if (clickBotStarted) {
|
||||
expect.step("onWillStart called");
|
||||
await runAllTimers();
|
||||
expect.step("response");
|
||||
}
|
||||
});
|
||||
onWillUpdateProps(async () => {
|
||||
if (clickBotStarted) {
|
||||
expect.step("onWillUpdateProps called");
|
||||
await runAllTimers();
|
||||
expect.step("response");
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
res_model: "foo",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
]);
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "app1",
|
||||
},
|
||||
]);
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
patchWithCleanup(odoo, {
|
||||
__WOWL_DEBUG__: { root: webClient },
|
||||
});
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
clickBotStarted = true;
|
||||
window.clickEverywhere();
|
||||
await clickEverywhereDef;
|
||||
expect.verifySteps([
|
||||
"onWillStart called", // click on APP
|
||||
"response",
|
||||
"onWillUpdateProps called", // click on filter
|
||||
"response",
|
||||
"onWillUpdateProps called", // click on second filter
|
||||
"response",
|
||||
SUCCESS_SIGNAL,
|
||||
]);
|
||||
});
|
||||
|
||||
test("clickbot clickeverywhere menu modal", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
mockDate("2017-10-08T15:35:11.000");
|
||||
Foo._views.form = /* xml */ `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`;
|
||||
const clickEverywhereDef = new Deferred();
|
||||
patchWithCleanup(browser, {
|
||||
console: {
|
||||
log: (msg) => {
|
||||
expect.step(msg);
|
||||
if (msg === SUCCESS_SIGNAL) {
|
||||
clickEverywhereDef.resolve();
|
||||
}
|
||||
},
|
||||
error: (msg) => {
|
||||
expect.step(msg);
|
||||
clickEverywhereDef.resolve();
|
||||
},
|
||||
},
|
||||
});
|
||||
defineActions([
|
||||
{
|
||||
id: 1099,
|
||||
name: "Modal",
|
||||
res_model: "foo",
|
||||
views: [[false, "form"]],
|
||||
view_mode: "form",
|
||||
target: "new",
|
||||
},
|
||||
]);
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
name: "App1",
|
||||
actionID: 1001,
|
||||
xmlid: "app1",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "App Modal",
|
||||
actionID: 1099,
|
||||
xmlid: "test.modal",
|
||||
},
|
||||
]);
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
patchWithCleanup(odoo, {
|
||||
__WOWL_DEBUG__: { root: webClient },
|
||||
});
|
||||
window.clickEverywhere();
|
||||
await clickEverywhereDef;
|
||||
expect.verifySteps([
|
||||
"Clicking on: apps menu toggle button",
|
||||
"Testing app menu: app1",
|
||||
"Testing menu App1 app1",
|
||||
'Clicking on: menu item "App1"',
|
||||
"Testing 2 filters",
|
||||
'Clicking on: filter "Not Bar"',
|
||||
'Clicking on: filter "Date"',
|
||||
'Clicking on: filter option "October"',
|
||||
"Testing view switch: kanban",
|
||||
"Clicking on: kanban view switcher",
|
||||
"Testing 2 filters",
|
||||
'Clicking on: filter "Not Bar"',
|
||||
'Clicking on: filter "Date"',
|
||||
'Clicking on: filter option "October"',
|
||||
"Clicking on: apps menu toggle button",
|
||||
"Testing app menu: test.modal",
|
||||
"Testing menu App Modal test.modal",
|
||||
'Clicking on: menu item "App Modal"',
|
||||
"Modal detected: App Modal test.modal",
|
||||
"Clicking on: modal close button",
|
||||
"Successfully tested 2 apps",
|
||||
"Successfully tested 0 menus",
|
||||
"Successfully tested 1 modals",
|
||||
"Successfully tested 4 filters",
|
||||
SUCCESS_SIGNAL,
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-dom";
|
||||
import {
|
||||
defineModels,
|
||||
getService,
|
||||
makeMockEnv,
|
||||
models,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { currencies } from "@web/core/currency";
|
||||
import { rpcBus } from "@web/core/network/rpc";
|
||||
|
||||
class Currency extends models.Model {
|
||||
_name = "res.currency";
|
||||
get_all_currencies() {
|
||||
return {
|
||||
1: { symbol: "$", position: "before", digits: 2 },
|
||||
};
|
||||
}
|
||||
}
|
||||
class Notcurrency extends models.Model {}
|
||||
|
||||
defineModels([Currency, Notcurrency]);
|
||||
|
||||
test("reload currencies when updating a res.currency", async () => {
|
||||
onRpc(({ model, method }) => {
|
||||
expect.step([model, method]);
|
||||
});
|
||||
await makeMockEnv();
|
||||
expect.verifySteps([]);
|
||||
await getService("orm").read("res.currency", [32]);
|
||||
expect.verifySteps([["res.currency", "read"]]);
|
||||
await getService("orm").unlink("res.currency", [32]);
|
||||
expect.verifySteps([
|
||||
["res.currency", "unlink"],
|
||||
["res.currency", "get_all_currencies"],
|
||||
]);
|
||||
await getService("orm").unlink("notcurrency", [32]);
|
||||
expect.verifySteps([["notcurrency", "unlink"]]);
|
||||
expect(Object.keys(currencies)).toEqual(["1"]);
|
||||
});
|
||||
|
||||
test("do not reload webclient when updating a res.currency, but there is an error", async () => {
|
||||
onRpc("get_all_currencies", ({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await makeMockEnv();
|
||||
expect.verifySteps([]);
|
||||
rpcBus.trigger("RPC:RESPONSE", {
|
||||
data: { params: { model: "res.currency", method: "write" } },
|
||||
settings: {},
|
||||
result: {},
|
||||
});
|
||||
await animationFrame();
|
||||
expect.verifySteps(["get_all_currencies"]);
|
||||
rpcBus.trigger("RPC:RESPONSE", {
|
||||
data: { params: { model: "res.currency", method: "write" } },
|
||||
settings: {},
|
||||
error: {},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -1,600 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { legacyServiceProvider } from "@web/legacy/legacy_service_provider";
|
||||
import {
|
||||
makeLegacyNotificationService,
|
||||
mapLegacyEnvToWowlEnv,
|
||||
makeLegacySessionService,
|
||||
} from "@web/legacy/utils";
|
||||
import { makeLegacyActionManagerService } from "@web/legacy/backend_utils";
|
||||
import { generateLegacyLoadViewsResult } from "@web/legacy/legacy_load_views";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
import { effectService } from "@web/core/effects/effect_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { menuService } from "@web/webclient/menus/menu_service";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
// This import is needed because of it's sideeffects, for exemple :
|
||||
// web.test_utils easyload xml templates at line : 124:130.
|
||||
// Also it set the autocomplete delay time for the field Many2One at 0 for the tests at line : 132:137
|
||||
import "web.test_legacy";
|
||||
import AbstractService from "web.AbstractService";
|
||||
import ActionMenus from "web.ActionMenus";
|
||||
import basicFields from "web.basic_fields";
|
||||
import Registry from "web.Registry";
|
||||
import core from "web.core";
|
||||
import makeTestEnvironment from "web.test_env";
|
||||
import { registerCleanup } from "../helpers/cleanup";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import {
|
||||
fakeTitleService,
|
||||
fakeCompanyService,
|
||||
makeFakeLocalizationService,
|
||||
makeFakeRouterService,
|
||||
makeFakeHTTPService,
|
||||
makeFakeUserService,
|
||||
} from "../helpers/mock_services";
|
||||
import {
|
||||
getFixture,
|
||||
legacyExtraNextTick,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "../helpers/utils";
|
||||
import session from "web.session";
|
||||
import LegacyMockServer from "web.MockServer";
|
||||
import Widget from "web.Widget";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { ClientActionAdapter, ViewAdapter } from "@web/legacy/action_adapters";
|
||||
import { commandService } from "@web/core/commands/command_service";
|
||||
import { ConnectionAbortedError } from "@web/core/network/rpc_service";
|
||||
import { CustomFavoriteItem } from "@web/search/favorite_menu/custom_favorite_item";
|
||||
import { standaloneAdapter } from "web.OwlCompatibility";
|
||||
|
||||
import { Component, onMounted, xml } from "@odoo/owl";
|
||||
|
||||
const actionRegistry = registry.category("actions");
|
||||
const serviceRegistry = registry.category("services");
|
||||
const favoriteMenuRegistry = registry.category("favoriteMenu");
|
||||
|
||||
/**
|
||||
* Builds the required registries for tests using a WebClient.
|
||||
* We use a default version of each required registry item.
|
||||
* If the registry already contains one of those items,
|
||||
* the existing one is kept (it means it has been added in the test
|
||||
* directly, e.g. to have a custom version of the item).
|
||||
*/
|
||||
export function setupWebClientRegistries() {
|
||||
const favoriveMenuItems = {
|
||||
"custom-favorite-item": {
|
||||
value: { Component: CustomFavoriteItem, groupNumber: 3 },
|
||||
options: { sequence: 0 },
|
||||
},
|
||||
};
|
||||
for (const [key, { value, options }] of Object.entries(favoriveMenuItems)) {
|
||||
if (!favoriteMenuRegistry.contains(key)) {
|
||||
favoriteMenuRegistry.add(key, value, options);
|
||||
}
|
||||
}
|
||||
const services = {
|
||||
action: () => actionService,
|
||||
command: () => commandService,
|
||||
dialog: () => dialogService,
|
||||
effect: () => effectService,
|
||||
hotkey: () => hotkeyService,
|
||||
http: () => makeFakeHTTPService(),
|
||||
legacy_service_provider: () => legacyServiceProvider,
|
||||
localization: () => makeFakeLocalizationService(),
|
||||
menu: () => menuService,
|
||||
notification: () => notificationService,
|
||||
orm: () => ormService,
|
||||
popover: () => popoverService,
|
||||
router: () => makeFakeRouterService(),
|
||||
title: () => fakeTitleService,
|
||||
ui: () => uiService,
|
||||
user: () => makeFakeUserService(),
|
||||
view: () => viewService,
|
||||
company: () => fakeCompanyService,
|
||||
};
|
||||
for (const serviceName in services) {
|
||||
if (!serviceRegistry.contains(serviceName)) {
|
||||
serviceRegistry.add(serviceName, services[serviceName]());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this as soon as we drop the legacy support
|
||||
*/
|
||||
export async function addLegacyMockEnvironment(env, legacyParams = {}) {
|
||||
// setup a legacy env
|
||||
const dataManager = Object.assign(
|
||||
{
|
||||
load_action: (actionID, context) => {
|
||||
return env.services.rpc("/web/action/load", {
|
||||
action_id: actionID,
|
||||
additional_context: context,
|
||||
});
|
||||
},
|
||||
load_views: async (params, options) => {
|
||||
let result = await env.services.rpc(`/web/dataset/call_kw/${params.model}`, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: params.context,
|
||||
options: options,
|
||||
views: params.views_descr,
|
||||
},
|
||||
method: "get_views",
|
||||
model: params.model,
|
||||
});
|
||||
const { models, views: _views } = result;
|
||||
result = generateLegacyLoadViewsResult(params.model, _views, models);
|
||||
const views = result.fields_views;
|
||||
for (const [, viewType] of params.views_descr) {
|
||||
const fvg = views[viewType];
|
||||
fvg.viewFields = fvg.fields;
|
||||
fvg.fields = result.fields;
|
||||
}
|
||||
if (params.favoriteFilters && "search" in views) {
|
||||
views.search.favoriteFilters = params.favoriteFilters;
|
||||
}
|
||||
return views;
|
||||
},
|
||||
load_filters: (params) => {
|
||||
if (QUnit.config.debug) {
|
||||
console.log("[mock] load_filters", params);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
},
|
||||
legacyParams.dataManager
|
||||
);
|
||||
|
||||
// clear the ActionMenus registry to prevent external code from doing unknown rpcs
|
||||
const actionMenusRegistry = ActionMenus.registry;
|
||||
ActionMenus.registry = new Registry();
|
||||
registerCleanup(() => (ActionMenus.registry = actionMenusRegistry));
|
||||
|
||||
let localSession;
|
||||
if (legacyParams && legacyParams.getTZOffset) {
|
||||
patchWithCleanup(session, {
|
||||
getTZOffset: legacyParams.getTZOffset,
|
||||
});
|
||||
localSession = { getTZOffset: legacyParams.getTZOffset };
|
||||
}
|
||||
|
||||
const baseEnv = { dataManager, bus: core.bus, session: localSession };
|
||||
const legacyEnv = makeTestEnvironment(Object.assign(baseEnv, legacyParams.env));
|
||||
|
||||
if (legacyParams.serviceRegistry) {
|
||||
const legacyServiceMap = core.serviceRegistry.map;
|
||||
core.serviceRegistry.map = legacyParams.serviceRegistry.map;
|
||||
// notification isn't a deployed service, but it is added by `makeTestEnvironment`.
|
||||
// Here, we want full control on the deployed services, so we simply remove it.
|
||||
delete legacyEnv.services.notification;
|
||||
AbstractService.prototype.deployServices(legacyEnv);
|
||||
registerCleanup(() => {
|
||||
core.serviceRegistry.map = legacyServiceMap;
|
||||
});
|
||||
}
|
||||
|
||||
Component.env = legacyEnv;
|
||||
mapLegacyEnvToWowlEnv(legacyEnv, env);
|
||||
function patchLegacySession() {
|
||||
const userContext = Object.getOwnPropertyDescriptor(session, "user_context");
|
||||
registerCleanup(() => {
|
||||
Object.defineProperty(session, "user_context", userContext);
|
||||
});
|
||||
}
|
||||
patchLegacySession();
|
||||
serviceRegistry.add("legacy_session", makeLegacySessionService(legacyEnv, session));
|
||||
// deploy the legacyActionManagerService (in Wowl env)
|
||||
const legacyActionManagerService = makeLegacyActionManagerService(legacyEnv);
|
||||
serviceRegistry.add("legacy_action_manager", legacyActionManagerService);
|
||||
serviceRegistry.add("legacy_notification", makeLegacyNotificationService(legacyEnv));
|
||||
// deploy wowl services into the legacy env.
|
||||
const wowlToLegacyServiceMappers = registry.category("wowlToLegacyServiceMappers").getEntries();
|
||||
for (const [legacyServiceName, wowlToLegacyServiceMapper] of wowlToLegacyServiceMappers) {
|
||||
serviceRegistry.add(legacyServiceName, wowlToLegacyServiceMapper(legacyEnv));
|
||||
}
|
||||
// patch DebouncedField delay
|
||||
const debouncedField = basicFields.DebouncedField;
|
||||
const initialDebouncedVal = debouncedField.prototype.DEBOUNCE;
|
||||
debouncedField.prototype.DEBOUNCE = 0;
|
||||
registerCleanup(() => (debouncedField.prototype.DEBOUNCE = initialDebouncedVal));
|
||||
|
||||
if (legacyParams.withLegacyMockServer) {
|
||||
const adapter = standaloneAdapter({ Component });
|
||||
registerCleanup(() => adapter.__owl__.app.destroy());
|
||||
adapter.env = legacyEnv;
|
||||
const W = Widget.extend({ do_push_state() {} });
|
||||
const widget = new W(adapter);
|
||||
const legacyMockServer = new LegacyMockServer(legacyParams.models, { widget });
|
||||
const originalRPC = env.services.rpc;
|
||||
const rpc = async (...args) => {
|
||||
try {
|
||||
return await originalRPC(...args);
|
||||
} catch (e) {
|
||||
if (e.message.includes("Unimplemented")) {
|
||||
return legacyMockServer._performRpc(...args);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
env.services.rpc = function () {
|
||||
let rejectFn;
|
||||
const rpcProm = new Promise((resolve, reject) => {
|
||||
rejectFn = reject;
|
||||
rpc(...arguments)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
rpcProm.abort = () => rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort"));
|
||||
return rpcProm;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method create a web client instance properly configured.
|
||||
*
|
||||
* Note that the returned web client will be automatically cleaned up after the
|
||||
* end of the test.
|
||||
*
|
||||
* @param {*} params
|
||||
*/
|
||||
export async function createWebClient(params) {
|
||||
setupWebClientRegistries();
|
||||
|
||||
// With the compatibility layer, the action manager keeps legacy alive if they
|
||||
// are still acessible from the breacrumbs. They are manually destroyed as soon
|
||||
// as they are no longer referenced in the stack. This works fine in production,
|
||||
// because the webclient is never destroyed. However, at the end of each test,
|
||||
// we destroy the webclient and expect every legacy that has been instantiated
|
||||
// to be destroyed. We thus need to manually destroy them here.
|
||||
const controllers = [];
|
||||
patchWithCleanup(ClientActionAdapter.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
onMounted(() => {
|
||||
controllers.push(this.widget);
|
||||
});
|
||||
},
|
||||
});
|
||||
patchWithCleanup(ViewAdapter.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
onMounted(() => {
|
||||
controllers.push(this.widget);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const legacyParams = params.legacyParams;
|
||||
params.serverData = params.serverData || {};
|
||||
const models = params.serverData.models;
|
||||
if (legacyParams && legacyParams.withLegacyMockServer && models) {
|
||||
legacyParams.models = Object.assign({}, models);
|
||||
// In lagacy, data may not be sole models, but can contain some other variables
|
||||
// So we filter them out for our WOWL mockServer
|
||||
Object.entries(legacyParams.models).forEach(([k, v]) => {
|
||||
if (!(v instanceof Object) || !("fields" in v)) {
|
||||
delete models[k];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mockRPC = params.mockRPC || undefined;
|
||||
const env = await makeTestEnv({
|
||||
serverData: params.serverData,
|
||||
mockRPC,
|
||||
});
|
||||
await addLegacyMockEnvironment(env, legacyParams);
|
||||
|
||||
const WebClientClass = params.WebClientClass || WebClient;
|
||||
const target = params && params.target ? params.target : getFixture();
|
||||
const wc = await mount(WebClientClass, target, { env });
|
||||
target.classList.add("o_web_client"); // necessary for the stylesheet
|
||||
registerCleanup(() => {
|
||||
target.classList.remove("o_web_client");
|
||||
for (const controller of controllers) {
|
||||
if (!controller.isDestroyed()) {
|
||||
controller.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
// Wait for visual changes caused by a potential loadState
|
||||
await nextTick();
|
||||
return wc;
|
||||
}
|
||||
|
||||
export async function doAction(env, ...args) {
|
||||
if (env instanceof Component) {
|
||||
env = env.env;
|
||||
}
|
||||
try {
|
||||
await env.services.action.doAction(...args);
|
||||
} finally {
|
||||
await legacyExtraNextTick();
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadState(env, state) {
|
||||
if (env instanceof Component) {
|
||||
env = env.env;
|
||||
}
|
||||
env.bus.trigger("test:hashchange", state);
|
||||
// wait the asynchronous hashchange
|
||||
// (the event hashchange must be triggered in a nonBlocking stack)
|
||||
await nextTick();
|
||||
// wait for the regular rendering
|
||||
await nextTick();
|
||||
// wait for the legacy rendering below owl layer
|
||||
await legacyExtraNextTick();
|
||||
}
|
||||
|
||||
export function getActionManagerServerData() {
|
||||
// additional basic client action
|
||||
class TestClientAction extends Component {}
|
||||
TestClientAction.template = xml`
|
||||
<div class="test_client_action">
|
||||
ClientAction_<t t-esc="props.action.params?.description"/>
|
||||
</div>`;
|
||||
actionRegistry.add("__test__client__action__", TestClientAction);
|
||||
|
||||
const menus = {
|
||||
root: { id: "root", children: [0, 1, 2], name: "root", appID: "root" },
|
||||
// id:0 is a hack to not load anything at webClient mount
|
||||
0: { id: 0, children: [], name: "UglyHack", appID: 0, xmlid: "menu_0" },
|
||||
1: { id: 1, children: [], name: "App1", appID: 1, actionID: 1001, xmlid: "menu_1" },
|
||||
2: { id: 2, children: [], name: "App2", appID: 2, actionID: 1002, xmlid: "menu_2" },
|
||||
};
|
||||
const actionsArray = [
|
||||
{
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[1, "kanban"]],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
xml_id: "action_2",
|
||||
type: "ir.actions.server",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
xml_id: "action_3",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[1, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
xml_id: "action_4",
|
||||
name: "Partners Action 4",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[1, "kanban"],
|
||||
[2, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
xml_id: "action_5",
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
xml_id: "action_6",
|
||||
name: "Partner",
|
||||
res_id: 2,
|
||||
res_model: "partner",
|
||||
target: "inline",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
xml_id: "action_7",
|
||||
name: "Some Report",
|
||||
report_name: "some_report",
|
||||
report_type: "qweb-pdf",
|
||||
type: "ir.actions.report",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
xml_id: "action_8",
|
||||
name: "Favorite Ponies",
|
||||
res_model: "pony",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
xml_id: "action_9",
|
||||
name: "A Client Action",
|
||||
tag: "ClientAction",
|
||||
type: "ir.actions.client",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
type: "ir.actions.act_window_close",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
xml_id: "action_11",
|
||||
name: "Another Report",
|
||||
report_name: "another_report",
|
||||
report_type: "qweb-pdf",
|
||||
type: "ir.actions.report",
|
||||
close_on_report_download: true,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
xml_id: "action_12",
|
||||
name: "Some HTML Report",
|
||||
report_name: "some_report",
|
||||
report_type: "qweb-html",
|
||||
type: "ir.actions.report",
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: "Partner",
|
||||
res_id: 2,
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[666, "form"]],
|
||||
},
|
||||
{
|
||||
id: 25,
|
||||
name: "Create a Partner",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[3, "form"]],
|
||||
},
|
||||
{
|
||||
id: 1001,
|
||||
tag: "__test__client__action__",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "Id 1" },
|
||||
},
|
||||
{
|
||||
id: 1002,
|
||||
tag: "__test__client__action__",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "Id 2" },
|
||||
},
|
||||
{
|
||||
xmlId: "wowl.client_action",
|
||||
id: 1099,
|
||||
tag: "__test__client__action__",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "xmlId" },
|
||||
},
|
||||
];
|
||||
const actions = {};
|
||||
actionsArray.forEach((act) => {
|
||||
actions[act.xmlId || act.id] = act;
|
||||
});
|
||||
const archs = {
|
||||
// kanban views
|
||||
"partner,1,kanban":
|
||||
'<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div class="oe_kanban_global_click"><field name="foo"/></div>' +
|
||||
"</t></templates></kanban>",
|
||||
// list views
|
||||
"partner,false,list": '<tree><field name="foo"/></tree>',
|
||||
"partner,2,list": '<tree limit="3"><field name="foo"/></tree>',
|
||||
"pony,false,list": '<tree><field name="name"/></tree>',
|
||||
// form views
|
||||
"partner,false,form":
|
||||
"<form>" +
|
||||
"<header>" +
|
||||
'<button name="object" string="Call method" type="object"/>' +
|
||||
'<button name="4" string="Execute action" type="action"/>' +
|
||||
"</header>" +
|
||||
"<group>" +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="foo"/>' +
|
||||
"</group>" +
|
||||
"</form>",
|
||||
"partner,3,form": `
|
||||
<form>
|
||||
<footer>
|
||||
<button class="btn-primary" string="Save" special="save"/>
|
||||
</footer>
|
||||
</form>`,
|
||||
"partner,666,form": `<form>
|
||||
<header></header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box" modifiers="{}">
|
||||
<button class="oe_stat_button" type="action" name="1" icon="fa-star" context="{'default_partner': active_id}">
|
||||
<field string="Partners" name="o2m" widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
<field name="display_name"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
"pony,false,form": "<form>" + '<field name="name"/>' + "</form>",
|
||||
// search views
|
||||
"partner,false,search": '<search><field name="foo" string="Foo"/></search>',
|
||||
"partner,4,search":
|
||||
"<search>" +
|
||||
'<filter name="bar" help="Bar" domain="[(\'bar\', \'=\', 1)]"/>' +
|
||||
"</search>",
|
||||
"pony,false,search": "<search></search>",
|
||||
};
|
||||
const models = {
|
||||
partner: {
|
||||
fields: {
|
||||
id: { string: "Id", type: "integer" },
|
||||
foo: { string: "Foo", type: "char" },
|
||||
bar: { string: "Bar", type: "many2one", relation: "partner" },
|
||||
o2m: {
|
||||
string: "One2Many",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
relation_field: "bar",
|
||||
},
|
||||
m2o: { string: "Many2one", type: "many2one", relation: "partner" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "First record", foo: "yop", bar: 2, o2m: [2, 3], m2o: 3 },
|
||||
{
|
||||
id: 2,
|
||||
display_name: "Second record",
|
||||
foo: "blip",
|
||||
bar: 1,
|
||||
o2m: [1, 4, 5],
|
||||
m2o: 3,
|
||||
},
|
||||
{ id: 3, display_name: "Third record", foo: "gnap", bar: 1, o2m: [], m2o: 1 },
|
||||
{ id: 4, display_name: "Fourth record", foo: "plop", bar: 2, o2m: [], m2o: 1 },
|
||||
{ id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, o2m: [], m2o: 1 },
|
||||
],
|
||||
},
|
||||
pony: {
|
||||
fields: {
|
||||
id: { string: "Id", type: "integer" },
|
||||
name: { string: "Name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 4, name: "Twilight Sparkle" },
|
||||
{ id: 6, name: "Applejack" },
|
||||
{ id: 9, name: "Fluttershy" },
|
||||
],
|
||||
},
|
||||
};
|
||||
return {
|
||||
models,
|
||||
views: archs,
|
||||
actions,
|
||||
menus,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { advanceTime, animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
getService,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { rpcBus } from "@web/core/network/rpc";
|
||||
import { config as transitionConfig } from "@web/core/transition";
|
||||
import { LoadingIndicator } from "@web/webclient/loading_indicator/loading_indicator";
|
||||
|
||||
const payload = (id) => ({ data: { id, params: { model: "", method: "" } }, settings: {} });
|
||||
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(transitionConfig, { disabled: true });
|
||||
});
|
||||
|
||||
test("displays the loading indicator in non debug mode", async () => {
|
||||
await mountWithCleanup(LoadingIndicator, { noMainContainer: true });
|
||||
expect(".o_loading_indicator").toHaveCount(0, {
|
||||
message: "the loading indicator should not be displayed",
|
||||
});
|
||||
rpcBus.trigger("RPC:REQUEST", payload(1));
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveCount(1, {
|
||||
message: "the loading indicator should be displayed",
|
||||
});
|
||||
expect(".o_loading_indicator").toHaveText("Loading", {
|
||||
message: "the loading indicator should display 'Loading'",
|
||||
});
|
||||
rpcBus.trigger("RPC:RESPONSE", payload(1));
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveCount(0, {
|
||||
message: "the loading indicator should not be displayed",
|
||||
});
|
||||
});
|
||||
|
||||
test("displays the loading indicator for one rpc in debug mode", async () => {
|
||||
serverState.debug = "1";
|
||||
await mountWithCleanup(LoadingIndicator, { noMainContainer: true });
|
||||
expect(".o_loading_indicator").toHaveCount(0, {
|
||||
message: "the loading indicator should not be displayed",
|
||||
});
|
||||
rpcBus.trigger("RPC:REQUEST", payload(1));
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveCount(1, {
|
||||
message: "the loading indicator should be displayed",
|
||||
});
|
||||
expect(".o_loading_indicator").toHaveText("Loading (1)", {
|
||||
message: "the loading indicator should indicate 1 request in progress",
|
||||
});
|
||||
rpcBus.trigger("RPC:RESPONSE", payload(1));
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveCount(0, {
|
||||
message: "the loading indicator should not be displayed",
|
||||
});
|
||||
});
|
||||
|
||||
test("displays the loading indicator for multi rpc in debug mode", async () => {
|
||||
serverState.debug = "1";
|
||||
await mountWithCleanup(LoadingIndicator, { noMainContainer: true });
|
||||
expect(".o_loading_indicator").toHaveCount(0, {
|
||||
message: "the loading indicator should not be displayed",
|
||||
});
|
||||
rpcBus.trigger("RPC:REQUEST", payload(1));
|
||||
rpcBus.trigger("RPC:REQUEST", payload(2));
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveCount(1, {
|
||||
message: "the loading indicator should be displayed",
|
||||
});
|
||||
expect(".o_loading_indicator").toHaveText("Loading (2)", {
|
||||
message: "the loading indicator should indicate 2 requests in progress.",
|
||||
});
|
||||
rpcBus.trigger("RPC:REQUEST", payload(3));
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveText("Loading (3)", {
|
||||
message: "the loading indicator should indicate 3 requests in progress.",
|
||||
});
|
||||
rpcBus.trigger("RPC:RESPONSE", payload(1));
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveText("Loading (2)", {
|
||||
message: "the loading indicator should indicate 2 requests in progress.",
|
||||
});
|
||||
rpcBus.trigger("RPC:REQUEST", payload(4));
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveText("Loading (3)", {
|
||||
message: "the loading indicator should indicate 3 requests in progress.",
|
||||
});
|
||||
rpcBus.trigger("RPC:RESPONSE", payload(2));
|
||||
rpcBus.trigger("RPC:RESPONSE", payload(3));
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveText("Loading (1)", {
|
||||
message: "the loading indicator should indicate 1 request in progress.",
|
||||
});
|
||||
rpcBus.trigger("RPC:RESPONSE", payload(4));
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveCount(0, {
|
||||
message: "the loading indicator should not be displayed",
|
||||
});
|
||||
});
|
||||
|
||||
test("loading indicator is not displayed immediately", async () => {
|
||||
await mountWithCleanup(LoadingIndicator, { noMainContainer: true });
|
||||
const ui = getService("ui");
|
||||
ui.bus.addEventListener("BLOCK", () => {
|
||||
expect.step("block");
|
||||
});
|
||||
ui.bus.addEventListener("UNBLOCK", () => {
|
||||
expect.step("unblock");
|
||||
});
|
||||
rpcBus.trigger("RPC:REQUEST", payload(1));
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveCount(0);
|
||||
await advanceTime(400);
|
||||
expect(".o_loading_indicator").toHaveCount(1);
|
||||
rpcBus.trigger("RPC:RESPONSE", payload(1));
|
||||
await animationFrame();
|
||||
expect(".o_loading_indicator").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser as originalBrowser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { LoadingIndicator } from "@web/webclient/loading_indicator/loading_indicator";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import {
|
||||
getFixture,
|
||||
mockTimeout,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
|
||||
QUnit.module("LoadingIndicator", {
|
||||
async beforeEach() {
|
||||
target = getFixture();
|
||||
serviceRegistry.add("ui", uiService);
|
||||
patchWithCleanup(originalBrowser, {
|
||||
setTimeout: async (f) => {
|
||||
await Promise.resolve();
|
||||
f();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("displays the loading indicator in non debug mode", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
await mount(LoadingIndicator, target, { env });
|
||||
let loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed");
|
||||
env.bus.trigger("RPC:REQUEST", 1);
|
||||
await nextTick();
|
||||
loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.notStrictEqual(loadingIndicator, null, "the loading indicator should be displayed");
|
||||
assert.strictEqual(
|
||||
loadingIndicator.textContent,
|
||||
"Loading",
|
||||
"the loading indicator should display 'Loading'"
|
||||
);
|
||||
env.bus.trigger("RPC:RESPONSE", 1);
|
||||
await nextTick();
|
||||
loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed");
|
||||
});
|
||||
|
||||
QUnit.test("displays the loading indicator for one rpc in debug mode", async (assert) => {
|
||||
patchWithCleanup(odoo, { debug: "1" });
|
||||
const env = await makeTestEnv();
|
||||
await mount(LoadingIndicator, target, { env });
|
||||
let loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed");
|
||||
env.bus.trigger("RPC:REQUEST", 1);
|
||||
await nextTick();
|
||||
loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.notStrictEqual(loadingIndicator, null, "the loading indicator should be displayed");
|
||||
assert.strictEqual(
|
||||
loadingIndicator.textContent,
|
||||
"Loading (1)",
|
||||
"the loading indicator should indicate 1 request in progress"
|
||||
);
|
||||
env.bus.trigger("RPC:RESPONSE", 1);
|
||||
await nextTick();
|
||||
loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed");
|
||||
});
|
||||
|
||||
QUnit.test("displays the loading indicator for multi rpc in debug mode", async (assert) => {
|
||||
patchWithCleanup(odoo, { debug: "1" });
|
||||
const env = await makeTestEnv();
|
||||
await mount(LoadingIndicator, target, { env });
|
||||
let loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed");
|
||||
env.bus.trigger("RPC:REQUEST", 1);
|
||||
env.bus.trigger("RPC:REQUEST", 2);
|
||||
await nextTick();
|
||||
loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.notStrictEqual(loadingIndicator, null, "the loading indicator should be displayed");
|
||||
assert.strictEqual(
|
||||
loadingIndicator.textContent,
|
||||
"Loading (2)",
|
||||
"the loading indicator should indicate 2 requests in progress."
|
||||
);
|
||||
env.bus.trigger("RPC:REQUEST", 3);
|
||||
await nextTick();
|
||||
loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.strictEqual(
|
||||
loadingIndicator.textContent,
|
||||
"Loading (3)",
|
||||
"the loading indicator should indicate 3 requests in progress."
|
||||
);
|
||||
env.bus.trigger("RPC:RESPONSE", 1);
|
||||
await nextTick();
|
||||
loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.strictEqual(
|
||||
loadingIndicator.textContent,
|
||||
"Loading (2)",
|
||||
"the loading indicator should indicate 2 requests in progress."
|
||||
);
|
||||
env.bus.trigger("RPC:REQUEST", 4);
|
||||
await nextTick();
|
||||
loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.strictEqual(
|
||||
loadingIndicator.textContent,
|
||||
"Loading (3)",
|
||||
"the loading indicator should indicate 3 requests in progress."
|
||||
);
|
||||
env.bus.trigger("RPC:RESPONSE", 2);
|
||||
env.bus.trigger("RPC:RESPONSE", 3);
|
||||
await nextTick();
|
||||
loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.strictEqual(
|
||||
loadingIndicator.textContent,
|
||||
"Loading (1)",
|
||||
"the loading indicator should indicate 1 request in progress."
|
||||
);
|
||||
env.bus.trigger("RPC:RESPONSE", 4);
|
||||
await nextTick();
|
||||
loadingIndicator = target.querySelector(".o_loading_indicator");
|
||||
assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed");
|
||||
});
|
||||
|
||||
QUnit.test("loading indicator blocks UI", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
patch(originalBrowser, "mock.settimeout", {
|
||||
setTimeout: async (callback, delay) => {
|
||||
assert.step(`set timeout ${delay}`);
|
||||
await Promise.resolve();
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const ui = env.services.ui;
|
||||
ui.bus.addEventListener("BLOCK", () => {
|
||||
assert.step("block");
|
||||
});
|
||||
ui.bus.addEventListener("UNBLOCK", () => {
|
||||
assert.step("unblock");
|
||||
});
|
||||
await mount(LoadingIndicator, target, { env });
|
||||
env.bus.trigger("RPC:REQUEST", 1);
|
||||
await nextTick();
|
||||
env.bus.trigger("RPC:RESPONSE", 1);
|
||||
await nextTick();
|
||||
assert.verifySteps(["set timeout 250", "set timeout 3000", "block", "unblock"]);
|
||||
unpatch(originalBrowser, "mock.settimeout");
|
||||
});
|
||||
|
||||
QUnit.test("loading indicator doesn't unblock ui if it didn't block it", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
const ui = env.services.ui;
|
||||
ui.bus.on("BLOCK", null, () => {
|
||||
assert.step("block");
|
||||
});
|
||||
ui.bus.on("UNBLOCK", null, () => {
|
||||
assert.step("unblock");
|
||||
});
|
||||
await mount(LoadingIndicator, target, { env });
|
||||
env.bus.trigger("RPC:REQUEST", 1);
|
||||
execRegisteredTimeouts();
|
||||
env.bus.trigger("RPC:RESPONSE", 1);
|
||||
assert.verifySteps(["block", "unblock"]);
|
||||
env.bus.trigger("RPC:REQUEST", 2);
|
||||
env.bus.trigger("RPC:RESPONSE", 2);
|
||||
execRegisteredTimeouts();
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("loading indicator is not displayed immediately", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
const { advanceTime } = mockTimeout();
|
||||
|
||||
const ui = env.services.ui;
|
||||
ui.bus.addEventListener("BLOCK", () => {
|
||||
assert.step("block");
|
||||
});
|
||||
ui.bus.addEventListener("UNBLOCK", () => {
|
||||
assert.step("unblock");
|
||||
});
|
||||
await mount(LoadingIndicator, target, { env });
|
||||
env.bus.trigger("RPC:REQUEST", 1);
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_loading_indicator");
|
||||
await advanceTime(400);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_loading_indicator");
|
||||
|
||||
env.bus.trigger("RPC:RESPONSE", 1);
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_loading_indicator");
|
||||
});
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { redirect } from "@web/core/utils/urls";
|
||||
import {
|
||||
defineActions,
|
||||
defineMenus,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountWebClient,
|
||||
onRpc,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { Deferred } from "@odoo/hoot-mock";
|
||||
import { animationFrame } from "@odoo/hoot-dom";
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 666,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
views: [[false, "kanban"]],
|
||||
},
|
||||
]);
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
foo = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "First record", foo: "yop" },
|
||||
{ id: 2, name: "Second record", foo: "blip" },
|
||||
{ id: 3, name: "Third record", foo: "gnap" },
|
||||
{ id: 4, name: "Fourth record", foo: "plop" },
|
||||
{ id: 5, name: "Fifth record", foo: "zoup" },
|
||||
];
|
||||
_views = {
|
||||
kanban: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
list: `<list><field name="foo"/></list>`,
|
||||
form: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
<field name="foo"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
search: `<search><field name="foo" string="Foo"/></search>`,
|
||||
};
|
||||
}
|
||||
const { ResCompany, ResPartner, ResUsers } = webModels;
|
||||
defineModels([Partner, ResCompany, ResPartner, ResUsers]);
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
children: [
|
||||
{ id: 2, name: "Test1", appID: 1, actionID: 666 },
|
||||
{ id: 3, name: "Test2", appID: 1, actionID: 666 },
|
||||
],
|
||||
name: "App1",
|
||||
appID: 1,
|
||||
actionID: 666,
|
||||
},
|
||||
]);
|
||||
|
||||
test.tags("desktop");
|
||||
test(`use stored menus, and don't update on load_menus return (if identical)`, async () => {
|
||||
const def = new Deferred();
|
||||
redirect("/odoo/action-666");
|
||||
onRpc("/web/webclient/load_menus", () => def);
|
||||
|
||||
// Initial Stored values
|
||||
browser.localStorage.webclient_menus_version =
|
||||
"05500d71e084497829aa807e3caa2e7e9782ff702c15b2f57f87f2d64d049bd0";
|
||||
browser.localStorage.webclient_menus = JSON.stringify({
|
||||
1: { appID: 1, children: [2, 3], name: "App1", id: 1, actionID: 666 },
|
||||
2: { appID: 1, children: [], name: "Test1", id: 2, actionID: 666 },
|
||||
3: { appID: 1, children: [], name: "Test2", id: 3, actionID: 666 },
|
||||
root: { id: "root", name: "root", appID: "root", children: [1] },
|
||||
});
|
||||
|
||||
const webClient = await mountWebClient();
|
||||
webClient.env.bus.addEventListener("MENUS:APP-CHANGED", () => expect.step("Don't Update"));
|
||||
expect(`.o_menu_brand`).toHaveText("App1");
|
||||
expect(browser.sessionStorage.getItem("menu_id")).toBe("1");
|
||||
expect(".o_menu_sections").toHaveText("Test1\nTest2");
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_menu_sections").toHaveText("Test1\nTest2");
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`use stored menus, and update on load_menus return`, async () => {
|
||||
const def = new Deferred();
|
||||
redirect("/odoo/action-666");
|
||||
onRpc("/web/webclient/load_menus", () => def);
|
||||
|
||||
// Initial Stored values
|
||||
// There is no menu "Test2" in the initial values
|
||||
browser.localStorage.webclient_menus_version =
|
||||
"05500d71e084497829aa807e3caa2e7e9782ff702c15b2f57f87f2d64d049bd0";
|
||||
browser.localStorage.webclient_menus = JSON.stringify({
|
||||
1: { id: 1, children: [2], name: "App1", appID: 1, actionID: 666 },
|
||||
2: { id: 2, children: [], name: "Test1", appID: 1, actionID: 666 },
|
||||
root: { id: "root", children: [1], name: "root", appID: "root" },
|
||||
});
|
||||
|
||||
const webClient = await mountWebClient();
|
||||
webClient.env.bus.addEventListener("MENUS:APP-CHANGED", () => expect.step("Update Menus"));
|
||||
expect(`.o_menu_brand`).toHaveText("App1");
|
||||
expect(browser.sessionStorage.getItem("menu_id")).toBe("1");
|
||||
expect(".o_menu_sections").toHaveText("Test1");
|
||||
expect.verifySteps([]);
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_menu_sections").toHaveText("Test1\nTest2");
|
||||
expect(JSON.parse(browser.localStorage.webclient_menus)).toEqual({
|
||||
1: {
|
||||
actionID: 666,
|
||||
appID: 1,
|
||||
children: [2, 3],
|
||||
id: 1,
|
||||
name: "App1",
|
||||
},
|
||||
2: {
|
||||
actionID: 666,
|
||||
appID: 1,
|
||||
children: [],
|
||||
id: 2,
|
||||
name: "Test1",
|
||||
},
|
||||
3: {
|
||||
actionID: 666,
|
||||
appID: 1,
|
||||
children: [],
|
||||
id: 3,
|
||||
name: "Test2",
|
||||
},
|
||||
root: {
|
||||
appID: "root",
|
||||
children: [1],
|
||||
id: "root",
|
||||
name: "root",
|
||||
},
|
||||
});
|
||||
expect.verifySteps(["Update Menus"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { click, queryAll } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineMenus,
|
||||
getService,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
useTestClientAction,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { config as transitionConfig } from "@web/core/transition";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
describe.current.tags("mobile");
|
||||
|
||||
beforeEach(() => {
|
||||
const testAction = useTestClientAction();
|
||||
defineActions([
|
||||
{ ...testAction, id: 1001, params: { description: "Id 1" } },
|
||||
{ ...testAction, id: 1002, params: { description: "Info" } },
|
||||
{ ...testAction, id: 1003, params: { description: "Report" } },
|
||||
]);
|
||||
defineMenus([
|
||||
{ id: 0 }, // prevents auto-loading the first action
|
||||
{ id: 1, name: "App1", actionID: 1001, xmlid: "menu_1" },
|
||||
]);
|
||||
patchWithCleanup(transitionConfig, { disabled: true });
|
||||
});
|
||||
|
||||
test("Burger menu can be opened and closed", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await contains(".o_mobile_menu_toggle", { root: document.body }).click();
|
||||
expect(queryAll(".o_burger_menu", { root: document.body })).toHaveCount(1);
|
||||
await contains(".o_sidebar_close", { root: document.body }).click();
|
||||
expect(queryAll(".o_burger_menu", { root: document.body })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Burger Menu on an App", async () => {
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
children: [
|
||||
{
|
||||
id: 99,
|
||||
name: "SubMenu",
|
||||
appID: 1,
|
||||
actionID: 1002,
|
||||
xmlid: "",
|
||||
webIconData: undefined,
|
||||
webIcon: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
await mountWithCleanup(WebClient);
|
||||
await contains("a.o_menu_toggle", { root: document.body }).click();
|
||||
await contains(".o_sidebar_topbar a.btn-primary", { root: document.body }).click();
|
||||
await contains(".o_burger_menu_content li:nth-of-type(2)", { root: document.body }).click();
|
||||
|
||||
expect(queryAll(".o_burger_menu_content", { root: document.body })).toHaveCount(0);
|
||||
|
||||
await contains("a.o_menu_toggle", { root: document.body }).click();
|
||||
|
||||
expect(
|
||||
queryAll(".o_app_menu_sidebar nav.o_burger_menu_content", { root: document.body })
|
||||
).toHaveText("App1\nSubMenu");
|
||||
await click(".modal-backdrop", { root: document.body });
|
||||
await contains(".o_mobile_menu_toggle", { root: document.body }).click();
|
||||
expect(queryAll(".o_burger_menu", { root: document.body })).toHaveCount(1);
|
||||
expect(
|
||||
queryAll(".o_burger_menu nav.o_burger_menu_content", { root: document.body })
|
||||
).toHaveCount(1);
|
||||
|
||||
expect(queryAll(".o_burger_menu_content", { root: document.body })).toHaveClass(
|
||||
"o_burger_menu_app"
|
||||
);
|
||||
|
||||
await click(".o_sidebar_topbar", { root: document.body });
|
||||
|
||||
expect(queryAll(".o_burger_menu_content", { root: document.body })).not.toHaveClass(
|
||||
"o_burger_menu_dark"
|
||||
);
|
||||
|
||||
await click(".o_sidebar_topbar", { root: document.body });
|
||||
|
||||
expect(queryAll(".o_burger_menu_content", { root: document.body })).toHaveClass(
|
||||
"o_burger_menu_app"
|
||||
);
|
||||
});
|
||||
|
||||
test("Burger Menu on an App without SubMenu", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await contains("a.o_menu_toggle", { root: document.body }).click();
|
||||
await contains(".o_sidebar_topbar a.btn-primary", { root: document.body }).click();
|
||||
await contains(".o_burger_menu_content li:nth-of-type(2)", { root: document.body }).click();
|
||||
|
||||
expect(queryAll(".o_burger_menu", { root: document.body })).toHaveCount(0);
|
||||
|
||||
await contains(".o_mobile_menu_toggle", { root: document.body }).click();
|
||||
expect(queryAll(".o_burger_menu", { root: document.body })).toHaveCount(1);
|
||||
expect(queryAll(".o_user_menu_mobile", { root: document.body })).toHaveCount(1);
|
||||
await click(".o_sidebar_close", { root: document.body });
|
||||
expect(queryAll(".o_burger_menu")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Burger menu closes when an action is requested", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
await contains(".o_mobile_menu_toggle", { root: document.body }).click();
|
||||
expect(queryAll(".o_burger_menu", { root: document.body })).toHaveCount(1);
|
||||
expect(queryAll(".test_client_action", { root: document.body })).toHaveCount(0);
|
||||
await getService("action").doAction(1001);
|
||||
expect(queryAll(".o_burger_menu", { root: document.body })).toHaveCount(0);
|
||||
expect(queryAll(".o_kanban_view", { root: document.body })).toHaveCount(0);
|
||||
expect(queryAll(".test_client_action", { root: document.body })).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Burger menu closes when click on menu item", async () => {
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
children: [
|
||||
{
|
||||
id: 99,
|
||||
name: "SubMenu",
|
||||
actionID: 1002,
|
||||
xmlid: "",
|
||||
webIconData: undefined,
|
||||
webIcon: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 2, name: "App2", actionID: 1003, xmlid: "menu_2" },
|
||||
]);
|
||||
await mountWithCleanup(WebClient);
|
||||
getService("menu").setCurrentMenu(2);
|
||||
|
||||
await contains(".o_menu_toggle", { root: document.body }).click();
|
||||
expect(
|
||||
queryAll(".o_app_menu_sidebar nav.o_burger_menu_content", { root: document.body })
|
||||
).toHaveText("App2");
|
||||
|
||||
await contains(".oi-apps", { root: document.body }).click();
|
||||
expect(
|
||||
queryAll(".o_app_menu_sidebar nav.o_burger_menu_content", { root: document.body })
|
||||
).toHaveText("App0\nApp1\nApp2");
|
||||
|
||||
await contains(".o_burger_menu_app > ul > li:nth-of-type(2)", { root: document.body }).click();
|
||||
expect(queryAll(".o_burger_menu_app")).toHaveCount(0);
|
||||
|
||||
await contains(".o_menu_toggle", { root: document.body }).click();
|
||||
expect(queryAll(".o_burger_menu_app", { root: document.body })).toHaveCount(1);
|
||||
expect(
|
||||
queryAll(".o_app_menu_sidebar nav.o_burger_menu_content", { root: document.body })
|
||||
).toHaveText("App1\nSubMenu");
|
||||
|
||||
await click(".o_burger_menu_content li:nth-of-type(1)", { root: document.body });
|
||||
// click
|
||||
await animationFrame();
|
||||
// action
|
||||
await animationFrame();
|
||||
// close burger
|
||||
await animationFrame();
|
||||
expect(queryAll(".o_burger_menu_content", { root: document.body })).toHaveCount(0);
|
||||
expect(queryAll(".test_client_action", { root: document.body })).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Burger menu closes when click on user menu item", async () => {
|
||||
registry.category("user_menuitems").add("ring_item", () => ({
|
||||
type: "item",
|
||||
id: "ring",
|
||||
description: "Ring",
|
||||
callback: () => {
|
||||
expect.step("callback ring_item");
|
||||
},
|
||||
sequence: 5,
|
||||
}));
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
expect(queryAll(".o_burger_menu", { root: document.body })).toHaveCount(0);
|
||||
|
||||
await click(queryAll(".o_mobile_menu_toggle", { root: document.body }));
|
||||
await animationFrame();
|
||||
expect(queryAll(".o_burger_menu", { root: document.body })).toHaveCount(1);
|
||||
|
||||
await click(queryAll(".o_burger_menu .o_user_menu_mobile a", { root: document.body }));
|
||||
await animationFrame();
|
||||
expect(queryAll(".o_burger_menu", { root: document.body })).toHaveCount(0);
|
||||
expect.verifySteps(["callback ring_item"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import { BurgerUserMenu } from "@web/webclient/burger_menu/burger_user_menu/burger_user_menu";
|
||||
import { preferencesItem } from "@web/webclient/user_menu/user_menu_items";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import {
|
||||
clearRegistry,
|
||||
mockService,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { click, queryAll, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
const userMenuRegistry = registry.category("user_menuitems");
|
||||
|
||||
beforeEach(() => clearRegistry(userMenuRegistry));
|
||||
|
||||
test.tags("mobile");
|
||||
test("can be rendered", async () => {
|
||||
userMenuRegistry.add("bad_item", () => ({
|
||||
type: "item",
|
||||
id: "bad",
|
||||
description: "Bad",
|
||||
callback: () => {
|
||||
expect.step("callback bad_item");
|
||||
},
|
||||
sequence: 10,
|
||||
}));
|
||||
userMenuRegistry.add("ring_item", () => ({
|
||||
type: "item",
|
||||
id: "ring",
|
||||
description: "Ring",
|
||||
callback: () => {
|
||||
expect.step("callback ring_item");
|
||||
},
|
||||
sequence: 5,
|
||||
}));
|
||||
userMenuRegistry.add("frodo_item", () => ({
|
||||
type: "switch",
|
||||
id: "frodo",
|
||||
description: "Frodo",
|
||||
callback: () => {
|
||||
expect.step("callback frodo_item");
|
||||
},
|
||||
sequence: 11,
|
||||
}));
|
||||
userMenuRegistry.add("separator", () => ({
|
||||
type: "separator",
|
||||
sequence: 15,
|
||||
}));
|
||||
userMenuRegistry.add("invisible_item", () => ({
|
||||
type: "item",
|
||||
id: "hidden",
|
||||
description: "Hidden Power",
|
||||
callback: () => {
|
||||
expect.step("callback hidden_item");
|
||||
},
|
||||
sequence: 5,
|
||||
hide: true,
|
||||
}));
|
||||
userMenuRegistry.add("eye_item", () => ({
|
||||
type: "item",
|
||||
id: "eye",
|
||||
description: "Eye",
|
||||
callback: () => {
|
||||
expect.step("callback eye_item");
|
||||
},
|
||||
}));
|
||||
userMenuRegistry.add("html_item", () => ({
|
||||
type: "item",
|
||||
id: "html",
|
||||
description: markup`<div>HTML<i class="fa fa-check px-2"></i></div>`,
|
||||
callback: () => {
|
||||
expect.step("callback html_item");
|
||||
},
|
||||
sequence: 20,
|
||||
}));
|
||||
await mountWithCleanup(BurgerUserMenu);
|
||||
expect("a").toHaveCount(4);
|
||||
expect(".form-switch input.form-check-input").toHaveCount(1);
|
||||
expect("hr").toHaveCount(1);
|
||||
expect(queryAllTexts("a, .form-switch")).toEqual(["Ring", "Bad", "Frodo", "HTML", "Eye"]);
|
||||
for (const item of queryAll("a, .form-switch")) {
|
||||
await click(item);
|
||||
}
|
||||
expect.verifySteps([
|
||||
"callback ring_item",
|
||||
"callback bad_item",
|
||||
"callback frodo_item",
|
||||
"callback html_item",
|
||||
"callback eye_item",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("can execute the callback of settings", async () => {
|
||||
onRpc("action_get", () => ({
|
||||
name: "Change My Preferences",
|
||||
res_id: 0,
|
||||
}));
|
||||
mockService("action", {
|
||||
async doAction(actionId) {
|
||||
expect.step(actionId.res_id);
|
||||
expect.step(actionId.name);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
userMenuRegistry.add("preferences", preferencesItem);
|
||||
await mountWithCleanup(BurgerUserMenu);
|
||||
expect("a").toHaveCount(1);
|
||||
expect("a").toHaveText("My Preferences");
|
||||
await click("a");
|
||||
await expect.waitForSteps([7, "Change My Preferences"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { cookie } from "@web/core/browser/cookie";
|
||||
import { user } from "@web/core/user";
|
||||
import { MobileSwitchCompanyMenu } from "@web/webclient/burger_menu/mobile_switch_company_menu/mobile_switch_company_menu";
|
||||
|
||||
const ORIGINAL_TOGGLE_DELAY = MobileSwitchCompanyMenu.toggleDelay;
|
||||
|
||||
async function createSwitchCompanyMenu(options = { toggleDelay: 0 }) {
|
||||
patchWithCleanup(MobileSwitchCompanyMenu, { toggleDelay: options.toggleDelay });
|
||||
await mountWithCleanup(MobileSwitchCompanyMenu);
|
||||
}
|
||||
|
||||
function patchUserActiveCompanies(cids) {
|
||||
patchWithCleanup(
|
||||
user.activeCompanies,
|
||||
cids.map((cid) => serverState.companies.find((company) => company.id === cid))
|
||||
);
|
||||
}
|
||||
|
||||
describe.current.tags("mobile");
|
||||
|
||||
const clickConfirm = () => contains(".o_switch_company_menu_buttons button:first").click();
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
*/
|
||||
const toggleCompany = async (index) =>
|
||||
contains(`[data-company-id] [role=menuitemcheckbox]:eq(${index})`).click();
|
||||
|
||||
beforeEach(() => {
|
||||
serverState.companies = [
|
||||
{ id: 1, name: "Hermit", parent_id: false, child_ids: [] },
|
||||
{ id: 2, name: "Herman's", parent_id: false, child_ids: [] },
|
||||
{ id: 3, name: "Heroes TM", parent_id: false, child_ids: [] },
|
||||
];
|
||||
});
|
||||
|
||||
test("basic rendering", async () => {
|
||||
await mountWithCleanup(MobileSwitchCompanyMenu);
|
||||
|
||||
expect(".o_burger_menu_companies").toHaveProperty("tagName", "DIV");
|
||||
expect(".o_burger_menu_companies").toHaveClass("o_burger_menu_companies");
|
||||
expect("[data-company-id]").toHaveCount(3);
|
||||
expect(".log_into").toHaveCount(3);
|
||||
expect(".fa-check-square").toHaveCount(1);
|
||||
expect(".fa-square-o").toHaveCount(2);
|
||||
|
||||
expect(".o_switch_company_item:eq(0)").toHaveText("Hermit");
|
||||
expect(".o_switch_company_item:eq(0)").toHaveClass("alert-secondary");
|
||||
expect(".o_switch_company_item:eq(1)").toHaveText("Herman's");
|
||||
expect(".o_switch_company_item:eq(2)").toHaveText("Heroes TM");
|
||||
|
||||
expect(".o_switch_company_item i:eq(0)").toHaveClass("fa-check-square");
|
||||
expect(".o_switch_company_item i:eq(1)").toHaveClass("fa-square-o");
|
||||
expect(".o_switch_company_item i:eq(2)").toHaveClass("fa-square-o");
|
||||
|
||||
expect(".o_burger_menu_companies").toHaveText("Companies\nHermit\nHerman's\nHeroes TM");
|
||||
});
|
||||
|
||||
test("companies can be toggled: toggle a second company", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([1]);
|
||||
expect(user.activeCompany.id).toBe(1);
|
||||
expect("[data-company-id]").toHaveCount(3);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(2);
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [x] Company 2 -> toggle
|
||||
* [ ] Company 3
|
||||
*/
|
||||
await toggleCompany(1);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(2);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(1);
|
||||
await clickConfirm();
|
||||
expect(cookie.get("cids")).toEqual("1-2");
|
||||
});
|
||||
|
||||
test("can toggle multiple companies at once", async () => {
|
||||
await createSwitchCompanyMenu({ toggleDelay: ORIGINAL_TOGGLE_DELAY });
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([1]);
|
||||
expect(user.activeCompany.id).toBe(1);
|
||||
expect("[data-company-id]").toHaveCount(3);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(2);
|
||||
|
||||
/**
|
||||
* [ ] **Company 1** -> toggle all
|
||||
* [x] Company 2 -> toggle all
|
||||
* [x] Company 3 -> toggle all
|
||||
*/
|
||||
await toggleCompany(0);
|
||||
await toggleCompany(1);
|
||||
await toggleCompany(2);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(2);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(1);
|
||||
|
||||
expect.verifySteps([]);
|
||||
await clickConfirm();
|
||||
expect(cookie.get("cids")).toEqual("2-3");
|
||||
});
|
||||
|
||||
test("single company selected: toggling it off will keep it", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
await runAllTimers();
|
||||
expect(cookie.get("cids")).toBe("1");
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([1]);
|
||||
expect(user.activeCompany.id).toBe(1);
|
||||
expect("[data-company-id]").toHaveCount(3);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(2);
|
||||
|
||||
/**
|
||||
* [ ] **Company 1** -> toggle off
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
await toggleCompany(0);
|
||||
await clickConfirm();
|
||||
expect(cookie.get("cids")).toEqual("1");
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([1]);
|
||||
expect(user.activeCompany.id).toBe(1);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("single company mode: companies can be logged in", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([1]);
|
||||
expect(user.activeCompany.id).toBe(1);
|
||||
expect("[data-company-id]").toHaveCount(3);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(2);
|
||||
|
||||
/**
|
||||
* [ ] **Company 1**
|
||||
* [x] Company 2 -> log into
|
||||
* [ ] Company 3
|
||||
*/
|
||||
await contains(".log_into:eq(1)").click();
|
||||
expect(cookie.get("cids")).toEqual("2");
|
||||
});
|
||||
|
||||
test("multi company mode: log into a non selected company", async () => {
|
||||
patchUserActiveCompanies([3, 1]);
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [x] Company 1
|
||||
* [ ] Company 2
|
||||
* [x] **Company 3**
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([3, 1]);
|
||||
expect(user.activeCompany.id).toBe(3);
|
||||
expect("[data-company-id]").toHaveCount(3);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(2);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(1);
|
||||
|
||||
/**
|
||||
* [x] Company 1
|
||||
* [ ] Company 2 -> log into
|
||||
* [x] **Company 3**
|
||||
*/
|
||||
await contains(".log_into:eq(1)").click();
|
||||
expect(cookie.get("cids")).toEqual("2-1-3"); // 1-3 in that order, they are sorted
|
||||
});
|
||||
|
||||
test("multi company mode: log into an already selected company", async () => {
|
||||
patchUserActiveCompanies([2, 3]);
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [ ] Company 1
|
||||
* [x] **Company 2**
|
||||
* [x] Company 3
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([2, 3]);
|
||||
expect(user.activeCompany.id).toBe(2);
|
||||
expect("[data-company-id]").toHaveCount(3);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(2);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(1);
|
||||
|
||||
/**
|
||||
* [ ] Company 1
|
||||
* [x] **Company 2**
|
||||
* [x] Company 3 -> log into
|
||||
*/
|
||||
await contains(".log_into:eq(2)").click();
|
||||
expect(cookie.get("cids")).toEqual("3-2");
|
||||
});
|
||||
|
||||
test("companies can be logged in even if some toggled within delay", async () => {
|
||||
await createSwitchCompanyMenu({ toggleDelay: ORIGINAL_TOGGLE_DELAY });
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([1]);
|
||||
expect(user.activeCompany.id).toBe(1);
|
||||
expect("[data-company-id]").toHaveCount(3);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(2);
|
||||
|
||||
/**
|
||||
* [ ] **Company 1** -> toggled
|
||||
* [ ] Company 2 -> logged in
|
||||
* [ ] Company 3 -> toggled
|
||||
*/
|
||||
await contains("[data-company-id] [role=menuitemcheckbox]:eq(2)").click();
|
||||
await contains("[data-company-id] [role=menuitemcheckbox]:eq(0)").click();
|
||||
await contains(".log_into:eq(1)").click();
|
||||
expect(cookie.get("cids")).toEqual("2");
|
||||
});
|
||||
|
||||
test("show confirm and reset buttons only when selection has changed", async () => {
|
||||
await mountWithCleanup(MobileSwitchCompanyMenu);
|
||||
expect(".o_switch_company_menu_buttons").toHaveCount(0);
|
||||
await toggleCompany(1);
|
||||
expect(".o_switch_company_menu_buttons button").toHaveCount(2);
|
||||
await toggleCompany(1);
|
||||
expect(".o_switch_company_menu_buttons").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("No collapse and no search input when less that 10 companies", async () => {
|
||||
await mountWithCleanup(MobileSwitchCompanyMenu);
|
||||
expect(".o_burger_menu_companies .fa-caret-right").toHaveCount(0);
|
||||
expect(".o_burger_menu_companies .visually-hidden input").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Show search input when more that 10 companies & search filters items but ignore case and spaces", async () => {
|
||||
serverState.companies = [
|
||||
{ id: 3, name: "Hermit", sequence: 1, parent_id: false, child_ids: [] },
|
||||
{ id: 2, name: "Herman's", sequence: 2, parent_id: false, child_ids: [] },
|
||||
{ id: 1, name: "Heroes TM", sequence: 3, parent_id: false, child_ids: [4, 5] },
|
||||
{ id: 4, name: "Hercules", sequence: 4, parent_id: 1, child_ids: [] },
|
||||
{ id: 5, name: "Hulk", sequence: 5, parent_id: 1, child_ids: [] },
|
||||
{ id: 6, name: "Random Company a", sequence: 6, parent_id: false, child_ids: [7, 8] },
|
||||
{ id: 7, name: "Random Company aa", sequence: 7, parent_id: 6, child_ids: [] },
|
||||
{ id: 8, name: "Random Company ab", sequence: 8, parent_id: 6, child_ids: [] },
|
||||
{ id: 9, name: "Random d", sequence: 9, parent_id: false, child_ids: [] },
|
||||
{ id: 10, name: "Random e", sequence: 10, parent_id: false, child_ids: [] },
|
||||
];
|
||||
await createSwitchCompanyMenu();
|
||||
await contains(".o_burger_menu_companies > div").click();
|
||||
expect(".o_burger_menu_companies input").toHaveCount(1);
|
||||
expect(".o_burger_menu_companies input").not.toBeFocused();
|
||||
|
||||
expect(".o_switch_company_item").toHaveCount(10);
|
||||
contains(".o_burger_menu_companies input").edit("omcom");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_switch_company_item").toHaveCount(3);
|
||||
expect(queryAllTexts(".o_switch_company_item.o-navigable")).toEqual([
|
||||
"Random Company a",
|
||||
"Random Company aa",
|
||||
"Random Company ab",
|
||||
]);
|
||||
});
|
||||
|
|
@ -1,319 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
mount,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { MobileSwitchCompanyMenu } from "@web/webclient/burger_menu/mobile_switch_company_menu/mobile_switch_company_menu";
|
||||
import { companyService } from "@web/webclient/company_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { session } from "@web/session";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
let target;
|
||||
|
||||
const ORIGINAL_TOGGLE_DELAY = MobileSwitchCompanyMenu.toggleDelay;
|
||||
async function createSwitchCompanyMenu(routerParams = {}, toggleDelay = 0) {
|
||||
patchWithCleanup(MobileSwitchCompanyMenu, { toggleDelay });
|
||||
if (routerParams.onPushState) {
|
||||
const pushState = browser.history.pushState;
|
||||
patchWithCleanup(browser, {
|
||||
history: Object.assign({}, browser.history, {
|
||||
pushState(state, title, url) {
|
||||
pushState(...arguments);
|
||||
if (routerParams.onPushState) {
|
||||
routerParams.onPushState(url);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
const env = await makeTestEnv();
|
||||
const scMenu = await mount(MobileSwitchCompanyMenu, target, { env });
|
||||
return scMenu;
|
||||
}
|
||||
|
||||
QUnit.module("MobileSwitchCompanyMenu", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
patchWithCleanup(session.user_companies, {
|
||||
allowed_companies: {
|
||||
1: { id: 1, name: "Hermit" },
|
||||
2: { id: 2, name: "Herman's" },
|
||||
3: { id: 3, name: "Heroes TM" },
|
||||
},
|
||||
current_company: 1,
|
||||
});
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("company", companyService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
});
|
||||
|
||||
QUnit.test("basic rendering", async (assert) => {
|
||||
assert.expect(13);
|
||||
|
||||
await createSwitchCompanyMenu();
|
||||
const scMenuEl = target.querySelector(".o_burger_menu_companies");
|
||||
|
||||
assert.strictEqual(scMenuEl.tagName.toUpperCase(), "DIV");
|
||||
assert.hasClass(scMenuEl, "o_burger_menu_companies");
|
||||
assert.containsN(scMenuEl, ".toggle_company", 3);
|
||||
assert.containsN(scMenuEl, ".log_into", 3);
|
||||
assert.containsOnce(scMenuEl, ".fa-check-square");
|
||||
assert.containsN(scMenuEl, ".fa-square-o", 2);
|
||||
|
||||
assert.strictEqual(
|
||||
scMenuEl.querySelectorAll(".menu_companies_item")[0].textContent,
|
||||
"Hermit(current)"
|
||||
);
|
||||
assert.strictEqual(
|
||||
scMenuEl.querySelectorAll(".menu_companies_item")[1].textContent,
|
||||
"Herman's"
|
||||
);
|
||||
assert.strictEqual(
|
||||
scMenuEl.querySelectorAll(".menu_companies_item")[2].textContent,
|
||||
"Heroes TM"
|
||||
);
|
||||
|
||||
assert.hasClass(scMenuEl.querySelectorAll(".menu_companies_item i")[0], "fa-check-square");
|
||||
assert.hasClass(scMenuEl.querySelectorAll(".menu_companies_item i")[1], "fa-square-o");
|
||||
assert.hasClass(scMenuEl.querySelectorAll(".menu_companies_item i")[2], "fa-square-o");
|
||||
|
||||
assert.strictEqual(scMenuEl.textContent, "CompaniesHermit(current)Herman'sHeroes TM");
|
||||
});
|
||||
|
||||
QUnit.test("companies can be toggled: toggle a second company", async (assert) => {
|
||||
assert.expect(9);
|
||||
|
||||
const prom = makeDeferred();
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
prom.resolve();
|
||||
}
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState });
|
||||
const scMenuEl = target.querySelector(".o_burger_menu_companies");
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id]", 3);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-check-square", 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 2);
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [x] Company 2 -> toggle
|
||||
* [ ] Company 3
|
||||
*/
|
||||
await click(scMenuEl.querySelectorAll(".toggle_company")[1]);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-check-square", 2);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 1);
|
||||
await prom;
|
||||
assert.verifySteps(["cids=1%2C2"]);
|
||||
});
|
||||
|
||||
QUnit.test("can toggle multiple companies at once", async (assert) => {
|
||||
assert.expect(10);
|
||||
|
||||
const prom = makeDeferred();
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
prom.resolve();
|
||||
}
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState }, ORIGINAL_TOGGLE_DELAY);
|
||||
const scMenuEl = target.querySelector(".o_burger_menu_companies");
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id]", 3);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-check-square", 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 2);
|
||||
|
||||
/**
|
||||
* [ ] **Company 1** -> toggle all
|
||||
* [x] Company 2 -> toggle all
|
||||
* [x] Company 3 -> toggle all
|
||||
*/
|
||||
await click(scMenuEl.querySelectorAll(".toggle_company")[0]);
|
||||
await click(scMenuEl.querySelectorAll(".toggle_company")[1]);
|
||||
await click(scMenuEl.querySelectorAll(".toggle_company")[2]);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-check-square", 2);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 1);
|
||||
|
||||
assert.verifySteps([]);
|
||||
await prom; // await toggle promise
|
||||
assert.verifySteps(["cids=2%2C3"]);
|
||||
});
|
||||
|
||||
QUnit.test("single company selected: toggling it off will keep it", async (assert) => {
|
||||
assert.expect(11);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout(fn) {
|
||||
return fn(); // s.t. we can directly assert changes in the hash
|
||||
},
|
||||
});
|
||||
const scMenu = await createSwitchCompanyMenu();
|
||||
const scMenuEl = target.querySelector(".o_burger_menu_companies");
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.router.current.hash, { cids: 1 });
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id]", 3);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-check-square", 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 2);
|
||||
|
||||
/**
|
||||
* [ ] **Company 1** -> toggle off
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
await click(scMenuEl.querySelectorAll(".toggle_company")[0]);
|
||||
assert.deepEqual(scMenu.env.services.router.current.hash, { cids: 1 });
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-check-squarqe", 0);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 3);
|
||||
});
|
||||
|
||||
QUnit.test("single company mode: companies can be logged in", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
}
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState });
|
||||
const scMenuEl = target.querySelector(".o_burger_menu_companies");
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id]", 3);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-check-square", 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 2);
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2 -> log into
|
||||
* [ ] Company 3
|
||||
*/
|
||||
await click(scMenuEl.querySelectorAll(".log_into")[1]);
|
||||
assert.verifySteps(["cids=2"]);
|
||||
});
|
||||
|
||||
QUnit.test("multi company mode: log into a non selected company", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
}
|
||||
Object.assign(browser.location, { hash: "cids=3%2C1" });
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState });
|
||||
const scMenuEl = target.querySelector(".o_burger_menu_companies");
|
||||
|
||||
/**
|
||||
* [x] Company 1
|
||||
* [ ] Company 2
|
||||
* [x] **Company 3**
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3, 1]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
|
||||
assert.containsN(scMenuEl, "[data-company-id]", 3);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-check-square", 2);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 1);
|
||||
|
||||
/**
|
||||
* [x] Company 1
|
||||
* [ ] Company 2 -> log into
|
||||
* [x] **Company 3**
|
||||
*/
|
||||
await click(scMenuEl.querySelectorAll(".log_into")[1]);
|
||||
assert.verifySteps(["cids=2%2C3%2C1"]);
|
||||
});
|
||||
|
||||
QUnit.test("multi company mode: log into an already selected company", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
}
|
||||
Object.assign(browser.location, { hash: "cids=2%2C3" });
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState });
|
||||
const scMenuEl = target.querySelector(".o_burger_menu_companies");
|
||||
|
||||
/**
|
||||
* [ ] Company 1
|
||||
* [x] **Company 2**
|
||||
* [x] Company 3
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [2, 3]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 2);
|
||||
assert.containsN(scMenuEl, "[data-company-id]", 3);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-check-square", 2);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 1);
|
||||
|
||||
/**
|
||||
* [ ] Company 1
|
||||
* [x] **Company 2**
|
||||
* [x] Company 3 -> log into
|
||||
*/
|
||||
await click(scMenuEl.querySelectorAll(".log_into")[2]);
|
||||
assert.verifySteps(["cids=3%2C2"]);
|
||||
});
|
||||
|
||||
QUnit.test("companies can be logged in even if some toggled within delay", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
}
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState }, ORIGINAL_TOGGLE_DELAY);
|
||||
const scMenuEl = target.querySelector(".o_burger_menu_companies");
|
||||
|
||||
/**
|
||||
* [x] **Company 1**
|
||||
* [ ] Company 2
|
||||
* [ ] Company 3
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [1]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id]", 3);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-check-square", 1);
|
||||
assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 2);
|
||||
|
||||
/**
|
||||
* [ ] **Company 1** -> toggled
|
||||
* [ ] Company 2 -> logged in
|
||||
* [ ] Company 3 -> toggled
|
||||
*/
|
||||
await click(scMenuEl.querySelectorAll(".toggle_company")[2]);
|
||||
await click(scMenuEl.querySelectorAll(".toggle_company")[0]);
|
||||
await click(scMenuEl.querySelectorAll(".log_into")[1]);
|
||||
assert.verifySteps(["cids=2"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,500 @@
|
|||
import { beforeEach, destroy, expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryAllAttributes, queryAllTexts, resize } from "@odoo/hoot-dom";
|
||||
import { advanceTime, animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clearRegistry,
|
||||
contains,
|
||||
defineMenus,
|
||||
getService,
|
||||
makeMockEnv,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Component, onRendered, xml } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { NavBar } from "@web/webclient/navbar/navbar";
|
||||
|
||||
const systrayRegistry = registry.category("systray");
|
||||
|
||||
// Debounce time for Adaptation (`debouncedAdapt`) on resize event in navbar
|
||||
const waitNavbarAdaptation = () => advanceTime(500);
|
||||
|
||||
class MySystrayItem extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<li class="my-item">my item</li>`;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
systrayRegistry.add("addon.myitem", { Component: MySystrayItem });
|
||||
defineMenus([{ id: 1 }]);
|
||||
return () => {
|
||||
clearRegistry(systrayRegistry);
|
||||
};
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("can be rendered", async () => {
|
||||
await mountWithCleanup(NavBar);
|
||||
expect(".o_navbar_apps_menu button.dropdown-toggle").toHaveCount(1, {
|
||||
message: "1 apps menu toggler present",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("dropdown menu can be toggled", async () => {
|
||||
await mountWithCleanup(NavBar);
|
||||
await contains(".o_navbar_apps_menu button.dropdown-toggle").click();
|
||||
expect(".dropdown-menu").toHaveCount(1);
|
||||
await contains(".o_navbar_apps_menu button.dropdown-toggle").click();
|
||||
expect(".dropdown-menu").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("href attribute on apps menu items", async () => {
|
||||
defineMenus([{ id: 1, actionID: 339 }]);
|
||||
await mountWithCleanup(NavBar);
|
||||
await contains(".o_navbar_apps_menu button.dropdown-toggle").click();
|
||||
expect(".o-dropdown--menu .dropdown-item").toHaveAttribute("href", "/odoo/action-339");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("href attribute with path on apps menu items", async () => {
|
||||
defineMenus([{ id: 1, actionID: 339, actionPath: "my-path" }]);
|
||||
await mountWithCleanup(NavBar);
|
||||
await contains(".o_navbar_apps_menu button.dropdown-toggle").click();
|
||||
expect(".o-dropdown--menu .dropdown-item").toHaveAttribute("href", "/odoo/my-path");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("many sublevels in app menu items", async () => {
|
||||
defineMenus([
|
||||
{ id: 1, children: [2], name: "My app" },
|
||||
{ id: 2, children: [3], name: "My menu" },
|
||||
{ id: 3, children: [4], name: "My submenu 1" },
|
||||
{ id: 4, children: [5], name: "My submenu 2" },
|
||||
{ id: 5, children: [6], name: "My submenu 3" },
|
||||
{ id: 6, children: [7], name: "My submenu 4" },
|
||||
{ id: 7, children: [8], name: "My submenu 5" },
|
||||
{ id: 8, children: [9], name: "My submenu 6" },
|
||||
{ id: 9, name: "My submenu 7" },
|
||||
]);
|
||||
await makeMockEnv();
|
||||
getService("menu").setCurrentMenu(1);
|
||||
await mountWithCleanup(NavBar);
|
||||
await contains(".o_menu_sections .o-dropdown").click();
|
||||
expect(
|
||||
queryAll(".o-dropdown--menu > *").map((el) => ({
|
||||
text: el.innerText,
|
||||
paddingLeft: el.style.paddingLeft,
|
||||
tagName: el.tagName,
|
||||
}))
|
||||
).toEqual([
|
||||
{ text: "My submenu 1", paddingLeft: "20px", tagName: "DIV" },
|
||||
{ text: "My submenu 2", paddingLeft: "32px", tagName: "DIV" },
|
||||
{ text: "My submenu 3", paddingLeft: "44px", tagName: "DIV" },
|
||||
{ text: "My submenu 4", paddingLeft: "56px", tagName: "DIV" },
|
||||
{ text: "My submenu 5", paddingLeft: "68px", tagName: "DIV" },
|
||||
{ text: "My submenu 6", paddingLeft: "80px", tagName: "DIV" },
|
||||
{ text: "My submenu 7", paddingLeft: "92px", tagName: "A" },
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("data-menu-xmlid attribute on AppsMenu items", async () => {
|
||||
// Replace all default menus and setting new one
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
children: [
|
||||
{ id: 3, xmlid: "menu_3" },
|
||||
{ id: 4, xmlid: "menu_4", children: [{ id: 5, xmlid: "menu_5" }] },
|
||||
],
|
||||
xmlid: "wowl",
|
||||
},
|
||||
{ id: 2 },
|
||||
]);
|
||||
await mountWithCleanup(NavBar);
|
||||
|
||||
// check apps
|
||||
await contains(".o_navbar_apps_menu button.dropdown-toggle").click();
|
||||
expect(queryAllAttributes(".o-dropdown--menu a", "data-menu-xmlid")).toEqual(["wowl", null], {
|
||||
message:
|
||||
"menu items should have the correct data-menu-xmlid attribute (only the first is set)",
|
||||
});
|
||||
|
||||
// check menus
|
||||
getService("menu").setCurrentMenu(1);
|
||||
await animationFrame();
|
||||
expect(".o_menu_sections .dropdown-item[data-menu-xmlid=menu_3]").toHaveCount(1);
|
||||
|
||||
// check sub menus toggler
|
||||
expect(".o_menu_sections button.dropdown-toggle[data-menu-xmlid=menu_4]").toHaveCount(1);
|
||||
|
||||
// check sub menus
|
||||
await contains(".o_menu_sections .dropdown-toggle").click();
|
||||
expect(".o-dropdown--menu .dropdown-item[data-menu-xmlid=menu_5]").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("navbar can display current active app", async () => {
|
||||
await mountWithCleanup(NavBar);
|
||||
// Open apps menu
|
||||
await contains(".o_navbar_apps_menu button.dropdown-toggle").click();
|
||||
expect(".o-dropdown--menu .dropdown-item:not(.focus)").toHaveCount(1, {
|
||||
message:
|
||||
"should not show the current active app as the menus service has not loaded an app yet",
|
||||
});
|
||||
|
||||
// Activate an app
|
||||
getService("menu").setCurrentMenu(1);
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu .dropdown-item.focus").toHaveCount(1, {
|
||||
message: "should show the current active app",
|
||||
});
|
||||
});
|
||||
|
||||
test("navbar can display systray items", async () => {
|
||||
await mountWithCleanup(NavBar);
|
||||
expect("li.my-item").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("navbar can display systray items ordered based on their sequence", async () => {
|
||||
class MyItem1 extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<li class="my-item-1">my item 1</li>`;
|
||||
}
|
||||
|
||||
class MyItem2 extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<li class="my-item-2">my item 2</li>`;
|
||||
}
|
||||
|
||||
class MyItem3 extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<li class="my-item-3">my item 3</li>`;
|
||||
}
|
||||
|
||||
class MyItem4 extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<li class="my-item-4">my item 4</li>`;
|
||||
}
|
||||
|
||||
// Remove systray added by beforeEach
|
||||
systrayRegistry.remove("addon.myitem");
|
||||
|
||||
systrayRegistry.add("addon.myitem2", { Component: MyItem2 });
|
||||
systrayRegistry.add("addon.myitem1", { Component: MyItem1 }, { sequence: 0 });
|
||||
systrayRegistry.add("addon.myitem3", { Component: MyItem3 }, { sequence: 100 });
|
||||
systrayRegistry.add("addon.myitem4", { Component: MyItem4 });
|
||||
|
||||
await mountWithCleanup(NavBar);
|
||||
expect(".o_menu_systray:eq(0) li").toHaveCount(4, {
|
||||
message: "four systray items should be displayed",
|
||||
});
|
||||
expect(queryAllTexts(".o_menu_systray:eq(0) li")).toEqual([
|
||||
"my item 3",
|
||||
"my item 4",
|
||||
"my item 2",
|
||||
"my item 1",
|
||||
]);
|
||||
});
|
||||
|
||||
test("navbar updates after adding a systray item", async () => {
|
||||
class MyItem1 extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<li class="my-item-1">my item 1</li>`;
|
||||
}
|
||||
|
||||
// Remove systray added by beforeEach
|
||||
systrayRegistry.remove("addon.myitem");
|
||||
|
||||
systrayRegistry.add("addon.myitem1", { Component: MyItem1 });
|
||||
|
||||
patchWithCleanup(NavBar.prototype, {
|
||||
setup() {
|
||||
onRendered(() => {
|
||||
if (!systrayRegistry.contains("addon.myitem2")) {
|
||||
class MyItem2 extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<li class="my-item-2">my item 2</li>`;
|
||||
}
|
||||
systrayRegistry.add("addon.myitem2", { Component: MyItem2 });
|
||||
}
|
||||
});
|
||||
super.setup();
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(NavBar);
|
||||
expect(".o_menu_systray:eq(0) li").toHaveCount(2, {
|
||||
message: "2 systray items should be displayed",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("can adapt with 'more' menu sections behavior", async () => {
|
||||
class MyNavbar extends NavBar {
|
||||
async adapt() {
|
||||
await super.adapt();
|
||||
const sectionsCount = this.currentAppSections.length;
|
||||
const hiddenSectionsCount = this.currentAppSectionsExtra.length;
|
||||
expect.step(`adapt -> hide ${hiddenSectionsCount}/${sectionsCount} sections`);
|
||||
}
|
||||
}
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
children: [
|
||||
{ id: 10 },
|
||||
{ id: 11 },
|
||||
{
|
||||
id: 12,
|
||||
children: [{ id: 120 }, { id: 121 }, { id: 122 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// Force the parent width, to make this test independent of screen size
|
||||
await resize({ width: 1080 });
|
||||
|
||||
// TODO: this test case doesn't make sense since it relies on small widths
|
||||
// with `env.isSmall` still returning `false`.
|
||||
const env = await makeMockEnv();
|
||||
Object.defineProperty(env, "isSmall", { get: () => false });
|
||||
|
||||
// Set menu and mount
|
||||
getService("menu").setCurrentMenu(1);
|
||||
await mountWithCleanup(MyNavbar);
|
||||
|
||||
expect(".o_menu_sections > *:not(.o_menu_sections_more):visible").toHaveCount(3, {
|
||||
message: "should have 3 menu sections displayed (that are not the 'more' menu)",
|
||||
});
|
||||
expect(".o_menu_sections_more").toHaveCount(0);
|
||||
|
||||
// Force minimal width
|
||||
await resize({ width: 0 });
|
||||
await waitNavbarAdaptation();
|
||||
|
||||
expect(".o_menu_sections").not.toBeVisible({
|
||||
message: "no menu section should be displayed",
|
||||
});
|
||||
|
||||
// Reset to full width
|
||||
await resize({ width: 1366 });
|
||||
await waitNavbarAdaptation();
|
||||
|
||||
expect(".o_menu_sections > *:not(.o_menu_sections_more):not(.d-none)").toHaveCount(3, {
|
||||
message: "should have 3 menu sections displayed (that are not the 'more' menu)",
|
||||
});
|
||||
expect(".o_menu_sections_more").toHaveCount(0, { message: "the 'more' menu should not exist" });
|
||||
expect.verifySteps([
|
||||
"adapt -> hide 0/3 sections",
|
||||
"adapt -> hide 3/3 sections",
|
||||
"adapt -> hide 0/3 sections",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("'more' menu sections adaptations do not trigger render in some cases", async () => {
|
||||
let adaptRunning = false;
|
||||
let adaptCount = 0;
|
||||
let adaptRenderCount = 0;
|
||||
class MyNavbar extends NavBar {
|
||||
async adapt() {
|
||||
adaptRunning = true;
|
||||
adaptCount++;
|
||||
await super.adapt();
|
||||
adaptRunning = false;
|
||||
}
|
||||
async render() {
|
||||
if (adaptRunning) {
|
||||
adaptRenderCount++;
|
||||
}
|
||||
await super.render(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
defineMenus([
|
||||
{
|
||||
id: 1,
|
||||
children: [
|
||||
{ id: 11, name: "Section with a very long name 1" },
|
||||
{ id: 12, name: "Section with a very long name 2" },
|
||||
{ id: 13, name: "Section with a very long name 3" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// Force the parent width, to make this test independent of screen size
|
||||
await resize({ width: 600 });
|
||||
|
||||
// TODO: this test case doesn't make sense since it relies on small widths
|
||||
// with `env.isSmall` still returning `false`.
|
||||
const env = await makeMockEnv();
|
||||
Object.defineProperty(env, "isSmall", { get: () => false });
|
||||
|
||||
const navbar = await mountWithCleanup(MyNavbar);
|
||||
|
||||
expect(navbar.currentAppSections).toHaveLength(0, { message: "0 app sub menus" });
|
||||
expect(".o_navbar").toHaveRect({ width: 600 });
|
||||
expect(adaptCount).toBe(1);
|
||||
expect(adaptRenderCount).toBe(0, {
|
||||
message: "during adapt, render not triggered as the navbar has no app sub menus",
|
||||
});
|
||||
|
||||
await resize({ width: 0 });
|
||||
await waitNavbarAdaptation();
|
||||
|
||||
expect(".o_navbar").toHaveRect({ width: 0 });
|
||||
expect(adaptCount).toBe(2);
|
||||
expect(adaptRenderCount).toBe(0, {
|
||||
message: "during adapt, render not triggered as the navbar has no app sub menus",
|
||||
});
|
||||
|
||||
// Set menu
|
||||
getService("menu").setCurrentMenu(1);
|
||||
await animationFrame();
|
||||
|
||||
expect(navbar.currentAppSections).toHaveLength(3, { message: "3 app sub menus" });
|
||||
expect(navbar.currentAppSectionsExtra).toHaveLength(3, {
|
||||
message: "all app sub menus are inside the more menu",
|
||||
});
|
||||
expect(adaptCount).toBe(3);
|
||||
expect(adaptRenderCount).toBe(1, {
|
||||
message:
|
||||
"during adapt, render triggered as the navbar does not have enough space for app sub menus",
|
||||
});
|
||||
|
||||
// Force small width
|
||||
await resize({ width: 240 });
|
||||
await waitNavbarAdaptation();
|
||||
|
||||
expect(navbar.currentAppSectionsExtra).toHaveLength(3, {
|
||||
message: "all app sub menus are inside the more menu",
|
||||
});
|
||||
expect(adaptCount).toBe(4);
|
||||
expect(adaptRenderCount).toBe(1, {
|
||||
message: "during adapt, render not triggered as the more menu dropdown is STILL the same",
|
||||
});
|
||||
|
||||
// Reset to full width
|
||||
await resize({ width: 1366 });
|
||||
await waitNavbarAdaptation();
|
||||
|
||||
expect(navbar.currentAppSections).toHaveLength(3, { message: "still 3 app sub menus" });
|
||||
expect(navbar.currentAppSectionsExtra).toHaveLength(0, {
|
||||
message: "all app sub menus are NO MORE inside the more menu",
|
||||
});
|
||||
expect(adaptCount).toBe(5);
|
||||
expect(adaptRenderCount).toBe(2, {
|
||||
message: "during adapt, render triggered as the more menu dropdown is NO MORE the same",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("'more' menu sections properly updated on app change", async () => {
|
||||
defineMenus([
|
||||
// First App
|
||||
{
|
||||
id: 1,
|
||||
children: [
|
||||
{ id: 10, name: "Section 10" },
|
||||
{ id: 11, name: "Section 11" },
|
||||
{
|
||||
id: 12,
|
||||
name: "Section 12",
|
||||
children: [
|
||||
{ id: 120, name: "Section 120" },
|
||||
{ id: 121, name: "Section 121" },
|
||||
{ id: 122, name: "Section 122" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Second App
|
||||
{
|
||||
id: 2,
|
||||
children: [
|
||||
{ id: 20, name: "Section 20" },
|
||||
{ id: 21, name: "Section 21" },
|
||||
{
|
||||
id: 22,
|
||||
name: "Section 22",
|
||||
children: [
|
||||
{ id: 220, name: "Section 220" },
|
||||
{ id: 221, name: "Section 221" },
|
||||
{ id: 222, name: "Section 222" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// Force the parent width, to make this test independent of screen size
|
||||
await resize({ width: 1080 });
|
||||
|
||||
// TODO: this test case doesn't make sense since it relies on small widths
|
||||
// with `env.isSmall` still returning `false`.
|
||||
const env = await makeMockEnv();
|
||||
Object.defineProperty(env, "isSmall", { get: () => false });
|
||||
|
||||
// Set menu and mount
|
||||
getService("menu").setCurrentMenu(1);
|
||||
await mountWithCleanup(NavBar);
|
||||
|
||||
// Force minimal width
|
||||
await resize({ width: 0 });
|
||||
await waitNavbarAdaptation();
|
||||
expect(".o_menu_sections > *:not(.d-none)").toHaveCount(1, {
|
||||
message: "only one menu section should be displayed",
|
||||
});
|
||||
expect(".o_menu_sections_more:not(.d-none)").toHaveCount(1, {
|
||||
message: "the displayed menu section should be the 'more' menu",
|
||||
});
|
||||
|
||||
// Open the more menu
|
||||
await contains(".o_menu_sections_more .dropdown-toggle").click();
|
||||
expect(queryAllTexts(".dropdown-menu > *")).toEqual(
|
||||
["Section 10", "Section 11", "Section 12", "Section 120", "Section 121", "Section 122"],
|
||||
{ message: "'more' menu should contain first app sections" }
|
||||
);
|
||||
// Close the more menu
|
||||
await contains(".o_menu_sections_more .dropdown-toggle").click();
|
||||
|
||||
// Set App2 menu
|
||||
getService("menu").setCurrentMenu(2);
|
||||
await animationFrame();
|
||||
|
||||
// Open the more menu
|
||||
await contains(".o_menu_sections_more .dropdown-toggle").click();
|
||||
expect(queryAllTexts(".dropdown-menu > *")).toEqual(
|
||||
["Section 20", "Section 21", "Section 22", "Section 220", "Section 221", "Section 222"],
|
||||
{ message: "'more' menu should contain second app sections" }
|
||||
);
|
||||
});
|
||||
|
||||
test("Do not execute adapt when navbar is destroyed", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
class MyNavbar extends NavBar {
|
||||
async adapt() {
|
||||
expect.step("adapt NavBar");
|
||||
return super.adapt();
|
||||
}
|
||||
}
|
||||
|
||||
await makeMockEnv();
|
||||
|
||||
// Set menu and mount
|
||||
getService("menu").setCurrentMenu(1);
|
||||
const navbar = await mountWithCleanup(MyNavbar);
|
||||
expect.verifySteps(["adapt NavBar"]);
|
||||
await resize();
|
||||
await runAllTimers();
|
||||
expect.verifySteps(["adapt NavBar"]);
|
||||
await resize();
|
||||
destroy(navbar);
|
||||
await runAllTimers();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -1,518 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { menuService } from "@web/webclient/menus/menu_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { NavBar } from "@web/webclient/navbar/navbar";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
destroy,
|
||||
getFixture,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
mockTimeout,
|
||||
} from "../helpers/utils";
|
||||
|
||||
import { Component, xml, onRendered } from "@odoo/owl";
|
||||
|
||||
const systrayRegistry = registry.category("systray");
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
class MySystrayItem extends Component {}
|
||||
MySystrayItem.template = xml`<li class="my-item">my item</li>`;
|
||||
let baseConfig;
|
||||
let target;
|
||||
|
||||
QUnit.module("Navbar", {
|
||||
async beforeEach() {
|
||||
target = getFixture();
|
||||
serviceRegistry.add("menu", menuService);
|
||||
serviceRegistry.add("action", actionService);
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("view", viewService); // #action-serv-leg-compat-js-class
|
||||
serviceRegistry.add("orm", ormService); // #action-serv-leg-compat-js-class
|
||||
systrayRegistry.add("addon.myitem", { Component: MySystrayItem });
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (handler, delay, ...args) => handler(...args),
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
const menus = {
|
||||
root: { id: "root", children: [1], name: "root", appID: "root" },
|
||||
1: { id: 1, children: [], name: "App0", appID: 1 },
|
||||
};
|
||||
const serverData = { menus };
|
||||
baseConfig = { serverData };
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("can be rendered", async (assert) => {
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
await mount(NavBar, target, { env });
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_navbar_apps_menu button.dropdown-toggle",
|
||||
"1 apps menu toggler present"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("dropdown menu can be toggled", async (assert) => {
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
await mount(NavBar, target, { env });
|
||||
const dropdown = target.querySelector(".o_navbar_apps_menu");
|
||||
await click(dropdown, "button.dropdown-toggle");
|
||||
assert.containsOnce(dropdown, ".dropdown-menu");
|
||||
await click(dropdown, "button.dropdown-toggle");
|
||||
assert.containsNone(dropdown, ".dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("href attribute on apps menu items", async (assert) => {
|
||||
baseConfig.serverData.menus = {
|
||||
root: { id: "root", children: [1], name: "root", appID: "root" },
|
||||
1: { id: 1, children: [2], name: "My app", appID: 1, actionID: 339 },
|
||||
};
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
await mount(NavBar, target, { env });
|
||||
const appsMenu = target.querySelector(".o_navbar_apps_menu");
|
||||
await click(appsMenu, "button.dropdown-toggle");
|
||||
const dropdownItem = target.querySelector(".o_navbar_apps_menu .dropdown-item");
|
||||
assert.strictEqual(dropdownItem.getAttribute("href"), "#menu_id=1&action=339");
|
||||
});
|
||||
|
||||
QUnit.test("many sublevels in app menu items", async (assert) => {
|
||||
baseConfig.serverData.menus = {
|
||||
root: { id: "root", children: [1], name: "root", appID: "root" },
|
||||
1: { id: 1, children: [2], name: "My app", appID: 1 },
|
||||
2: { id: 2, children: [3], name: "My menu", appID: 1 },
|
||||
3: { id: 3, children: [4], name: "My submenu 1", appID: 1 },
|
||||
4: { id: 4, children: [5], name: "My submenu 2", appID: 1 },
|
||||
5: { id: 5, children: [6], name: "My submenu 3", appID: 1 },
|
||||
6: { id: 6, children: [7], name: "My submenu 4", appID: 1 },
|
||||
7: { id: 7, children: [8], name: "My submenu 5", appID: 1 },
|
||||
8: { id: 8, children: [9], name: "My submenu 6", appID: 1 },
|
||||
9: { id: 9, children: [], name: "My submenu 7", appID: 1 },
|
||||
};
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
env.services.menu.setCurrentMenu(1);
|
||||
await mount(NavBar, target, { env });
|
||||
const firstSectionMenu = target.querySelector(".o_menu_sections .dropdown");
|
||||
await click(firstSectionMenu, "button.dropdown-toggle");
|
||||
const menuChildren = [...firstSectionMenu.querySelectorAll(".dropdown-menu > *")];
|
||||
assert.deepEqual(
|
||||
menuChildren.map((el) => ({
|
||||
text: el.textContent,
|
||||
paddingLeft: el.style.paddingLeft,
|
||||
tagName: el.tagName,
|
||||
})),
|
||||
[
|
||||
{ text: "My submenu 1", paddingLeft: "20px", tagName: "DIV" },
|
||||
{ text: "My submenu 2", paddingLeft: "32px", tagName: "DIV" },
|
||||
{ text: "My submenu 3", paddingLeft: "44px", tagName: "DIV" },
|
||||
{ text: "My submenu 4", paddingLeft: "56px", tagName: "DIV" },
|
||||
{ text: "My submenu 5", paddingLeft: "68px", tagName: "DIV" },
|
||||
{ text: "My submenu 6", paddingLeft: "80px", tagName: "DIV" },
|
||||
{ text: "My submenu 7", paddingLeft: "92px", tagName: "A" },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("data-menu-xmlid attribute on AppsMenu items", async (assert) => {
|
||||
baseConfig.serverData.menus = {
|
||||
root: { id: "root", children: [1, 2], name: "root", appID: "root" },
|
||||
1: { id: 1, children: [3, 4], name: "App0 with xmlid", appID: 1, xmlid: "wowl" },
|
||||
2: { id: 2, children: [], name: "App1 without xmlid", appID: 2 },
|
||||
3: { id: 3, children: [], name: "Menu without children", appID: 1, xmlid: "menu_3" },
|
||||
4: { id: 4, children: [5], name: "Menu with children", appID: 1, xmlid: "menu_4" },
|
||||
5: { id: 5, children: [], name: "Sub menu", appID: 1, xmlid: "menu_5" },
|
||||
};
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
await mount(NavBar, target, { env });
|
||||
|
||||
// check apps
|
||||
const appsMenu = target.querySelector(".o_navbar_apps_menu");
|
||||
await click(appsMenu, "button.dropdown-toggle");
|
||||
const menuItems = appsMenu.querySelectorAll("a");
|
||||
assert.strictEqual(
|
||||
menuItems[0].dataset.menuXmlid,
|
||||
"wowl",
|
||||
"first menu item should have the correct data-menu-xmlid attribute set"
|
||||
);
|
||||
assert.strictEqual(
|
||||
menuItems[1].dataset.menuXmlid,
|
||||
undefined,
|
||||
"second menu item should not have any data-menu-xmlid attribute set"
|
||||
);
|
||||
|
||||
// check menus
|
||||
env.services.menu.setCurrentMenu(1);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_menu_sections .dropdown-item[data-menu-xmlid=menu_3]");
|
||||
|
||||
// check sub menus toggler
|
||||
assert.containsOnce(target, ".o_menu_sections button.dropdown-toggle[data-menu-xmlid=menu_4]");
|
||||
|
||||
// check sub menus
|
||||
await click(target.querySelector(".o_menu_sections .dropdown-toggle"));
|
||||
assert.containsOnce(target, ".o_menu_sections .dropdown-item[data-menu-xmlid=menu_5]");
|
||||
});
|
||||
|
||||
QUnit.test("navbar can display current active app", async (assert) => {
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
await mount(NavBar, target, { env });
|
||||
const dropdown = target.querySelector(".o_navbar_apps_menu");
|
||||
// Open apps menu
|
||||
await click(dropdown, "button.dropdown-toggle");
|
||||
assert.containsOnce(
|
||||
dropdown,
|
||||
".dropdown-menu .dropdown-item:not(.focus)",
|
||||
"should not show the current active app as the menus service has not loaded an app yet"
|
||||
);
|
||||
|
||||
// Activate an app
|
||||
env.services.menu.setCurrentMenu(1);
|
||||
await nextTick();
|
||||
assert.containsOnce(
|
||||
dropdown,
|
||||
".dropdown-menu .dropdown-item.focus",
|
||||
"should show the current active app"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("navbar can display systray items", async (assert) => {
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
await mount(NavBar, target, { env });
|
||||
assert.containsOnce(target, "li.my-item");
|
||||
});
|
||||
|
||||
QUnit.test("navbar can display systray items ordered based on their sequence", async (assert) => {
|
||||
class MyItem1 extends Component {}
|
||||
MyItem1.template = xml`<li class="my-item-1">my item 1</li>`;
|
||||
class MyItem2 extends Component {}
|
||||
MyItem2.template = xml`<li class="my-item-2">my item 2</li>`;
|
||||
class MyItem3 extends Component {}
|
||||
MyItem3.template = xml`<li class="my-item-3">my item 3</li>`;
|
||||
class MyItem4 extends Component {}
|
||||
MyItem4.template = xml`<li class="my-item-4">my item 4</li>`;
|
||||
|
||||
clearRegistryWithCleanup(systrayRegistry);
|
||||
systrayRegistry.add("addon.myitem2", { Component: MyItem2 });
|
||||
systrayRegistry.add("addon.myitem1", { Component: MyItem1 }, { sequence: 0 });
|
||||
systrayRegistry.add("addon.myitem3", { Component: MyItem3 }, { sequence: 100 });
|
||||
systrayRegistry.add("addon.myitem4", { Component: MyItem4 });
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
await mount(NavBar, target, { env });
|
||||
const menuSystray = target.getElementsByClassName("o_menu_systray")[0];
|
||||
assert.containsN(menuSystray, "li", 4, "four systray items should be displayed");
|
||||
assert.strictEqual(menuSystray.innerText, "my item 3\nmy item 4\nmy item 2\nmy item 1");
|
||||
});
|
||||
|
||||
QUnit.test("navbar updates after adding a systray item", async (assert) => {
|
||||
class MyItem1 extends Component {}
|
||||
MyItem1.template = xml`<li class="my-item-1">my item 1</li>`;
|
||||
|
||||
clearRegistryWithCleanup(systrayRegistry);
|
||||
systrayRegistry.add("addon.myitem1", { Component: MyItem1 });
|
||||
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
|
||||
patchWithCleanup(NavBar.prototype, {
|
||||
setup() {
|
||||
onRendered(() => {
|
||||
if (!systrayRegistry.contains("addon.myitem2")) {
|
||||
class MyItem2 extends Component {}
|
||||
MyItem2.template = xml`<li class="my-item-2">my item 2</li>`;
|
||||
systrayRegistry.add("addon.myitem2", { Component: MyItem2 });
|
||||
}
|
||||
});
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
await mount(NavBar, target, { env });
|
||||
const menuSystray = target.getElementsByClassName("o_menu_systray")[0];
|
||||
assert.containsN(menuSystray, "li", 2, "2 systray items should be displayed");
|
||||
});
|
||||
|
||||
QUnit.test("can adapt with 'more' menu sections behavior", async (assert) => {
|
||||
class MyNavbar extends NavBar {
|
||||
async adapt() {
|
||||
await super.adapt();
|
||||
const sectionsCount = this.currentAppSections.length;
|
||||
const hiddenSectionsCount = this.currentAppSectionsExtra.length;
|
||||
assert.step(`adapt -> hide ${hiddenSectionsCount}/${sectionsCount} sections`);
|
||||
}
|
||||
}
|
||||
const newMenus = {
|
||||
root: { id: "root", children: [1, 2], name: "root", appID: "root" },
|
||||
1: { id: 1, children: [10, 11, 12], name: "App0", appID: 1 },
|
||||
10: { id: 10, children: [], name: "Section 10", appID: 1 },
|
||||
11: { id: 11, children: [], name: "Section 11", appID: 1 },
|
||||
12: { id: 12, children: [120, 121, 122], name: "Section 12", appID: 1 },
|
||||
120: { id: 120, children: [], name: "Section 120", appID: 1 },
|
||||
121: { id: 121, children: [], name: "Section 121", appID: 1 },
|
||||
122: { id: 122, children: [], name: "Section 122", appID: 1 },
|
||||
};
|
||||
baseConfig.serverData.menus = newMenus;
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
|
||||
// Force the parent width, to make this test independent of screen size
|
||||
target.style.width = "1080px";
|
||||
|
||||
// Set menu and mount
|
||||
env.services.menu.setCurrentMenu(1);
|
||||
await mount(MyNavbar, target, { env });
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_menu_sections > *:not(.o_menu_sections_more):not(.d-none)",
|
||||
3,
|
||||
"should have 3 menu sections displayed (that are not the 'more' menu)"
|
||||
);
|
||||
assert.containsNone(target, ".o_menu_sections_more", "the 'more' menu should not exist");
|
||||
// Force minimal width and dispatch window resize event
|
||||
target.style.width = "0%";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await nextTick();
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_menu_sections > *:not(.d-none)",
|
||||
"only one menu section should be displayed"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_menu_sections_more:not(.d-none)",
|
||||
"the displayed menu section should be the 'more' menu"
|
||||
);
|
||||
// Open the more menu
|
||||
await click(target, ".o_menu_sections_more .dropdown-toggle");
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".dropdown-menu > *")].map((el) => el.textContent),
|
||||
["Section 10", "Section 11", "Section 12", "Section 120", "Section 121", "Section 122"],
|
||||
"'more' menu should contain all hidden sections in correct order"
|
||||
);
|
||||
// Reset to full width and dispatch window resize event
|
||||
target.style.width = "100%";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await nextTick();
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_menu_sections > *:not(.o_menu_sections_more):not(.d-none)",
|
||||
3,
|
||||
"should have 3 menu sections displayed (that are not the 'more' menu)"
|
||||
);
|
||||
assert.containsNone(target, ".o_menu_sections_more", "the 'more' menu should not exist");
|
||||
// Check the navbar adaptation calls
|
||||
assert.verifySteps([
|
||||
"adapt -> hide 0/3 sections",
|
||||
"adapt -> hide 3/3 sections",
|
||||
"adapt -> hide 0/3 sections",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"'more' menu sections adaptations do not trigger render in some cases",
|
||||
async (assert) => {
|
||||
let adaptRunning = false;
|
||||
let adaptCount = 0;
|
||||
let adaptRenderCount = 0;
|
||||
class MyNavbar extends NavBar {
|
||||
async adapt() {
|
||||
adaptRunning = true;
|
||||
adaptCount++;
|
||||
await super.adapt();
|
||||
adaptRunning = false;
|
||||
}
|
||||
async render() {
|
||||
if (adaptRunning) {
|
||||
adaptRenderCount++;
|
||||
}
|
||||
await super.render(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
const newMenus = {
|
||||
root: { id: "root", children: [1], name: "root", appID: "root" },
|
||||
1: { id: 1, children: [11, 12, 13], name: "App1", appID: 1 },
|
||||
11: { id: 11, children: [], name: "Section 1", appID: 1 },
|
||||
12: { id: 12, children: [], name: "Section 2", appID: 1 },
|
||||
13: { id: 13, children: [], name: "Section 3", appID: 1 },
|
||||
};
|
||||
baseConfig.serverData.menus = newMenus;
|
||||
|
||||
// Force the parent width, to make this test independent of screen size
|
||||
target.style.width = "600px";
|
||||
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
const navbar = await mount(MyNavbar, target, { env });
|
||||
assert.strictEqual(navbar.currentAppSections.length, 0, "0 app sub menus");
|
||||
assert.strictEqual(target.querySelector(".o_navbar").offsetWidth, 600);
|
||||
assert.strictEqual(adaptCount, 1);
|
||||
assert.strictEqual(
|
||||
adaptRenderCount,
|
||||
0,
|
||||
"during adapt, render not triggered as the navbar has no app sub menus"
|
||||
);
|
||||
|
||||
// Force minimal width and dispatch window resize event
|
||||
target.querySelector(".o_navbar").style.width = "0%";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".o_navbar").offsetWidth, 0);
|
||||
assert.strictEqual(adaptCount, 2);
|
||||
assert.strictEqual(
|
||||
adaptRenderCount,
|
||||
0,
|
||||
"during adapt, render not triggered as the navbar has no app sub menus"
|
||||
);
|
||||
|
||||
// Set menu
|
||||
env.services.menu.setCurrentMenu(1);
|
||||
await nextTick();
|
||||
assert.strictEqual(navbar.currentAppSections.length, 3, "3 app sub menus");
|
||||
assert.strictEqual(
|
||||
navbar.currentAppSectionsExtra.length,
|
||||
3,
|
||||
"all app sub menus are inside the more menu"
|
||||
);
|
||||
assert.strictEqual(adaptCount, 3);
|
||||
assert.strictEqual(
|
||||
adaptRenderCount,
|
||||
1,
|
||||
"during adapt, render triggered as the navbar does not have enough space for app sub menus"
|
||||
);
|
||||
|
||||
// Force 40% width and dispatch window resize event
|
||||
target.querySelector(".o_navbar").style.width = "40%";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
navbar.currentAppSectionsExtra.length,
|
||||
3,
|
||||
"all app sub menus are STILL inside the more menu"
|
||||
);
|
||||
assert.strictEqual(adaptCount, 4);
|
||||
assert.strictEqual(
|
||||
adaptRenderCount,
|
||||
1,
|
||||
"during adapt, render not triggered as the more menu dropdown is STILL the same"
|
||||
);
|
||||
|
||||
// Reset to full width and dispatch window resize event
|
||||
target.querySelector(".o_navbar").style.width = "100%";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await nextTick();
|
||||
assert.strictEqual(navbar.currentAppSections.length, 3, "still 3 app sub menus");
|
||||
assert.strictEqual(
|
||||
navbar.currentAppSectionsExtra.length,
|
||||
0,
|
||||
"all app sub menus are NO MORE inside the more menu"
|
||||
);
|
||||
assert.strictEqual(adaptCount, 5);
|
||||
assert.strictEqual(
|
||||
adaptRenderCount,
|
||||
2,
|
||||
"during adapt, render triggered as the more menu dropdown is NO MORE the same"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("'more' menu sections properly updated on app change", async (assert) => {
|
||||
const newMenus = {
|
||||
root: { id: "root", children: [1, 2], name: "root", appID: "root" },
|
||||
// First App
|
||||
1: { id: 1, children: [10, 11, 12], name: "App1", appID: 1 },
|
||||
10: { id: 10, children: [], name: "Section 10", appID: 1 },
|
||||
11: { id: 11, children: [], name: "Section 11", appID: 1 },
|
||||
12: { id: 12, children: [120, 121, 122], name: "Section 12", appID: 1 },
|
||||
120: { id: 120, children: [], name: "Section 120", appID: 1 },
|
||||
121: { id: 121, children: [], name: "Section 121", appID: 1 },
|
||||
122: { id: 122, children: [], name: "Section 122", appID: 1 },
|
||||
// Second App
|
||||
2: { id: 2, children: [20, 21, 22], name: "App2", appID: 2 },
|
||||
20: { id: 20, children: [], name: "Section 20", appID: 2 },
|
||||
21: { id: 21, children: [], name: "Section 21", appID: 2 },
|
||||
22: { id: 22, children: [220, 221, 222], name: "Section 22", appID: 2 },
|
||||
220: { id: 220, children: [], name: "Section 220", appID: 2 },
|
||||
221: { id: 221, children: [], name: "Section 221", appID: 2 },
|
||||
222: { id: 222, children: [], name: "Section 222", appID: 2 },
|
||||
};
|
||||
baseConfig.serverData.menus = newMenus;
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
|
||||
// Force the parent width, to make this test independent of screen size
|
||||
target.style.width = "1080px";
|
||||
|
||||
// Set App1 menu and mount
|
||||
env.services.menu.setCurrentMenu(1);
|
||||
await mount(NavBar, target, { env });
|
||||
|
||||
// Force minimal width and dispatch window resize event
|
||||
target.style.width = "0%";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await nextTick();
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_menu_sections > *:not(.d-none)",
|
||||
"only one menu section should be displayed"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_menu_sections_more:not(.d-none)",
|
||||
"the displayed menu section should be the 'more' menu"
|
||||
);
|
||||
|
||||
// Open the more menu
|
||||
await click(target, ".o_menu_sections_more .dropdown-toggle");
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".dropdown-menu > *")].map((el) => el.textContent),
|
||||
["Section 10", "Section 11", "Section 12", "Section 120", "Section 121", "Section 122"],
|
||||
"'more' menu should contain App1 sections"
|
||||
);
|
||||
// Close the more menu
|
||||
await click(target, ".o_menu_sections_more .dropdown-toggle");
|
||||
|
||||
// Set App2 menu
|
||||
env.services.menu.setCurrentMenu(2);
|
||||
await nextTick();
|
||||
|
||||
// Open the more menu
|
||||
await click(target, ".o_menu_sections_more .dropdown-toggle");
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".dropdown-menu > *")].map((el) => el.textContent),
|
||||
["Section 20", "Section 21", "Section 22", "Section 220", "Section 221", "Section 222"],
|
||||
"'more' menu should contain App2 sections"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Do not execute adapt when navbar is destroyed", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
class MyNavbar extends NavBar {
|
||||
async adapt() {
|
||||
assert.step("adapt NavBar");
|
||||
return super.adapt();
|
||||
}
|
||||
}
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
|
||||
// Set menu and mount
|
||||
env.services.menu.setCurrentMenu(1);
|
||||
const navbar = await mount(MyNavbar, target, { env });
|
||||
assert.verifySteps(["adapt NavBar"]);
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
execRegisteredTimeouts();
|
||||
assert.verifySteps(["adapt NavBar"]);
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
destroy(navbar);
|
||||
execRegisteredTimeouts();
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
defineModels,
|
||||
getService,
|
||||
makeMockEnv,
|
||||
mockService,
|
||||
models,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { rpcBus } from "@web/core/network/rpc";
|
||||
|
||||
class Company extends models.Model {
|
||||
_name = "res.company";
|
||||
}
|
||||
class Notacompany extends models.Model {}
|
||||
|
||||
defineModels([Company, Notacompany]);
|
||||
|
||||
test("reload webclient when updating a res.company", async () => {
|
||||
mockService("action", {
|
||||
async doAction(action) {
|
||||
expect.step(action);
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
expect.verifySteps([]);
|
||||
await getService("orm").read("res.company", [32]);
|
||||
expect.verifySteps([]);
|
||||
await getService("orm").unlink("res.company", [32]);
|
||||
expect.verifySteps(["reload_context"]);
|
||||
await getService("orm").unlink("notacompany", [32]);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("do not reload webclient when updating a res.company, but there is an error", async () => {
|
||||
mockService("action", {
|
||||
async doAction(action) {
|
||||
expect.step(action);
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
expect.verifySteps([]);
|
||||
rpcBus.trigger("RPC:RESPONSE", {
|
||||
data: { params: { model: "res.company", method: "write" } },
|
||||
settings: {},
|
||||
result: {},
|
||||
});
|
||||
expect.verifySteps(["reload_context"]);
|
||||
rpcBus.trigger("RPC:RESPONSE", {
|
||||
data: { params: { model: "res.company", method: "write" } },
|
||||
settings: {},
|
||||
error: {},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -0,0 +1,740 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { hover, queryAllTexts, queryAllValues, queryFirst, runAllTimers } from "@odoo/hoot-dom";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
editSelectMenu,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
const { ResCompany, ResGroups, ResPartner, ResUsers } = webModels;
|
||||
|
||||
defineModels([ResCompany, ResGroups, ResPartner, ResUsers]);
|
||||
|
||||
beforeEach(() => {
|
||||
ResPartner._records = [{ id: 1, name: "Partner" }];
|
||||
ResGroups._records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Access Rights",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Settings",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Project User",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Project Manager",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Project Administrator",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Helpdesk User",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Helpdesk Administrator",
|
||||
},
|
||||
{
|
||||
id: 91,
|
||||
name: "Internal user",
|
||||
},
|
||||
{
|
||||
id: 92,
|
||||
name: "Portal user",
|
||||
},
|
||||
{
|
||||
id: 93,
|
||||
name: "Something related to project",
|
||||
},
|
||||
{
|
||||
id: 94,
|
||||
name: "Something related to helpdesk",
|
||||
},
|
||||
];
|
||||
ResUsers._records = [
|
||||
{
|
||||
id: 1,
|
||||
company_id: 1,
|
||||
company_ids: [1],
|
||||
login: "my_user",
|
||||
partner_id: 1,
|
||||
password: "password",
|
||||
group_ids: [1, 11, 91],
|
||||
view_group_hierarchy: {
|
||||
groups: {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "Access Rights",
|
||||
all_implied_by_ids: [1, 2],
|
||||
all_implied_ids: [],
|
||||
comment: false,
|
||||
disjoint_ids: [],
|
||||
implied_ids: [],
|
||||
privilege_id: 122,
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
name: "Settings",
|
||||
all_implied_by_ids: [2],
|
||||
all_implied_ids: [1, 2, 15],
|
||||
comment: false,
|
||||
disjoint_ids: [],
|
||||
implied_ids: [1, 15],
|
||||
privilege_id: 122,
|
||||
},
|
||||
11: {
|
||||
id: 11,
|
||||
name: "Project User",
|
||||
all_implied_by_ids: [11, 12, 13],
|
||||
all_implied_ids: [11],
|
||||
comment: "Can access Project as a user",
|
||||
disjoint_ids: [],
|
||||
implied_ids: [],
|
||||
privilege_id: 222,
|
||||
},
|
||||
12: {
|
||||
id: 12,
|
||||
name: "Project Manager",
|
||||
all_implied_by_ids: [2, 12, 13, 15],
|
||||
all_implied_ids: [11, 12],
|
||||
comment: "Can access Project as a manager",
|
||||
disjoint_ids: [],
|
||||
implied_ids: [11],
|
||||
privilege_id: 222,
|
||||
},
|
||||
13: {
|
||||
id: 13,
|
||||
name: "Project Administrator",
|
||||
all_implied_by_ids: [13],
|
||||
all_implied_ids: [11, 12, 13, 93],
|
||||
comment: "Can access Project as an admistrator",
|
||||
disjoint_ids: [],
|
||||
implied_ids: [11, 12, 93],
|
||||
privilege_id: 222,
|
||||
},
|
||||
14: {
|
||||
id: 14,
|
||||
name: "Helpdesk User",
|
||||
all_implied_by_ids: [14, 15],
|
||||
all_implied_ids: [11, 14],
|
||||
comment: false,
|
||||
disjoint_ids: [],
|
||||
implied_ids: [11],
|
||||
privilege_id: 223,
|
||||
},
|
||||
15: {
|
||||
id: 15,
|
||||
name: "Helpdesk Administrator",
|
||||
all_implied_by_ids: [15],
|
||||
all_implied_ids: [11, 12, 14, 15, 93, 94],
|
||||
comment: false,
|
||||
disjoint_ids: [],
|
||||
implied_ids: [14, 94],
|
||||
privilege_id: 223,
|
||||
},
|
||||
91: {
|
||||
id: 91,
|
||||
name: "Internal user",
|
||||
all_implied_by_ids: [1, 2, 91],
|
||||
all_implied_ids: [91],
|
||||
comment: false,
|
||||
disjoint_ids: [92],
|
||||
implied_ids: [11],
|
||||
privilege_id: false,
|
||||
},
|
||||
92: {
|
||||
id: 92,
|
||||
name: "Portal user",
|
||||
all_implied_by_ids: [92],
|
||||
all_implied_ids: [92],
|
||||
comment: "Portal members have specific access rights",
|
||||
disjoint_ids: [91],
|
||||
implied_ids: [],
|
||||
privilege_id: false,
|
||||
},
|
||||
93: {
|
||||
id: 93,
|
||||
name: "Something related to project",
|
||||
all_implied_by_ids: [13, 15, 93],
|
||||
all_implied_ids: [93],
|
||||
comment: false,
|
||||
disjoint_ids: [],
|
||||
implied_ids: [],
|
||||
privilege_id: false,
|
||||
},
|
||||
94: {
|
||||
id: 94,
|
||||
name: "Something related to helpdesk",
|
||||
all_implied_by_ids: [15, 94],
|
||||
all_implied_ids: [94],
|
||||
comment: false,
|
||||
disjoint_ids: [],
|
||||
implied_ids: [],
|
||||
privilege_id: false,
|
||||
},
|
||||
},
|
||||
privileges: {
|
||||
122: {
|
||||
id: 122,
|
||||
name: "Administration",
|
||||
description: false,
|
||||
group_ids: [1, 2],
|
||||
category_id: 121,
|
||||
},
|
||||
222: {
|
||||
id: 222,
|
||||
name: "Project",
|
||||
description: "Project access rights description",
|
||||
group_ids: [11, 12, 13],
|
||||
category_id: 221,
|
||||
},
|
||||
223: {
|
||||
id: 223,
|
||||
name: "Helpdesk",
|
||||
description: "",
|
||||
group_ids: [14, 15],
|
||||
category_id: 221,
|
||||
},
|
||||
},
|
||||
categories: [
|
||||
{
|
||||
id: 121,
|
||||
name: "Administration (category)",
|
||||
privilege_ids: [122],
|
||||
},
|
||||
{
|
||||
id: 221,
|
||||
name: "Project (category)",
|
||||
privilege_ids: [222, 223],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
serverState.userId = 1;
|
||||
});
|
||||
|
||||
test("simple rendering", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// 1 group with 2 inner groups
|
||||
expect(".o_field_widget[name=group_ids] .o_group").toHaveCount(1);
|
||||
expect(".o_field_widget[name=group_ids] .o_group .o_inner_group").toHaveCount(2);
|
||||
|
||||
// first group has one privilege
|
||||
expect(
|
||||
".o_field_widget[name=group_ids] .o_inner_group:eq(0) .o_horizontal_separator"
|
||||
).toHaveText("ADMINISTRATION (CATEGORY)");
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(0) .o_form_label").toHaveCount(1);
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(0) .o_form_label").toHaveText(
|
||||
"Administration"
|
||||
);
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(0) input").toHaveCount(1);
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(0) input").toHaveValue(
|
||||
"Access Rights"
|
||||
);
|
||||
|
||||
// second group has 2 privileges
|
||||
expect(
|
||||
".o_field_widget[name=group_ids] .o_inner_group:eq(1) .o_horizontal_separator"
|
||||
).toHaveText("PROJECT (CATEGORY)");
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(1) .o_form_label").toHaveCount(2);
|
||||
expect(
|
||||
queryAllTexts(".o_field_widget[name=group_ids] .o_inner_group:eq(1) .o_form_label")
|
||||
).toEqual(["Project?", "Helpdesk"]);
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:nth-child(2) input").toHaveCount(2);
|
||||
expect(
|
||||
queryAllValues(
|
||||
".o_field_widget[name=group_ids] .o_inner_group:nth-child(2) .o_wrap_input input"
|
||||
)
|
||||
).toEqual(["Project User", ""]);
|
||||
|
||||
expect(".o_group_info_button").toHaveCount(0); // not displayed in non debug mode
|
||||
});
|
||||
|
||||
test("simple rendering (debug)", async () => {
|
||||
serverState.debug = "1";
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// 2 group and 4 inner groups
|
||||
expect(".o_field_widget[name=group_ids] .o_group").toHaveCount(2);
|
||||
expect(".o_field_widget[name=group_ids] .o_group .o_inner_group").toHaveCount(4);
|
||||
expect(".o_group:eq(1) .o_horizontal_separator").toHaveText("EXTRA RIGHTS");
|
||||
expect(".o_group:eq(1) .o_inner_group").toHaveCount(2);
|
||||
expect(".o_group:eq(1) .o_inner_group:eq(0) input[type=checkbox]").toHaveCount(2);
|
||||
expect(".o_group:eq(1) .o_inner_group:eq(0) input[type=checkbox]:checked").toHaveCount(1);
|
||||
expect(".o_group:eq(1) .o_inner_group:eq(1) input[type=checkbox]").toHaveCount(2);
|
||||
expect(".o_group:eq(1) .o_inner_group:eq(1) input[type=checkbox]:checked").toHaveCount(0);
|
||||
|
||||
expect(".o_group_info_button:not(.invisible)").toHaveCount(3);
|
||||
});
|
||||
|
||||
test("add and remove groups", async () => {
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].group_ids).toEqual([[6, false, [1, 15, 91]]]);
|
||||
expect.step("web_save");
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await editSelectMenu(".o_field_widget[name='group_ids'] .o_inner_group:eq(1) input", {
|
||||
value: "",
|
||||
});
|
||||
await editSelectMenu(
|
||||
".o_field_widget[name='group_ids'] .o_inner_group:nth-child(2) .o_wrap_input:last-child input",
|
||||
{ value: "Helpdesk Administrator" }
|
||||
);
|
||||
await contains(`.o_form_button_save`).click();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("editing groups doesn't remove groups (debug)", async () => {
|
||||
serverState.debug = "1";
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].group_ids).toEqual([[6, false, [1, 15, 91]]]);
|
||||
expect.step("web_save");
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await editSelectMenu(".o_field_widget[name='group_ids'] .o_inner_group:eq(1) input", {
|
||||
value: "",
|
||||
});
|
||||
await editSelectMenu(
|
||||
".o_field_widget[name='group_ids'] .o_inner_group:nth-child(2) .o_wrap_input:last-child input",
|
||||
{ value: "Helpdesk Administrator" }
|
||||
);
|
||||
await contains(`.o_form_button_save`).click();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`privilege tooltips`, async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await hover(`.o_form_label sup`);
|
||||
await runAllTimers();
|
||||
expect(`.o-tooltip .o-tooltip--help`).toHaveText(
|
||||
"Project access rights description\n- Project User: Can access Project as a user\n- Project Manager: Can access Project as a manager\n- Project Administrator: Can access Project as an admistrator"
|
||||
);
|
||||
});
|
||||
|
||||
test("implied groups rendering", async () => {
|
||||
ResUsers._records[0].group_ids = [2, 15];
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=group_ids] .o_group .o_inner_group").toHaveCount(2);
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input").toHaveCount(2);
|
||||
expect(queryAllValues(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input")).toEqual([
|
||||
"",
|
||||
"Helpdesk Administrator",
|
||||
]);
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input:eq(0)").toHaveAttribute(
|
||||
"placeholder",
|
||||
"Project Manager"
|
||||
);
|
||||
});
|
||||
|
||||
test("implied groups rendering (debug)", async () => {
|
||||
serverState.debug = "1";
|
||||
ResUsers._records[0].group_ids = [2, 15];
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=group_ids] .o_group .o_inner_group").toHaveCount(4);
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input").toHaveCount(2);
|
||||
expect(queryAllValues(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input")).toEqual([
|
||||
"",
|
||||
"Helpdesk Administrator",
|
||||
]);
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input:eq(0)").toHaveAttribute(
|
||||
"placeholder",
|
||||
"Project Manager"
|
||||
);
|
||||
|
||||
await contains(".o_inner_group:eq(1) .o_group_info_button:eq(0)").click();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(queryAllTexts(".o_popover table td")).toEqual([
|
||||
"Project",
|
||||
"Project Manager",
|
||||
"Implied by",
|
||||
"- Administration/Settings\n- Helpdesk/Helpdesk Administrator",
|
||||
]);
|
||||
await contains(".o_inner_group:eq(1) .o_group_info_button:eq(1)").click();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(queryAllTexts(".o_popover table td")).toEqual([
|
||||
"Helpdesk",
|
||||
"Helpdesk Administrator",
|
||||
"Exclusively implies",
|
||||
"- Something related to project\n- Something related to helpdesk",
|
||||
"Jointly implies",
|
||||
"- Project/Project Manager",
|
||||
]);
|
||||
|
||||
expect(".o_inner_group:eq(2) .o_is_implied input").not.toBeChecked();
|
||||
await contains(".o_inner_group:eq(2) .o_group_info_button").click();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(queryAllTexts(".o_popover table td")).toEqual([
|
||||
"Group",
|
||||
"Internal user",
|
||||
"Implied by",
|
||||
"- Administration/Settings",
|
||||
]);
|
||||
});
|
||||
|
||||
test("implied groups rendering: exclusive (debug)", async () => {
|
||||
serverState.debug = "1";
|
||||
ResUsers._records[0].group_ids = [15];
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=group_ids] .o_group .o_inner_group").toHaveCount(4);
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input").toHaveCount(2);
|
||||
expect(queryAllValues(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input")).toEqual([
|
||||
"",
|
||||
"Helpdesk Administrator",
|
||||
]);
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input:eq(0)").toHaveAttribute(
|
||||
"placeholder",
|
||||
"Project Manager"
|
||||
);
|
||||
|
||||
await contains(".o_inner_group:eq(1) .o_group_info_button:eq(0)").click();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(queryAllTexts(".o_popover table td")).toEqual([
|
||||
"Project",
|
||||
"Project Manager",
|
||||
"Implied by",
|
||||
"- Helpdesk/Helpdesk Administrator",
|
||||
]);
|
||||
await contains(".o_inner_group:eq(1) .o_group_info_button:eq(1)").click();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(queryAllTexts(".o_popover table td")).toEqual([
|
||||
"Helpdesk",
|
||||
"Helpdesk Administrator",
|
||||
"Exclusively implies",
|
||||
"- Something related to project\n- Something related to helpdesk\n- Project/Project Manager",
|
||||
]);
|
||||
});
|
||||
|
||||
test("implied groups: lower level groups no longer available", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_inner_group:eq(1) .o_select_menu").toHaveCount(2);
|
||||
await contains(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).click();
|
||||
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveValue("Project User");
|
||||
expect(".o_select_menu_item").toHaveCount(3);
|
||||
expect(".o_inner_group:eq(1) .o_wrap_input:last-child input").toHaveValue("");
|
||||
await editSelectMenu(
|
||||
".o_field_widget[name='group_ids'] .o_inner_group:nth-child(2) .o_wrap_input:last-child input",
|
||||
{ value: "Helpdesk Administrator" }
|
||||
);
|
||||
|
||||
await contains(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).click();
|
||||
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveValue("");
|
||||
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveAttribute(
|
||||
"placeholder",
|
||||
"Project Manager"
|
||||
);
|
||||
expect(".o_select_menu_item").toHaveCount(2);
|
||||
await editSelectMenu(
|
||||
".o_field_widget[name='group_ids'] .o_inner_group:nth-child(2) .o_wrap_input:last-child input",
|
||||
{ value: "Helpdesk User" }
|
||||
);
|
||||
|
||||
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveValue("Project User");
|
||||
await contains(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).click();
|
||||
expect(".o_select_menu_item").toHaveCount(3);
|
||||
});
|
||||
|
||||
test("implied groups: lower level groups of same privilege still available", async () => {
|
||||
ResUsers._records[0].group_ids = [13];
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
await contains(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).click();
|
||||
expect(".o_select_menu_item").toHaveCount(3);
|
||||
});
|
||||
|
||||
test("do not lose shadowed groups when editing", async () => {
|
||||
ResUsers._records[0].group_ids = [11, 15];
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1]).toEqual({
|
||||
group_ids: [[6, false, [2, 15, 11]]],
|
||||
});
|
||||
expect.step("web_save");
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await contains(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).click();
|
||||
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveValue("");
|
||||
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveAttribute(
|
||||
"placeholder",
|
||||
"Project Manager"
|
||||
);
|
||||
expect(".o_select_menu_item").toHaveCount(2);
|
||||
|
||||
await editSelectMenu(".o_inner_group:eq(0) .o_wrap_input input", { value: "Settings " });
|
||||
await contains(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).click();
|
||||
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveValue("");
|
||||
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveAttribute(
|
||||
"placeholder",
|
||||
"Project Manager"
|
||||
);
|
||||
expect(".o_select_menu_item").toHaveCount(2);
|
||||
|
||||
await contains(".o_form_button_save").click();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("do not keep shadowed group if higher level group is set", async () => {
|
||||
ResUsers._records[0].group_ids = [11, 15];
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1]).toEqual({
|
||||
group_ids: [[6, false, [13, 15]]],
|
||||
});
|
||||
expect.step("web_save");
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await contains(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).click();
|
||||
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveValue("");
|
||||
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveAttribute(
|
||||
"placeholder",
|
||||
"Project Manager"
|
||||
);
|
||||
expect(".o_select_menu_item").toHaveCount(2);
|
||||
await editSelectMenu(".o_inner_group:eq(1) .o_wrap_input input", {
|
||||
value: "Project Administrator",
|
||||
});
|
||||
await contains(".o_form_button_save").click();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("disjoint groups", async () => {
|
||||
serverState.debug = "1";
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_group_info_button.fa-info-circle:not(.invisible)").toHaveCount(3);
|
||||
expect(".o_group_info_button.fa-exclamation-triangle:not(.invisible)").toHaveCount(0);
|
||||
expect(".o_is_disjoint").toHaveCount(0);
|
||||
|
||||
await contains(".o_inner_group:eq(3) input[type=checkbox]").click();
|
||||
expect(".o_group_info_button.fa-info-circle:not(.invisible)").toHaveCount(2);
|
||||
expect(".o_group_info_button.fa-exclamation-triangle:not(.invisible)").toHaveCount(2);
|
||||
expect(".o_is_disjoint").toHaveCount(2);
|
||||
|
||||
await contains(".o_inner_group:eq(3) .o_group_info_button").click();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(queryAllTexts(".o_popover table td")).toEqual([
|
||||
"Group",
|
||||
"Portal user",
|
||||
"Incompatibility",
|
||||
"- Internal user",
|
||||
]);
|
||||
});
|
||||
|
||||
test("privileges without category", async () => {
|
||||
Object.assign(ResUsers._records[0].view_group_hierarchy.privileges, {
|
||||
600: {
|
||||
id: 600,
|
||||
name: "Other privilege",
|
||||
sequence: 10,
|
||||
group_ids: [693, 694],
|
||||
category_id: false,
|
||||
},
|
||||
});
|
||||
Object.assign(ResUsers._records[0].view_group_hierarchy.groups, {
|
||||
693: {
|
||||
id: 693,
|
||||
name: "Group 1 in Other Privilege",
|
||||
all_implied_by_ids: [693],
|
||||
all_implied_ids: [693],
|
||||
comment: false,
|
||||
disjoint_ids: [],
|
||||
implied_ids: [],
|
||||
privilege_id: 600,
|
||||
},
|
||||
694: {
|
||||
id: 694,
|
||||
name: "Group 2 in Other Privilege",
|
||||
all_implied_by_ids: [694],
|
||||
all_implied_ids: [694],
|
||||
comment: false,
|
||||
disjoint_ids: [],
|
||||
implied_ids: [],
|
||||
privilege_id: 600,
|
||||
},
|
||||
});
|
||||
ResGroups._records.push({ id: 693, name: "Group 1 in Other Privilege" });
|
||||
ResGroups._records.push({ id: 694, name: "Group 2 in Other Privilege" });
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].group_ids).toEqual([[6, false, [1, 11, 694, 91]]]);
|
||||
expect.step("web_save");
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="group_ids" widget="res_user_group_ids"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resModel: "res.users",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=group_ids] .o_group").toHaveCount(1);
|
||||
expect(".o_field_widget[name=group_ids] .o_group .o_inner_group").toHaveCount(3);
|
||||
expect(
|
||||
".o_field_widget[name=group_ids] .o_inner_group:eq(2) .o_horizontal_separator"
|
||||
).toHaveText("OTHER");
|
||||
expect(".o_field_widget[name=group_ids] .o_inner_group:eq(2) .o_form_label").toHaveText(
|
||||
"Other privilege"
|
||||
);
|
||||
await contains(".o_field_widget[name='group_ids'] .o_inner_group:eq(2) input").click();
|
||||
expect(`.o_select_menu_item`).toHaveCount(2);
|
||||
await editSelectMenu(".o_field_widget[name='group_ids'] .o_inner_group:eq(2) input", {
|
||||
value: "Group 2 in Other Privilege",
|
||||
});
|
||||
await contains(`.o_form_button_save`).click();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { tick } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { router } from "@web/core/browser/router";
|
||||
import { redirect } from "@web/core/utils/urls";
|
||||
|
||||
class ResConfigSettings extends models.Model {
|
||||
_name = "res.config.settings";
|
||||
bar = fields.Boolean();
|
||||
}
|
||||
defineModels([ResConfigSettings]);
|
||||
|
||||
test("Simple render", async () => {
|
||||
onRpc("/base_setup/demo_active", () => {
|
||||
return true;
|
||||
});
|
||||
redirect("/odoo");
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: /* xml */ `
|
||||
<form js_class="base_settings">
|
||||
<app string="MyApp" name="my_app">
|
||||
<widget name='res_config_dev_tool'/>
|
||||
</app>
|
||||
</form>`,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
expect(router.current).toEqual({});
|
||||
expect(".o_widget_res_config_dev_tool").toHaveCount(1);
|
||||
expect(queryAllTexts`#developer_tool h2`).toEqual(["Developer Tools"]);
|
||||
expect(queryAllTexts`#developer_tool .o_setting_right_pane .d-block`).toEqual([
|
||||
"Activate the developer mode",
|
||||
"Activate the developer mode (with assets)",
|
||||
"Activate the developer mode (with tests assets)",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Activate the developer mode", async () => {
|
||||
onRpc("/base_setup/demo_active", () => {
|
||||
return true;
|
||||
});
|
||||
patchWithCleanup(browser.location, {
|
||||
reload() {
|
||||
expect.step("location reload");
|
||||
},
|
||||
});
|
||||
redirect("/odoo");
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: /* xml */ `
|
||||
<form js_class="base_settings">
|
||||
<app string="MyApp" name="my_app">
|
||||
<widget name='res_config_dev_tool'/>
|
||||
</app>
|
||||
</form>`,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
expect(router.current).toEqual({});
|
||||
await click("a:contains('Activate the developer mode')");
|
||||
await tick();
|
||||
expect(router.current).toEqual({ debug: 1 });
|
||||
expect.verifySteps(["location reload"]);
|
||||
});
|
||||
|
||||
test("Activate the developer mode (with assets)", async () => {
|
||||
onRpc("/base_setup/demo_active", () => {
|
||||
return true;
|
||||
});
|
||||
patchWithCleanup(browser.location, {
|
||||
reload() {
|
||||
expect.step("location reload");
|
||||
},
|
||||
});
|
||||
redirect("/odoo");
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: /* xml */ `
|
||||
<form js_class="base_settings">
|
||||
<app string="MyApp" name="my_app">
|
||||
<widget name='res_config_dev_tool'/>
|
||||
</app>
|
||||
</form>`,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
expect(router.current).toEqual({});
|
||||
await click("a:contains('Activate the developer mode (with assets)')");
|
||||
await tick();
|
||||
expect(router.current).toEqual({ debug: "assets" });
|
||||
expect.verifySteps(["location reload"]);
|
||||
});
|
||||
|
||||
test("Activate the developer mode (with tests assets)", async () => {
|
||||
onRpc("/base_setup/demo_active", () => {
|
||||
return true;
|
||||
});
|
||||
patchWithCleanup(browser.location, {
|
||||
reload() {
|
||||
expect.step("location reload");
|
||||
},
|
||||
});
|
||||
redirect("/odoo");
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: /* xml */ `
|
||||
<form js_class="base_settings">
|
||||
<app string="MyApp" name="my_app">
|
||||
<widget name='res_config_dev_tool'/>
|
||||
</app>
|
||||
</form>`,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
expect(router.current).toEqual({});
|
||||
|
||||
await click("a:contains('Activate the developer mode (with tests assets)')");
|
||||
await tick();
|
||||
expect(router.current).toEqual({ debug: "assets,tests" });
|
||||
expect.verifySteps(["location reload"]);
|
||||
});
|
||||
|
||||
test("Activate the developer modeddd (with tests assets)", async () => {
|
||||
serverState.debug = "assets,tests";
|
||||
onRpc("/base_setup/demo_active", () => {
|
||||
return true;
|
||||
});
|
||||
patchWithCleanup(browser.location, {
|
||||
reload() {
|
||||
expect.step("location reload");
|
||||
},
|
||||
});
|
||||
redirect("/odoo?debug=assets%2Ctests");
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: /* xml */ `
|
||||
<form js_class="base_settings">
|
||||
<app string="MyApp" name="my_app">
|
||||
<widget name='res_config_dev_tool'/>
|
||||
</app>
|
||||
</form>`,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
expect(router.current).toEqual({ debug: "assets,tests" });
|
||||
|
||||
expect(queryAllTexts`#developer_tool .o_setting_right_pane .d-block`).toEqual([
|
||||
"Deactivate the developer mode",
|
||||
]);
|
||||
|
||||
await click("a:contains('Deactivate the developer mode')");
|
||||
await tick();
|
||||
expect(router.current).toEqual({ debug: 0 });
|
||||
expect.verifySteps(["location reload"]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,107 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class ResConfigSettings extends models.Model {
|
||||
_name = "res.config.settings";
|
||||
bar = fields.Boolean({ string: "Bar" });
|
||||
}
|
||||
defineModels([ResConfigSettings]);
|
||||
|
||||
test("widget upgrade_boolean in a form view - dialog", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: /* xml */ `
|
||||
<form js_class="base_settings">
|
||||
<app string="CRM" name="crm">
|
||||
<field name="bar" widget="upgrade_boolean"/>
|
||||
</app>
|
||||
</form>`,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
|
||||
await click(".o-checkbox .form-check-input");
|
||||
await animationFrame();
|
||||
expect(".o_dialog .modal").toHaveCount(1, {
|
||||
message: "the 'Upgrade to Enterprise' dialog should be opened",
|
||||
});
|
||||
});
|
||||
|
||||
test("widget upgrade_boolean in a form view - label", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: /* xml */ `
|
||||
<form js_class="base_settings">
|
||||
<app string="CRM" name="crm">
|
||||
<setting string="Coucou">
|
||||
<field name="bar" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
</app>
|
||||
</form>`,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
|
||||
expect(".o_field .badge").toHaveCount(0, {
|
||||
message: "the upgrade badge shouldn't be inside the field section",
|
||||
});
|
||||
expect(".o_form_label .badge").toHaveCount(1, {
|
||||
message: "the upgrade badge should be inside the label section",
|
||||
});
|
||||
expect(".o_form_label").toHaveText("Coucou\nEnterprise", {
|
||||
message: "the upgrade label should be inside the label section",
|
||||
});
|
||||
});
|
||||
|
||||
test("widget upgrade_boolean in a form view - dialog (enterprise version)", async () => {
|
||||
patchWithCleanup(odoo, { info: { isEnterprise: 1 } });
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: /* xml */ `
|
||||
<form js_class="base_settings">
|
||||
<app string="CRM" name="crm">
|
||||
<field name="bar" widget="upgrade_boolean"/>
|
||||
</app>
|
||||
</form>`,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
|
||||
await click(".o-checkbox .form-check-input");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dialog .modal").toHaveCount(0, {
|
||||
message: "the 'Upgrade to Enterprise' dialog shouldn't be opened",
|
||||
});
|
||||
});
|
||||
|
||||
test("widget upgrade_boolean in a form view - label (enterprise version)", async () => {
|
||||
patchWithCleanup(odoo, { info: { isEnterprise: 1 } });
|
||||
await mountView({
|
||||
type: "form",
|
||||
arch: /* xml */ `
|
||||
<form js_class="base_settings">
|
||||
<app string="CRM" name="crm">
|
||||
<setting string="Coucou">
|
||||
<field name="bar" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
</app>
|
||||
</form>`,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
|
||||
expect(".o_field .badge").toHaveCount(0, {
|
||||
message: "the upgrade badge shouldn't be inside the field section",
|
||||
});
|
||||
expect(".o_form_label .badge").toHaveCount(0, {
|
||||
message: "the upgrade badge shouldn't be inside the label section",
|
||||
});
|
||||
expect(".o_form_label").toHaveText("Coucou", {
|
||||
message: "the label shouldn't contains the upgrade label",
|
||||
});
|
||||
});
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("SettingsUpgradeBoolean", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
"res.config.settings": {
|
||||
fields: {
|
||||
bar: { string: "Bar", type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
});
|
||||
QUnit.test("widget upgrade_boolean in a form view - dialog", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form js_class="base_settings">
|
||||
<field name="bar" widget="upgrade_boolean"/>
|
||||
</form>`,
|
||||
serverData,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
|
||||
await click(target.querySelector(".o-checkbox .form-check-input"));
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_dialog .modal",
|
||||
"the 'Upgrade to Enterprise' dialog should be opened"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("widget upgrade_boolean in a form view - label", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form js_class="base_settings">
|
||||
<div class="o_field">
|
||||
<field name="bar" widget="upgrade_boolean"/>
|
||||
</div>
|
||||
<div class="o_label"><label for="bar"/><div>Coucou</div></div>
|
||||
</form>`,
|
||||
serverData,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field .badge",
|
||||
"the upgrade badge shouldn't be inside the field section"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_label .badge",
|
||||
"the upgrade badge should be inside the label section"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_label").textContent,
|
||||
"BarEnterpriseCoucou",
|
||||
"the upgrade label should be inside the label section"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"widget upgrade_boolean in a form view - dialog (enterprise version)",
|
||||
async function (assert) {
|
||||
patchWithCleanup(odoo, { info: { isEnterprise: 1 } });
|
||||
await makeView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form js_class="base_settings">
|
||||
<field name="bar" widget="upgrade_boolean"/>
|
||||
</form>`,
|
||||
serverData,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
|
||||
await click(target.querySelector(".o-checkbox .form-check-input"));
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_dialog .modal",
|
||||
"the 'Upgrade to Enterprise' dialog shouldn't be opened"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"widget upgrade_boolean in a form view - label (enterprise version)",
|
||||
async function (assert) {
|
||||
patchWithCleanup(odoo, { info: { isEnterprise: 1 } });
|
||||
await makeView({
|
||||
type: "form",
|
||||
arch: `
|
||||
<form js_class="base_settings">
|
||||
<div class="o_field">
|
||||
<field name="bar" widget="upgrade_boolean"/>
|
||||
</div>
|
||||
<div class="o_label"><label for="bar"/><div>Coucou</div></div>
|
||||
</form>`,
|
||||
serverData,
|
||||
resModel: "res.config.settings",
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field .badge",
|
||||
"the upgrade badge shouldn't be inside the field section"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_label .badge",
|
||||
"the upgrade badge shouldn't be inside the label section"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_label").textContent,
|
||||
"BarCoucou",
|
||||
"the label shouldn't contains the upgrade label"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,646 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { edit, keyDown, press, queryAllAttributes, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { cookie } from "@web/core/browser/cookie";
|
||||
import { user } from "@web/core/user";
|
||||
import { SwitchCompanyMenu } from "@web/webclient/switch_company_menu/switch_company_menu";
|
||||
|
||||
const ORIGINAL_TOGGLE_DELAY = SwitchCompanyMenu.toggleDelay;
|
||||
|
||||
async function createSwitchCompanyMenu(options = { toggleDelay: 0 }) {
|
||||
patchWithCleanup(SwitchCompanyMenu, { toggleDelay: options.toggleDelay });
|
||||
await mountWithCleanup(SwitchCompanyMenu);
|
||||
}
|
||||
|
||||
function patchUserActiveCompanies(cids) {
|
||||
patchWithCleanup(
|
||||
user.activeCompanies,
|
||||
cids.map((cid) => serverState.companies.find((company) => company.id === cid))
|
||||
);
|
||||
}
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
const clickConfirm = () => contains(".o_switch_company_menu_buttons button:first").click();
|
||||
|
||||
const openCompanyMenu = () => contains(".dropdown-toggle").click();
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
*/
|
||||
const toggleCompany = (index) =>
|
||||
contains(`[data-company-id] [role=menuitemcheckbox]:eq(${index})`).click();
|
||||
|
||||
beforeEach(() => {
|
||||
cookie.set("cids", "3");
|
||||
serverState.companies = [
|
||||
{ id: 3, name: "Hermit", sequence: 1, parent_id: false, child_ids: [] },
|
||||
{ id: 2, name: "Herman's", sequence: 2, parent_id: false, child_ids: [] },
|
||||
{ id: 1, name: "Heroes TM", sequence: 3, parent_id: false, child_ids: [4, 5] },
|
||||
{ id: 4, name: "Hercules", sequence: 4, parent_id: 1, child_ids: [] },
|
||||
{ id: 5, name: "Hulk", sequence: 5, parent_id: 1, child_ids: [] },
|
||||
];
|
||||
});
|
||||
|
||||
test("basic rendering", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
expect("div.o_switch_company_menu").toHaveCount(1);
|
||||
expect("div.o_switch_company_menu").toHaveText("Hermit");
|
||||
|
||||
await openCompanyMenu();
|
||||
|
||||
expect("[data-company-id] [role=menuitemcheckbox]").toHaveCount(5);
|
||||
expect(".log_into").toHaveCount(5);
|
||||
expect(".fa-check-square").toHaveCount(1);
|
||||
expect(".fa-square-o").toHaveCount(4);
|
||||
expect(".dropdown-item:has(.fa-check-square)").toHaveText("Hermit");
|
||||
expect(".dropdown-item:has(.fa-square-o):eq(0)").toHaveText("Herman's");
|
||||
expect(".dropdown-menu").toHaveText("Hermit\nHerman's\nHeroes TM\nHercules\nHulk");
|
||||
});
|
||||
|
||||
test("companies can be toggled: toggle a second company", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([3]);
|
||||
expect(user.activeCompany.id).toBe(3);
|
||||
await openCompanyMenu();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(4);
|
||||
expect(queryAllAttributes("[data-company-id] [role=menuitemcheckbox]", "aria-checked")).toEqual(
|
||||
["true", "false", "false", "false", "false"]
|
||||
);
|
||||
expect(queryAllAttributes("[data-company-id] .log_into", "aria-pressed")).toEqual([
|
||||
"true",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
]);
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [x] Herman's -> toggle
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
await toggleCompany(1);
|
||||
expect(".dropdown-menu").toHaveCount(1, { message: "dropdown is still opened" });
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(2);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(3);
|
||||
expect(queryAllAttributes("[data-company-id] [role=menuitemcheckbox]", "aria-checked")).toEqual(
|
||||
["true", "true", "false", "false", "false"]
|
||||
);
|
||||
expect(queryAllAttributes("[data-company-id] .log_into", "aria-pressed")).toEqual([
|
||||
"true",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
"false",
|
||||
]);
|
||||
await clickConfirm();
|
||||
expect(cookie.get("cids")).toEqual("3-2");
|
||||
});
|
||||
|
||||
test("can toggle multiple companies at once", async () => {
|
||||
await createSwitchCompanyMenu({ toggleDelay: ORIGINAL_TOGGLE_DELAY });
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([3]);
|
||||
expect(user.activeCompany.id).toBe(3);
|
||||
await openCompanyMenu();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(4);
|
||||
|
||||
/**
|
||||
* [ ] Hermit -> toggle all
|
||||
* [x] **Herman's** -> toggle all
|
||||
* [x] Heroes TM -> toggle all
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
await toggleCompany(0);
|
||||
await toggleCompany(1);
|
||||
await toggleCompany(2);
|
||||
expect(".dropdown-menu").toHaveCount(1, { message: "dropdown is still opened" });
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(4);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(1);
|
||||
|
||||
expect.verifySteps([]);
|
||||
await clickConfirm();
|
||||
expect(cookie.get("cids")).toEqual("2-1-4-5");
|
||||
});
|
||||
|
||||
test("single company selected: toggling it off will keep it", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
await runAllTimers();
|
||||
expect(cookie.get("cids")).toBe("3");
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([3]);
|
||||
expect(user.activeCompany.id).toBe(3);
|
||||
await openCompanyMenu();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(4);
|
||||
|
||||
/**
|
||||
* [x] **Hermit** -> toggle off
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
await toggleCompany(0);
|
||||
await clickConfirm();
|
||||
await animationFrame();
|
||||
expect(cookie.get("cids")).toEqual("3");
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([3]);
|
||||
expect(user.activeCompany.id).toBe(3);
|
||||
|
||||
await openCompanyMenu();
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(4);
|
||||
});
|
||||
|
||||
test("single company mode: companies can be logged in", async () => {
|
||||
await createSwitchCompanyMenu({ toggleDelay: ORIGINAL_TOGGLE_DELAY });
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([3]);
|
||||
expect(user.activeCompany.id).toBe(3);
|
||||
await openCompanyMenu();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(4);
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [x] **Herman's** -> log into
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
await contains(".log_into:eq(1)").click();
|
||||
expect(".dropdown-menu").toHaveCount(0, { message: "dropdown is directly closed" });
|
||||
expect(cookie.get("cids")).toEqual("2");
|
||||
});
|
||||
|
||||
test("multi company mode: log into a non selected company", async () => {
|
||||
patchUserActiveCompanies([3, 1]);
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [x] Hermit
|
||||
* [ ] Herman's
|
||||
* [x] **Heroes TM**
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([3, 1]);
|
||||
expect(user.activeCompany.id).toBe(3);
|
||||
await openCompanyMenu();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(2);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(3);
|
||||
|
||||
/**
|
||||
* [x] Hermit
|
||||
* [x] **Herman's** -> log into
|
||||
* [x] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
await contains(".log_into:eq(1)").click();
|
||||
expect(".dropdown-menu").toHaveCount(0, { message: "dropdown is directly closed" });
|
||||
expect(cookie.get("cids")).toEqual("2-1-3"); // 1-3 in that order, they are sorted
|
||||
});
|
||||
|
||||
test("multi company mode: log into an already selected company", async () => {
|
||||
patchUserActiveCompanies([2, 1]);
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [x] **Herman's**
|
||||
* [x] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([2, 1]);
|
||||
expect(user.activeCompany.id).toBe(2);
|
||||
await openCompanyMenu();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(2);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(3);
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [x] Herman's
|
||||
* [x] **Heroes TM** -> log into
|
||||
* [x] Hercules
|
||||
* [x] Hulk
|
||||
*/
|
||||
await contains(".log_into:eq(2)").click();
|
||||
expect(".dropdown-menu").toHaveCount(0, { message: "dropdown is directly closed" });
|
||||
expect(cookie.get("cids")).toEqual("1-2-4-5");
|
||||
});
|
||||
|
||||
test("companies can be logged in even if some toggled within delay", async () => {
|
||||
await createSwitchCompanyMenu({ toggleDelay: ORIGINAL_TOGGLE_DELAY });
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([3]);
|
||||
expect(user.activeCompany.id).toBe(3);
|
||||
await openCompanyMenu();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(4);
|
||||
|
||||
/**
|
||||
* [ ] Hermit -> toggled
|
||||
* [x] **Herman's** -> logged in
|
||||
* [ ] Heroes TM -> toggled
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
await contains("[data-company-id] [role=menuitemcheckbox]:eq(2)").click();
|
||||
await contains("[data-company-id] [role=menuitemcheckbox]:eq(0)").click();
|
||||
await contains(".log_into:eq(1)").click();
|
||||
expect(".dropdown-menu").toHaveCount(0, { message: "dropdown is directly closed" });
|
||||
expect(cookie.get("cids")).toEqual("2");
|
||||
});
|
||||
|
||||
test("always show the name of the company on the top right of the app", async () => {
|
||||
// initialize a single company
|
||||
const companyName = "Single company";
|
||||
serverState.companies = [
|
||||
{ id: 1, name: companyName, sequence: 1, parent_id: false, child_ids: [] },
|
||||
];
|
||||
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
// in case of a single company, drop down button should be displayed but disabled
|
||||
expect(".dropdown-toggle").toBeVisible();
|
||||
expect(".dropdown-toggle").not.toBeEnabled();
|
||||
expect(".dropdown-toggle").toHaveText(companyName);
|
||||
});
|
||||
|
||||
test("single company mode: from company loginto branch", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([3]);
|
||||
expect(user.activeCompany.id).toBe(3);
|
||||
await contains(".dropdown-toggle").click();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(4);
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [ ] Herman's
|
||||
* [x] **Heroes TM** -> log into
|
||||
* [x] Hercules
|
||||
* [x] Hulk
|
||||
*/
|
||||
await contains(".log_into:eq(2)").click();
|
||||
expect(cookie.get("cids")).toEqual("1-4-5");
|
||||
});
|
||||
|
||||
test("single company mode: from branch loginto company", async () => {
|
||||
patchUserActiveCompanies([1, 4, 5]);
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [ ] Herman's
|
||||
* [x] **Heroes TM**
|
||||
* [x] Hercules
|
||||
* [x] Hulk
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([1, 4, 5]);
|
||||
expect(user.activeCompany.id).toBe(1);
|
||||
await contains(".dropdown-toggle").click();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(3);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(2);
|
||||
|
||||
/**
|
||||
* [x] Hermit -> log into
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
await contains(".log_into:eq(0)").click();
|
||||
expect(cookie.get("cids")).toEqual("3");
|
||||
});
|
||||
|
||||
test("single company mode: from leaf (only one company in branch selected) loginto company", async () => {
|
||||
patchUserActiveCompanies([1]);
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [ ] Herman's
|
||||
* [x] **Heroes TM**
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([1]);
|
||||
expect(user.activeCompany.id).toBe(1);
|
||||
await contains(".dropdown-toggle").click();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(1);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(4);
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [x] **Herman's** -> log into
|
||||
* [ ] Heroes TM
|
||||
* [ ] Hercules
|
||||
* [ ] Hulk
|
||||
*/
|
||||
await contains(".log_into:eq(1)").click();
|
||||
expect(cookie.get("cids")).toEqual("2");
|
||||
});
|
||||
|
||||
test("multi company mode: switching company doesn't deselect already selected ones", async () => {
|
||||
patchUserActiveCompanies([1, 2, 4, 5]);
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [x] Herman's
|
||||
* [x] **Heroes TM**
|
||||
* [x] Hercules
|
||||
* [x] Hulk
|
||||
*/
|
||||
expect(user.activeCompanies.map((c) => c.id)).toEqual([1, 2, 4, 5]);
|
||||
expect(user.activeCompany.id).toBe(1);
|
||||
await contains(".dropdown-toggle").click();
|
||||
expect("[data-company-id]").toHaveCount(5);
|
||||
expect("[data-company-id] .fa-check-square").toHaveCount(4);
|
||||
expect("[data-company-id] .fa-square-o").toHaveCount(1);
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [x] **Herman's** -> log into
|
||||
* [x] Heroes TM
|
||||
* [x] Hercules
|
||||
* [x] Hulk
|
||||
*/
|
||||
await contains(".log_into:eq(1)").click();
|
||||
expect(cookie.get("cids")).toEqual("2-1-4-5");
|
||||
});
|
||||
|
||||
test("show confirm and reset buttons only when selection has changed", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
await openCompanyMenu();
|
||||
|
||||
expect(".o_switch_company_menu_buttons").toHaveCount(0);
|
||||
|
||||
await toggleCompany(1);
|
||||
expect(".o_switch_company_menu_buttons button").toHaveCount(2);
|
||||
|
||||
await toggleCompany(1);
|
||||
expect(".o_switch_company_menu_buttons").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("no search input when less that 10 companies", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
await openCompanyMenu();
|
||||
expect(".o-dropdown--menu .visually-hidden input").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("show search input when more that 10 companies & search filters items but ignore case and spaces", async () => {
|
||||
serverState.companies = [
|
||||
{ id: 3, name: "Hermit", sequence: 1, parent_id: false, child_ids: [] },
|
||||
{ id: 2, name: "Herman's", sequence: 2, parent_id: false, child_ids: [] },
|
||||
{ id: 1, name: "Heroes TM", sequence: 3, parent_id: false, child_ids: [4, 5] },
|
||||
{ id: 4, name: "Hercules", sequence: 4, parent_id: 1, child_ids: [] },
|
||||
{ id: 5, name: "Hulk", sequence: 5, parent_id: 1, child_ids: [] },
|
||||
{ id: 6, name: "Random Company a", sequence: 6, parent_id: false, child_ids: [7, 8] },
|
||||
{ id: 7, name: "Random Company aa", sequence: 7, parent_id: 6, child_ids: [] },
|
||||
{ id: 8, name: "Random Company ab", sequence: 8, parent_id: 6, child_ids: [] },
|
||||
{ id: 9, name: "Random d", sequence: 9, parent_id: false, child_ids: [] },
|
||||
{ id: 10, name: "Random e", sequence: 10, parent_id: false, child_ids: [] },
|
||||
];
|
||||
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
await openCompanyMenu();
|
||||
expect(".o-dropdown--menu input").toHaveCount(1);
|
||||
expect(".o-dropdown--menu input").toBeFocused();
|
||||
expect(".o-dropdown--menu .o_switch_company_item").toHaveCount(10);
|
||||
|
||||
await edit("omcom");
|
||||
await animationFrame();
|
||||
expect(".o-dropdown--menu .o_switch_company_item").toHaveCount(3);
|
||||
|
||||
expect(queryAllTexts(".o-dropdown--menu .o_switch_company_item")).toEqual([
|
||||
"Random Company a",
|
||||
"Random Company aa",
|
||||
"Random Company ab",
|
||||
]);
|
||||
});
|
||||
|
||||
test("when less than 10 companies, typing key makes the search input visible", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
await openCompanyMenu();
|
||||
|
||||
expect(".o-dropdown--menu input").toHaveCount(1);
|
||||
expect(".o-dropdown--menu input").toBeFocused();
|
||||
expect(".o-dropdown--menu .visually-hidden input").toHaveCount(1);
|
||||
|
||||
await edit("a");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o-dropdown--menu input").toHaveValue("a");
|
||||
expect(".o-dropdown--menu :not(.visually-hidden) input").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("focus required");
|
||||
test("navigation with search input", async () => {
|
||||
serverState.companies = [
|
||||
{ id: 3, name: "Hermit", sequence: 1, parent_id: false, child_ids: [] },
|
||||
{ id: 2, name: "Herman's", sequence: 2, parent_id: false, child_ids: [] },
|
||||
{ id: 1, name: "Heroes TM", sequence: 3, parent_id: false, child_ids: [4, 5] },
|
||||
{ id: 4, name: "Hercules", sequence: 4, parent_id: 1, child_ids: [] },
|
||||
{ id: 5, name: "Hulk", sequence: 5, parent_id: 1, child_ids: [] },
|
||||
{ id: 6, name: "Random Company a", sequence: 6, parent_id: false, child_ids: [7, 8] },
|
||||
{ id: 7, name: "Random Company aa", sequence: 7, parent_id: 6, child_ids: [] },
|
||||
{ id: 8, name: "Random Company ab", sequence: 8, parent_id: 6, child_ids: [] },
|
||||
{ id: 9, name: "Random d", sequence: 9, parent_id: false, child_ids: [] },
|
||||
{ id: 10, name: "Random e", sequence: 10, parent_id: false, child_ids: [] },
|
||||
];
|
||||
|
||||
await createSwitchCompanyMenu();
|
||||
await openCompanyMenu();
|
||||
|
||||
expect(".o-dropdown--menu input").toBeFocused();
|
||||
expect(".o_switch_company_item.focus").toHaveCount(0);
|
||||
|
||||
const navigationSteps = [
|
||||
{ hotkey: "arrowdown", focused: 1, selectedCompanies: [3] }, // Go to first item
|
||||
{ hotkey: "arrowup", focused: 0 }, // Go to search input
|
||||
{ hotkey: "arrowup", focused: 10 }, // Go to last item
|
||||
{ hotkey: "Space", focused: 10, selectedCompanies: [3, 10] }, // Select last item
|
||||
{ hotkey: ["shift", "tab"], focused: 9, selectedCompanies: [3, 10] }, // Go to previous item
|
||||
{ hotkey: "tab", focused: 10, selectedCompanies: [3, 10] }, // Go to next item
|
||||
{ hotkey: "arrowdown", focused: 11 }, // Go to Confirm
|
||||
{ hotkey: "arrowdown", focused: 12 }, // Go to Reset
|
||||
{ hotkey: "enter", focused: 10, selectedCompanies: [3] }, // Reset, focus is on last item
|
||||
{ hotkey: "arrowdown", focused: 0 }, // Go to seach input
|
||||
{ input: "a", focused: 0 }, // Type "a"
|
||||
{ hotkey: "arrowdown", focused: 1 }, // Go to first item
|
||||
{ hotkey: "Space", focused: 1, selectedCompanies: [2] }, // Select first item
|
||||
];
|
||||
|
||||
for (const navigationStep of navigationSteps) {
|
||||
expect.step(navigationStep);
|
||||
const { hotkey, focused, selectedCompanies, input } = navigationStep;
|
||||
if (hotkey) {
|
||||
await press(hotkey);
|
||||
}
|
||||
if (input) {
|
||||
await edit(input);
|
||||
}
|
||||
|
||||
// Ensure debounced mutation listener update and owl re-render
|
||||
await animationFrame();
|
||||
await runAllTimers();
|
||||
|
||||
expect(`.o_popover .o-navigable:eq(${focused})`).toHaveClass("focus");
|
||||
expect(`.o_popover .o-navigable:eq(${focused})`).toBeFocused();
|
||||
|
||||
if (selectedCompanies) {
|
||||
expect(
|
||||
queryAllAttributes(
|
||||
".o_switch_company_item:has([role=menuitemcheckbox][aria-checked=true])",
|
||||
"data-company-id"
|
||||
).map(Number)
|
||||
).toEqual(selectedCompanies);
|
||||
}
|
||||
}
|
||||
|
||||
await keyDown(["control", "enter"]);
|
||||
await animationFrame();
|
||||
|
||||
expect(cookie.get("cids")).toEqual("3-2");
|
||||
expect(".o_switch_company_item").toHaveCount(0);
|
||||
expect.verifySteps(navigationSteps);
|
||||
});
|
||||
|
||||
test("select and de-select all", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
await openCompanyMenu();
|
||||
|
||||
// Show search
|
||||
await edit(" ");
|
||||
await animationFrame();
|
||||
|
||||
// One company is selected, there should be a check box with minus inside
|
||||
expect("[role=menuitemcheckbox][title='Deselect all'] i").toHaveClass("fa-minus-square-o");
|
||||
|
||||
await contains("[role=menuitemcheckbox][title='Deselect all']").click();
|
||||
// No company is selected, there should be a empty check box
|
||||
expect("[role=menuitemcheckbox][title='Select all'] i").toHaveClass("fa-square-o");
|
||||
expect(".o_switch_company_item:has([role=menuitemcheckbox][aria-checked=true])").toHaveCount(0);
|
||||
|
||||
await contains("[role=menuitemcheckbox][title='Select all']").click();
|
||||
// All companies are selected, there should be a checked check box
|
||||
expect("[role=menuitemcheckbox][title='Deselect all'] i").toHaveClass("fa-check-square");
|
||||
expect(".o_switch_company_item:has([role=menuitemcheckbox][aria-checked=true])").toHaveCount(5);
|
||||
|
||||
await contains("[role=menuitemcheckbox][title='Deselect all']").click();
|
||||
// No company is selected, there should be a empty check box
|
||||
expect("[role=menuitemcheckbox][title='Select all'] i").toHaveClass("fa-square-o");
|
||||
expect(".o_switch_company_item:has([role=menuitemcheckbox][aria-checked=true])").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("de-select only changes visible companies", async () => {
|
||||
await createSwitchCompanyMenu();
|
||||
await openCompanyMenu();
|
||||
|
||||
// Show search
|
||||
await edit(" ");
|
||||
await toggleCompany(4);
|
||||
expect(".o_switch_company_item:has([role=menuitemcheckbox][aria-checked=true])").toHaveCount(2);
|
||||
|
||||
// Show search
|
||||
await contains("input").edit("m");
|
||||
await animationFrame();
|
||||
|
||||
// One company is selected, unselect all
|
||||
await contains("[role=menuitemcheckbox][title='Deselect all']").click();
|
||||
expect(".o_switch_company_item:has([role=menuitemcheckbox][aria-checked=true])").toHaveCount(0);
|
||||
|
||||
// Hidden company is still selected
|
||||
await contains("input").clear();
|
||||
await animationFrame();
|
||||
expect(".o_switch_company_item:has([role=menuitemcheckbox][aria-checked=true])").toHaveCount(1);
|
||||
|
||||
// Filter and select all visible companies
|
||||
await contains("input").edit("m");
|
||||
await animationFrame();
|
||||
await contains("[role=menuitemcheckbox][title='Select all']").click();
|
||||
expect(".o_switch_company_item:has([role=menuitemcheckbox][aria-checked=true])").toHaveCount(3);
|
||||
|
||||
// Hidden company is unchanged
|
||||
await contains("input").clear();
|
||||
await animationFrame();
|
||||
expect(".o_switch_company_item:has([role=menuitemcheckbox][aria-checked=true])").toHaveCount(4);
|
||||
expect(".o_switch_company_item:has([role=menuitemcheckbox][aria-checked=false])").toHaveCount(
|
||||
1
|
||||
);
|
||||
});
|
||||
|
|
@ -1,333 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { SwitchCompanyMenu } from "@web/webclient/switch_company_menu/switch_company_menu";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { companyService } from "@web/webclient/company_service";
|
||||
import { click, getFixture, makeDeferred, mount, patchWithCleanup } from "../helpers/utils";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { session } from "@web/session";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
|
||||
const ORIGINAL_TOGGLE_DELAY = SwitchCompanyMenu.toggleDelay;
|
||||
async function createSwitchCompanyMenu(routerParams = {}, toggleDelay = 0) {
|
||||
patchWithCleanup(SwitchCompanyMenu, { toggleDelay });
|
||||
if (routerParams.onPushState) {
|
||||
const pushState = browser.history.pushState;
|
||||
patchWithCleanup(browser, {
|
||||
history: Object.assign({}, browser.history, {
|
||||
pushState(state, title, url) {
|
||||
pushState(...arguments);
|
||||
if (routerParams.onPushState) {
|
||||
routerParams.onPushState(url);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
const env = await makeTestEnv();
|
||||
const scMenu = await mount(SwitchCompanyMenu, target, { env });
|
||||
return scMenu;
|
||||
}
|
||||
|
||||
QUnit.module("SwitchCompanyMenu", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
patchWithCleanup(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,
|
||||
});
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("company", companyService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test("basic rendering", async (assert) => {
|
||||
assert.expect(9);
|
||||
|
||||
await createSwitchCompanyMenu();
|
||||
|
||||
assert.containsOnce(target, "div.o_switch_company_menu");
|
||||
assert.strictEqual(target.querySelector("div.o_switch_company_menu").textContent, "Hermit");
|
||||
|
||||
await click(target.querySelector(".dropdown-toggle"));
|
||||
assert.containsN(target, ".toggle_company", 3);
|
||||
assert.containsN(target, ".log_into", 3);
|
||||
assert.containsOnce(target, ".fa-check-square");
|
||||
assert.containsN(target, ".fa-square-o", 2);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".fa-check-square").closest(".dropdown-item").textContent,
|
||||
"Hermit"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".fa-square-o").closest(".dropdown-item").textContent,
|
||||
"Herman's"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".dropdown-menu").textContent,
|
||||
"HermitHerman'sHeroes TM"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("companies can be toggled: toggle a second company", async (assert) => {
|
||||
const prom = makeDeferred();
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
prom.resolve();
|
||||
}
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState });
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
|
||||
await click(target.querySelector(".dropdown-toggle"));
|
||||
assert.containsN(target, "[data-company-id]", 3);
|
||||
assert.containsN(target, "[data-company-id] .fa-check-square", 1);
|
||||
assert.containsN(target, "[data-company-id] .fa-square-o", 2);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("[data-company-id] .toggle_company")].map(
|
||||
(el) => el.ariaChecked
|
||||
),
|
||||
["true", "false", "false"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("[data-company-id] .log_into")].map((el) => el.ariaPressed),
|
||||
["true", "false", "false"]
|
||||
);
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [x] Herman's -> toggle
|
||||
* [ ] Heroes TM
|
||||
*/
|
||||
await click(target.querySelectorAll(".toggle_company")[1]);
|
||||
assert.containsOnce(target, ".dropdown-menu", "dropdown is still opened");
|
||||
assert.containsN(target, "[data-company-id] .fa-check-square", 2);
|
||||
assert.containsN(target, "[data-company-id] .fa-square-o", 1);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("[data-company-id] .toggle_company")].map(
|
||||
(el) => el.ariaChecked
|
||||
),
|
||||
["true", "true", "false"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("[data-company-id] .log_into")].map((el) => el.ariaPressed),
|
||||
["true", "false", "false"]
|
||||
);
|
||||
await prom;
|
||||
assert.verifySteps(["cids=3%2C2"]);
|
||||
});
|
||||
|
||||
QUnit.test("can toggle multiple companies at once", async (assert) => {
|
||||
assert.expect(11);
|
||||
|
||||
const prom = makeDeferred();
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
prom.resolve();
|
||||
}
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState }, ORIGINAL_TOGGLE_DELAY);
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
|
||||
await click(target.querySelector(".dropdown-toggle"));
|
||||
assert.containsN(target, "[data-company-id]", 3);
|
||||
assert.containsN(target, "[data-company-id] .fa-check-square", 1);
|
||||
assert.containsN(target, "[data-company-id] .fa-square-o", 2);
|
||||
|
||||
/**
|
||||
* [ ] **Hermit** -> toggle all
|
||||
* [x] Herman's -> toggle all
|
||||
* [x] Heroes TM -> toggle all
|
||||
*/
|
||||
await click(target.querySelectorAll(".toggle_company")[0]);
|
||||
await click(target.querySelectorAll(".toggle_company")[1]);
|
||||
await click(target.querySelectorAll(".toggle_company")[2]);
|
||||
assert.containsOnce(target, ".dropdown-menu", "dropdown is still opened");
|
||||
assert.containsN(target, "[data-company-id] .fa-check-square", 2);
|
||||
assert.containsN(target, "[data-company-id] .fa-square-o", 1);
|
||||
|
||||
assert.verifySteps([]);
|
||||
await prom; // await toggle promise
|
||||
assert.verifySteps(["cids=2%2C1"]);
|
||||
});
|
||||
|
||||
QUnit.test("single company selected: toggling it off will keep it", async (assert) => {
|
||||
assert.expect(12);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout(fn) {
|
||||
return fn(); // s.t. we can directly assert changes in the hash
|
||||
},
|
||||
});
|
||||
const scMenu = await createSwitchCompanyMenu();
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.router.current.hash, { cids: 3 });
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
|
||||
await click(target.querySelector(".dropdown-toggle"));
|
||||
assert.containsN(target, "[data-company-id]", 3);
|
||||
assert.containsN(target, "[data-company-id] .fa-check-square", 1);
|
||||
assert.containsN(target, "[data-company-id] .fa-square-o", 2);
|
||||
|
||||
/**
|
||||
* [ ] **Hermit** -> toggle off
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
*/
|
||||
await click(target.querySelectorAll(".toggle_company")[0]);
|
||||
assert.deepEqual(scMenu.env.services.router.current.hash, { cids: 3 });
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
|
||||
assert.containsOnce(target, ".dropdown-menu", "dropdown is still opened");
|
||||
assert.containsN(target, "[data-company-id] .fa-check-square", 0);
|
||||
assert.containsN(target, "[data-company-id] .fa-square-o", 3);
|
||||
});
|
||||
|
||||
QUnit.test("single company mode: companies can be logged in", async (assert) => {
|
||||
assert.expect(8);
|
||||
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
}
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState });
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
|
||||
await click(target.querySelector(".dropdown-toggle"));
|
||||
assert.containsN(target, "[data-company-id]", 3);
|
||||
assert.containsN(target, "[data-company-id] .fa-check-square", 1);
|
||||
assert.containsN(target, "[data-company-id] .fa-square-o", 2);
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's -> log into
|
||||
* [ ] Heroes TM
|
||||
*/
|
||||
await click(target.querySelectorAll(".log_into")[1]);
|
||||
assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed");
|
||||
assert.verifySteps(["cids=2"]);
|
||||
});
|
||||
|
||||
QUnit.test("multi company mode: log into a non selected company", async (assert) => {
|
||||
assert.expect(8);
|
||||
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
}
|
||||
Object.assign(browser.location, { hash: "cids=3%2C1" });
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState });
|
||||
|
||||
/**
|
||||
* [x] Hermit
|
||||
* [ ] Herman's
|
||||
* [x] **Heroes TM**
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3, 1]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
|
||||
await click(target.querySelector(".dropdown-toggle"));
|
||||
assert.containsN(target, "[data-company-id]", 3);
|
||||
assert.containsN(target, "[data-company-id] .fa-check-square", 2);
|
||||
assert.containsN(target, "[data-company-id] .fa-square-o", 1);
|
||||
|
||||
/**
|
||||
* [x] Hermit
|
||||
* [ ] Herman's -> log into
|
||||
* [x] **Heroes TM**
|
||||
*/
|
||||
await click(target.querySelectorAll(".log_into")[1]);
|
||||
assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed");
|
||||
assert.verifySteps(["cids=2%2C3%2C1"]);
|
||||
});
|
||||
|
||||
QUnit.test("multi company mode: log into an already selected company", async (assert) => {
|
||||
assert.expect(8);
|
||||
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
}
|
||||
Object.assign(browser.location, { hash: "cids=2%2C1" });
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState });
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [x] **Herman's**
|
||||
* [x] Heroes TM
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [2, 1]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 2);
|
||||
await click(target.querySelector(".dropdown-toggle"));
|
||||
assert.containsN(target, "[data-company-id]", 3);
|
||||
assert.containsN(target, "[data-company-id] .fa-check-square", 2);
|
||||
assert.containsN(target, "[data-company-id] .fa-square-o", 1);
|
||||
|
||||
/**
|
||||
* [ ] Hermit
|
||||
* [x] **Herman's**
|
||||
* [x] Heroes TM -> log into
|
||||
*/
|
||||
await click(target.querySelectorAll(".log_into")[2]);
|
||||
assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed");
|
||||
assert.verifySteps(["cids=1%2C2"]);
|
||||
});
|
||||
|
||||
QUnit.test("companies can be logged in even if some toggled within delay", async (assert) => {
|
||||
assert.expect(8);
|
||||
|
||||
function onPushState(url) {
|
||||
assert.step(url.split("#")[1]);
|
||||
}
|
||||
const scMenu = await createSwitchCompanyMenu({ onPushState }, ORIGINAL_TOGGLE_DELAY);
|
||||
|
||||
/**
|
||||
* [x] **Hermit**
|
||||
* [ ] Herman's
|
||||
* [ ] Heroes TM
|
||||
*/
|
||||
assert.deepEqual(scMenu.env.services.company.allowedCompanyIds, [3]);
|
||||
assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3);
|
||||
await click(target.querySelector(".dropdown-toggle"));
|
||||
assert.containsN(target, "[data-company-id]", 3);
|
||||
assert.containsN(target, "[data-company-id] .fa-check-square", 1);
|
||||
assert.containsN(target, "[data-company-id] .fa-square-o", 2);
|
||||
|
||||
/**
|
||||
* [ ] **Hermit** -> toggled
|
||||
* [ ] Herman's -> logged in
|
||||
* [ ] Heroes TM -> toggled
|
||||
*/
|
||||
await click(target.querySelectorAll(".toggle_company")[2]);
|
||||
await click(target.querySelectorAll(".toggle_company")[0]);
|
||||
await click(target.querySelectorAll(".log_into")[1]);
|
||||
assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed");
|
||||
assert.verifySteps(["cids=2"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { click, queryAllAttributes, queryAllProperties, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import {
|
||||
clearRegistry,
|
||||
contains,
|
||||
mockService,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
stepAllNetworkCalls,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { user } from "@web/core/user";
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
|
||||
import { UserMenu } from "@web/webclient/user_menu/user_menu";
|
||||
import { odooAccountItem, preferencesItem } from "@web/webclient/user_menu/user_menu_items";
|
||||
|
||||
const userMenuRegistry = registry.category("user_menuitems");
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
beforeEach(async () => {
|
||||
serverState.partnerName = "Sauron";
|
||||
clearRegistry(userMenuRegistry);
|
||||
});
|
||||
|
||||
test("can be rendered", async () => {
|
||||
patchWithCleanup(user, { writeDate: "2024-01-01 12:00:00" });
|
||||
userMenuRegistry.add("bad_item", () => ({
|
||||
type: "item",
|
||||
id: "bad",
|
||||
description: "Bad",
|
||||
callback: () => {
|
||||
expect.step("callback bad_item");
|
||||
},
|
||||
sequence: 10,
|
||||
}));
|
||||
userMenuRegistry.add("ring_item", () => ({
|
||||
type: "item",
|
||||
id: "ring",
|
||||
description: "Ring",
|
||||
callback: () => {
|
||||
expect.step("callback ring_item");
|
||||
},
|
||||
sequence: 5,
|
||||
}));
|
||||
userMenuRegistry.add("frodo_item", () => ({
|
||||
type: "switch",
|
||||
id: "frodo",
|
||||
description: "Frodo",
|
||||
callback: () => {
|
||||
expect.step("callback frodo_item");
|
||||
},
|
||||
sequence: 11,
|
||||
}));
|
||||
userMenuRegistry.add("separator", () => ({
|
||||
type: "separator",
|
||||
sequence: 15,
|
||||
}));
|
||||
userMenuRegistry.add("invisible_item", () => ({
|
||||
type: "item",
|
||||
id: "hidden",
|
||||
description: "Hidden Power",
|
||||
callback: () => {},
|
||||
sequence: 5,
|
||||
hide: true,
|
||||
}));
|
||||
userMenuRegistry.add("eye_item", () => ({
|
||||
type: "item",
|
||||
id: "eye",
|
||||
description: "Eye",
|
||||
callback: () => {
|
||||
expect.step("callback eye_item");
|
||||
},
|
||||
}));
|
||||
await mountWithCleanup(UserMenu);
|
||||
expect("img.o_user_avatar").toHaveCount(1);
|
||||
expect("img.o_user_avatar").toHaveAttribute(
|
||||
"data-src",
|
||||
`${getOrigin()}/web/image/res.partner/17/avatar_128?unique=1704106800000`
|
||||
);
|
||||
expect(".dropdown-menu .dropdown-item").toHaveCount(0);
|
||||
await contains("button.dropdown-toggle").click();
|
||||
expect(".dropdown-menu .dropdown-item").toHaveCount(4);
|
||||
expect(".dropdown-menu .dropdown-item input.form-check-input").toHaveCount(1);
|
||||
expect("div.dropdown-divider").toHaveCount(1);
|
||||
expect(queryAllProperties(".dropdown-menu > *", "tagName")).toEqual([
|
||||
"SPAN",
|
||||
"SPAN",
|
||||
"SPAN",
|
||||
"DIV",
|
||||
"SPAN",
|
||||
]);
|
||||
expect(queryAllAttributes(".dropdown-menu .dropdown-item", "data-menu")).toEqual([
|
||||
"ring",
|
||||
"bad",
|
||||
"frodo",
|
||||
"eye",
|
||||
]);
|
||||
expect(queryAllTexts(".dropdown-menu .dropdown-item")).toEqual(["Ring", "Bad", "Frodo", "Eye"]);
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await click(`.dropdown-menu .dropdown-item:eq(${i})`);
|
||||
|
||||
await click("button.dropdown-toggle"); // re-open the dropdown
|
||||
await animationFrame();
|
||||
}
|
||||
|
||||
expect.verifySteps([
|
||||
"callback ring_item",
|
||||
"callback bad_item",
|
||||
"callback frodo_item",
|
||||
"callback eye_item",
|
||||
]);
|
||||
});
|
||||
|
||||
test("display the correct name in debug mode", async () => {
|
||||
serverState.debug = "1";
|
||||
await mountWithCleanup(UserMenu);
|
||||
expect("img.o_user_avatar").toHaveCount(1);
|
||||
expect("small.oe_topbar_name").toHaveCount(1);
|
||||
expect(".oe_topbar_name").toHaveText("Sauron" + "\n" + "test");
|
||||
});
|
||||
|
||||
test("can execute the callback of settings", async () => {
|
||||
onRpc("action_get", () => ({
|
||||
name: "Change My Preferences",
|
||||
res_id: 0,
|
||||
}));
|
||||
mockService("action", {
|
||||
async doAction(actionId) {
|
||||
expect.step(String(actionId.res_id));
|
||||
expect.step(actionId.name);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
userMenuRegistry.add("preferences", preferencesItem);
|
||||
await mountWithCleanup(UserMenu);
|
||||
await contains("button.dropdown-toggle").click();
|
||||
expect(".dropdown-menu .dropdown-item").toHaveCount(1);
|
||||
expect(".dropdown-menu .dropdown-item").toHaveText("My Preferences");
|
||||
await contains(".dropdown-menu .dropdown-item").click();
|
||||
expect.verifySteps(["7", "Change My Preferences"]);
|
||||
});
|
||||
|
||||
test("click on odoo account item", async () => {
|
||||
patchWithCleanup(browser, {
|
||||
open: (url) => expect.step(`open ${url}`),
|
||||
});
|
||||
userMenuRegistry.add("odoo_account", odooAccountItem);
|
||||
await mountWithCleanup(UserMenu);
|
||||
onRpc("/web/session/account", () => "https://account-url.com");
|
||||
stepAllNetworkCalls();
|
||||
await contains("button.dropdown-toggle").click();
|
||||
expect(".o-dropdown--menu .dropdown-item").toHaveCount(1);
|
||||
expect(".o-dropdown--menu .dropdown-item").toHaveText("My Odoo.com Account");
|
||||
await contains(".o-dropdown--menu .dropdown-item").click();
|
||||
expect.verifySteps(["/web/session/account", "open https://account-url.com"]);
|
||||
});
|
||||
|
||||
test("can use component as registry item", async () => {
|
||||
class ExampleComponent extends Component {
|
||||
static template = xml`<span class='component-class'>Example Component</span>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
userMenuRegistry.add("component-item", () => ({
|
||||
type: "component",
|
||||
contentComponent: ExampleComponent,
|
||||
sequence: 10,
|
||||
}));
|
||||
await mountWithCleanup(UserMenu);
|
||||
await contains("button.dropdown-toggle").click();
|
||||
expect(".o-dropdown--menu span.component-class").toHaveText("Example Component");
|
||||
});
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { UserMenu } from "@web/webclient/user_menu/user_menu";
|
||||
import { preferencesItem } from "@web/webclient/user_menu/user_menu_items";
|
||||
import { userService } from "@web/core/user_service";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
import { click, getFixture, mount, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { session } from "@web/session";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
const userMenuRegistry = registry.category("user_menuitems");
|
||||
let target;
|
||||
let env;
|
||||
|
||||
QUnit.module("UserMenu", {
|
||||
async beforeEach() {
|
||||
patchWithCleanup(session, { name: "Sauron" });
|
||||
patchWithCleanup(browser, {
|
||||
location: {
|
||||
origin: "http://lordofthering",
|
||||
},
|
||||
});
|
||||
serviceRegistry.add("user", userService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("ui", uiService);
|
||||
target = getFixture();
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("can be rendered", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
userMenuRegistry.add("bad_item", function () {
|
||||
return {
|
||||
type: "item",
|
||||
id: "bad",
|
||||
description: "Bad",
|
||||
callback: () => {
|
||||
assert.step("callback bad_item");
|
||||
},
|
||||
sequence: 10,
|
||||
};
|
||||
});
|
||||
userMenuRegistry.add("ring_item", function () {
|
||||
return {
|
||||
type: "item",
|
||||
id: "ring",
|
||||
description: "Ring",
|
||||
callback: () => {
|
||||
assert.step("callback ring_item");
|
||||
},
|
||||
sequence: 5,
|
||||
};
|
||||
});
|
||||
userMenuRegistry.add("frodo_item", function () {
|
||||
return {
|
||||
type: "switch",
|
||||
id: "frodo",
|
||||
description: "Frodo",
|
||||
callback: () => {
|
||||
assert.step("callback frodo_item");
|
||||
},
|
||||
sequence: 11,
|
||||
};
|
||||
});
|
||||
userMenuRegistry.add("separator", function () {
|
||||
return {
|
||||
type: "separator",
|
||||
sequence: 15,
|
||||
};
|
||||
});
|
||||
userMenuRegistry.add("invisible_item", function () {
|
||||
return {
|
||||
type: "item",
|
||||
id: "hidden",
|
||||
description: "Hidden Power",
|
||||
callback: () => {},
|
||||
sequence: 5,
|
||||
hide: true,
|
||||
};
|
||||
});
|
||||
userMenuRegistry.add("eye_item", function () {
|
||||
return {
|
||||
type: "item",
|
||||
id: "eye",
|
||||
description: "Eye",
|
||||
callback: () => {
|
||||
assert.step("callback eye_item");
|
||||
},
|
||||
};
|
||||
});
|
||||
await mount(UserMenu, target, { env });
|
||||
assert.containsOnce(target, "img.o_user_avatar");
|
||||
assert.strictEqual(
|
||||
target.querySelector("img.o_user_avatar").dataset.src,
|
||||
"http://lordofthering/web/image?model=res.users&field=avatar_128&id=7"
|
||||
);
|
||||
assert.containsOnce(target, "span.oe_topbar_name");
|
||||
assert.strictEqual(target.querySelector(".oe_topbar_name").textContent, "Sauron");
|
||||
assert.containsNone(target, ".dropdown-menu .dropdown-item");
|
||||
await click(target.querySelector("button.dropdown-toggle"));
|
||||
assert.containsN(target, ".dropdown-menu .dropdown-item", 4);
|
||||
assert.containsOnce(target, ".dropdown-menu .dropdown-item input.form-check-input");
|
||||
assert.containsOnce(target, "div.dropdown-divider");
|
||||
const children = [...(target.querySelector(".dropdown-menu").children || [])];
|
||||
assert.deepEqual(
|
||||
children.map((el) => el.tagName),
|
||||
["SPAN", "SPAN", "SPAN", "DIV", "SPAN"]
|
||||
);
|
||||
const items = [...target.querySelectorAll(".dropdown-menu .dropdown-item")] || [];
|
||||
assert.deepEqual(
|
||||
items.map((el) => el.dataset.menu),
|
||||
["ring", "bad", "frodo", "eye"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
items.map((el) => el.textContent),
|
||||
["Ring", "Bad", "Frodo", "Eye"]
|
||||
);
|
||||
for (const item of items) {
|
||||
click(item);
|
||||
}
|
||||
assert.verifySteps(["callback ring_item", "callback bad_item", "callback frodo_item", "callback eye_item"]);
|
||||
});
|
||||
|
||||
QUnit.test("display the correct name in debug mode", async (assert) => {
|
||||
patchWithCleanup(odoo, { debug: "1" });
|
||||
env = await makeTestEnv();
|
||||
await mount(UserMenu, target, { env });
|
||||
assert.containsOnce(target, "img.o_user_avatar");
|
||||
assert.containsOnce(target, "span.oe_topbar_name");
|
||||
assert.strictEqual(target.querySelector(".oe_topbar_name").textContent, "Sauron (test)");
|
||||
});
|
||||
|
||||
QUnit.test("can execute the callback of settings", async (assert) => {
|
||||
const mockRPC = (route) => {
|
||||
if (route === "/web/dataset/call_kw/res.users/action_get") {
|
||||
return Promise.resolve({
|
||||
name: "Change My Preferences",
|
||||
res_id: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
const testConfig = { mockRPC };
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
serviceRegistry.add("orm", ormService);
|
||||
const fakeActionService = {
|
||||
name: "action",
|
||||
start() {
|
||||
return {
|
||||
doAction(actionId) {
|
||||
assert.step("" + actionId.res_id);
|
||||
assert.step(actionId.name);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("action", fakeActionService, { force: true });
|
||||
|
||||
env = await makeTestEnv(testConfig);
|
||||
userMenuRegistry.add("profile", preferencesItem);
|
||||
await mount(UserMenu, target, { env });
|
||||
await click(target.querySelector("button.dropdown-toggle"));
|
||||
assert.containsOnce(target, ".dropdown-menu .dropdown-item");
|
||||
const item = target.querySelector(".dropdown-menu .dropdown-item");
|
||||
assert.strictEqual(item.textContent, "Preferences");
|
||||
await click(item);
|
||||
assert.verifySteps(["7", "Change My Preferences"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
import {
|
||||
contains,
|
||||
makeMockEnv,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
test("can be rendered", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
expect(`header > nav.o_main_navbar`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("can render a main component", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = {};
|
||||
static template = xml`<span class="chocolate">MyComponent</span>`;
|
||||
}
|
||||
|
||||
const env = await makeMockEnv();
|
||||
registry.category("main_components").add("mycomponent", { Component: MyComponent });
|
||||
|
||||
await mountWithCleanup(WebClient, { env });
|
||||
|
||||
expect(`.chocolate`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("control-click <a href/> in a standalone component", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = {};
|
||||
static template = xml`<a href="#" class="MyComponent" t-on-click="onclick">Some link</a>`;
|
||||
|
||||
/** @param {MouseEvent} ev */
|
||||
onclick(ev) {
|
||||
expect.step(ev.ctrlKey ? "ctrl-click" : "click");
|
||||
// Necessary in order to prevent the test browser to open in new tab on ctrl-click
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
await contains(".MyComponent").click();
|
||||
await contains(".MyComponent").click({ ctrlKey: true });
|
||||
|
||||
expect.verifySteps(["click", "ctrl-click"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("control-click propagation stopped on <a href/>", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
patchWithCleanup(WebClient.prototype, {
|
||||
/** @param {MouseEvent} ev */
|
||||
onGlobalClick(ev) {
|
||||
super.onGlobalClick(ev);
|
||||
if (ev.ctrlKey) {
|
||||
expect(ev.defaultPrevented).toBe(false, {
|
||||
message:
|
||||
"the global click should not prevent the default behavior on ctrl-click an <a href/>",
|
||||
});
|
||||
// Necessary in order to prevent the test browser to open in new tab on ctrl-click
|
||||
ev.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = {};
|
||||
static template = xml`<a href="#" class="MyComponent" t-on-click="onclick">Some link</a>`;
|
||||
|
||||
/** @param {MouseEvent} ev */
|
||||
onclick(ev) {
|
||||
expect.step(ev.ctrlKey ? "ctrl-click" : "click");
|
||||
// Necessary in order to prevent the test browser to open in new tab on ctrl-click
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
registry.category("main_components").add("mycomponent", { Component: MyComponent });
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
await contains(".MyComponent").click();
|
||||
await contains(".MyComponent").click({ ctrlKey: true });
|
||||
|
||||
expect.verifySteps(["click"]);
|
||||
});
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { legacyServiceProvider } from "@web/legacy/legacy_service_provider";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { menuService } from "@web/webclient/menus/menu_service";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../helpers/mock_env";
|
||||
import { fakeTitleService } from "../helpers/mock_services";
|
||||
import { destroy, getFixture, mount, patchWithCleanup, triggerEvent } from "../helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
const mainComponentRegistry = registry.category("main_components");
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let baseConfig;
|
||||
let target;
|
||||
|
||||
QUnit.module("WebClient", {
|
||||
async beforeEach() {
|
||||
serviceRegistry
|
||||
.add("action", actionService)
|
||||
.add("dialog", dialogService)
|
||||
.add("hotkey", hotkeyService)
|
||||
.add("legacy_service_provider", legacyServiceProvider)
|
||||
.add("menu", menuService)
|
||||
.add("notification", notificationService)
|
||||
.add("popover", popoverService)
|
||||
.add("title", fakeTitleService)
|
||||
.add("ui", uiService)
|
||||
.add("view", viewService) // #action-serv-leg-compat-js-class
|
||||
.add("orm", ormService); // #action-serv-leg-compat-js-class
|
||||
baseConfig = { activateMockServer: true };
|
||||
target = getFixture();
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("can be rendered", async (assert) => {
|
||||
assert.expect(1);
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
await mount(WebClient, target, { env });
|
||||
assert.containsOnce(target, "header > nav.o_main_navbar");
|
||||
});
|
||||
|
||||
QUnit.test("can render a main component", async (assert) => {
|
||||
assert.expect(1);
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<span class="chocolate">MyComponent</span>`;
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
mainComponentRegistry.add("mycomponent", { Component: MyComponent });
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
await mount(WebClient, target, { env });
|
||||
assert.containsOnce(target, ".chocolate");
|
||||
});
|
||||
|
||||
QUnit.test("control-click propagation stopped on <a href/>", async (assert) => {
|
||||
assert.expect(8);
|
||||
|
||||
patchWithCleanup(WebClient.prototype, {
|
||||
/** @param {MouseEvent} ev */
|
||||
onGlobalClick(ev) {
|
||||
this._super(ev);
|
||||
if (ev.ctrlKey) {
|
||||
assert.ok(
|
||||
ev.defaultPrevented === false,
|
||||
"the global click should not prevent the default behavior on ctrl-click an <a href/>"
|
||||
);
|
||||
// Necessary in order to prevent the test browser to open in new tab on ctrl-click
|
||||
ev.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
class MyComponent extends Component {
|
||||
/** @param {MouseEvent} ev */
|
||||
onclick(ev) {
|
||||
assert.step(ev.ctrlKey ? "ctrl-click" : "click");
|
||||
// Necessary in order to prevent the test browser to open in new tab on ctrl-click
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<a href="#" class="MyComponent" t-on-click="onclick">Some link</a>`;
|
||||
let env = await makeTestEnv(baseConfig);
|
||||
|
||||
// Mount the component as standalone and control-click the <a href/>
|
||||
const standaloneComponent = await mount(MyComponent, target, { env });
|
||||
assert.verifySteps([]);
|
||||
await triggerEvent(target.querySelector(".MyComponent"), "", "click", { ctrlKey: false });
|
||||
await triggerEvent(target.querySelector(".MyComponent"), "", "click", { ctrlKey: true });
|
||||
assert.verifySteps(["click", "ctrl-click"]);
|
||||
destroy(standaloneComponent);
|
||||
|
||||
// Register the component as a main one, mount the webclient and control-click the <a href/>
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
mainComponentRegistry.add("mycomponent", { Component: MyComponent });
|
||||
env = await makeTestEnv(baseConfig);
|
||||
await mount(WebClient, target, { env });
|
||||
assert.verifySteps([]);
|
||||
await triggerEvent(target, ".MyComponent", "click", { ctrlKey: false });
|
||||
await triggerEvent(target, ".MyComponent", "click", { ctrlKey: true });
|
||||
assert.verifySteps(["click"]);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue