19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -116,7 +116,7 @@ test("can display client actions in Dialog and close the dialog", async () => {
expect(".modal .test_client_action").toHaveCount(1);
expect(".modal-title").toHaveText("Dialog Test");
await contains(".modal footer .btn.btn-primary").click();
await contains(".modal .btn-close").click();
expect(".modal .test_client_action").toHaveCount(0);
});
@ -498,3 +498,27 @@ test("test home client action", async () => {
await animationFrame();
expect.verifySteps(["/web/webclient/version_info", "assign /"]);
});
test("test display_exception client action", async () => {
expect.errors(1);
await mountWithCleanup(WebClient);
getService("action").doAction({
type: "ir.actions.client",
tag: "display_exception",
params: {
code: 0,
message: "Odoo Server Error",
data: {
name: `odoo.exceptions.UserError`,
debug: "traceback",
arguments: [],
context: {},
message: "This is an error",
},
},
});
await animationFrame();
expect(".o_dialog").toHaveCount(1);
expect("header .modal-title").toHaveText("Invalid Operation");
expect.verifyErrors([/RPC_ERROR/]);
});

View file

@ -15,9 +15,10 @@ import {
getKwArgs,
} from "@web/../tests/web_test_helpers";
import { mockTouch, runAllTimers } from "@odoo/hoot-mock";
import { animationFrame, mockTouch, runAllTimers } from "@odoo/hoot-mock";
import { browser } from "@web/core/browser/browser";
import { router } from "@web/core/browser/router";
import { router, routerBus } from "@web/core/browser/router";
import { rpcBus } from "@web/core/network/rpc";
import { user } from "@web/core/user";
import { WebClient } from "@web/webclient/webclient";
@ -147,7 +148,6 @@ class ResUsersSettings extends WebResUsersSettings {
} else {
ResUsersSettingsEmbeddedAction.write(embeddedSettings.id, vals);
}
return embeddedSettings;
}
}
@ -195,7 +195,7 @@ defineModels([
IrActionsAct_Window,
]);
defineActions([
const actions = [
{
id: 1,
xml_id: "action_1",
@ -264,10 +264,13 @@ defineActions([
parent_action_id: 4,
action_id: 4,
},
]);
];
defineActions(actions);
beforeEach(() => {
user.updateUserSettings("id", 1); // workaround to populate the user settings
user.updateUserSettings("embedded_actions_config_ids", {}); // workaround to populate the embedded user settings
});
test("can display embedded actions linked to the current action", async () => {
@ -311,6 +314,14 @@ test("can toggle visibility of embedded actions", async () => {
expect(".o_embedded_actions > button").toHaveCount(3, {
message: "Should have 2 embedded actions in the embedded + the dropdown button",
});
expect(user.settings.embedded_actions_config_ids).toEqual({
"1+": {
embedded_actions_order: [],
embedded_actions_visibility: [false, 102],
embedded_visibility: true,
res_model: "partner",
},
});
});
test("can click on a embedded action and execute the corresponding action (with xml_id)", async () => {
@ -441,6 +452,14 @@ test("a view coming from a embedded can be saved in the embedded actions", async
expect(".o_embedded_actions > button").toHaveCount(4, {
message: "Should have 2 embedded actions in the embedded + the dropdown button",
});
expect(user.settings.embedded_actions_config_ids).toEqual({
"1+": {
embedded_actions_order: [false, 102, 103, 4],
embedded_actions_visibility: [false, 102, 4],
embedded_visibility: true,
res_model: "partner",
},
});
});
test("a view coming from a embedded with python_method can be saved in the embedded actions", async () => {
@ -499,6 +518,14 @@ test("a view coming from a embedded with python_method can be saved in the embed
expect(".o_embedded_actions > button").toHaveCount(4, {
message: "Should have 2 embedded actions in the embedded + the dropdown button",
});
expect(user.settings.embedded_actions_config_ids).toEqual({
"1+": {
embedded_actions_order: [false, 102, 103, 4],
embedded_actions_visibility: [false, 103, 4],
embedded_visibility: true,
res_model: "partner",
},
});
});
test("the embedded actions should not be displayed when switching view", async () => {
@ -532,6 +559,14 @@ test("User can move the main (first) embedded action", async () => {
expect(".o_embedded_actions > button:nth-child(2) > span").toHaveText("Partners Action 1", {
message: "Main embedded action should've been moved to 2nd position",
});
expect(user.settings.embedded_actions_config_ids).toEqual({
"1+": {
embedded_actions_order: [102, false, 103],
embedded_actions_visibility: [false, 102],
embedded_visibility: true,
res_model: "partner",
},
});
});
test("User can unselect the main (first) embedded action", async () => {
@ -548,6 +583,14 @@ test("User can unselect the main (first) embedded action", async () => {
expect(dropdownItem).not.toHaveClass("selected", {
message: "Main embedded action should be unselected",
});
expect(user.settings.embedded_actions_config_ids).toEqual({
"1+": {
embedded_actions_order: [],
embedded_actions_visibility: [],
embedded_visibility: true,
res_model: "partner",
},
});
});
test("User should be redirected to the first embedded action set in user settings", async () => {
@ -636,3 +679,72 @@ test("custom embedded action loaded first", async () => {
message: "'Favorite Ponies' view should be loaded",
});
});
test("test get_embedded_actions_settings rpc args", async () => {
onRpc("res.users.settings", "get_embedded_actions_settings", ({ args, kwargs }) => {
expect(args.length).toBe(1, {
message: "Should have one positional argument, which is the id of the user setting.",
});
expect(args[0]).toBe(1, { message: "The id of the user setting should be 1." });
expect(kwargs.context.res_id).toBe(5, {
message: "The context should contain the res_id passed to the action.",
});
expect(kwargs.context.res_model).toBe("partner", {
message: "The context should contain the res_model passed to the action.",
});
expect.step("get_embedded_actions_settings");
});
await mountWithCleanup(WebClient);
await getService("action").doAction(1, {
additionalContext: { active_id: 5 },
});
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
expect.verifySteps(["get_embedded_actions_settings"]);
});
test("an action containing embedded actions should reload if the page is refreshed", async () => {
onRpc("create", ({ args }) => {
const values = args[0][0];
expect(values.name).toBe("Custom Partners Action 1");
expect(values.action_id).toBe(1);
// Add the created embedded action to the actions list so that the mock server knows it when reloading (/web/action/load)
defineActions([
...actions,
{
id: 4,
name: "Custom Partners Action 1",
parent_res_model: values.parent_res_model,
type: "ir.embedded.actions",
parent_action_id: 1,
action_id: values.action_id,
},
]);
return [4, values.name]; // Fake new embedded action id
});
onRpc(
"create_filter",
() => [5] // Fake new filter id
);
await mountWithCleanup(WebClient);
await getService("action").doAction(1);
// First, we create a new (custom) embedded action based on the current one
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
await waitFor(".o_popover.dropdown-menu");
await contains(".o_save_current_view ").click();
await contains(".o_save_favorite ").click();
expect(".o_embedded_actions > button").toHaveCount(3, {
message: "Should have 2 embedded actions in the embedded + the dropdown button",
});
// Emulate a hard refresh of the page
rpcBus.trigger("CLEAR-CACHES", "/web/action/load");
routerBus.trigger("ROUTE_CHANGE");
await animationFrame();
// Check that the created embedded action is still there, as the reload should be done
expect(".o_embedded_actions > button").toHaveCount(3, {
message:
"After refresh, we should still have 2 embedded actions in the embedded + the dropdown button",
});
});

View file

@ -1869,7 +1869,9 @@ describe(`new urls`, () => {
await mountWebClient();
await getService("action").doAction(100);
await runAllTimers(); // wait for the router to be updated
await contains(".o_data_cell").click();
await runAllTimers(); // wait for the router to be updated
await getService("action").doAction(200);
expect.verifySteps(["/web/action/load", "/web/action/load"]);

View file

@ -16,6 +16,7 @@ import {
import { animationFrame } from "@odoo/hoot-dom";
import { browser } from "@web/core/browser/browser";
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
import { router } from "@web/core/browser/router";
class Partner extends models.Model {
_name = "res.partner";
@ -81,6 +82,17 @@ test("open record withtout the correct company (doAction)", async () => {
});
});
const _pushState = router.pushState;
patchWithCleanup(router, {
pushState: (state, options) => {
expect(browser.location.href).toBe("https://www.hoot.test/");
const res = _pushState(state, options);
expect.step("pushState");
expect(browser.location.href).toBe("http://example.com/odoo/res.partner/1");
return res;
},
});
await mountWebClient();
getService("action").doAction({
type: "ir.actions.act_window",
@ -90,7 +102,7 @@ test("open record withtout the correct company (doAction)", async () => {
});
await animationFrame();
expect(cookie.get("cids")).toBe("1-2");
expect.verifySteps(["reload"]);
expect.verifySteps(["pushState", "reload"]);
expect(browser.location.href).toBe("http://example.com/odoo/res.partner/1", {
message: "url should contain the information of the doAction",
});

View file

@ -1,5 +1,6 @@
import { afterEach, expect, test } from "@odoo/hoot";
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
import { waitFor } from "@odoo/hoot-dom";
import {
contains,
defineActions,
@ -349,6 +350,54 @@ test("can use custom handlers for report actions", async () => {
]);
});
test("custom handlers can close modals", 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);
registry.category("ir.actions.report handlers").add("custom_handler", async (action) => {
expect.step("calling custom handler for action " + action.id);
return true;
});
await getService("action").doAction(5);
await waitFor(".o_technical_modal .o_form_view");
expect(".o_technical_modal .o_form_view").toHaveCount(1, {
message: "should have rendered a form view in a modal",
});
await getService("action").doAction(7);
expect(".o_technical_modal .o_form_view").toHaveCount(1, {
message: "The modal should still exist",
});
await getService("action").doAction(11);
await animationFrame();
expect(".o_technical_modal .o_form_view").toHaveCount(0, {
message: "the modal should have been closed after the custom handler",
});
expect.verifySteps([
"calling custom handler for action 7",
"calling custom handler for action 11",
]);
});
test.tags("desktop");
test("context is correctly passed to the client action report", async (assert) => {
patchWithCleanup(download, {