mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 17:31:59 +02:00
vanilla 18.0
This commit is contained in:
parent
5454004ff9
commit
d7f6d2725e
979 changed files with 428093 additions and 0 deletions
|
|
@ -0,0 +1,465 @@
|
|||
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("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("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: {
|
||||
title: "title",
|
||||
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_title").toHaveText("title");
|
||||
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: {
|
||||
title: "title",
|
||||
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").toHaveText("title");
|
||||
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 /"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, Deferred } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineModels,
|
||||
findComponent,
|
||||
getService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
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",
|
||||
});
|
||||
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("history back calls on_close handler of dialog action", async () => {
|
||||
const webClient = await mountWithCleanup(WebClient);
|
||||
function onClose() {
|
||||
expect.step("on_close");
|
||||
}
|
||||
// open a new dialog form
|
||||
await getService("action").doAction(5, { onClose });
|
||||
expect(".modal").toHaveCount(1);
|
||||
const form = findComponent(webClient, (c) => c instanceof formView.Controller);
|
||||
form.env.config.historyBack();
|
||||
expect.verifySteps(["on_close"]);
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(0);
|
||||
});
|
||||
|
||||
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("history back calls onclose handler of dialog action with 2 breadcrumbs", async () => {
|
||||
let list;
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
list = this;
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1); // kanban
|
||||
await getService("action").doAction(3); // list
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
function onClose() {
|
||||
expect.step("on_close");
|
||||
}
|
||||
// open a new dialog form
|
||||
await getService("action").doAction(5, { onClose });
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
list.env.config.historyBack();
|
||||
expect.verifySteps(["on_close"]);
|
||||
await animationFrame();
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
expect(".modal").toHaveCount(0);
|
||||
});
|
||||
|
||||
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" });
|
||||
});
|
||||
|
|
@ -0,0 +1,794 @@
|
|||
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";
|
||||
|
||||
_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,2": /* xml */ `
|
||||
<list>
|
||||
<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: 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: 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",
|
||||
"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 () => {
|
||||
redirect("/odoo/action-4/2?cids=1");
|
||||
|
||||
let def;
|
||||
onRpc("partner", "web_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",
|
||||
"web_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",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("execute a new action while loading data of default view", async () => {
|
||||
const def = new Deferred();
|
||||
stepAllNetworkCalls();
|
||||
onRpc("web_search_read", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
// execute a first action (its 'search_read' 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);
|
||||
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_search_read",
|
||||
"has_group",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("open a record while reloading the list view", async () => {
|
||||
let def;
|
||||
onRpc("web_search_read", () => def);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
expect(".o_list_view .o_data_row").toHaveCount(2);
|
||||
expect(".o_control_panel .o_list_buttons").toHaveCount(1);
|
||||
|
||||
// reload (the search_read RPC will be blocked)
|
||||
def = new Deferred();
|
||||
await switchView("list");
|
||||
expect(".o_list_view .o_data_row").toHaveCount(2);
|
||||
expect(".o_control_panel .o_list_buttons").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_buttons").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_control_panel .o_list_buttons").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();
|
||||
const defs = [null, def, null];
|
||||
onRpc("web_search_read", () => defs.shift());
|
||||
stepAllNetworkCalls();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(3);
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
getService("action").doAction(4);
|
||||
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",
|
||||
"/web/action/load",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"web_search_read",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("click multiple times to open a record", async () => {
|
||||
const def = new Deferred();
|
||||
const defs = [null, def];
|
||||
onRpc("web_read", () => defs.shift());
|
||||
|
||||
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);
|
||||
|
||||
await contains(".o_back_button").click();
|
||||
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();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
|
||||
"Partners",
|
||||
"Second record",
|
||||
]);
|
||||
|
||||
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(JSON.stringify(this.props.state || "no state"));
|
||||
useSetupAction({
|
||||
getLocalState: () => {
|
||||
return { 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");
|
||||
});
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
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",
|
||||
target: "inline",
|
||||
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");
|
||||
});
|
||||
|
|
@ -0,0 +1,528 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineModels,
|
||||
fields,
|
||||
getService,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
toggleMenuItem,
|
||||
toggleSearchBarMenu,
|
||||
webModels,
|
||||
} 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 } = 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>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner, Pony, ResCompany, ResPartner, ResUsers]);
|
||||
|
||||
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,
|
||||
},
|
||||
]);
|
||||
|
||||
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",
|
||||
});
|
||||
});
|
||||
|
||||
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 contains(".o_embedded_actions .dropdown").click();
|
||||
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 contains(".o_embedded_actions .dropdown").click();
|
||||
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", () => {
|
||||
return {
|
||||
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 contains(".o_embedded_actions .dropdown").click();
|
||||
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", () => {
|
||||
return {
|
||||
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 contains(".o_embedded_actions .dropdown").click();
|
||||
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_or_replace", ({ args }) => {
|
||||
expect(args[0].domain).toBe(`[["name", "=", "Applejack"]]`);
|
||||
expect(args[0].embedded_action_id).toBe(4);
|
||||
expect(args[0].user_id).toBe(false);
|
||||
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 contains(".o_embedded_actions .dropdown").click();
|
||||
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_or_replace") {
|
||||
values = args[0][0];
|
||||
expect(args[0].domain).toBe(`[["name", "=", "Applejack"]]`);
|
||||
expect(args[0].embedded_action_id).toBe(4);
|
||||
expect(args[0].user_id).toBe(false);
|
||||
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 contains(".o_embedded_actions .dropdown").click();
|
||||
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 contains(".o_embedded_actions .dropdown").click();
|
||||
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 contains(".o_embedded_actions .dropdown").click();
|
||||
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 contains(".o_embedded_actions .dropdown").click();
|
||||
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 localStorage", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
browser.localStorage.setItem(
|
||||
`orderEmbedded1++${user.userId}`,
|
||||
JSON.stringify([102, false, 103])
|
||||
); // set embedded action 2 in first
|
||||
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 contains(".o_embedded_actions .dropdown").click();
|
||||
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 () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
browser.localStorage.setItem(`orderEmbedded4++${user.userId}`, JSON.stringify([104, false])); // set embedded action 4 in first
|
||||
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,270 @@
|
|||
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",
|
||||
"/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);
|
||||
|
||||
stepAllNetworkCalls();
|
||||
|
||||
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);
|
||||
|
||||
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_breadcrumb .o_back_button a").click();
|
||||
await animationFrame();
|
||||
expect(".o_form_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",
|
||||
"web_read",
|
||||
"/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]);
|
||||
});
|
||||
|
||||
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",
|
||||
]);
|
||||
|
||||
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"]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,669 @@
|
|||
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,
|
||||
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";
|
||||
|
||||
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("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(JSON.stringify(params));
|
||||
});
|
||||
|
||||
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
|
||||
expect.verifySteps([
|
||||
'{"action_id":3,"context":{"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1]}}',
|
||||
'{"action_id":3,"context":{"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1],"configuratorMode":"add"}}',
|
||||
'{"action_id":3,"context":{"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1],"configuratorMode":"edit"}}',
|
||||
'{"action_id":3,"context":{"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1],"active_id":1}}',
|
||||
'{"action_id":3,"context":{"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1],"active_id":2}}',
|
||||
'{"action_id":3,"context":{"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1],"active_ids":[1,2]}}',
|
||||
'{"action_id":3,"context":{"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1],"active_ids":[1,2,3]}}',
|
||||
'{"action_id":3,"context":{"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1],"active_model":"a"}}',
|
||||
'{"action_id":3,"context":{"lang":"en","tz":"taht","uid":7,"allowed_company_ids":[1],"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", 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.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");
|
||||
});
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { cookie } from "@web/core/browser/cookie";
|
||||
import { redirect } from "@web/core/utils/urls";
|
||||
import {
|
||||
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="display_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.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,false"] = `<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,666 @@
|
|||
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>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner, Pony]);
|
||||
|
||||
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("web_search_read", () => 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":"","fold":false,"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":"","fold":false,"viewTypes":["kanban","list"],"loaded":false,"shouldReload":true},"sections":[]}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,419 @@
|
|||
import { afterEach, expect, test } from "@odoo/hoot";
|
||||
import { 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);
|
||||
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);
|
||||
});
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
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>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
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 () => {
|
||||
return 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",
|
||||
]);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
return 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);
|
||||
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 () => {
|
||||
return {
|
||||
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");
|
||||
});
|
||||
|
|
@ -0,0 +1,772 @@
|
|||
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:not(.d-none)").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:not(.d-none)").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:not(.d-none)").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", () => {
|
||||
return {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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"]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue