Odoo18-Base/addons/web/static/tests/webclient/actions/window_action_tests.js
2025-03-10 11:12:23 +07:00

2731 lines
110 KiB
JavaScript

/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { editView } from "@web/views/debug_items";
import { clearUncommittedChanges } from "@web/webclient/actions/action_service";
import { listView } from "@web/views/list/list_view";
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
import { useSetupAction } from "@web/webclient/actions/action_hook";
import testUtils from "web.test_utils";
import { session } from "@web/session";
import {
click,
getFixture,
editInput,
getNodesTextContent,
legacyExtraNextTick,
makeDeferred,
nextTick,
patchWithCleanup,
clickSave,
} from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerServerData, loadState } from "./../helpers";
import { errorService } from "../../../src/core/errors/error_service";
import { RPCError } from "@web/core/network/rpc_service";
import { registerCleanup } from "../../helpers/cleanup";
import { WarningDialog } from "@web/core/errors/error_dialogs";
import { makeFakeUserService, fakeCookieService } from "@web/../tests/helpers/mock_services";
import * as cpHelpers from "@web/../tests/search/helpers";
import { onMounted } from "@odoo/owl";
let serverData;
let target;
const serviceRegistry = registry.category("services");
QUnit.module("ActionManager", (hooks) => {
hooks.beforeEach(() => {
serverData = getActionManagerServerData();
target = getFixture();
});
QUnit.module("Window Actions");
QUnit.test("can execute act_window actions from db ID", async function (assert) {
assert.expect(7);
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1);
assert.containsOnce(
document.body,
".o_control_panel",
"should have rendered a control panel"
);
assert.containsOnce(target, ".o_kanban_view", "should have rendered a kanban view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
]);
});
QUnit.test("sidebar is present in list view", async function (assert) {
assert.expect(4);
serverData.models.partner.toolbar = {
print: [{ name: "Print that record" }],
};
const mockRPC = async (route, args) => {
if (args && args.method === "get_views") {
assert.strictEqual(
args.kwargs.options.toolbar,
true,
"should ask for toolbar information"
);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsNone(target, ".o_cp_action_menus");
await click(target.querySelector("input.form-check-input"));
assert.isVisible(
$(target).find('.o_cp_action_menus button.dropdown-toggle:contains("Print")')[0]
);
assert.isVisible(
$(target).find('.o_cp_action_menus button.dropdown-toggle:contains("Action")')[0]
);
});
QUnit.test("can switch between views", async function (assert) {
assert.expect(19);
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view", "should display the list view");
// switch to kanban view
await cpHelpers.switchView(target, "kanban");
await legacyExtraNextTick();
assert.containsNone(target, ".o_list_view", "should no longer display the list view");
assert.containsOnce(target, ".o_kanban_view", "should display the kanban view");
// switch back to list view
await cpHelpers.switchView(target, "list");
await legacyExtraNextTick();
assert.containsOnce(target, ".o_list_view", "should display the list view");
assert.containsNone(target, ".o_kanban_view", "should no longer display the kanban view");
// open a record in form view
await testUtils.dom.click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.containsNone(target, ".o_list_view", "should no longer display the list view");
assert.containsOnce(target, ".o_form_view", "should display the form view");
assert.strictEqual(
$(target).find(".o_field_widget[name=foo] input").val(),
"yop",
"should have opened the correct record"
);
// go back to list view using the breadcrumbs
await testUtils.dom.click(target.querySelector(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_list_view", "should display the list view");
assert.containsNone(target, ".o_form_view", "should no longer display the form view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"web_search_read",
"web_search_read",
"read",
"web_search_read",
]);
});
QUnit.test("switching into a view with mode=edit lands in edit mode", async function (assert) {
serverData.views["partner,1,kanban"] = `
<kanban on_create="quick_create" default_group_by="m2o">
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click"><field name="foo"/></div>
</t>
</templates>
</kanban>`;
serverData.actions[1] = {
id: 1,
xml_id: "action_1",
name: "Partners Action 1 patched",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "kanban"],
[false, "form"],
],
};
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1);
assert.containsOnce(target, ".o_kanban_view", "should display the kanban view");
// quick create record
await testUtils.dom.click(target.querySelector(".o-kanban-button-new"));
await editInput(target, ".o_field_widget[name=display_name] input", "New name");
// edit quick-created record
await testUtils.dom.click(target.querySelector(".o_kanban_edit"));
assert.containsOnce(
target,
".o_form_view .o_form_editable",
"should display the form view in edit mode"
);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_read_group",
"web_search_read",
"web_search_read",
"onchange",
"name_create",
"read",
"read",
]);
});
QUnit.test(
"orderedBy in context is not propagated when executing another action",
async function (assert) {
assert.expect(6);
serverData.models.partner.fields.foo.sortable = true;
serverData.views["partner,false,form"] = `
<form>
<header>
<button name="8" string="Execute action" type="action"/>
</header>
</form>`;
serverData.models.partner.filters = [
{
id: 1,
context: "{}",
domain: "[]",
sort: "[]",
is_default: true,
name: "My filter",
},
];
let searchReadCount = 1;
const mockRPC = async (route, args) => {
if (args.method === "web_search_read") {
args = args || {};
if (searchReadCount === 1) {
assert.strictEqual(args.model, "partner");
assert.notOk(args.sort);
}
if (searchReadCount === 2) {
assert.strictEqual(args.model, "partner");
assert.strictEqual(args.kwargs.order, "foo ASC");
}
if (searchReadCount === 3) {
assert.strictEqual(args.model, "pony");
assert.notOk(args.sort);
}
searchReadCount += 1;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
// Sort records
await click(target.querySelector(".o_list_view th.o_column_sortable"));
// Get to the form view of the model, on the first record
await click(target.querySelector(".o_data_cell"));
// Execute another action by clicking on the button within the form
await click(target.querySelector('button[name="8"]'));
}
);
QUnit.test("breadcrumbs are updated when switching between views", async function (assert) {
assert.expect(15);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should be one controller in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners",
"breadcrumbs should display the display_name of the action"
);
// switch to kanban view
await cpHelpers.switchView(target, "kanban");
await legacyExtraNextTick();
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should still be one controller in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners",
"breadcrumbs should still display the display_name of the action"
);
// open a record in form view
await testUtils.dom.click(target.querySelector(".o_kanban_view .o_kanban_record"));
await legacyExtraNextTick();
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
2,
"there should be two controllers in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"First record"
);
// go back to kanban view using the breadcrumbs
await testUtils.dom.click(target.querySelector(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should be one controller in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners",
"breadcrumbs should display the display_name of the action"
);
// switch back to list view
await cpHelpers.switchView(target, "list");
await legacyExtraNextTick();
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should still be one controller in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners",
"breadcrumbs should still display the display_name of the action"
);
// open a record in form view
await testUtils.dom.click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
2,
"there should be two controllers in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"First record"
);
// go back to list view using the breadcrumbs
await testUtils.dom.click(target.querySelector(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_list_view", "should be back on list view");
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should be one controller in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item").text(),
"Partners",
"breadcrumbs should display the display_name of the action"
);
});
QUnit.test("switch buttons are updated when switching between views", async function (assert) {
assert.expect(13);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsN(
target,
".o_control_panel button.o_switch_view",
2,
"should have two switch buttons (list and kanban)"
);
assert.containsOnce(
target,
".o_control_panel button.o_switch_view.active",
"should have only one active button"
);
assert.hasClass(
target.querySelector(".o_control_panel .o_switch_view"),
"o_list",
"list switch button should be the first one"
);
assert.hasClass(
target.querySelector(".o_control_panel .o_switch_view.o_list"),
"active",
"list should be the active view"
);
// switch to kanban view
await cpHelpers.switchView(target, "kanban");
await legacyExtraNextTick();
assert.containsN(
target,
".o_control_panel .o_switch_view",
2,
"should still have two switch buttons (list and kanban)"
);
assert.containsOnce(
target,
".o_control_panel .o_switch_view.active",
"should still have only one active button"
);
assert.hasClass(
target.querySelector(".o_control_panel .o_switch_view"),
"o_list",
"list switch button should still be the first one"
);
assert.hasClass(
target.querySelector(".o_control_panel .o_switch_view.o_kanban"),
"active",
"kanban should now be the active view"
);
// switch back to list view
await cpHelpers.switchView(target, "list");
await legacyExtraNextTick();
assert.containsN(
target,
".o_control_panel .o_switch_view",
2,
"should still have two switch buttons (list and kanban)"
);
assert.hasClass(
target.querySelector(".o_control_panel .o_switch_view.o_list"),
"active",
"list should now be the active view"
);
// open a record in form view
await testUtils.dom.click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.containsNone(
target,
".o_control_panel .o_switch_view",
"should not have any switch buttons"
);
// go back to list view using the breadcrumbs
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
assert.containsN(
target,
".o_control_panel .o_switch_view",
2,
"should have two switch buttons (list and kanban)"
);
assert.hasClass(
target.querySelector(".o_control_panel .o_switch_view.o_list"),
"active",
"list should be the active view"
);
});
QUnit.test("pager is updated when switching between views", async function (assert) {
assert.expect(10);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 4);
assert.strictEqual(
$(target).find(".o_control_panel .o_pager_value").text(),
"1-5",
"value should be correct for kanban"
);
assert.strictEqual(
$(target).find(".o_control_panel .o_pager_limit").text(),
"5",
"limit should be correct for kanban"
);
// switch to list view
await cpHelpers.switchView(target, "list");
await legacyExtraNextTick();
assert.strictEqual(
$(target).find(".o_control_panel .o_pager_value").text(),
"1-3",
"value should be correct for list"
);
assert.strictEqual(
$(target).find(".o_control_panel .o_pager_limit").text(),
"5",
"limit should be correct for list"
);
// open a record in form view
await testUtils.dom.click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.strictEqual(
$(target).find(".o_control_panel .o_pager_value").text(),
"1",
"value should be correct for form"
);
assert.strictEqual(
$(target).find(".o_control_panel .o_pager_limit").text(),
"3",
"limit should be correct for form"
);
// go back to list view using the breadcrumbs
await testUtils.dom.click(target.querySelector(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
assert.strictEqual(
$(target).find(".o_control_panel .o_pager_value").text(),
"1-3",
"value should be correct for list"
);
assert.strictEqual(
$(target).find(".o_control_panel .o_pager_limit").text(),
"5",
"limit should be correct for list"
);
// switch back to kanban view
await cpHelpers.switchView(target, "kanban");
await legacyExtraNextTick();
assert.strictEqual(
$(target).find(".o_control_panel .o_pager_value").text(),
"1-5",
"value should be correct for kanban"
);
assert.strictEqual(
$(target).find(".o_control_panel .o_pager_limit").text(),
"5",
"limit should be correct for kanban"
);
});
QUnit.test("Props are updated and kept when switching/restoring views", async (assert) => {
serverData.views["partner,false,form"] = /* xml */ `
<form>
<group>
<field name="display_name" />
<field name="m2o" open_target="current" />
</group>
</form>
`;
const mockRPC = async (_route, { args, method, model }) => {
if (method === "get_formview_action") {
return {
res_id: args[0][0],
res_model: model,
type: "ir.actions.act_window",
views: [[false, "form"]],
};
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
// 5 records initially
assert.containsN(target, ".o_data_row", 5);
await click(target, ".o_data_row:first-of-type .o_data_cell");
// Open 1 / 5
assert.strictEqual(target.querySelector(".o_field_char input").value, "First record");
assert.deepEqual(cpHelpers.getPagerValue(target), [1]);
assert.strictEqual(cpHelpers.getPagerLimit(target), 5);
await click(target, ".o_field_many2one .o_external_button");
// Click on M2O -> 1 / 1
assert.strictEqual(target.querySelector(".o_field_char input").value, "Third record");
assert.deepEqual(cpHelpers.getPagerValue(target), [1]);
assert.strictEqual(cpHelpers.getPagerLimit(target), 1);
await click(target, ".o_back_button");
// Back to 1 / 5
assert.strictEqual(target.querySelector(".o_field_char input").value, "First record");
assert.deepEqual(cpHelpers.getPagerValue(target), [1]);
assert.strictEqual(cpHelpers.getPagerLimit(target), 5);
await cpHelpers.pagerNext(target);
// Next page -> 2 / 5
assert.strictEqual(target.querySelector(".o_field_char input").value, "Second record");
assert.deepEqual(cpHelpers.getPagerValue(target), [2]);
assert.strictEqual(cpHelpers.getPagerLimit(target), 5);
await click(target, ".o_field_many2one .o_external_button");
// Click on M2O -> still 1 / 1
assert.strictEqual(target.querySelector(".o_field_char input").value, "Third record");
assert.deepEqual(cpHelpers.getPagerValue(target), [1]);
assert.strictEqual(cpHelpers.getPagerLimit(target), 1);
await click(target, ".o_back_button");
// Back to 2 / 5
assert.strictEqual(target.querySelector(".o_field_char input").value, "Second record");
assert.deepEqual(cpHelpers.getPagerValue(target), [2]);
assert.strictEqual(cpHelpers.getPagerLimit(target), 5);
});
QUnit.test("domain is kept when switching between views", async function (assert) {
assert.expect(5);
serverData.actions[3].search_view_id = [4, "a custom search view"];
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsN(target, ".o_data_row", 5);
// activate a domain
await cpHelpers.toggleFilterMenu(target);
await cpHelpers.toggleMenuItem(target, "Bar");
await legacyExtraNextTick();
assert.containsN(target, ".o_data_row", 2);
// switch to kanban
await cpHelpers.switchView(target, "kanban");
await legacyExtraNextTick();
assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2);
// remove the domain
await testUtils.dom.click(target.querySelector(".o_searchview .o_facet_remove"));
await legacyExtraNextTick();
assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 5);
// switch back to list
await cpHelpers.switchView(target, "list");
await legacyExtraNextTick();
assert.containsN(target, ".o_data_row", 5);
});
QUnit.test("A new form view can be reloaded after a failed one", async function (assert) {
assert.expect(5);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
await cpHelpers.switchView(target, "list");
assert.containsOnce(target, ".o_list_view", "The list view should be displayed");
// Click on the first record
await testUtils.dom.click(
$(target).find(".o_list_view .o_data_row:first .o_data_cell:first")
);
await legacyExtraNextTick();
assert.containsOnce(target, ".o_form_view", "The form view should be displayed");
// Delete the current record
await testUtils.controlPanel.toggleActionMenu(target);
await testUtils.dom.click(
Array.from(document.querySelectorAll(".o_menu_item")).find(
(e) => e.textContent === "Delete"
)
);
await legacyExtraNextTick();
assert.containsOnce(target, ".modal", "a confirm modal should be displayed");
await testUtils.dom.click(target.querySelector(".modal-footer button.btn-primary"));
await legacyExtraNextTick();
// The form view is automatically switched to the next record
// Go back to the previous (now deleted) record
webClient.env.bus.trigger("test:hashchange", {
model: "partner",
id: 1,
action: 3,
view_type: "form",
});
await testUtils.nextTick();
await legacyExtraNextTick();
// Go back to the list view
webClient.env.bus.trigger("test:hashchange", {
model: "partner",
action: 3,
view_type: "list",
});
await testUtils.nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_list_view", "should still display the list view");
await testUtils.dom.click(
$(target).find(".o_list_view .o_data_row:first .o_data_cell:first")
);
await legacyExtraNextTick();
assert.containsOnce(
target,
".o_form_view",
"The form view should still load after a previous failed update | reload"
);
});
QUnit.test("there is no flickering when switching between views", async function (assert) {
assert.expect(20);
let def;
const mockRPC = async (route, args) => {
await def;
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
// switch to kanban view
def = testUtils.makeTestPromise();
await cpHelpers.switchView(target, "kanban");
await legacyExtraNextTick();
assert.containsOnce(target, ".o_list_view", "should still display the list view");
assert.containsNone(target, ".o_kanban_view", "shouldn't display the kanban view yet");
def.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".o_list_view", "shouldn't display the list view anymore");
assert.containsOnce(target, ".o_kanban_view", "should now display the kanban view");
// switch back to list view
def = testUtils.makeTestPromise();
await cpHelpers.switchView(target, "list");
await legacyExtraNextTick();
assert.containsOnce(target, ".o_kanban_view", "should still display the kanban view");
assert.containsNone(target, ".o_list_view", "shouldn't display the list view yet");
def.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".o_kanban_view", "shouldn't display the kanban view anymore");
assert.containsOnce(target, ".o_list_view", "should now display the list view");
// open a record in form view
def = testUtils.makeTestPromise();
await click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_list_view", "should still display the list view");
assert.containsNone(target, ".o_form_view", "shouldn't display the form view yet");
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should still be one controller in the breadcrumbs"
);
def.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".o_list_view", "should no longer display the list view");
assert.containsOnce(target, ".o_form_view", "should display the form view");
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
2,
"there should be two controllers in the breadcrumbs"
);
// go back to list view using the breadcrumbs
def = testUtils.makeTestPromise();
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_form_view", "should still display the form view");
assert.containsNone(target, ".o_list_view", "shouldn't display the list view yet");
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
2,
"there should still be two controllers in the breadcrumbs"
);
def.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".o_form_view", "should no longer display the form view");
assert.containsOnce(target, ".o_list_view", "should display the list view");
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should be one controller in the breadcrumbs"
);
});
QUnit.test("breadcrumbs are updated when display_name changes", async function (assert) {
assert.expect(4);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
2,
"there should be two controllers in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"First record",
"breadcrumbs should contain the display_name of the opened record"
);
// switch to edit mode and change the display_name
await editInput(target, ".o_field_widget[name=display_name] input", "New name");
await clickSave(target);
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
2,
"there should still be two controllers in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"New name",
"breadcrumbs should contain the display_name of the opened record"
);
});
QUnit.test('reverse breadcrumb works on accesskey "b"', async function (assert) {
assert.expect(4);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
await testUtils.dom.click($(target).find(".o_form_view button:contains(Execute action)"));
assert.containsN(target, ".o_control_panel .breadcrumb li", 3);
let $previousBreadcrumb = $(target).find(".o_control_panel .breadcrumb li.active").prev();
assert.strictEqual(
$previousBreadcrumb.attr("data-hotkey"),
"b",
"previous breadcrumb should have accessKey 'b'"
);
await testUtils.dom.click($previousBreadcrumb);
assert.containsN(target, ".o_control_panel .breadcrumb li", 2);
$previousBreadcrumb = $(target).find(".o_control_panel .breadcrumb li.active").prev();
assert.strictEqual(
$previousBreadcrumb.attr("data-hotkey"),
"b",
"previous breadcrumb should have accessKey 'b'"
);
});
QUnit.test("reload previous controller when discarding a new record", async function (assert) {
assert.expect(9);
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
// create a new record
await testUtils.dom.click($(target).find(".o_control_panel .o_list_button_add"));
await legacyExtraNextTick();
assert.containsOnce(
target,
".o_form_view .o_form_editable",
"should have opened the form view in edit mode"
);
// discard
await testUtils.dom.click($(target).find(".o_control_panel .o_form_button_cancel"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_list_view", "should have switched back to the list view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"onchange",
"web_search_read",
]);
});
QUnit.test("requests for execute_action of type object are handled", async function (assert) {
assert.expect(11);
patchWithCleanup(session.user_context, { some_key: 2 });
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
if (route === "/web/dataset/call_button") {
assert.deepEqual(
args,
{
args: [[1]],
kwargs: {
context: {
lang: "en",
uid: 7,
tz: "taht",
some_key: 2,
},
},
method: "object",
model: "partner",
},
"should call route with correct arguments"
);
const record = serverData.models.partner.records.find(
(r) => r.id === args.args[0][0]
);
record.foo = "value changed";
return Promise.resolve(false);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.strictEqual(
$(target).find(".o_field_widget[name=foo] input").val(),
"yop",
"check initial value of 'yop' field"
);
// click on 'Call method' button (should call an Object method)
await testUtils.dom.click($(target).find(".o_form_view button:contains(Call method)"));
await legacyExtraNextTick();
assert.strictEqual(
$(target).find(".o_field_widget[name=foo] input").val(),
"value changed",
"'yop' has been changed by the server, and should be updated in the UI"
);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"read",
"object",
"read",
]);
});
QUnit.test(
"requests for execute_action of type object: disable buttons (2)",
async function (assert) {
assert.expect(6);
serverData.views["pony,44,form"] = `
<form>
<field name="name"/>
<button string="Cancel" class="cancel-btn" special="cancel"/>
</form>`;
serverData.actions[4] = {
id: 4,
name: "Create a Partner",
res_model: "pony",
target: "new",
type: "ir.actions.act_window",
views: [[44, "form"]],
};
const def = testUtils.makeTestPromise();
const mockRPC = async (route, args) => {
if (args.method === "onchange") {
// delay the opening of the dialog
await def;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
// open first record in form view
await testUtils.dom.click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_form_view");
// click on 'Execute action', to execute action 4 in a dialog
await testUtils.dom.click(target.querySelector('.o_form_view button[name="4"]'));
await legacyExtraNextTick();
assert.ok(
target.querySelector(".o_form_button_create").disabled,
"control panel buttons should be disabled"
);
def.resolve();
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".modal .o_form_view");
assert.notOk(
target.querySelector(".o_form_button_create").disabled,
"control panel buttons should have been re-enabled"
);
await testUtils.dom.click(target.querySelector(".modal .cancel-btn"));
await legacyExtraNextTick();
assert.notOk(
target.querySelector(".o_form_button_create").disabled,
"control panel buttons should still be enabled"
);
}
);
QUnit.test(
"requests for execute_action of type object raises error: re-enables buttons",
async function (assert) {
assert.expect(3);
const mockRPC = async (route, args) => {
if (route === "/web/dataset/call_button") {
return Promise.reject();
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3, { viewType: "form" });
assert.containsOnce(target, ".o_form_view");
// save to ensure the presence of the create button
await click(target.querySelector(".o_form_button_save"));
// click on 'Execute action', to execute action 4 in a dialogdebugger
click(target.querySelector('.o_form_view button[name="object"]'));
assert.ok(target.querySelector(".o_form_button_create").disabled);
await nextTick();
await legacyExtraNextTick();
assert.notOk(target.querySelector(".o_form_button_create").disabled);
}
);
QUnit.test(
"requests for execute_action of type object raises error in modal: re-enables buttons",
async function (assert) {
assert.expect(5);
serverData.views["partner,false,form"] = `
<form>
<field name="display_name"/>
<footer>
<button name="object" string="Call method" type="object"/>
</footer>
</form>
`;
const mockRPC = async (route, args) => {
if (route === "/web/dataset/call_button") {
return Promise.reject();
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 5);
assert.containsOnce(target, ".modal .o_form_view");
testUtils.dom.click(target.querySelector('.modal footer button[name="object"]'));
assert.containsOnce(target, ".modal .o_form_view");
assert.ok(target.querySelector(".modal footer button").disabled);
await testUtils.nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".modal .o_form_view");
assert.notOk(target.querySelector(".modal footer button").disabled);
}
);
QUnit.test("requests for execute_action of type action are handled", async function (assert) {
assert.expect(12);
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
// click on 'Execute action' button (should execute an action)
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
2,
"there should be two parts in the breadcrumbs"
);
await testUtils.dom.click($(target).find(".o_form_view button:contains(Execute action)"));
await legacyExtraNextTick();
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
3,
"the returned action should have been stacked over the previous one"
);
assert.containsOnce(
target,
".o_kanban_view",
"the returned action should have been executed"
);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"read",
"/web/action/load",
"get_views",
"web_search_read",
]);
});
QUnit.test("execute smart button and back", async function (assert) {
const mockRPC = async (route, args) => {
if (args.method === "read") {
assert.step("read");
assert.notOk("default_partner" in args.kwargs.context);
}
if (args.method === "web_search_read") {
assert.step("web_search_read");
assert.strictEqual(args.kwargs.context.default_partner, 2);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 24);
assert.containsOnce(target, ".o_form_view");
assert.containsOnce(target, ".o_form_button_create:not([disabled])");
await testUtils.dom.click(target.querySelector(".oe_stat_button"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_kanban_view");
await testUtils.dom.click(target.querySelector(".breadcrumb-item"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_form_view");
assert.containsOnce(target, ".o_form_button_create:not([disabled])");
assert.verifySteps(["read", "web_search_read", "read"]);
});
QUnit.test("execute smart button and fails", async function (assert) {
assert.expect(13);
const mockRPC = async (route, args) => {
assert.step(route);
if (args.method === "web_search_read") {
return Promise.reject();
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 24);
assert.containsOnce(target, ".o_form_view");
assert.containsOnce(target, ".o_form_button_create:not([disabled])");
await testUtils.dom.click(target.querySelector(".oe_stat_button"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_form_view");
assert.containsOnce(target, ".o_form_button_create:not([disabled])");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/read",
"/web/action/load",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/web_search_read",
"/web/dataset/call_kw/partner/read",
]);
});
QUnit.test(
"requests for execute_action of type object: disable buttons",
async function (assert) {
assert.expect(2);
let def = undefined;
const mockRPC = async (route, args) => {
if (route === "/web/dataset/call_button") {
return Promise.resolve(false);
} else if (args && args.method === "read") {
await def; // block the 'read' call
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
// click on 'Call method' button (should call an Object method)
def = testUtils.makeTestPromise();
await testUtils.dom.click($(target).find(".o_form_view button:contains(Call method)"));
await legacyExtraNextTick();
// Buttons should be disabled
assert.strictEqual(
$(target).find(".o_form_view button:contains(Call method)").attr("disabled"),
"disabled",
"buttons should be disabled"
);
// Release the 'read' call
def.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
// Buttons should be enabled after the reload
assert.strictEqual(
$(target).find(".o_form_view button:contains(Call method)").attr("disabled"),
undefined,
"buttons should not be disabled anymore"
);
}
);
QUnit.test("action with html help returned by a call_button", async function (assert) {
const mockRPC = async (route, args) => {
if (route === "/web/dataset/call_button") {
return Promise.resolve({
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "list"]],
help: "<p>I am not a helper</p>",
domain: [[0, "=", 1]],
});
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_row .o_data_cell"));
await click(target.querySelector(".o_statusbar_buttons button"));
assert.strictEqual(
target.querySelector(".o_list_view .o_nocontent_help p").innerText,
"I am not a helper"
);
});
QUnit.test("can open different records from a multi record view", async function (assert) {
assert.expect(12);
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
// open the first record in form view
await click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"First record",
"breadcrumbs should contain the display_name of the opened record"
);
assert.strictEqual(
$(target).find(".o_field_widget[name=foo] input").val(),
"yop",
"should have opened the correct record"
);
// go back to list view using the breadcrumbs
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
// open the second record in form view
await testUtils.dom.click(
$(target).find(".o_list_view .o_data_row:nth(1) .o_data_cell:first")
);
await legacyExtraNextTick();
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"Second record",
"breadcrumbs should contain the display_name of the opened record"
);
assert.strictEqual(
$(target).find(".o_field_widget[name=foo] input").val(),
"blip",
"should have opened the correct record"
);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"read",
"web_search_read",
"read",
]);
});
QUnit.test("restore previous view state when switching back", async function (assert) {
assert.expect(5);
registry.category("services").add("cookie", fakeCookieService);
serverData.actions[3].views.unshift([false, "graph"]);
serverData.views["partner,false,graph"] = "<graph/>";
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.hasClass(
$(target).find(".o_control_panel [data-mode='bar']")[0],
"active",
"bar chart button is active"
);
assert.doesNotHaveClass(
$(target).find(".o_control_panel [data-mode='line']")[0],
"active",
"line chart button is not active"
);
// display line chart
await testUtils.dom.click($(target).find(".o_control_panel [data-mode='line']"));
await legacyExtraNextTick();
assert.hasClass(
$(target).find(".o_control_panel [data-mode='line']")[0],
"active",
"line chart button is now active"
);
// switch to kanban and back to graph view
await cpHelpers.switchView(target, "kanban");
await legacyExtraNextTick();
assert.containsNone(
target,
".o_control_panel [data-mode='line']",
"graph buttons are no longer in control panel"
);
await cpHelpers.switchView(target, "graph");
await legacyExtraNextTick();
assert.hasClass(
$(target).find(".o_control_panel [data-mode='line']")[0],
"active",
"line chart button is still active"
);
});
QUnit.test("can't restore previous action if form is invalid", async function (assert) {
serverData.models.partner.fields.foo.required = true;
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
await click(target.querySelector(".o_list_button_add"));
assert.containsOnce(target, ".o_form_view");
assert.hasClass(target.querySelector(".o_field_widget[name=foo]"), "o_required_modifier");
await editInput(target, ".o_field_widget[name=display_name] input", "make record dirty");
await click(target.querySelector(".breadcrumb .o_back_button"));
assert.containsNone(target, ".o_list_view");
assert.containsOnce(target, ".o_form_view");
assert.containsOnce(target, ".o_notification_manager .o_notification");
assert.hasClass(target.querySelector(".o_field_widget[name=foo]"), "o_field_invalid");
});
QUnit.test("view switcher is properly highlighted in graph view", async function (assert) {
assert.expect(4);
registry.category("services").add("cookie", fakeCookieService);
serverData.actions[3].views.splice(1, 1, [false, "graph"]);
serverData.views["partner,false,graph"] = "<graph/>";
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.hasClass(
$(target).find(".o_control_panel .o_switch_view.o_list")[0],
"active",
"list button in control panel is active"
);
assert.doesNotHaveClass(
$(target).find(".o_control_panel .o_switch_view.o_graph")[0],
"active",
"graph button in control panel is not active"
);
// switch to graph view
await cpHelpers.switchView(target, "graph");
await legacyExtraNextTick();
assert.doesNotHaveClass(
$(target).find(".o_control_panel .o_switch_view.o_list")[0],
"active",
"list button in control panel is not active"
);
assert.hasClass(
$(target).find(".o_control_panel .o_switch_view.o_graph")[0],
"active",
"graph button in control panel is active"
);
});
QUnit.test("can interact with search view", async function (assert) {
assert.expect(2);
serverData.views["partner,false,search"] = `
<search>
<group>
<filter name="foo" string="foo" context="{'group_by': 'foo'}"/>
</group>
</search>`;
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.doesNotHaveClass(
$(target).find(".o_list_table")[0],
"o_list_table_grouped",
"list view is not grouped"
);
// open group by dropdown
await cpHelpers.toggleGroupByMenu(target);
// click on foo link
await cpHelpers.toggleMenuItem(target, "foo");
await legacyExtraNextTick();
assert.hasClass(
$(target).find(".o_list_table")[0],
"o_list_table_grouped",
"list view is now grouped"
);
});
QUnit.test("can open a many2one external window", async function (assert) {
serverData.models.partner.records[0].bar = 2;
serverData.views["partner,false,search"] = `
<search>
<group>
<filter name="foo" string="foo" context="{'group_by': 'foo'}"/>
</group>
</search>`;
serverData.views["partner,false,form"] = `
<form>
<field name="foo"/>
<field name="bar" open_target="new"/>
</form>`;
const mockRPC = async (route, args) => {
assert.step(route);
if (args && args.method === "get_formview_id") {
return Promise.resolve(false);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
// open first record in form view
await testUtils.dom.click(target.querySelector(".o_data_row .o_data_cell"));
// click on external button for m2o
await testUtils.dom.click(target.querySelector(".o_external_button"));
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/web_search_read",
"/web/dataset/call_kw/partner/read",
"/web/dataset/call_kw/partner/get_formview_id",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/read",
]);
});
QUnit.test('save when leaving a "dirty" view', async function (assert) {
assert.expect(4);
const mockRPC = async (route, { args, method, model }) => {
if (model === "partner" && method === "write") {
assert.deepEqual(args, [[1], { foo: "pinkypie" }]);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 4);
// open record in form view
await click(target.querySelector(".o_kanban_record"));
await editInput(target, '.o_field_widget[name="foo"] input', "pinkypie");
// go back to kanban view
await click(target.querySelector(".o_control_panel .breadcrumb-item a"));
assert.containsNone(document.body, ".modal", "should not display a modal dialog");
assert.containsNone(target, ".o_form_view", "should no longer be in form view");
assert.containsOnce(target, ".o_kanban_view", "should be in kanban view");
});
QUnit.test("limit set in action is passed to each created controller", async function (assert) {
assert.expect(2);
serverData.actions[3].limit = 2;
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsN(target, ".o_data_row", 2);
// switch to kanban view
await cpHelpers.switchView(target, "kanban");
await legacyExtraNextTick();
assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2);
});
QUnit.test("go back to a previous action using the breadcrumbs", async function (assert) {
assert.expect(10);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
2,
"there should be two controllers in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"First record",
"breadcrumbs should contain the display_name of the opened record"
);
// push another action on top of the first one, and come back to the form view
await doAction(webClient, 4);
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
3,
"there should be three controllers in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"Partners Action 4",
"breadcrumbs should contain the name of the current action"
);
// go back using the breadcrumbs
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a:nth(1)"));
await legacyExtraNextTick();
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
2,
"there should be two controllers in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"First record",
"breadcrumbs should contain the display_name of the opened record"
);
// push again the other action on top of the first one, and come back to the list view
await doAction(webClient, 4);
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
3,
"there should be three controllers in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"Partners Action 4",
"breadcrumbs should contain the name of the current action"
);
// go back using the breadcrumbs
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a:first"));
await legacyExtraNextTick();
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
1,
"there should be one controller in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"Partners",
"breadcrumbs should contain the name of the current action"
);
});
QUnit.test(
"form views are restored in edit when coming back in breadcrumbs",
async function (assert) {
assert.expect(2);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_cell"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_form_view .o_form_editable");
// do some other action
await doAction(webClient, 4);
// go back to form view
await click(target.querySelectorAll(".o_control_panel .breadcrumb a")[1]);
await legacyExtraNextTick();
assert.containsOnce(target, ".o_form_view .o_form_editable");
}
);
QUnit.test(
"form views are restored with the correct id in its url when coming back in breadcrumbs",
async function (assert) {
assert.expect(3);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_row .o_data_cell"));
await nextTick(); // wait for the router to update its state
assert.strictEqual(webClient.env.services.router.current.hash.id, 1);
// do some other action
await doAction(webClient, 4);
await nextTick(); // wait for the router to update its state
assert.notOk(webClient.env.services.router.current.hash.id);
// go back to form view
await click(target.querySelectorAll(".o_control_panel .breadcrumb a")[1]);
await nextTick(); // wait for the router to update its state
assert.strictEqual(webClient.env.services.router.current.hash.id, 1);
}
);
QUnit.test("honor group_by specified in actions context", async function (assert) {
assert.expect(5);
serverData.actions[3].context = "{'group_by': 'bar'}";
serverData.views["partner,false,search"] = `
<search>
<group>
<filter name="foo" string="foo" context="{'group_by': 'foo'}"/>
</group>
</search>`;
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_table_grouped", "should be grouped");
assert.containsN(
target,
".o_group_header",
2,
"should be grouped by 'bar' (two groups) at first load"
);
// groupby 'bar' using the searchview
await click(target.querySelectorAll(".o_control_panel .o_cp_bottom_right button")[1]);
await click(target.querySelector(".o_control_panel .o_group_by_menu .o_menu_item"));
assert.containsN(target, ".o_group_header", 5, "should be grouped by 'foo' (five groups)");
// remove the groupby in the searchview
await click(target.querySelector(".o_control_panel .o_searchview .o_facet_remove"));
assert.containsOnce(target, ".o_list_table_grouped", "should still be grouped");
assert.containsN(
target,
".o_group_header",
2,
"should be grouped by 'bar' (two groups) at reload"
);
});
QUnit.test("switch request to unknown view type", async function (assert) {
assert.expect(8);
serverData.actions[33] = {
id: 33,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[1, "kanban"],
],
};
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 33);
assert.containsOnce(target, ".o_list_view", "should display the list view");
// try to open a record in a form view
testUtils.dom.click($(target).find(".o_list_view .o_data_row:first"));
await legacyExtraNextTick();
assert.containsOnce(target, ".o_list_view", "should still display the list view");
assert.containsNone(target, ".o_form_view", "should not display the form view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
]);
});
QUnit.test("execute action with unknown view type", async function (assert) {
serverData.views["partner,false,unknown"] = "<unknown/>";
serverData.actions[33] = {
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"],
],
};
const webClient = await createWebClient({ serverData });
await doAction(webClient, 33);
assert.containsOnce(target, ".o_list_view");
assert.containsN(target, ".o_cp_switch_buttons button", 2);
});
QUnit.test("flags field of ir.actions.act_window is used", async function (assert) {
// more info about flags field : https://github.com/odoo/odoo/commit/c9b133813b250e89f1f61816b0eabfb9bee2009d
assert.expect(6);
serverData.actions[44] = {
id: 33,
name: "Partners",
res_id: 1,
res_model: "partner",
type: "ir.actions.act_window",
flags: {
mode: "edit",
},
views: [[false, "form"]],
};
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 44);
assert.containsOnce(
target,
".o_form_view .o_form_editable",
"should display the form view in edit mode"
); // provided that the default mode is readonly
assert.verifySteps(["/web/webclient/load_menus", "/web/action/load", "get_views", "read"]);
});
QUnit.test("save current search", async function (assert) {
assert.expect(4);
serverData.actions[33] = {
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(browser, { setTimeout: (fn) => fn() });
patchWithCleanup(listView.Controller.prototype, {
setup() {
this._super(...arguments);
useSetupAction({
getContext: () => ({ shouldBeInFilterContext: true }),
});
},
});
const mockRPC = async (route, args) => {
if (args.method === "create_or_replace") {
assert.strictEqual(args.args[0].domain, `[("bar", "=", 1)]`);
assert.deepEqual(args.args[0].context, {
group_by: [],
shouldBeInFilterContext: true,
});
return 3; // fake filter id
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 33);
assert.containsN(target, ".o_data_row", 5, "should contain 5 records");
// filter on bar
await cpHelpers.toggleFilterMenu(target);
await cpHelpers.toggleMenuItem(target, "Bar");
assert.containsN(target, ".o_data_row", 2);
// save filter
await cpHelpers.toggleFavoriteMenu(target);
await cpHelpers.toggleSaveFavorite(target);
await cpHelpers.editFavoriteName(target, "some name");
await cpHelpers.saveFavorite(target);
});
QUnit.test(
"list with default_order and favorite filter with no orderedBy",
async function (assert) {
serverData.views["partner,1,list"] =
'<tree default_order="foo desc"><field name="foo"/></tree>';
serverData.actions[100] = {
id: 100,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[1, "list"],
[false, "form"],
],
};
serverData.models.partner.filters = [
{
name: "favorite filter",
id: 5,
context: "{}",
sort: "[]",
domain: '[("bar", "=", 1)]',
is_default: false,
},
];
const webClient = await createWebClient({ serverData });
await doAction(webClient, 100);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_data_row .o_data_cell")),
["zoup", "yop", "plop", "gnap", "blip"],
"record should be in descending order as default_order applies"
);
await cpHelpers.toggleFavoriteMenu(target);
await cpHelpers.toggleMenuItem(target, "favorite filter");
assert.strictEqual(
target.querySelector(".o_control_panel .o_facet_values").innerText.trim(),
"favorite filter",
"favorite filter should be applied"
);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_data_row .o_data_cell")),
["gnap", "blip"],
"record should still be in descending order after default_order applied"
);
// go to formview and come back to listview
await click(target.querySelector(".o_list_view .o_data_row .o_data_cell"));
await testUtils.dom.click(target.querySelector(".o_control_panel .breadcrumb a"));
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_data_row .o_data_cell")),
["gnap", "blip"],
"order of records should not be changed, while coming back through breadcrumb"
);
// remove filter
await cpHelpers.removeFacet(target, 0);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_data_row .o_data_cell")),
["zoup", "yop", "plop", "gnap", "blip"],
"order of records should not be changed, after removing current filter"
);
}
);
QUnit.test("action with default favorite and context.active_id", async function (assert) {
assert.expect(4);
serverData.actions[3].context = { active_id: 4, active_ids: [4], active_model: "whatever" };
serverData.models.partner.filters = [
{
name: "favorite filter",
id: 5,
context: "{}",
sort: "[]",
domain: '[("bar", "=", 1)]',
is_default: true,
},
];
const mockRPC = (route, args) => {
if (args.method === "web_search_read") {
assert.deepEqual(args.kwargs.domain, [["bar", "=", 1]]);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
assert.containsOnce(target, ".o_searchview .o_searchview_facet");
assert.strictEqual(target.querySelector(".o_facet_value").innerText, "favorite filter");
});
QUnit.test(
"search menus are still available when switching between actions",
async function (assert) {
assert.expect(3);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 1);
assert.isVisible(
target.querySelector(".o_search_options .dropdown.o_filter_menu"),
"the search options should be available"
);
await doAction(webClient, 3);
assert.isVisible(
target.querySelector(".o_search_options .dropdown.o_filter_menu"),
"the search options should be available"
);
// go back using the breadcrumbs
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a:first"));
await legacyExtraNextTick();
assert.isVisible(
target.querySelector(".o_search_options .dropdown.o_filter_menu"),
"the search options should be available"
);
}
);
QUnit.test(
"current act_window action is stored in session_storage if possible",
async function (assert) {
let expectedAction;
patchWithCleanup(browser, {
sessionStorage: Object.assign(Object.create(sessionStorage), {
setItem(k, value) {
assert.deepEqual(JSON.parse(value), expectedAction);
},
}),
});
const webClient = await createWebClient({ serverData });
// execute an action that can be stringified -> should be stored
expectedAction = serverData.actions[3];
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
// execute an action that can't be stringified -> should not crash
expectedAction = {};
const x = {};
x.y = x;
await doAction(webClient, {
type: "ir.actions.act_window",
res_model: "partner",
views: [[false, "kanban"]],
flags: { x },
});
assert.containsOnce(target, ".o_kanban_view");
}
);
QUnit.test("destroy action with lazy loaded controller", async function (assert) {
assert.expect(6);
const webClient = await createWebClient({ serverData });
await loadState(webClient, {
action: 3,
id: 2,
view_type: "form",
});
assert.containsNone(target, ".o_list_view");
assert.containsOnce(target, ".o_form_view");
assert.containsN(
target,
".o_control_panel .breadcrumb-item",
2,
"there should be two controllers in the breadcrumbs"
);
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item:last").text(),
"Second record",
"breadcrumbs should contain the display_name of the opened record"
);
await doAction(webClient, 1, { clearBreadcrumbs: true });
assert.containsNone(target, ".o_form_view");
assert.containsOnce(target, ".o_kanban_view");
});
QUnit.test("execute action from dirty, new record, and come back", async function (assert) {
serverData.models.partner.fields.bar.default = 1;
serverData.views["partner,false,form"] = `
<form>
<field name="display_name"/>
<field name="foo"/>
<field name="bar" readonly="1"/>
</form>`;
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
if (args && args.method === "get_formview_action") {
return Promise.resolve({
res_id: 1,
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "form"]],
});
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// execute an action and create a new record
await doAction(webClient, 3);
await click(target.querySelector(".o_list_button_add"));
assert.containsOnce(target, ".o_form_view .o_form_editable");
assert.containsOnce(target, ".o_form_uri:contains(First record)");
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_control_panel .breadcrumb-item")),
["Partners", "New"]
);
// set form view dirty and open m2o record
await editInput(target, '.o_field_widget[name="display_name"] input', "test");
await editInput(target, ".o_field_widget[name=foo] input", "val");
await click(target.querySelector(".o_form_uri"));
assert.containsOnce(target, ".o_form_view .o_form_editable");
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_control_panel .breadcrumb-item")),
["Partners", "test", "First record"]
);
// go back to test using the breadcrumbs
await testUtils.dom.click(
target.querySelectorAll(".o_control_panel .breadcrumb-item a")[1]
);
assert.containsOnce(target, ".o_form_view .o_form_editable");
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_control_panel .breadcrumb-item")),
["Partners", "test"]
);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"onchange",
"get_formview_action",
"create", // FIXME: to check with mcm
"get_views",
"read",
"read",
]);
});
QUnit.test("execute a contextual action from a form view", async function (assert) {
assert.expect(4);
const contextualAction = serverData.actions[8];
contextualAction.context = "{}"; // need a context to evaluate
serverData.models.partner.toolbar = {
action: [contextualAction],
print: [],
};
const mockRPC = async (route, args) => {
if (args && args.method === "get_views" && args.model === "partner") {
assert.strictEqual(
args.kwargs.options.toolbar,
true,
"should ask for toolbar information"
);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// execute an action and open a record
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
// execute the custom action from the action menu
await cpHelpers.toggleActionMenu(target);
await cpHelpers.toggleMenuItem(target, "Favorite Ponies");
assert.containsOnce(target, ".o_list_view");
});
QUnit.test(
"go back to action with form view as main view, and res_id",
async function (assert) {
serverData.actions[999] = {
id: 999,
name: "Partner",
res_model: "partner",
type: "ir.actions.act_window",
res_id: 2,
views: [[44, "form"]],
};
serverData.views["partner,44,form"] =
'<form><field name="m2o" open_target="current"/></form>';
const mockRPC = async (route, args) => {
if (args.method === "get_formview_action") {
return Promise.resolve({
res_id: 3,
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "form"]],
});
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 999);
assert.containsOnce(target, ".o_form_view .o_form_editable");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
"Second record"
);
// push another action in the breadcrumb
await click(target, ".o_field_many2one .o_external_button");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
"Second recordThird record"
);
// go back to the form view
await click(target.querySelector(".o_control_panel .breadcrumb a"));
assert.containsOnce(target, ".o_form_view .o_form_editable");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb-item").textContent,
"Second record"
);
}
);
QUnit.test(
"action with res_id, load another id on current action, do new action, restore previous",
async (assert) => {
serverData.actions[999] = {
id: 999,
name: "Partner",
res_model: "partner",
type: "ir.actions.act_window",
res_id: 1,
views: [[44, "form"]],
};
serverData.views["partner,44,form"] =
'<form><field name="m2o" open_target="current"/></form>';
const mockRPC = async (route, { method }) => {
if (method === "get_formview_action") {
return Promise.resolve({ ...serverData.actions[999], res_id: 3 });
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 999, { props: { resIds: [1, 2] } });
assert.containsOnce(target, ".o_form_view .o_form_editable");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
"First record"
);
assert.strictEqual(
target.querySelector(".o_control_panel .o_pager_counter").textContent,
"1 / 2"
);
// load another id on current action (through pager)
await click(target.querySelector(".o_pager_next"));
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
"Second record"
);
assert.strictEqual(
target.querySelector(".o_control_panel .o_pager_counter").textContent,
"2 / 2"
);
// push another action in the breadcrumb
await click(target, ".o_field_many2one .o_external_button");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
"Second recordThird record"
);
// restore previous action through breadcrumb
await click(target.querySelector(".o_control_panel .breadcrumb a"));
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb").textContent,
"Second record"
);
assert.strictEqual(
target.querySelector(".o_control_panel .o_pager_counter").textContent,
"2 / 2"
);
}
);
QUnit.test("open a record, come back, and create a new record", async function (assert) {
assert.expect(7);
const webClient = await createWebClient({ serverData });
// execute an action and open a record
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
assert.containsN(target, ".o_list_view .o_data_row", 5);
await click(target.querySelector(".o_list_view .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
assert.containsOnce(target, ".o_form_view .o_form_editable");
// go back using the breadcrumbs
await click(target.querySelector(".o_control_panel .breadcrumb-item a"));
assert.containsOnce(target, ".o_list_view");
// create a new record
await click(target.querySelector(".o_list_button_add"));
assert.containsOnce(target, ".o_form_view");
assert.containsOnce(target, ".o_form_view .o_form_editable");
});
QUnit.test(
"open form view, use the pager, execute action, and come back",
async function (assert) {
assert.expect(8);
const webClient = await createWebClient({ serverData });
// execute an action and open a record
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
assert.containsN(target, ".o_list_view .o_data_row", 5);
await click(target.querySelector(".o_list_view .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
assert.strictEqual(
$(target).find(".o_field_widget[name=display_name] input").val(),
"First record"
);
// switch to second record
await click(target.querySelector(".o_pager_next"));
assert.strictEqual(
$(target).find(".o_field_widget[name=display_name] input").val(),
"Second record"
);
// execute an action from the second record
await testUtils.dom.click($(target).find(".o_statusbar_buttons button[name=4]"));
assert.containsOnce(target, ".o_kanban_view");
// go back using the breadcrumbs
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb-item:nth(1) a"));
assert.containsOnce(target, ".o_form_view");
assert.strictEqual(
$(target).find(".o_field_widget[name=display_name] input").val(),
"Second record"
);
}
);
QUnit.test(
"create a new record in a form view, execute action, and come back",
async function (assert) {
assert.expect(8);
const webClient = await createWebClient({ serverData });
// execute an action and create a new record
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
await click(target.querySelector(".o_list_button_add"));
assert.containsOnce(target, ".o_form_view");
assert.containsOnce(target, ".o_form_view .o_form_editable");
await editInput(target, ".o_field_widget[name=display_name] input", "another record");
await click(target.querySelector(".o_form_button_save"));
assert.containsOnce(target, ".o_form_view .o_form_editable");
// execute an action from the second record
await testUtils.dom.click($(target).find(".o_statusbar_buttons button[name=4]"));
assert.containsOnce(target, ".o_kanban_view");
// go back using the breadcrumbs
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb-item:nth(1) a"));
assert.containsOnce(target, ".o_form_view");
assert.containsOnce(target, ".o_form_view .o_form_editable");
assert.strictEqual(
$(target).find(".o_field_widget[name=display_name] input").val(),
"another record"
);
}
);
QUnit.test("onClose should be called only once with right parameters", async function (assert) {
assert.expect(5);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 24); // main form view
await doAction(webClient, 25, {
// form view in target new
onClose(infos) {
assert.step("onClose");
assert.deepEqual(infos, { cantaloupe: "island" });
},
});
assert.containsOnce(target, ".modal");
await doAction(webClient, {
type: "ir.actions.act_window_close",
infos: { cantaloupe: "island" },
});
assert.verifySteps(["onClose"]);
assert.containsNone(target, ".modal");
});
QUnit.test("search view should keep focus during do_search", async function (assert) {
assert.expect(5);
// 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 = testUtils.makeTestPromise();
const mockRPC = async (route, args) => {
if (args.method === "web_search_read") {
assert.step("search_read " + args.kwargs.domain);
if (JSON.stringify(args.domain) === JSON.stringify([["foo", "ilike", "m"]])) {
await searchPromise;
}
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
await cpHelpers.editSearch(target, "m");
await cpHelpers.validateSearch(target);
assert.verifySteps(["search_read ", "search_read foo,ilike,m"]);
// Triggering the do_search above will kill the current searchview Input
await cpHelpers.editSearch(target, "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 cpHelpers.validateSearch(target);
assert.verifySteps(["search_read |,foo,ilike,m,foo,ilike,o"]);
});
QUnit.test(
"Call twice clearUncommittedChanges in a row does not save twice",
async function (assert) {
assert.expect(4);
let writeCalls = 0;
const mockRPC = async (route, { method }) => {
if (method === "write") {
writeCalls += 1;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// execute an action and edit existing record
await doAction(webClient, 3);
await click(target.querySelector(".o_list_view .o_data_cell"));
assert.containsOnce(target, ".o_form_view .o_form_editable");
await editInput(target, ".o_field_widget[name=foo] input", "val");
clearUncommittedChanges(webClient.env);
await nextTick();
assert.containsNone(document.body, ".modal");
clearUncommittedChanges(webClient.env);
await nextTick();
assert.containsNone(document.body, ".modal");
assert.strictEqual(writeCalls, 1);
}
);
QUnit.test(
"executing a window action with onchange warning does not hide it",
async function (assert) {
serverData.views["partner,false,form"] = `<form><field name="foo"/></form>`;
const mockRPC = (route, args) => {
if (args.method === "onchange") {
return Promise.resolve({
value: {},
warning: {
title: "Warning",
message: "Everything is alright",
type: "dialog",
},
});
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
await click(target.querySelector(".o_list_button_add"));
assert.containsOnce(
document.body,
".modal.o_technical_modal",
"Warning modal should be opened"
);
await click(document.querySelector(".modal.o_technical_modal button.btn-close"));
assert.containsNone(
document.body,
".modal.o_technical_modal",
"Warning modal should be closed"
);
}
);
QUnit.test(
"do not call clearUncommittedChanges() when target=new and dialog is opened",
async function (assert) {
const webClient = await createWebClient({ serverData });
// Open Partner form view and enter some text
await doAction(webClient, 3, { viewType: "form" });
assert.containsOnce(target, ".o_action_manager .o_form_view .o_form_editable");
await editInput(target, ".o_field_widget[name=display_name] input", "TEST");
// Open dialog without saving should not ask to discard
await doAction(webClient, 5);
assert.containsOnce(target, ".o_action_manager .o_form_view .o_form_editable");
assert.containsOnce(target, ".o_dialog .o_view_controller");
}
);
QUnit.test("do not pushState when target=new and dialog is opened", async function (assert) {
const webClient = await createWebClient({ serverData });
// Open Partner form in create mode
await doAction(webClient, 3, { viewType: "form" });
const prevHash = Object.assign({}, webClient.env.services.router.current.hash);
// Edit another partner in a dialog
await doAction(webClient, {
name: "Edit a Partner",
res_model: "partner",
res_id: 3,
type: "ir.actions.act_window",
views: [[3, "form"]],
target: "new",
view_mode: "form",
});
assert.deepEqual(
webClient.env.services.router.current.hash,
prevHash,
"push_state in dialog shouldn't change the hash"
);
});
QUnit.test("do not restore after action button clicked", async function (assert) {
assert.expect(4);
const mockRPC = async (route, args) => {
if (route === "/web/dataset/call_button" && args.method === "do_something") {
return true;
}
};
serverData.views["partner,false,form"] = `
<form>
<header><button name="do_something" string="Call button" type="object"/></header>
<sheet>
<field name="display_name"/>
</sheet>
</form>`;
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3, { viewType: "form", props: { resId: 1 } });
await legacyExtraNextTick();
await editInput(target, "div[name='display_name'] input", "Edited value");
assert.isVisible(target.querySelector(".o_form_button_save"));
assert.isVisible(target.querySelector(".o_statusbar_buttons button[name=do_something]"));
await click(target.querySelector(".o_statusbar_buttons button[name=do_something]"));
await legacyExtraNextTick();
assert.isVisible(target.querySelector(".o_form_button_save"));
await click(target.querySelector(".o_form_button_save"));
await legacyExtraNextTick();
assert.isNotVisible(target.querySelector(".o_form_buttons_view .o_form_button_save"));
});
QUnit.test("debugManager is active for (legacy) views", async function (assert) {
assert.expect(2);
registry.category("debug").category("view").add("editView", editView);
patchWithCleanup(odoo, { debug: "1" });
const mockRPC = async (route) => {
if (route.includes("check_access_rights")) {
return true;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1);
assert.containsNone(
target,
".o_debug_manager .dropdown-item:contains('Edit View: Kanban')"
);
await click(target.querySelector(".o_debug_manager .dropdown-toggle"));
assert.containsOnce(
target,
".o_debug_manager .dropdown-item:contains('Edit View: Kanban')"
);
});
QUnit.test("reload a view via the view switcher keep state", async function (assert) {
assert.expect(6);
serverData.actions[3].views.unshift([false, "pivot"]);
serverData.views["partner,false,pivot"] = "<pivot/>";
const mockRPC = async (route, args) => {
if (args.method === "read_group") {
assert.step(args.method);
}
};
registry.category("services").add("user", makeFakeUserService());
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.doesNotHaveClass(
target.querySelector(".o_pivot_measure_row"),
"o_pivot_sort_order_asc"
);
await click(target.querySelector(".o_pivot_measure_row"));
assert.hasClass(target.querySelector(".o_pivot_measure_row"), "o_pivot_sort_order_asc");
await cpHelpers.switchView(target, "pivot");
await legacyExtraNextTick();
assert.hasClass(target.querySelector(".o_pivot_measure_row"), "o_pivot_sort_order_asc");
assert.verifySteps([
"read_group", // initial read_group
"read_group", // read_group at reload after switch view
]);
});
QUnit.test("doAction supports being passed globalState prop", async function (assert) {
assert.expect(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: [],
});
const mockRPC = async (route, args) => {
if (args.method === "web_search_read") {
assert.deepEqual(args.kwargs.domain, [["id", "=", 99]]);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1, {
props: {
globalState: { searchModel },
},
});
});
QUnit.test("window action in target new fails (onchange)", async (assert) => {
/*
* By-pass QUnit's and test's error handling because the error service needs to be active
*/
const handler = (ev) => {
// need to preventDefault to remove error from console (so python test pass)
ev.preventDefault();
};
window.addEventListener("unhandledrejection", handler);
registerCleanup(() => window.removeEventListener("unhandledrejection", handler));
patchWithCleanup(QUnit, {
onUnhandledRejection: () => {},
});
const originOnunhandledrejection = window.onunhandledrejection;
window.onunhandledrejection = () => {};
registerCleanup(() => {
window.onunhandledrejection = originOnunhandledrejection;
});
/*
* End By pass error handling
*/
const warningOpened = makeDeferred();
class WarningDialogWait extends WarningDialog {
setup() {
super.setup();
onMounted(() => warningOpened.resolve());
}
}
serviceRegistry.add("error", errorService);
registry
.category("error_dialogs")
.add("odoo.exceptions.ValidationError", WarningDialogWait);
const mockRPC = (route, args) => {
if (args.method === "onchange" && args.model === "partner") {
const error = new RPCError();
error.exceptionName = "odoo.exceptions.ValidationError";
error.code = 200;
return Promise.reject(error);
}
};
serverData.views["partner,666,form"] = /*xml*/ `
<form>
<header>
<button name="5" type="action"/>
</header>
<field name="display_name"/>
</form>`;
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 24);
await click(target, ".o_form_view button[name='5']");
await warningOpened;
assert.containsOnce(target, ".modal");
assert.containsOnce(target, ".modal .o_dialog_warning");
assert.strictEqual(
target.querySelector(".modal .modal-title").textContent,
"Validation Error"
);
});
QUnit.test("action and get_views rpcs are cached", async function (assert) {
const mockRPC = async (route, args) => {
assert.step(args.method || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
assert.verifySteps(["/web/webclient/load_menus"]);
await doAction(webClient, 1);
assert.containsOnce(target, ".o_kanban_view");
assert.verifySteps(["/web/action/load", "get_views", "web_search_read"]);
await doAction(webClient, 1);
assert.containsOnce(target, ".o_kanban_view");
assert.verifySteps(["web_search_read"]);
});
QUnit.test("pushState also changes the title of the tab", async (assert) => {
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3); // list view
const titleService = webClient.env.services.title;
assert.strictEqual(titleService.current, '{"zopenerp":"Odoo","action":"Partners"}');
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.strictEqual(titleService.current, '{"zopenerp":"Odoo","action":"First record"}');
await click(target.querySelector(".o_pager_next"));
assert.strictEqual(titleService.current, '{"zopenerp":"Odoo","action":"Second record"}');
});
QUnit.test("action part of title is updated when an action is mounted", async (assert) => {
// use a PivotView because we need a view converted to wowl
// those two lines can be removed once the list view is converted to wowl
serverData.actions[3].views.unshift([false, "pivot"]);
serverData.views["partner,false,pivot"] = "<pivot/>";
serviceRegistry.add("user", makeFakeUserService());
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
const titleService = webClient.env.services.title;
assert.strictEqual(titleService.current, '{"zopenerp":"Odoo","action":"Partners"}');
});
QUnit.test("action group_by of type string", async function (assert) {
assert.expect(2);
serverData.views["partner,false,pivot"] = `<pivot/>`;
registry.category("services").add("user", makeFakeUserService());
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
name: "Partner",
res_model: "partner",
type: "ir.actions.act_window",
views: [[3, "pivot"]],
context: { group_by: "foo" },
});
assert.containsOnce(target, ".o_pivot_view");
assert.containsN(target, ".o_pivot_view tbody th", 6);
});
QUnit.test("action help given to View in props if not empty", async function (assert) {
serverData.models.partner.records = [];
const action = serverData.actions[3];
serverData.actions[3] = {
...action,
help: '<p class="hello">Hello</p>',
};
serverData.actions[4] = {
...action,
id: 4,
help: '<p class="hello"></p>',
};
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
assert.containsOnce(target, ".o_view_nocontent");
assert.strictEqual(target.querySelector(".o_view_nocontent").innerText, "Hello");
assert.containsOnce(target, "table");
await doAction(webClient, 4);
assert.containsOnce(target, ".o_list_view");
assert.containsNone(target, ".o_view_nocontent");
});
QUnit.test("process context.form_view_initial_mode", async function (assert) {
serverData.views = {
"partner,false,form": `<form><field name="name"/></form>`,
"partner,false,search": `<search/>`,
};
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
res_id: 1,
type: "ir.actions.act_window",
target: "current",
res_model: "partner",
views: [[false, "form"]],
context: {
form_view_initial_mode: "edit",
},
});
assert.containsOnce(target, ".o_form_view .o_form_editable");
});
QUnit.test("process context.form_view_initial_mode (2)", async function (assert) {
serverData.views = {
"partner,false,form": `<form><field name="name"/></form>`,
"partner,false,search": `<search/>`,
};
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
res_id: 1,
type: "ir.actions.act_window",
target: "new",
res_model: "partner",
views: [[false, "form"]],
context: {
form_view_initial_mode: "readonly",
},
});
// mode is "edit" because target="new"
assert.containsOnce(target, ".o_form_view .o_form_editable");
});
QUnit.test("load a tree", async function (assert) {
serverData.views = {
"partner,false,list": `<list><field name="name"/></list>`,
"partner,false,search": `<search/>`,
};
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
res_id: 1,
type: "ir.actions.act_window",
target: "current",
res_model: "partner",
views: [[false, "tree"]],
});
assert.containsOnce(target, ".o_list_view");
});
QUnit.test("sample server: populate groups", async function (assert) {
serverData.models.partner.records = [];
serverData.models.partner.fields.write_date = {
string: "Write Date",
type: "date",
store: true,
sortable: true,
};
serverData.views = {
"partner,false,kanban": `
<kanban sample="1" default_group_by="write_date:month">
<templates>
<t t-name="kanban-box">
<div><field name="display_name"/></div>
</t>
</templates>
</kanban>
`,
"partner,false,pivot": `
<pivot sample="1">
<field name="write_date" type="row"/>
</pivot>
`,
"partner,false,search": `<search/>`,
};
const webClient = await createWebClient({
serverData,
mockRPC(_, args) {
if (args.method === "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 doAction(webClient, {
res_id: 1,
type: "ir.actions.act_window",
target: "current",
res_model: "partner",
views: [
[false, "kanban"],
[false, "pivot"],
],
});
assert.containsOnce(target, ".o_kanban_view .o_view_sample_data");
assert.strictEqual(target.querySelector(".o_column_title").innerText, "December 2022");
await cpHelpers.switchView(target, "pivot");
assert.containsOnce(target, ".o_pivot_view .o_view_sample_data");
});
QUnit.test("click on breadcrumb of a deleted record", async function (assert) {
serviceRegistry.add("error", errorService);
// In tests we catch unhandledrejection of promises rejected with something that isn't an
// error (see tests/qunit.js). In this scenario, with the legacy basic_model, the promise
// is rejected with undefined, so without the following lines, we can't reproduce the issue,
// which was that the error handlers were executed in a wrong order, and one of them crashed.
const windowUnhandledReject = window.onunhandledrejection;
window.onunhandledrejection = null;
registerCleanup(() => {
window.onunhandledrejection = windowUnhandledReject;
});
const handler = (ev) => {
// need to preventDefault to remove error from console (so python test pass)
ev.preventDefault();
};
window.addEventListener("unhandledrejection", handler);
registerCleanup(() => window.removeEventListener("unhandledrejection", handler));
patchWithCleanup(QUnit, {
onUnhandledRejection: () => {},
});
serverData.views["partner,false,form"] = `
<form>
<button type="action" name="3" string="Open Action 3" class="my_btn"/>
</form>`;
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
await click(target.querySelector(".my_btn"));
assert.containsOnce(target, ".o_list_view");
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".breadcrumb-item")), [
"Partners",
"First record",
"Partners",
"First record",
]);
// open action menu and delete
await cpHelpers.toggleActionMenu(target);
await cpHelpers.toggleMenuItem(target, "Delete");
assert.containsOnce(target, ".o_dialog");
// confirm
await click(target.querySelector(".o_dialog .modal-footer .btn-primary"));
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".breadcrumb-item")), [
"Partners",
"First record",
"Partners",
"Second record",
]);
// click on "First record" in breadcrumbs, which doesn't exist anymore
await click(target.querySelectorAll(".breadcrumb-item a")[1]);
await nextTick();
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".breadcrumb-item")), [
"Partners",
"Partners",
"Partners",
"Second record",
]);
});
QUnit.test("executing an action closes dialogs", async function (assert) {
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
webClient.env.services.dialog.add(FormViewDialog, { resModel: "partner", resId: 1 });
await nextTick();
assert.containsOnce(target, ".o_dialog .o_form_view");
await click(
target.querySelector(".o_dialog .o_form_view .o_statusbar_buttons button[name='4']")
);
assert.containsOnce(target, ".o_kanban_view");
assert.containsNone(target, ".o_dialog");
});
});