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

2786 lines
92 KiB
JavaScript

import { expect, test } from "@odoo/hoot";
import { click, queryAllTexts, waitFor } from "@odoo/hoot-dom";
import { Deferred, animationFrame, runAllTimers } from "@odoo/hoot-mock";
import {
MockServer,
contains,
createKanbanRecord,
defineActions,
defineModels,
editKanbanRecord,
editKanbanRecordQuickCreateInput,
fields,
getService,
models,
mountWithCleanup,
onRpc,
patchWithCleanup,
stepAllNetworkCalls,
switchView,
toggleMenuItem,
toggleSearchBarMenu,
webModels,
} from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
import { router } from "@web/core/browser/router";
import { redirect } from "@web/core/utils/urls";
import { useSetupAction } from "@web/search/action_hook";
import { listView } from "@web/views/list/list_view";
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
import { clearUncommittedChanges } from "@web/webclient/actions/action_service";
import { WebClient } from "@web/webclient/webclient";
import {
clickSave,
editFavoriteName,
editSearch,
getPagerLimit,
getPagerValue,
makeMockEnv,
makeServerError,
pagerNext,
removeFacet,
saveFavorite,
serverState,
toggleSaveFavorite,
validateSearch,
} from "../../web_test_helpers";
const { ResCompany, ResPartner, ResUsers } = webModels;
function clickListNew() {
return contains(".o_control_panel_main_buttons .o_list_button_add").click();
}
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>`,
"search,4": `
<search>
<filter name="m2o" help="M2O" domain="[('m2o', '=', 1)]"/>
</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"/></list>`,
"form,false": `<form><field name="name"/></form>`,
"search,false": `<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: "Partner",
res_id: 2,
res_model: "partner",
type: "ir.actions.act_window",
views: [[74, "form"]],
},
{
id: 3,
xml_id: "action_3",
name: "Partners",
res_model: "partner",
mobile_view_mode: "kanban",
type: "ir.actions.act_window",
views: [
[false, "list"],
[1, "kanban"],
[false, "form"],
],
},
{
id: 4,
xml_id: "action_4",
name: "Partners Action 4",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[1, "kanban"],
[false, "list"],
[false, "form"],
],
},
{
id: 5,
xml_id: "action_5",
name: "Create a Partner",
res_model: "partner",
target: "new",
type: "ir.actions.act_window",
views: [[false, "form"]],
},
{
id: 8,
xml_id: "action_8",
name: "Favorite Ponies",
res_model: "pony",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
},
{
id: 9,
xml_id: "action_9",
name: "Ponies",
res_model: "pony",
type: "ir.actions.act_window",
views: [[false, "list"]],
},
]);
test("can execute act_window actions from db ID", async () => {
stepAllNetworkCalls();
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.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
]);
});
test("click on a list row when there is no form in the action", async () => {
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(9);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"has_group",
]);
await contains(".o_data_row:eq(0) .o_data_cell").click();
expect.verifySteps([]);
});
test("click on open form view button when there is no form in the action", async () => {
Pony._views[
"list,false"
] = `<list editable="top" open_form_view="1"><field name="name"/></list>`;
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(9);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"has_group",
]);
await contains(".o_data_row:eq(0) .o_list_record_open_form_view").click();
expect(".o_form_view").toHaveCount(1, { message: "should display the form view" });
expect.verifySteps(["get_views", "web_read"]);
});
test("click on new record button in list when there is no form in the action", async () => {
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(9);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"has_group",
]);
await contains(".o_list_button_add").click();
expect(".o_form_view").toHaveCount(1, { message: "should display the form view" });
expect.verifySteps(["get_views", "onchange"]);
});
test.tags("desktop");
test("sidebar is present in list view", async () => {
expect.assertions(4);
Partner._toolbar = {
print: [{ name: "Print that record" }],
};
onRpc("get_views", ({ kwargs }) => {
expect(kwargs.options.toolbar).toBe(true, {
message: "should ask for toolbar information",
});
});
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_cp_action_menus .o_dropdown_title").toHaveCount(0); // no action menu
await contains("input.form-check-input").click();
expect('.o_cp_action_menus button.dropdown-toggle:contains("Print")').toBeVisible();
expect('.o_cp_action_menus button.dropdown-toggle:contains("Action")').toBeVisible();
});
test.tags("desktop");
test("can switch between views", async () => {
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1, { message: "should display the list view" });
// switch to kanban view
await switchView("kanban");
expect(".o_list_view").toHaveCount(0, { message: "should no longer display the list view" });
expect(".o_kanban_view").toHaveCount(1, { message: "should display the kanban view" });
// switch back to list view
await switchView("list");
expect(".o_list_view").toHaveCount(1, { message: "should display the list view" });
expect(".o_kanban_view").toHaveCount(0, {
message: "should no longer display the kanban view",
});
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
expect(".o_list_view").toHaveCount(0, { message: "should no longer display the list view" });
expect(".o_form_view").toHaveCount(1, { message: "should display the form view" });
expect(".o_field_widget[name=foo] input").toHaveValue("yop", {
message: "should have opened the correct record",
});
// go back to list view using the breadcrumbs
await contains(".o_control_panel .breadcrumb a").click();
expect(".o_list_view").toHaveCount(1, { message: "should display the list view" });
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_search_read",
"web_search_read",
"web_read",
"web_search_read",
]);
});
test.tags("desktop");
test("switching into a view with mode=edit lands in edit mode", async () => {
Partner._views["kanban,1"] = `
<kanban on_create="quick_create" default_group_by="m2o">
<templates>
<t t-name="card">
<field name="foo"/>
</t>
</templates>
</kanban>`;
defineActions([
{
id: 1,
xml_id: "action_1",
name: "Partners Action 1 patched",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "kanban"],
[false, "form"],
],
},
]);
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(1);
expect(".o_kanban_view").toHaveCount(1, { message: "should display the kanban view" });
// quick create record and click Edit
await createKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "New name");
await editKanbanRecord();
expect(".o_form_view .o_form_editable").toHaveCount(1, {
message: "should display the form view in edit mode",
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_read_group",
"web_search_read",
"web_search_read",
"onchange",
"name_create",
"web_read",
"web_read",
]);
});
test.tags("desktop");
test("orderedBy in context is not propagated when executing another action", async () => {
expect.assertions(6);
Partner._views["form,false"] = `
<form>
<header>
<button name="8" string="Execute action" type="action"/>
</header>
</form>`;
Partner._filters = [
{
id: 1,
context: "{}",
domain: "[]",
sort: "[]",
is_default: true,
name: "My filter",
},
];
let searchReadCount = 1;
onRpc("web_search_read", ({ model, sort, kwargs }) => {
if (searchReadCount === 1) {
expect(model).toBe("partner");
expect(sort).toBe(undefined);
}
if (searchReadCount === 2) {
expect(model).toBe("partner");
expect(kwargs.order).toBe("foo ASC");
}
if (searchReadCount === 3) {
expect(model).toBe("pony");
expect(sort).toBe(undefined);
}
searchReadCount += 1;
});
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// Sort records
await contains(".o_list_view th.o_column_sortable").click();
// Get to the form view of the model, on the first record
await contains(".o_data_cell").click();
// Execute another action by clicking on the button within the form
await contains('button[name="8"]').click();
});
test.tags("desktop");
test("breadcrumbs are updated when switching between views", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_control_panel .breadcrumb-item").toHaveCount(0);
expect(".o_control_panel .o_breadcrumb .active").toHaveText("Partners");
// switch to kanban view
await switchView("kanban");
expect(".o_control_panel .breadcrumb-item").toHaveCount(0);
expect(".o_control_panel .o_breadcrumb .active").toHaveText("Partners");
// open a record in form view
await contains(".o_kanban_view .o_kanban_record").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
]);
// go back to kanban view using the breadcrumbs
await contains(".o_control_panel .breadcrumb a").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners"]);
// switch back to list view
await switchView("list");
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners"]);
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
]);
// go back to list view using the breadcrumbs
await contains(".o_control_panel .breadcrumb a").click();
expect(".o_list_view").toHaveCount(1, { message: "should be back on list view" });
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners"]);
});
test.tags("desktop");
test("switch buttons are updated when switching between views", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_control_panel button.o_switch_view").toHaveCount(2, {
message: "should have two switch buttons (list and kanban)",
});
expect(".o_control_panel button.o_switch_view.active").toHaveCount(1, {
message: "should have only one active button",
});
expect(".o_control_panel .o_switch_view:first").toHaveClass("o_list", {
message: "list switch button should be the first one",
});
expect(".o_control_panel .o_switch_view.o_list").toHaveClass("active", {
message: "list should be the active view",
});
// switch to kanban view
await switchView("kanban");
expect(".o_control_panel .o_switch_view").toHaveCount(2, {
message: "should still have two switch buttons (list and kanban)",
});
expect(".o_control_panel .o_switch_view.active").toHaveCount(1, {
message: "should still have only one active button",
});
expect(".o_control_panel .o_switch_view:first").toHaveClass("o_list", {
message: "list switch button should still be the first one",
});
expect(".o_control_panel .o_switch_view.o_kanban").toHaveClass("active", {
message: "kanban should now be the active view",
});
// switch back to list view
await switchView("list");
expect(".o_control_panel .o_switch_view").toHaveCount(2, {
message: "should still have two switch buttons (list and kanban)",
});
expect(".o_control_panel .o_switch_view.o_list").toHaveClass("active", {
message: "list should now be the active view",
});
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
expect(".o_control_panel .o_switch_view").toHaveCount(0, {
message: "should not have any switch buttons",
});
// go back to list view using the breadcrumbs
await contains(".o_control_panel .breadcrumb a").click();
expect(".o_control_panel .o_switch_view").toHaveCount(2, {
message: "should have two switch buttons (list and kanban)",
});
expect(".o_control_panel .o_switch_view.o_list").toHaveClass("active", {
message: "list should be the active view",
});
});
test.tags("desktop");
test("pager is updated when switching between views", async () => {
Partner._views["list,false"] = `<list limit="3"><field name="foo"/></list>`;
await mountWithCleanup(WebClient);
await getService("action").doAction(4);
expect(".o_control_panel .o_pager_value").toHaveText("1-5", {
message: "value should be correct for kanban",
});
expect(".o_control_panel .o_pager_limit").toHaveText("5", {
message: "limit should be correct for kanban",
});
// switch to list view
await switchView("list");
expect(".o_control_panel .o_pager_value").toHaveText("1-3", {
message: "value should be correct for list",
});
expect(".o_control_panel .o_pager_limit").toHaveText("5", {
message: "limit should be correct for list",
});
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
expect(".o_control_panel .o_pager_value").toHaveText("1", {
message: "value should be correct for form",
});
expect(".o_control_panel .o_pager_limit").toHaveText("3", {
message: "limit should be correct for form",
});
// go back to list view using the breadcrumbs
await contains(".o_control_panel .breadcrumb a").click();
expect(".o_control_panel .o_pager_value").toHaveText("1-3", {
message: "value should be correct for list",
});
expect(".o_control_panel .o_pager_limit").toHaveText("5", {
message: "limit should be correct for list",
});
// switch back to kanban view
await switchView("kanban");
expect(".o_control_panel .o_pager_value").toHaveText("1-5", {
message: "value should be correct for kanban",
});
expect(".o_control_panel .o_pager_limit").toHaveText("5", {
message: "limit should be correct for kanban",
});
});
test.tags("desktop");
test("Props are updated and kept when switching/restoring views", async () => {
Partner._views["form,false"] = /* xml */ `
<form>
<group>
<field name="display_name" />
<field name="m2o" />
</group>
</form>`;
onRpc("get_formview_action", ({ args, model }) => {
return {
res_id: args[0][0],
res_model: model,
type: "ir.actions.act_window",
views: [[false, "form"]],
};
});
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// 5 records initially
expect(".o_data_row").toHaveCount(5);
await contains(".o_data_row:first-of-type .o_data_cell").click();
// Open 1 / 5
expect(".o_field_char input").toHaveValue("First record");
expect(getPagerValue()).toEqual([1]);
expect(getPagerLimit()).toBe(5);
await contains(".o_field_many2one .o_external_button", { visible: false }).click();
// Click on M2O -> 1 / 1
expect(".o_field_char input").toHaveValue("Third record");
expect(getPagerValue()).toEqual([1]);
expect(getPagerLimit()).toBe(1);
await contains(".o_back_button").click();
// Back to 1 / 5
expect(".o_field_char input").toHaveValue("First record");
expect(getPagerValue()).toEqual([1]);
expect(getPagerLimit()).toBe(5);
await pagerNext();
// Next page -> 2 / 5
expect(".o_field_char input").toHaveValue("Second record");
expect(getPagerValue()).toEqual([2]);
expect(getPagerLimit()).toBe(5);
await contains(".o_field_many2one .o_external_button", { visible: false }).click();
// Click on M2O -> still 1 / 1
expect(".o_field_char input").toHaveValue("Third record");
expect(getPagerValue()).toEqual([1]);
expect(getPagerLimit()).toBe(1);
await contains(".o_back_button").click();
// Back to 2 / 5
expect(".o_field_char input").toHaveValue("Second record");
expect(getPagerValue()).toEqual([2]);
expect(getPagerLimit()).toBe(5);
});
test.tags("desktop");
test("domain is kept when switching between views", async () => {
defineActions([
{
id: 3,
name: "Partners",
res_model: "partner",
search_view_id: [4, "a custom search view"],
type: "ir.actions.act_window",
views: [
[false, "list"],
[1, "kanban"],
[false, "form"],
],
},
]);
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_data_row").toHaveCount(5);
// activate a domain
await toggleSearchBarMenu();
await toggleMenuItem("M2O");
expect(".o_data_row").toHaveCount(3);
// switch to kanban
await switchView("kanban");
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(3);
// remove the domain
await contains(".o_searchview .o_facet_remove").click();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(5);
// switch back to list
await switchView("list");
expect(".o_data_row").toHaveCount(5);
});
test.tags("desktop");
test("A new form view can be reloaded after a failed one", async () => {
expect.errors(1);
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1, { message: "The list view should be displayed" });
await runAllTimers(); // wait for the update of the router
expect(router.current).toEqual({
action: 3,
actionStack: [
{
action: 3,
displayName: "Partners",
view_type: "list",
},
],
});
// Click on the first record
await contains(".o_list_view .o_data_row .o_data_cell").click();
expect(".o_form_view").toHaveCount(1, { message: "The form view should be displayed" });
expect(".o_last_breadcrumb_item").toHaveText("First record");
await runAllTimers(); // wait for the update of the router
expect(browser.location.pathname).toBe("/odoo/action-3/1");
// Delete the current record
await contains(".o_cp_action_menus .fa-cog").click();
await contains(".o_menu_item:contains(Delete)").click();
expect(".modal").toHaveCount(1, { message: "a confirm modal should be displayed" });
await contains(".modal-footer button.btn-primary").click();
// The form view is automatically switched to the next record
expect(".o_last_breadcrumb_item").toHaveText("Second record");
await runAllTimers(); // wait for the update of the router
expect(browser.location.pathname).toBe("/odoo/action-3/2");
// Go back to the previous (now deleted) record
browser.history.back();
await runAllTimers();
expect(browser.location.pathname).toBe("/odoo/action-3/1");
// As the previous one is deleted, we go back to the list
await runAllTimers(); // wait for the update of the router
expect(".o_list_view").toHaveCount(1, { message: "should still display the list view" });
// Click on the first record
await contains(".o_list_view .o_data_row .o_data_cell").click();
expect(".o_form_view").toHaveCount(1, {
message: "The form view should still load after a previous failed update | reload",
});
expect(".o_last_breadcrumb_item").toHaveText("Second record");
expect.verifyErrors([
"It seems the records with IDs 1 cannot be found. They might have been deleted.",
]);
});
test.tags("desktop");
test("there is no flickering when switching between views", async () => {
let def;
onRpc(() => def);
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// switch to kanban view
def = new Deferred();
await switchView("kanban");
expect(".o_list_view").toHaveCount(1, { message: "should still display the list view" });
expect(".o_kanban_view").toHaveCount(0, { message: "shouldn't display the kanban view yet" });
def.resolve();
await animationFrame();
expect(".o_list_view").toHaveCount(0, { message: "shouldn't display the list view anymore" });
expect(".o_kanban_view").toHaveCount(1, { message: "should now display the kanban view" });
// switch back to list view
def = new Deferred();
await switchView("list");
expect(".o_kanban_view").toHaveCount(1, { message: "should still display the kanban view" });
expect(".o_list_view").toHaveCount(0, { message: "shouldn't display the list view yet" });
def.resolve();
await animationFrame();
expect(".o_kanban_view").toHaveCount(0, {
message: "shouldn't display the kanban view anymore",
});
expect(".o_list_view").toHaveCount(1, { message: "should now display the list view" });
// open a record in form view
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" });
expect(".o_form_view").toHaveCount(0, { message: "shouldn't display the form view yet" });
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners"]);
def.resolve();
await animationFrame();
expect(".o_list_view").toHaveCount(0, { message: "should no longer display the list view" });
expect(".o_form_view").toHaveCount(1, { message: "should display the form view" });
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
]);
// go back to list view using the breadcrumbs
def = new Deferred();
await contains(".o_control_panel .breadcrumb a").click();
expect(".o_form_view").toHaveCount(1, { message: "should still display the form view" });
expect(".o_list_view").toHaveCount(0, { message: "shouldn't display the list view yet" });
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
]);
def.resolve();
await animationFrame();
expect(".o_form_view").toHaveCount(0, { message: "should no longer display the form view" });
expect(".o_list_view").toHaveCount(1, { message: "should display the list view" });
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners"]);
});
test.tags("desktop");
test("breadcrumbs are updated when display_name changes", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
]);
// change the display_name
await contains(".o_field_widget[name=display_name] input").edit("New name");
await clickSave();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"New name",
]);
});
test.tags("desktop");
test('reverse breadcrumb works on accesskey "b"', async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
await contains(".o_form_view button:contains(Execute action)").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
"Partners Action 4",
]);
expect(".breadcrumb-item.o_back_button").toHaveAttribute("data-hotkey", "b", {
message: "previous breadcrumb should have accessKey 'b'",
});
await contains(".breadcrumb-item.o_back_button").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
]);
expect(".breadcrumb-item.o_back_button").toHaveAttribute("data-hotkey", "b", {
message: "previous breadcrumb should have accessKey 'b'",
});
});
test.tags("desktop");
test("reload previous controller when discarding a new record", async () => {
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// create a new record
await clickListNew();
expect(".o_form_view .o_form_editable").toHaveCount(1, {
message: "should have opened the form view in edit mode",
});
// discard
await contains(".o_control_panel .o_form_button_cancel").click();
expect(".o_list_view").toHaveCount(1, {
message: "should have switched back to the list view",
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"has_group",
"onchange",
"web_search_read",
]);
});
test.tags("desktop");
test("execute_action of type object are handled", async () => {
expect.assertions(4);
serverState.userContext = { some_key: 2 };
onRpc("partner", "object", async function ({ args, kwargs }) {
expect(kwargs).toEqual(
{
context: {
lang: "en",
uid: 7,
tz: "taht",
allowed_company_ids: [1],
some_key: 2,
},
},
{ message: "should call route with correct arguments" }
);
return this.env["partner"].write(args[0], { foo: "value changed" });
});
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
expect(".o_field_widget[name=foo] input").toHaveValue("yop", {
message: "check initial value of 'yop' field",
});
// click on 'Call method' button (should call an Object method)
await contains(".o_form_view button:contains(Call method)").click();
expect(".o_field_widget[name=foo] input").toHaveValue("value changed", {
message: "'yop' has been changed by the server, and should be updated in the UI",
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"has_group",
"web_read",
"object",
"web_read",
]);
});
test.tags("desktop");
test("execute_action of type object: disable buttons (2)", async () => {
Pony._views["form,44"] = `
<form>
<field name="name"/>
<button string="Cancel" class="cancel-btn" special="cancel"/>
</form>`;
defineActions([
{
id: 4,
name: "Create a Partner",
res_model: "pony",
target: "new",
type: "ir.actions.act_window",
views: [[44, "form"]],
},
]);
const def = new Deferred();
// delay the opening of the dialog
onRpc("onchange", () => def);
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
// open first record in form view
await contains(".o_list_view .o_data_cell").click();
expect(".o_form_view").toHaveCount(1);
// click on 'Execute action', to execute action 4 in a dialog
await contains('.o_form_view button[name="4"]').click();
expect(".o_form_button_create").toHaveProperty("disabled", true, {
message: "control panel buttons should be disabled",
});
def.resolve();
await animationFrame();
expect(".modal .o_form_view").toHaveCount(1);
expect(".o_form_button_create").not.toHaveProperty("disabled", true, {
message: "control panel buttons should have been re-enabled",
});
await contains(".modal .cancel-btn").click();
expect(".o_form_button_create").not.toHaveProperty("disabled", true, {
message: "control panel buttons should still be enabled",
});
});
test.tags("desktop");
test("view button: block ui attribute", async () => {
Partner._views["form,false"] = `
<form>
<header>
<button name="4" string="Execute action" type="action" block-ui="1"/>
</header>
</form>`;
const def = new Deferred();
// delay the action
onRpc("onchange", () => def);
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
// open first record in form view
await contains(".o_list_view .o_data_cell").click();
expect(".o_form_view").toHaveCount(1);
expect(".o-main-components-container .o_blockUI").toHaveCount(0);
// click on 'Execute action', to execute action 4
await contains('.o_form_view button[name="4"]').click();
expect(".o-main-components-container .o_blockUI").toHaveCount(1, {
message: "interface should be blocked during loading",
});
def.resolve();
await animationFrame();
expect(".o_kanban_view").toHaveCount(1);
expect(".o-main-components-container .o_blockUI").toHaveCount(0);
});
test("execute_action of type object raises error: re-enables buttons", async () => {
expect.errors(1);
onRpc("/web/dataset/call_button/*", () => {
throw makeServerError({ message: "This is a user error" });
});
await mountWithCleanup(WebClient);
await getService("action").doAction(3, { viewType: "form" });
expect(".o_form_view").toHaveCount(1);
// save to ensure the presence of the create button
await contains(".o_form_button_save").click();
// click on 'Execute action', to execute action 4 in a dialog
await click('.o_form_view button[name="object"]');
expect(".o_form_button_create").toHaveProperty("disabled", true);
await animationFrame();
expect(".o_form_button_create").toHaveProperty("disabled", false);
});
test("execute_action of type object raises error in modal: re-enables buttons", async () => {
expect.errors(1);
Partner._views["form,false"] = `
<form>
<field name="display_name"/>
<footer>
<button name="object" string="Call method" type="object"/>
</footer>
</form>`;
onRpc("/web/dataset/call_button/*", () => {
throw makeServerError();
});
await mountWithCleanup(WebClient);
await getService("action").doAction(5);
expect(".modal .o_form_view").toHaveCount(1);
await click('.modal footer button[name="object"]');
expect(".modal .o_form_view").toHaveCount(1);
expect(".modal footer button").toHaveProperty("disabled", true);
await animationFrame();
expect(".modal .o_form_view").toHaveCount(1);
expect(".modal footer button").not.toHaveProperty("disabled", true);
});
test.tags("desktop");
test("execute_action of type action are handled", async () => {
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
// click on 'Execute action' button (should execute an action)
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
]);
await contains(".o_form_view button:contains(Execute action)").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
"Partners Action 4",
]);
expect(".o_kanban_view").toHaveCount(1, {
message: "the returned action should have been executed",
});
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",
]);
});
test.tags("desktop");
test("execute smart button and back", async () => {
onRpc("web_read", ({ kwargs }) => {
expect.step("web_read");
expect(kwargs.context).not.toInclude("default_partner");
});
onRpc("web_search_read", ({ kwargs }) => {
expect.step("web_search_read");
expect(kwargs.context.default_partner).toBe(2);
});
await mountWithCleanup(WebClient);
await getService("action").doAction(2);
expect(".o_form_view").toHaveCount(1);
expect(".o_form_button_create:not([disabled]):visible").toHaveCount(1);
await contains(".oe_stat_button").click();
expect(".o_kanban_view").toHaveCount(1);
await contains(".breadcrumb-item").click();
expect(".o_form_view").toHaveCount(1);
expect(".o_form_button_create:not([disabled]):visible").toHaveCount(1);
expect.verifySteps(["web_read", "web_search_read", "web_read"]);
});
test.tags("desktop");
test("execute smart button and fails on desktop", async () => {
expect.errors(1);
onRpc("web_search_read", () => {
throw makeServerError({ message: "Oups" });
});
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(2);
expect(".o_form_view").toHaveCount(1);
expect(".o_form_button_create:not([disabled]):visible").toHaveCount(1);
await contains("button.oe_stat_button").click();
expect(".o_form_view").toHaveCount(1);
expect(".o_form_button_create:not([disabled]):visible").toHaveCount(1);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_read",
"/web/action/load",
"get_views",
"web_search_read",
"web_read",
]);
expect.verifyErrors(["Oups"]);
});
test.tags("mobile");
test("execute smart button and fails on mobile", async () => {
expect.errors(1);
onRpc("web_search_read", () => {
throw makeServerError({ message: "Oups" });
});
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(2);
expect(".o_form_view").toHaveCount(1);
expect(".o_form_button_create:not([disabled]):visible").toHaveCount(1);
await contains(".o-form-buttonbox .o_button_more").click();
await contains("button.oe_stat_button").click();
expect(".o_form_view").toHaveCount(1);
expect(".o_form_button_create:not([disabled]):visible").toHaveCount(1);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_read",
"/web/action/load",
"get_views",
"web_search_read",
"web_read",
]);
expect.verifyErrors(["Oups"]);
});
test.tags("desktop");
test("requests for execute_action of type object: disable buttons", async () => {
let def = undefined;
onRpc("web_read", () => def); // block the 'read' call
onRpc("/web/dataset/call_button/*", () => false);
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
// click on 'Call method' button (should call an Object method)
def = new Deferred();
await contains(".o_form_view button:contains(Call method)").click();
// Buttons should be disabled
expect(".o_form_view button:contains(Call method)").toHaveProperty("disabled", true, {
message: "buttons should be disabled",
});
// Release the 'read' call
def.resolve();
await animationFrame();
// Buttons should be enabled after the reload
expect(".o_form_view button:contains(Call method)").not.toHaveProperty("disabled", true, {
message: "buttons should not be disabled anymore",
});
});
test.tags("desktop");
test("action with html help returned by a call_button", async () => {
onRpc("/web/dataset/call_button/*", () => {
return {
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "list"]],
help: "<p>I am not a helper</p>",
domain: [[0, "=", 1]],
};
});
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open a record in form view
await contains(".o_list_view .o_data_row .o_data_cell").click();
await contains(".o_statusbar_buttons button").click();
expect(".o_list_view .o_nocontent_help p").toHaveText("I am not a helper");
});
test.tags("desktop");
test("can open different records from a multi record view", async () => {
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open the first record in form view
await contains(".o_list_view .o_data_cell").click();
expect(".o_breadcrumb .active").toHaveText("First record", {
message: "breadcrumbs should contain the display_name of the opened record",
});
expect(".o_field_widget[name=foo] input").toHaveValue("yop", {
message: "should have opened the correct record",
});
// go back to list view using the breadcrumbs
await contains(".o_control_panel .breadcrumb a").click();
// open the second record in form view
await contains(".o_list_view .o_data_row:eq(1) .o_data_cell:first").click();
expect(".o_breadcrumb .active").toHaveText("Second record", {
message: "breadcrumbs should contain the display_name of the opened record",
});
expect(".o_field_widget[name=foo] input").toHaveValue("blip", {
message: "should have opened the correct record",
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"has_group",
"web_read",
"web_search_read",
"web_read",
]);
});
test.tags("desktop");
test("restore previous view state when switching back", async () => {
defineActions([
{
id: 3,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "graph"],
[1, "kanban"],
[false, "form"],
],
},
]);
Partner._views["graph,false"] = "<graph/>";
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_graph_renderer [data-mode='bar']").toHaveClass("active");
expect(".o_graph_renderer [data-mode='line']").not.toHaveClass("active");
// display line chart
await contains(".o_graph_renderer [data-mode='line']").click();
expect(".o_graph_renderer [data-mode='line']").toHaveClass("active");
// switch to kanban and back to graph view
await switchView("kanban");
expect(".o_graph_renderer [data-mode='line']").toHaveCount(0);
await switchView("graph");
expect(".o_graph_renderer [data-mode='line']").toHaveClass("active");
});
test.tags("desktop");
test("can't restore previous action if form is invalid", async () => {
Partner._fields.foo = fields.Char({ required: true });
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
await clickListNew();
expect(".o_form_view").toHaveCount(1);
expect(".o_field_widget[name=foo]").toHaveClass("o_required_modifier");
await contains(".o_field_widget[name=display_name] input").edit("make record dirty");
await contains(".breadcrumb .o_back_button").click();
expect(".o_list_view").toHaveCount(0);
expect(".o_form_view").toHaveCount(1);
expect(".o_notification_manager .o_notification").toHaveCount(1);
expect(".o_field_widget[name=foo]").toHaveClass("o_field_invalid");
});
test.tags("desktop");
test("view switcher is properly highlighted in pivot view", async () => {
defineActions([
{
id: 3,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "pivot"],
[false, "form"],
],
},
]);
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_control_panel .o_switch_view.o_list").toHaveClass("active", {
message: "list button in control panel is active",
});
expect(".o_control_panel .o_switch_view.o_pivot").not.toHaveClass("active", {
message: "pivot button in control panel is not active",
});
// switch to pivot view
await switchView("pivot");
expect(".o_control_panel .o_switch_view.o_list").not.toHaveClass("active", {
message: "list button in control panel is not active",
});
expect(".o_control_panel .o_switch_view.o_pivot").toHaveClass("active", {
message: "pivot button in control panel is active",
});
});
test.tags("desktop");
test("can interact with search view", async () => {
Partner._views["search,false"] = `
<search>
<group>
<filter name="foo" string="foo" context="{'group_by': 'foo'}"/>
</group>
</search>`;
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_list_table").not.toHaveClass("o_list_table_grouped", {
message: "list view is not grouped",
});
// open group by dropdown
await toggleSearchBarMenu();
// click on foo link
await toggleMenuItem("foo");
expect(".o_list_table").toHaveClass("o_list_table_grouped", {
message: "list view is now grouped",
});
});
test.tags("desktop");
test("can open a many2one external window", async () => {
Partner._views["search,false"] = `
<search>
<group>
<filter name="foo" string="foo" context="{'group_by': 'foo'}"/>
</group>
</search>`;
Partner._views["form,false"] = `
<form>
<field name="foo"/>
<field name="m2o"/>
</form>`;
stepAllNetworkCalls();
onRpc("get_formview_action", () => {
return {
name: "Partner",
res_model: "partner",
type: "ir.actions.act_window",
res_id: 3,
views: [[false, "form"]],
};
});
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open first record in form view
await contains(".o_data_row .o_data_cell").click();
// click on external button for m2o
await contains(".o_external_button", { visible: false }).click();
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"has_group",
"web_read",
"get_formview_action",
"get_views",
"web_read",
]);
});
test.tags("desktop");
test('save when leaving a "dirty" view', async () => {
expect.assertions(4);
onRpc("partner", "web_save", ({ args }) => {
expect(args).toEqual([[1], { foo: "pinkypie" }]);
});
await mountWithCleanup(WebClient);
await getService("action").doAction(4);
// open record in form view
await contains(".o_kanban_record").click();
await contains('.o_field_widget[name="foo"] input').edit("pinkypie");
// go back to kanban view
await contains(".o_control_panel .breadcrumb-item a").click();
expect(".modal").toHaveCount(0, { message: "should not display a modal dialog" });
expect(".o_form_view").toHaveCount(0, { message: "should no longer be in form view" });
expect(".o_kanban_view").toHaveCount(1, { message: "should be in kanban view" });
});
test.tags("desktop");
test("limit set in action is passed to each created controller", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction({
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
limit: 2,
views: [
[false, "list"],
[false, "kanban"],
],
});
expect(".o_data_row").toHaveCount(2);
// switch to kanban view
await switchView("kanban");
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
});
test.tags("desktop");
test("go back to a previous action using the breadcrumbs", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
]);
// push another action on top of the first one, and come back to the form view
await getService("action").doAction(4);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
"Partners Action 4",
]);
// go back using the breadcrumbs
await contains(".o_control_panel .breadcrumb a:eq(1)").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
]);
// push again the other action on top of the first one, and come back to the list view
await getService("action").doAction(4);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"First record",
"Partners Action 4",
]);
// go back using the breadcrumbs
await contains(".o_control_panel .breadcrumb a:first").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners"]);
});
test.tags("desktop");
test("form views are restored in edit when coming back in breadcrumbs", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open a record in form view
await contains(".o_list_view .o_data_cell").click();
expect(".o_form_view .o_form_editable").toHaveCount(1);
// do some other action
await getService("action").doAction(4);
// go back to form view
await contains(".o_control_panel .breadcrumb a:eq(1)").click();
expect(".o_form_view .o_form_editable").toHaveCount(1);
});
test.tags("desktop");
test("form views restore the correct id in url when coming back in breadcrumbs", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
// open a record in form view
await contains(".o_list_view .o_data_row .o_data_cell").click();
await runAllTimers(); // wait for the router to update its state
expect(router.current.resId).toBe(1);
// do some other action
await getService("action").doAction(4);
await runAllTimers(); // wait for the router to update its state
expect(router.current).not.toInclude("resId");
// go back to form view
await contains(".o_control_panel .breadcrumb a:eq(1)").click();
await runAllTimers(); // wait for the router to update its state
expect(router.current.resId).toBe(1);
});
test.tags("desktop");
test("honor group_by specified in actions context", async () => {
defineActions([
{
id: 3,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
context: "{'group_by': 'm2o'}",
views: [[false, "list"]],
},
]);
Partner._views["search,false"] = `
<search>
<group>
<filter name="foo" string="Foo" context="{'group_by': 'foo'}"/>
</group>
</search>`;
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_list_table_grouped").toHaveCount(1, { message: "should be grouped" });
expect(".o_group_header").toHaveCount(2, {
message: "should be grouped by 'bar' (two groups) at first load",
});
// groupby 'foo' using the searchview
await toggleSearchBarMenu();
await toggleMenuItem("Foo");
expect(".o_group_header").toHaveCount(5, {
message: "should be grouped by 'foo' (five groups)",
});
// remove the groupby in the searchview
await contains(".o_control_panel .o_searchview .o_facet_remove").click();
expect(".o_list_table_grouped").toHaveCount(1, { message: "should still be grouped" });
expect(".o_group_header").toHaveCount(2, {
message: "should be grouped by 'bar' (two groups) at reload",
});
});
test("switch request to unknown view type", async () => {
defineActions([
{
id: 33,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[1, "kanban"],
],
},
]);
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
await getService("action").doAction(33);
expect(".o_list_view").toHaveCount(1, { message: "should display the list view" });
// try to open a record in a form view
contains(".o_list_view .o_data_row:first").click();
expect(".o_list_view").toHaveCount(1, { message: "should still display the list view" });
expect(".o_form_view").toHaveCount(0, { message: "should not display the form view" });
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"has_group",
]);
});
test.tags("desktop");
test("execute action with unknown view type", async () => {
defineActions([
{
id: 33,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "unknown"], // typically, an enterprise-only view on a community db
[false, "kanban"],
[false, "form"],
],
},
]);
await mountWithCleanup(WebClient);
await expect(getService("action").doAction(33)).rejects.toThrow(
/View types not defined unknown found in act_window action 33/
);
});
test("flags field of ir.actions.act_window is used", async () => {
// more info about flags field : https://github.com/odoo/odoo/commit/c9b133813b250e89f1f61816b0eabfb9bee2009d
defineActions([
{
id: 43,
name: "Partners",
res_id: 1,
res_model: "partner",
type: "ir.actions.act_window",
flags: {
mode: "edit",
},
views: [[false, "form"]],
},
{
id: 44,
name: "Partners",
res_id: 1,
res_model: "partner",
type: "ir.actions.act_window",
flags: {
mode: "readonly",
},
views: [[false, "form"]],
},
]);
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
// action 43 -> form in edit mode
await getService("action").doAction(43);
expect(".o_form_view .o_form_editable").toHaveCount(1, {
message: "should display the form view in edit mode",
});
// action 44 -> form in readonly mode
await getService("action").doAction(44);
expect(".o_form_view .o_form_readonly").toHaveCount(1, {
message: "should display the form view in readonly mode",
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_read",
"/web/action/load",
"get_views",
"web_read",
]);
});
test.tags("desktop");
test("save current search", async () => {
expect.assertions(4);
defineActions([
{
id: 33,
context: {
shouldNotBeInFilterContext: false,
},
name: "Partners",
res_model: "partner",
search_view_id: [4, "a custom search view"],
type: "ir.actions.act_window",
views: [[false, "list"]],
},
]);
patchWithCleanup(listView.Controller.prototype, {
setup() {
super.setup(...arguments);
useSetupAction({
getContext: () => ({ shouldBeInFilterContext: true }),
});
},
});
onRpc("create_or_replace", ({ args }) => {
expect(args[0].domain).toBe(`[("m2o", "=", 1)]`);
expect(args[0].context).toEqual({
group_by: [],
shouldBeInFilterContext: true,
});
return 3; // fake filter id
});
await mountWithCleanup(WebClient);
await getService("action").doAction(33);
expect(".o_data_row").toHaveCount(5, { message: "should contain 5 records" });
// filter on bar
await toggleSearchBarMenu();
await toggleMenuItem("M2O");
expect(".o_data_row").toHaveCount(3);
// save filter
await toggleSaveFavorite();
await editFavoriteName("some name");
await saveFavorite();
});
test.tags("desktop");
test("list with default_order and favorite filter with no orderedBy", async () => {
Partner._views["list,1"] = '<list default_order="foo desc"><field name="foo"/></list>';
defineActions([
{
id: 100,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[1, "list"],
[false, "form"],
],
},
]);
Partner._filters = [
{
name: "favorite filter",
id: 5,
context: "{}",
sort: "[]",
domain: '[("m2o", "=", 1)]',
is_default: false,
},
];
await mountWithCleanup(WebClient);
await getService("action").doAction(100);
expect(queryAllTexts(".o_data_row .o_data_cell")).toEqual(
["zoup", "yop", "plop", "gnap", "blip"],
{ message: "record should be in descending order as default_order applies" }
);
await toggleSearchBarMenu();
await toggleMenuItem("favorite filter");
expect(".o_control_panel .o_facet_values").toHaveText("favorite filter", {
message: "favorite filter should be applied",
});
expect(queryAllTexts(".o_data_row .o_data_cell")).toEqual(["zoup", "plop", "gnap"], {
message: "record should still be in descending order after default_order applied",
});
// go to formview and come back to listview
await contains(".o_list_view .o_data_row .o_data_cell").click();
await contains(".o_control_panel .breadcrumb a").click();
expect(queryAllTexts(".o_data_row .o_data_cell")).toEqual(["zoup", "plop", "gnap"], {
message: "order of records should not be changed, while coming back through breadcrumb",
});
// remove filter
await removeFacet("favorite filter");
expect(queryAllTexts(".o_data_row .o_data_cell")).toEqual(
["zoup", "yop", "plop", "gnap", "blip"],
{ message: "order of records should not be changed, after removing current filter" }
);
});
test.tags("desktop");
test("action with default favorite and context.active_id", async () => {
expect.assertions(4);
defineActions([
{
id: 3,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
context: "{ 'active_id': 4, 'active_ids': [4], 'active_model': 'whatever' }",
views: [[false, "list"]],
},
]);
Partner._filters = [
{
name: "favorite filter",
id: 5,
context: "{}",
sort: "[]",
domain: '[("bar", "=", 1)]',
is_default: true,
},
];
onRpc("web_search_read", ({ kwargs }) => {
expect(kwargs.domain).toEqual([["bar", "=", 1]]);
});
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
expect(".o_searchview .o_searchview_facet").toHaveCount(1);
expect(".o_facet_value").toHaveText("favorite filter");
});
test.tags("desktop");
test("search menus are still available when switching between actions", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction(1);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners Action 1"]);
expect(".o_searchview_dropdown_toggler").toHaveCount(1);
await getService("action").doAction(3);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners Action 1",
"Partners",
]);
expect(".o_searchview_dropdown_toggler").toHaveCount(1);
// go back using the breadcrumbs
await contains(".o_control_panel .breadcrumb-item a").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners Action 1"]);
expect(".o_searchview_dropdown_toggler").toHaveCount(1);
});
test.tags("desktop");
test("current act_window action is stored in session_storage if possible", async () => {
let expectedAction;
patchWithCleanup(browser, {
sessionStorage: Object.assign(Object.create(sessionStorage), {
setItem(k, value) {
expect(JSON.parse(value)).toEqual(expectedAction);
},
}),
});
await mountWithCleanup(WebClient);
// execute an action that can be stringified -> should be stored
expectedAction = MockServer.current.actions[3];
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
// execute an action that can't be stringified -> should not crash
expectedAction = {};
const x = {};
x.y = x;
await getService("action").doAction({
type: "ir.actions.act_window",
res_model: "partner",
views: [[false, "kanban"]],
flags: { x },
});
expect(".o_kanban_view").toHaveCount(1);
});
test.tags("desktop");
test("destroy action with lazy loaded controller", async () => {
redirect("/odoo/action-3/2");
await mountWithCleanup(WebClient);
await animationFrame(); // blank component
expect(".o_list_view").toHaveCount(0);
expect(".o_form_view").toHaveCount(1);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"Second record",
]);
await getService("action").doAction(1, { clearBreadcrumbs: true });
expect(".o_form_view").toHaveCount(0);
expect(".o_kanban_view").toHaveCount(1);
});
test.tags("desktop");
test("execute action from dirty, new record, and come back", async () => {
Partner._fields.bar = fields.Many2one({ relation: "partner", default: 1 });
Partner._views["form,false"] = `
<form>
<field name="display_name"/>
<field name="foo"/>
<field name="bar" readonly="1"/>
</form>`;
onRpc("get_formview_action", () => {
return {
res_id: 1,
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "form"]],
};
});
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
// execute an action and create a new record
await getService("action").doAction(3);
await clickListNew();
expect(".o_form_view .o_form_editable").toHaveCount(1);
expect(".o_form_uri:contains(First record)").toHaveCount(1);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners", "New"]);
// set form view dirty and open m2o record
await contains('.o_field_widget[name="display_name"] input').edit("test");
await contains(".o_field_widget[name=foo] input").edit("val");
await contains(".o_form_uri").click();
expect(".o_form_view .o_form_editable").toHaveCount(1);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Partners",
"test",
"First record",
]);
// go back to test using the breadcrumbs
await contains(".o_control_panel .breadcrumb-item a:eq(1)").click();
expect(".o_form_view .o_form_editable").toHaveCount(1);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Partners", "test"]);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"has_group",
"onchange",
"get_formview_action",
"web_save",
"get_views",
"web_read",
"web_read",
]);
});
test.tags("desktop");
test("execute a contextual action from a form view", async () => {
expect.assertions(4);
const contextualAction = {
id: 8,
name: "Favorite Ponies",
res_model: "pony",
type: "ir.actions.act_window",
context: "{}", // need a context to evaluate
views: [
[false, "list"],
[false, "form"],
],
};
defineActions([contextualAction]);
Partner._toolbar = {
action: [contextualAction],
print: [],
};
onRpc("partner", "get_views", ({ kwargs }) => {
expect(kwargs.options.toolbar).toBe(true, {
message: "should ask for toolbar information",
});
});
await mountWithCleanup(WebClient);
// execute an action and open a record
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
await contains(".o_data_row .o_data_cell").click();
expect(".o_form_view").toHaveCount(1);
// execute the custom action from the action menu
await contains(".o_cp_action_menus .fa-cog").click();
await toggleMenuItem("Favorite Ponies");
expect(".o_list_view").toHaveCount(1);
});
test.tags("desktop");
test("go back to action with form view as main view, and res_id", async () => {
defineActions([
{
id: 999,
name: "Partner",
res_model: "partner",
type: "ir.actions.act_window",
res_id: 2,
views: [[44, "form"]],
},
]);
Partner._views["form,44"] = '<form><field name="m2o"/></form>';
onRpc("get_formview_action", () => {
return {
res_id: 3,
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "form"]],
};
});
await mountWithCleanup(WebClient);
await getService("action").doAction(999);
expect(".o_form_view .o_form_editable").toHaveCount(1);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Second record"]);
// push another action in the breadcrumb
await contains(".o_field_many2one .o_external_button", { visible: false }).click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Second record",
"Third record",
]);
// go back to the form view
await contains(".o_control_panel .breadcrumb a").click();
expect(".o_form_view .o_form_editable").toHaveCount(1);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Second record"]);
});
test.tags("desktop");
test("action with res_id, load another res_id, do new action, restore previous", async () => {
const action = {
id: 999,
name: "Partner",
res_model: "partner",
type: "ir.actions.act_window",
res_id: 1,
views: [[44, "form"]],
};
defineActions([action]);
Partner._views["form,44"] = '<form><field name="m2o"/></form>';
onRpc("get_formview_action", () => {
return { ...action, res_id: 3 };
});
await mountWithCleanup(WebClient);
await getService("action").doAction(999, { props: { resIds: [1, 2] } });
expect(".o_form_view .o_form_editable").toHaveCount(1);
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["First record"]);
expect(".o_control_panel .o_pager_counter").toHaveText("1 / 2");
// load another id on current action (through pager)
await contains(".o_pager_next").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Second record"]);
expect(".o_control_panel .o_pager_counter").toHaveText("2 / 2");
// push another action in the breadcrumb
await contains(".o_field_many2one .o_external_button", { visible: false }).click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual([
"Second record",
"Third record",
]);
// restore previous action through breadcrumb
await contains(".o_control_panel .breadcrumb a").click();
expect(queryAllTexts(".breadcrumb-item, .o_breadcrumb .active")).toEqual(["Second record"]);
expect(".o_control_panel .o_pager_counter").toHaveText("2 / 2");
});
test.tags("desktop");
test("open a record, come back, and create a new record", async () => {
await mountWithCleanup(WebClient);
// execute an action and open a record
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
expect(".o_list_view .o_data_row").toHaveCount(5);
await contains(".o_list_view .o_data_cell").click();
expect(".o_form_view").toHaveCount(1);
expect(".o_form_view .o_form_editable").toHaveCount(1);
// go back using the breadcrumbs
await contains(".o_control_panel .breadcrumb-item a").click();
expect(".o_list_view").toHaveCount(1);
// create a new record
await clickListNew();
expect(".o_form_view").toHaveCount(1);
expect(".o_form_view .o_form_editable").toHaveCount(1);
});
test.tags("desktop");
test("open form view, use the pager, execute action, and come back", async () => {
await mountWithCleanup(WebClient);
// execute an action and open a record
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
expect(".o_list_view .o_data_row").toHaveCount(5);
await contains(".o_list_view .o_data_cell").click();
expect(".o_form_view").toHaveCount(1);
expect(".o_field_widget[name=display_name] input").toHaveValue("First record");
// switch to second record
await contains(".o_pager_next").click();
expect(".o_field_widget[name=display_name] input").toHaveValue("Second record");
// execute an action from the second record
await contains(".o_statusbar_buttons button[name='4']").click();
expect(".o_kanban_view").toHaveCount(1);
// go back using the breadcrumbs
await contains(".o_control_panel .breadcrumb-item:eq(1) a").click();
expect(".o_form_view").toHaveCount(1);
expect(".o_field_widget[name=display_name] input").toHaveValue("Second record");
});
test.tags("desktop");
test("create a new record in a form view, execute action, and come back", async () => {
await mountWithCleanup(WebClient);
// execute an action and create a new record
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
await clickListNew();
expect(".o_form_view").toHaveCount(1);
expect(".o_form_view .o_form_editable").toHaveCount(1);
await contains(".o_field_widget[name=display_name] input").edit("another record");
await contains(".o_form_button_save").click();
expect(".o_form_view .o_form_editable").toHaveCount(1);
// execute an action from the second record
await contains(".o_statusbar_buttons button[name='4']").click();
expect(".o_kanban_view").toHaveCount(1);
// go back using the breadcrumbs
await contains(".o_control_panel .breadcrumb-item:eq(1) a").click();
expect(".o_form_view").toHaveCount(1);
expect(".o_form_view .o_form_editable").toHaveCount(1);
expect(".o_field_widget[name=display_name] input").toHaveValue("another record");
});
test("onClose should be called only once with right parameters", async () => {
expect.assertions(4);
await mountWithCleanup(WebClient);
await getService("action").doAction(2); // main form view
await getService("action").doAction(5, {
// form view in target new
onClose(infos) {
expect.step("onClose");
expect(infos).toEqual({ cantaloupe: "island" });
},
});
expect(".modal").toHaveCount(1);
await getService("action").doAction({
type: "ir.actions.act_window_close",
infos: { cantaloupe: "island" },
});
expect.verifySteps(["onClose"]);
expect(".modal").toHaveCount(0);
});
test.tags("desktop");
test("search view should keep focus during do_search", async () => {
// One should be able to type something in the search view, press on enter to
// make the facet and trigger the search, then do this process
// over and over again seamlessly.
// Verifying the input's value is a lot trickier than verifying the search_read
// because of how native events are handled in tests
const searchPromise = new Deferred();
onRpc("web_search_read", async ({ kwargs }) => {
expect.step("search_read " + kwargs.domain);
if (JSON.stringify(kwargs.domain) === JSON.stringify([["foo", "ilike", "m"]])) {
await searchPromise;
}
});
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
await editSearch("m");
await validateSearch();
expect.verifySteps(["search_read ", "search_read foo,ilike,m"]);
// Triggering the do_search above will kill the current searchview Input
await editSearch("o");
// We have something in the input of the search view. Making the search_read
// return at this point will trigger the redraw of the view.
// However we want to hold on to what we just typed
searchPromise.resolve();
await validateSearch();
expect.verifySteps(["search_read |,foo,ilike,m,foo,ilike,o"]);
});
test.tags("desktop");
test("Call twice clearUncommittedChanges in a row does not save twice", async () => {
let writeCalls = 0;
onRpc("web_save", () => {
writeCalls += 1;
});
const env = await makeMockEnv();
await mountWithCleanup(WebClient, { env });
// execute an action and edit existing record
await getService("action").doAction(3);
await contains(".o_list_view .o_data_cell").click();
expect(".o_form_view .o_form_editable").toHaveCount(1);
await contains(".o_field_widget[name=foo] input").edit("val");
clearUncommittedChanges(env);
await animationFrame();
expect(".modal").toHaveCount(0);
clearUncommittedChanges(env);
await animationFrame();
expect(".modal").toHaveCount(0);
expect(writeCalls).toBe(1);
});
test.tags("desktop");
test("executing a window action with onchange warning does not hide it", async () => {
Partner._views["form,false"] = `<form><field name="foo"/></form>`;
onRpc("onchange", () => {
return {
value: {},
warning: {
title: "Warning",
message: "Everything is alright",
type: "dialog",
},
};
});
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
await clickListNew();
expect(".modal.o_technical_modal").toHaveCount(1, {
message: "Warning modal should be opened",
});
await contains(".modal.o_technical_modal button.btn-primary").click();
expect(".modal.o_technical_modal").toHaveCount(0, {
message: "Warning modal should be closed",
});
});
test("do not call clearUncommittedChanges() when target=new and dialog is opened", async () => {
await mountWithCleanup(WebClient);
// Open Partner form view and enter some text
await getService("action").doAction(3, { viewType: "form" });
expect(".o_action_manager .o_form_view .o_form_editable").toHaveCount(1);
await contains(".o_field_widget[name=display_name] input").edit("TEST");
// Open dialog without saving should not ask to discard
await getService("action").doAction(5);
expect(".o_action_manager .o_form_view .o_form_editable").toHaveCount(1);
expect(".o_dialog .o_view_controller").toHaveCount(1);
});
test("do not pushState when target=new and dialog is opened", async () => {
await mountWithCleanup(WebClient);
// Open Partner form in create mode
await getService("action").doAction(3, { viewType: "form" });
await runAllTimers();
const prevUrlState = Object.assign({}, router.current);
// Edit another partner in a dialog
await getService("action").doAction({
name: "Edit a Partner",
res_model: "partner",
res_id: 3,
type: "ir.actions.act_window",
views: [[3, "form"]],
target: "new",
view_mode: "form",
});
await runAllTimers();
expect(router.current).toEqual(prevUrlState, {
message: "push_state in dialog shouldn't change the hash",
});
});
test.tags("desktop");
test("do not restore after action button clicked on desktop", async () => {
Partner._views["form,false"] = `
<form>
<header>
<button name="do_something" string="Call button" type="object"/>
</header>
<sheet>
<field name="display_name"/>
</sheet>
</form>`;
onRpc("/web/dataset/call_button/*", () => true);
await mountWithCleanup(WebClient);
await getService("action").doAction(3, { viewType: "form", props: { resId: 1 } });
await contains("div[name='display_name'] input").edit("Edited value");
expect(".o_form_button_save").toBeVisible();
expect(".o_statusbar_buttons button[name=do_something]").toBeVisible();
await contains(".o_statusbar_buttons button[name=do_something]").click();
expect(".o_form_buttons_view .o_form_button_save").not.toBeVisible();
});
test.tags("mobile");
test("do not restore after action button clicked on mobile", async () => {
Partner._views["form,false"] = `
<form>
<header>
<button name="do_something" string="Call button" type="object"/>
</header>
<sheet>
<field name="display_name"/>
</sheet>
</form>`;
onRpc("/web/dataset/call_button/*", () => true);
await mountWithCleanup(WebClient);
await getService("action").doAction(3, { viewType: "form", props: { resId: 1 } });
await contains("div[name='display_name'] input").edit("Edited value");
expect(".o_form_button_save").toBeVisible();
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
expect(".o-dropdown-item-unstyled-button button[name=do_something]").toBeVisible();
await contains(".o-dropdown-item-unstyled-button button[name=do_something]").click();
expect(".o_form_buttons_view .o_form_button_save").not.toBeVisible();
});
test("debugManager is active for views", async () => {
serverState.debug = "1";
onRpc("has_access", () => true);
await mountWithCleanup(WebClient);
await getService("action").doAction(1);
expect(".o-dropdown--menu .o-dropdown-item:contains('View: Kanban')").toHaveCount(0);
await contains(".o_debug_manager .dropdown-toggle").click();
expect(".o-dropdown--menu .o-dropdown-item:contains('View: Kanban')").toHaveCount(1);
});
test.tags("desktop");
test("reload a view via the view switcher keep state", async () => {
onRpc("read_group", () => {
expect.step("read_group");
});
await mountWithCleanup(WebClient);
await getService("action").doAction({
id: 3,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "pivot"],
[false, "list"],
],
});
expect(".o_pivot_measure_row").not.toHaveClass("o_pivot_sort_order_asc");
await contains(".o_pivot_measure_row").click();
expect(".o_pivot_measure_row").toHaveClass("o_pivot_sort_order_asc");
await switchView("pivot");
expect(".o_pivot_measure_row").toHaveClass("o_pivot_sort_order_asc");
expect.verifySteps([
"read_group", // initial read_group
"read_group", // read_group at reload after switch view
]);
});
test("doAction supports being passed globalState prop", async () => {
expect.assertions(1);
const searchModel = JSON.stringify({
nextGroupId: 2,
nextGroupNumber: 2,
nextId: 2,
searchItems: {
1: {
description: `ID is "99"`,
domain: `[("id","=",99)]`,
type: "filter",
groupId: 1,
groupNumber: 1,
id: 1,
},
},
query: [{ searchItemId: 1 }],
sections: [],
});
onRpc("web_search_read", ({ kwargs }) => {
expect(kwargs.domain).toEqual([["id", "=", 99]]);
});
await mountWithCleanup(WebClient);
await getService("action").doAction(1, {
props: {
globalState: { searchModel },
},
});
});
test.tags("desktop");
test("window action in target new fails (onchange) on desktop", async () => {
expect.errors(1);
onRpc("partner", "onchange", () => {
throw makeServerError({ type: "ValidationError" });
});
Partner._views["form,74"] = /*xml*/ `
<form>
<header>
<button name="5" string="Test" type="action"/>
</header>
<field name="display_name"/>
</form>`;
await mountWithCleanup(WebClient);
await getService("action").doAction(2);
await contains(".o_form_view button[name='5']").click();
await waitFor(".modal"); // errors are async
expect(".modal").toHaveCount(1);
expect(".modal .o_error_dialog").toHaveCount(1);
expect(".modal .modal-title").toHaveText("Validation Error");
});
test.tags("mobile");
test("window action in target new fails (onchange) on mobile", async () => {
expect.errors(1);
onRpc("partner", "onchange", () => {
throw makeServerError({ type: "ValidationError" });
});
Partner._views["form,74"] = /*xml*/ `
<form>
<header>
<button name="5" string="Test" type="action"/>
</header>
<field name="display_name"/>
</form>`;
await mountWithCleanup(WebClient);
await getService("action").doAction(2);
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
await contains(".o-dropdown-item-unstyled-button button[name='5']").click();
await waitFor(".modal"); // errors are async
expect(".modal").toHaveCount(1);
expect(".modal .o_error_dialog").toHaveCount(1);
expect(".modal .modal-title").toHaveText("Validation Error");
});
test.tags("desktop");
test("Uncaught error in target new is catch only once on desktop", async () => {
expect.errors(1);
defineActions([
{
id: 26,
name: "Partner",
res_model: "partner",
target: "new",
type: "ir.actions.act_window",
views: [[false, "list"]],
},
]);
onRpc("partner", "web_search_read", () => {
throw makeServerError({ type: "ValidationError" });
});
Partner._views["form,74"] = /*xml*/ `
<form>
<header>
<button name="26" string="Test" type="action"/>
</header>
<field name="display_name"/>
</form>`;
await mountWithCleanup(WebClient);
await getService("action").doAction(2);
await contains(".o_form_view button[name='26']").click();
await waitFor(".modal"); // errors are async
expect(".modal").toHaveCount(1);
expect(".modal .o_error_dialog").toHaveCount(1);
expect(".modal .modal-title").toHaveText("Validation Error");
});
test.tags("mobile");
test("Uncaught error in target new is catch only once on mobile", async () => {
expect.errors(1);
defineActions([
{
id: 26,
name: "Partner",
res_model: "partner",
target: "new",
type: "ir.actions.act_window",
views: [[false, "list"]],
},
]);
onRpc("partner", "web_search_read", () => {
throw makeServerError({ type: "ValidationError" });
});
Partner._views["form,74"] = /*xml*/ `
<form>
<header>
<button name="26" string="Test" type="action"/>
</header>
<field name="display_name"/>
</form>`;
await mountWithCleanup(WebClient);
await getService("action").doAction(2);
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
await contains(".o-dropdown-item-unstyled-button button[name='26']").click();
await waitFor(".modal"); // errors are async
expect(".modal").toHaveCount(1);
expect(".modal .o_error_dialog").toHaveCount(1);
expect(".modal .modal-title").toHaveText("Validation Error");
});
test("action and get_views rpcs are cached", async () => {
class IrActionsAct_Window extends models.Model {
_name = "ir.actions.act_window";
}
defineModels([IrActionsAct_Window]);
stepAllNetworkCalls();
await mountWithCleanup(WebClient);
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
await getService("action").doAction(1);
expect(".o_kanban_view").toHaveCount(1);
expect.verifySteps(["/web/action/load", "get_views", "web_search_read"]);
await getService("action").doAction(1);
expect(".o_kanban_view").toHaveCount(1);
expect.verifySteps(["web_search_read"]);
await getService("orm").unlink("ir.actions.act_window", [333]);
expect.verifySteps(["unlink"]);
await getService("action").doAction(1);
// cache was cleared => reload the action
expect.verifySteps(["/web/action/load", "web_search_read"]);
});
test.tags("desktop");
test("pushState also changes the title of the tab", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction(3); // list view
const titleService = getService("title");
expect(titleService.current).toBe("Partners");
await contains(".o_data_row .o_data_cell").click();
expect(titleService.current).toBe("First record");
await contains(".o_pager_next").click();
expect(titleService.current).toBe("Second record");
});
test("action group_by of type string", async () => {
Partner._views["pivot,false"] = `<pivot/>`;
await mountWithCleanup(WebClient);
await getService("action").doAction({
name: "Partner",
res_model: "partner",
type: "ir.actions.act_window",
views: [[3, "pivot"]],
context: { group_by: "foo" },
});
expect(".o_pivot_view").toHaveCount(1);
expect(".o_pivot_view tbody th").toHaveCount(6);
});
test("action help given to View in props if not empty", async () => {
Partner._records = [];
const action = {
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "list"]],
};
defineActions([
{
...action,
id: 14,
help: '<p class="hello">Hello</p>',
},
{
...action,
id: 15,
help: '<p class="hello"></p>',
},
]);
await mountWithCleanup(WebClient);
await getService("action").doAction(14);
expect(".o_list_view").toHaveCount(1);
expect(".o_view_nocontent").toHaveCount(1);
expect(".o_view_nocontent").toHaveText("Hello");
expect("table").toHaveCount(1);
await getService("action").doAction(15);
expect(".o_list_view").toHaveCount(1);
expect(".o_view_nocontent").toHaveCount(0);
});
test("load a tree", async () => {
Partner._views = {
"list,false": `<list><field name="foo"/></list>`,
"search,false": `<search/>`,
};
await mountWithCleanup(WebClient);
await getService("action").doAction({
res_id: 1,
type: "ir.actions.act_window",
target: "current",
res_model: "partner",
views: [[false, "list"]],
});
expect(".o_list_view").toHaveCount(1);
});
test.tags("desktop");
test("sample server: populate groups", async () => {
Partner._records = [];
Partner._views = {
"kanban,false": `
<kanban sample="1" default_group_by="write_date:month">
<templates>
<t t-name="card">
<field name="display_name"/>
</t>
</templates>
</kanban>`,
"pivot,false": `
<pivot sample="1">
<field name="write_date" type="row"/>
</pivot>`,
"search,false": `<search/>`,
};
onRpc("web_read_group", () => {
return {
groups: [
{
date_count: 0,
"write_date:month": "December 2022",
__range: {
"write_date:month": {
from: "2022-12-01",
to: "2023-01-01",
},
},
__domain: [
["write_date", ">=", "2022-12-01"],
["write_date", "<", "2023-01-01"],
],
},
],
length: 1,
};
});
await mountWithCleanup(WebClient);
await getService("action").doAction({
res_id: 1,
type: "ir.actions.act_window",
target: "current",
res_model: "partner",
views: [
[false, "kanban"],
[false, "pivot"],
],
});
expect(".o_kanban_view .o_view_sample_data").toHaveCount(1);
expect(".o_column_title").toHaveText("December 2022");
await switchView("pivot");
expect(".o_pivot_view .o_view_sample_data").toHaveCount(1);
});
test.tags("desktop");
test("click on breadcrumb of a deleted record", async () => {
expect.errors(1);
Partner._views["form,false"] = `
<form>
<button type="action" name="3" string="Open Action 3" class="my_btn"/>
</form>`;
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
await contains(".o_data_row .o_data_cell").click();
expect(".o_form_view").toHaveCount(1);
await contains(".my_btn").click();
expect(".o_list_view").toHaveCount(1);
await contains(".o_data_row .o_data_cell").click();
expect(".o_form_view").toHaveCount(1);
expect(queryAllTexts(".breadcrumb-item")).toEqual(["", "First record", "Partners"]);
expect(".o_breadcrumb .active").toHaveText("First record");
// open action menu and delete
await contains(".o_cp_action_menus .fa-cog").click();
await toggleMenuItem("Delete");
expect(".o_dialog").toHaveCount(1);
// confirm
await contains(".o_dialog .modal-footer .btn-primary").click();
expect(".o_form_view").toHaveCount(1);
expect(queryAllTexts(".breadcrumb-item")).toEqual(["", "First record", "Partners"]);
expect(".o_breadcrumb .active").toHaveText("Second record");
// click on "First record" in breadcrumbs, which doesn't exist anymore
await contains(".breadcrumb-item a").click();
await animationFrame();
expect(".o_list_view").toHaveCount(1);
expect(queryAllTexts(".breadcrumb-item")).toEqual([]);
expect(".o_breadcrumb .active").toHaveText("Partners");
expect.verifyErrors([
"It seems the records with IDs 1 cannot be found. They might have been deleted.",
]);
});
test.tags("desktop");
test("executing an action closes dialogs", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction(3);
expect(".o_list_view").toHaveCount(1);
getService("dialog").add(FormViewDialog, { resModel: "partner", resId: 1 });
await animationFrame();
expect(".o_dialog .o_form_view").toHaveCount(1);
await contains(".o_dialog .o_form_view .o_statusbar_buttons button[name='4']").click();
expect(".o_kanban_view").toHaveCount(1);
expect(".o_dialog").toHaveCount(0);
});