Odoo18-Base/addons/web/static/tests/webclient/actions/embedded_action.test.js
2025-01-06 10:57:38 +07:00

537 lines
22 KiB
JavaScript

import { describe, expect, test } from "@odoo/hoot";
import { queryAllTexts } from "@odoo/hoot-dom";
import {
contains,
defineActions,
defineEmbeddedActions,
defineModels,
fields,
getService,
models,
mountWithCleanup,
onRpc,
webModels,
toggleSearchBarMenu,
toggleMenuItem,
} from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
import { WebClient } from "@web/webclient/webclient";
import { router } from "@web/core/browser/router";
import { user } from "@web/core/user";
import { runAllTimers, mockTouch } from "@odoo/hoot-mock";
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,false": `
<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,false": `<list><field name="foo"/></list>`,
"pivot,false": `<pivot/>`,
"search,false": `<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,false": `<list>
<field name="name"/>
<button name="action_test" type="object" string="Action Test" column_invisible="not context.get('display_button')"/>
</list>`,
"kanban,false": `<kanban>
<templates>
<t t-name="card">
<field name="name"/>
</t>
</templates>
</kanban>`,
"form,false": `<form><field name="name"/></form>`,
"search,false": `<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",
type: "ir.actions.act_window",
views: [[1, "kanban"]],
},
{
id: 2,
xml_id: "action_2",
name: "Partners",
res_model: "partner",
mobile_view_mode: "kanban",
type: "ir.actions.act_window",
views: [
[false, "list"],
[1, "kanban"],
[false, "form"],
],
},
{
id: 3,
xml_id: "action_3",
name: "Favorite Ponies",
res_model: "pony",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "kanban"],
[false, "form"],
],
},
{
id: 4,
xml_id: "action_4",
name: "Ponies",
res_model: "pony",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "kanban"],
[false, "form"],
],
},
]);
defineEmbeddedActions([
{
id: 2,
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: 3,
name: "Embedded Action 3",
parent_res_model: "partner",
type: "ir.embedded.actions",
parent_action_id: 1,
python_method: "do_python_method",
},
{
id: 4,
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");
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(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners Action 1",
"Favorite Ponies",
]);
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(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners Action 1",
"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 () => {
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();
mockTouch(true);
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([2, false, 3])); // 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,false"] = `
<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([4, 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",
});
});