Files
odoo_source/addons/web/static/tests/webclient/actions/misc_tests.js
T
36988d0edc [IMP] *: use new dropdown in legacy too
This commit replaces the control panel dropdowns with the new <Dropdown/> component, in order to get consistent through the new/legacy views (because the current Odoo version is in a state where some views uses the new infrastructure and some others are still not converted - see odoo/odoo#73311).

The diff seems massive, but it is mostly due to tests adaptations.

closes odoo/odoo#77001

X-original-commit: d679cd0d8ba9a2420e81a42a698763e9be2327e1
Related: odoo/enterprise#21077
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
2021-09-23 07:17:43 +00:00

624 lines
24 KiB
JavaScript

/** @odoo-module **/
import { registry } from "@web/core/registry";
import AbstractAction from "web.AbstractAction";
import core from "web.core";
import testUtils from "web.test_utils";
import Widget from "web.Widget";
import { makeTestEnv } from "../../helpers/mock_env";
import {
click,
hushConsole,
legacyExtraNextTick,
makeDeferred,
patchWithCleanup,
triggerEvents,
} from "../../helpers/utils";
import {
createWebClient,
doAction,
getActionManagerServerData,
setupWebClientRegistries,
} from "./../helpers";
import * as cpHelpers from "@web/../tests/search/helpers";
let serverData;
// legacy stuff
const actionRegistry = registry.category("actions");
const actionHandlersRegistry = registry.category("action_handlers");
QUnit.module("ActionManager", (hooks) => {
hooks.beforeEach(() => {
serverData = getActionManagerServerData();
});
QUnit.module("Misc");
QUnit.test("can execute actions from id, xmlid and tag", async (assert) => {
assert.expect(6);
serverData.actions[1] = {
tag: "client_action_by_db_id",
target: "main",
type: "ir.actions.client",
};
serverData.actions["wowl.some_action"] = {
tag: "client_action_by_xml_id",
target: "main",
type: "ir.actions.client",
};
actionRegistry
.add("client_action_by_db_id", () => assert.step("client_action_db_id"))
.add("client_action_by_xml_id", () => assert.step("client_action_xml_id"))
.add("client_action_by_object", () => assert.step("client_action_object"));
setupWebClientRegistries();
const env = await makeTestEnv({ serverData });
await doAction(env, 1);
assert.verifySteps(["client_action_db_id"]);
await doAction(env, "wowl.some_action");
assert.verifySteps(["client_action_xml_id"]);
await doAction(env, {
tag: "client_action_by_object",
target: "current",
type: "ir.actions.client",
});
assert.verifySteps(["client_action_object"]);
});
QUnit.test("action doesn't exists", async (assert) => {
assert.expect(1);
setupWebClientRegistries();
const env = await makeTestEnv({ serverData });
try {
await doAction(env, {
tag: "this_is_a_tag",
target: "current",
type: "ir.not_action.error",
});
} catch (e) {
assert.strictEqual(
e.message,
"The ActionManager service can't handle actions of type ir.not_action.error"
);
}
});
QUnit.test("action in handler registry", async (assert) => {
assert.expect(2);
setupWebClientRegistries();
const env = await makeTestEnv({ serverData });
actionHandlersRegistry.add("ir.action_in_handler_registry", ({ action }) =>
assert.step(action.type)
);
await doAction(env, {
tag: "this_is_a_tag",
target: "current",
type: "ir.action_in_handler_registry",
});
assert.verifySteps(["ir.action_in_handler_registry"]);
});
QUnit.test("properly handle case when action id does not exist", async (assert) => {
assert.expect(2);
const webClient = await createWebClient({ serverData });
patchWithCleanup(window, { console: hushConsole }, { pure: true });
patchWithCleanup(webClient.env.services.notification, {
add(message) {
assert.strictEqual(message, "No action with id '4448' could be found");
},
});
await doAction(webClient, 4448);
assert.containsOnce(webClient.el, "div.o_invalid_action");
});
QUnit.test("actions can be cached", async function (assert) {
assert.expect(8);
const mockRPC = async (route, args) => {
if (route === "/web/action/load") {
assert.step(JSON.stringify(args));
}
};
setupWebClientRegistries();
const env = await makeTestEnv({ serverData, mockRPC });
const loadAction = env.services.action.loadAction;
// With no additional params
await loadAction(3);
await loadAction(3);
// With specific additionalContext
await loadAction(3, { additionalContext: { configuratorMode: "add" } });
await loadAction(3, { additionalContext: { configuratorMode: "edit" } });
// With same active_id
await loadAction(3, { active_id: 1 });
await loadAction(3, { active_id: 1 });
// With active_id change
await loadAction(3, { active_id: 2 });
// With same active_ids
await loadAction(3, { active_ids: [1, 2] });
await loadAction(3, { active_ids: [1, 2] });
// With active_ids change
await loadAction(3, { active_ids: [1, 2, 3] });
// With same active_model
await loadAction(3, { active_model: "a" });
await loadAction(3, { active_model: "a" });
// With active_model change
await loadAction(3, { active_model: "b" });
assert.verifySteps(
[
'{"action_id":3,"additional_context":{}}',
'{"action_id":3,"additional_context":{"active_id":1}}',
'{"action_id":3,"additional_context":{"active_id":2}}',
'{"action_id":3,"additional_context":{"active_ids":[1,2]}}',
'{"action_id":3,"additional_context":{"active_ids":[1,2,3]}}',
'{"action_id":3,"additional_context":{"active_model":"a"}}',
'{"action_id":3,"additional_context":{"active_model":"b"}}',
],
"should load from server once per active_id/active_ids/active_model change, nothing else"
);
});
QUnit.test("action cache: additionalContext is respected", async function (assert) {
assert.expect(5);
const mockRPC = async (route) => {
if (route === "/web/action/load") {
assert.step("server loaded");
}
};
setupWebClientRegistries();
const env = await makeTestEnv({ serverData, mockRPC });
const { loadAction } = env.services.action;
const actionParams = {
additionalContext: {
some: { deep: { nested: "Robert" } },
},
};
let action = await loadAction(3, actionParams);
assert.verifySteps(["server loaded"]);
const baseContext = {
lang: "en",
tz: "taht",
uid: 7,
};
assert.deepEqual(action.context, { ...baseContext, ...actionParams });
// Modify the action in place
action.context.additionalContext.some.deep.nested = "Nesta";
// Change additionalContext and reload from cache
actionParams.additionalContext.some.deep.nested = "Marley";
action = await loadAction(3, actionParams);
assert.verifySteps([], "loaded from cache");
assert.deepEqual(action.context, { ...baseContext, ...actionParams });
});
QUnit.test("no widget memory leaks when doing some action stuff", async function (assert) {
assert.expect(1);
let delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta++;
this._super.apply(this, arguments);
},
destroy: function () {
delta--;
this._super.apply(this, arguments);
},
});
const webClient = await createWebClient({ serverData });
await doAction(webClient, 8);
const n = delta;
await doAction(webClient, 4);
// kanban view is loaded, switch to list view
await cpHelpers.switchView(webClient.el, "list");
await legacyExtraNextTick();
// open a record in form view
await testUtils.dom.click(webClient.el.querySelector(".o_list_view .o_data_row"));
await legacyExtraNextTick();
// go back to action 7 in breadcrumbs
await testUtils.dom.click(webClient.el.querySelector(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
assert.strictEqual(delta, n, "should have properly destroyed all other widgets");
testUtils.mock.unpatch(Widget);
});
QUnit.test("no widget memory leaks when executing actions in dialog", async function (assert) {
assert.expect(1);
let delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta++;
this._super.apply(this, arguments);
},
destroy: function () {
if (!this.isDestroyed()) {
delta--;
}
this._super.apply(this, arguments);
},
});
const webClient = await createWebClient({ serverData });
const n = delta;
await doAction(webClient, 5);
await doAction(webClient, { type: "ir.actions.act_window_close" });
assert.strictEqual(delta, n, "should have properly destroyed all widgets");
testUtils.mock.unpatch(Widget);
});
QUnit.test(
"no memory leaks when executing an action while switching view",
async function (assert) {
assert.expect(1);
let def;
let delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
const mockRPC = async function (route, args) {
if (args && args.method === "read") {
await Promise.resolve(def);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 4);
const n = delta;
await doAction(webClient, 3, { clearBreadcrumbs: true });
// switch to the form view (this request is blocked)
def = testUtils.makeTestPromise();
await testUtils.dom.click(webClient.el.querySelector(".o_list_view .o_data_row"));
// execute another action meanwhile (don't block this request)
await doAction(webClient, 4, { clearBreadcrumbs: true });
// unblock the switch to the form view in action 3
def.resolve();
await testUtils.nextTick();
assert.strictEqual(n, delta, "all widgets of action 3 should have been destroyed");
testUtils.mock.unpatch(Widget);
}
);
QUnit.test(
"no memory leaks when executing an action while loading views",
async function (assert) {
assert.expect(1);
let def;
let delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
const mockRPC = async function (route, args) {
if (args && args.method === "load_views") {
await Promise.resolve(def);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// execute action 4 to know the number of widgets it instantiates
await doAction(webClient, 4);
const n = delta;
// execute a first action (its 'load_views' RPC is blocked)
def = testUtils.makeTestPromise();
doAction(webClient, 3, { clearBreadcrumbs: true });
await testUtils.nextTick();
await legacyExtraNextTick();
// execute another action meanwhile (and unlock the RPC)
doAction(webClient, 4, { clearBreadcrumbs: true });
def.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.strictEqual(n, delta, "all widgets of action 3 should have been destroyed");
testUtils.mock.unpatch(Widget);
}
);
QUnit.test(
"no memory leaks when executing an action while loading data of default view",
async function (assert) {
assert.expect(1);
let def;
let delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
const mockRPC = async function (route) {
if (route === "/web/dataset/search_read") {
await Promise.resolve(def);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// execute action 4 to know the number of widgets it instantiates
await doAction(webClient, 4);
const n = delta;
// execute a first action (its 'search_read' RPC is blocked)
def = testUtils.makeTestPromise();
doAction(webClient, 3, { clearBreadcrumbs: true });
await testUtils.nextTick();
await legacyExtraNextTick();
// execute another action meanwhile (and unlock the RPC)
doAction(webClient, 4, { clearBreadcrumbs: true });
def.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.strictEqual(n, delta, "all widgets of action 3 should have been destroyed");
testUtils.mock.unpatch(Widget);
}
);
QUnit.test('action with "no_breadcrumbs" set to true', async function (assert) {
assert.expect(2);
serverData.actions[4].context = { no_breadcrumbs: true };
const webClient = await createWebClient({
serverData,
});
await doAction(webClient, 3);
assert.containsOnce(webClient, ".o_control_panel .breadcrumb-item");
// push another action flagged with 'no_breadcrumbs=true'
await doAction(webClient, 4);
assert.containsNone(webClient, ".o_control_panel .breadcrumb-item");
});
QUnit.test("document's title is updated when an action is executed", async function (assert) {
assert.expect(8);
const defaultTitle = { zopenerp: "Odoo" };
const webClient = await createWebClient({ serverData });
let currentTitle = webClient.env.services.title.getParts();
assert.deepEqual(currentTitle, defaultTitle);
let currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {});
await doAction(webClient, 4);
currentTitle = webClient.env.services.title.getParts();
assert.deepEqual(currentTitle, {
...defaultTitle,
action: "Partners Action 4",
});
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, { action: 4, model: "partner", view_type: "kanban" });
await doAction(webClient, 8);
currentTitle = webClient.env.services.title.getParts();
assert.deepEqual(currentTitle, {
...defaultTitle,
action: "Favorite Ponies",
});
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, { action: 8, model: "pony", view_type: "list" });
await testUtils.dom.click($(webClient.el).find("tr.o_data_row:first"));
await legacyExtraNextTick();
currentTitle = webClient.env.services.title.getParts();
assert.deepEqual(currentTitle, {
...defaultTitle,
action: "Twilight Sparkle",
});
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, { action: 8, id: 4, model: "pony", view_type: "form" });
});
QUnit.test(
"on_reverse_breadcrumb handler is correctly called (legacy)",
async function (assert) {
// This test can be removed as soon as we no longer support legacy actions as the new
// ActionManager doesn't support this option. Indeed, it is used to reload the previous
// action when coming back, but we won't need such an artefact to that with Wowl, as the
// controller will be re-instantiated with an (exported) state given in props.
assert.expect(5);
const ClientAction = AbstractAction.extend({
events: {
"click button": "_onClick",
},
start() {
this.$el.html('<button class="my_button">Execute another action</button>');
},
_onClick() {
this.do_action(4, {
on_reverse_breadcrumb: () => assert.step("on_reverse_breadcrumb"),
});
},
});
core.action_registry.add("ClientAction", ClientAction);
const webClient = await createWebClient({ serverData });
await doAction(webClient, "ClientAction");
assert.containsOnce(webClient, ".my_button");
await testUtils.dom.click(webClient.el.querySelector(".my_button"));
await legacyExtraNextTick();
assert.containsOnce(webClient, ".o_kanban_view");
await testUtils.dom.click($(webClient.el).find(".o_control_panel .breadcrumb a:first"));
await legacyExtraNextTick();
assert.containsOnce(webClient, ".my_button");
assert.verifySteps(["on_reverse_breadcrumb"]);
delete core.action_registry.map.ClientAction;
}
);
QUnit.test('handles "history_back" event', async function (assert) {
assert.expect(3);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 4);
await doAction(webClient, 3);
assert.containsN(webClient, ".o_control_panel .breadcrumb-item", 2);
// simulate an "history-back" event
const ev = new Event("history-back", { bubbles: true, cancelable: true });
webClient.el.querySelector(".o_view_controller").dispatchEvent(ev);
await testUtils.nextTick();
await legacyExtraNextTick();
assert.containsOnce(webClient, ".o_control_panel .breadcrumb-item");
assert.strictEqual(
$(webClient.el).find(".o_control_panel .breadcrumb-item").text(),
"Partners Action 4",
"breadcrumbs should display the display_name of the action"
);
});
QUnit.test("stores and restores scroll position", async function (assert) {
assert.expect(3);
for (let i = 0; i < 60; i++) {
serverData.models.partner.records.push({ id: 100 + i, foo: `Record ${i}` });
}
const webClient = await createWebClient({ serverData });
webClient.el.style.height = "250px";
// execute a first action
await doAction(webClient, 3);
assert.strictEqual(webClient.el.querySelector(".o_content").scrollTop, 0);
// simulate a scroll
webClient.el.querySelector(".o_content").scrollTop = 100;
// execute a second action (in which we don't scroll)
await doAction(webClient, 4);
assert.strictEqual(webClient.el.querySelector(".o_content").scrollTop, 0);
// go back using the breadcrumbs
await testUtils.dom.click($(webClient.el).find(".o_control_panel .breadcrumb a"));
await legacyExtraNextTick();
assert.strictEqual(webClient.el.querySelector(".o_content").scrollTop, 100);
});
QUnit.test(
'executing an action with target != "new" closes all dialogs',
async function (assert) {
assert.expect(4);
serverData.views["partner,false,form"] = `
<form>
<field name="o2m">
<tree><field name="foo"/></tree>
<form><field name="foo"/></form>
</field>
</form>`;
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsOnce(webClient, ".o_list_view");
await testUtils.dom.click($(webClient.el).find(".o_list_view .o_data_row:first"));
await legacyExtraNextTick();
assert.containsOnce(webClient, ".o_form_view");
await testUtils.dom.click($(webClient.el).find(".o_form_view .o_data_row:first"));
await legacyExtraNextTick();
assert.containsOnce(document.body, ".modal .o_form_view");
await doAction(webClient, 1); // target != 'new'
assert.containsNone(document.body, ".modal");
}
);
QUnit.test(
'executing an action with target "new" does not close dialogs',
async function (assert) {
assert.expect(4);
serverData.views["partner,false,form"] = `
<form>
<field name="o2m">
<tree><field name="foo"/></tree>
<form><field name="foo"/></form>
</field>
</form>`;
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsOnce(webClient, ".o_list_view");
await testUtils.dom.click($(webClient.el).find(".o_list_view .o_data_row:first"));
await legacyExtraNextTick();
assert.containsOnce(webClient, ".o_form_view");
await testUtils.dom.click($(webClient.el).find(".o_form_view .o_data_row:first"));
await legacyExtraNextTick();
assert.containsOnce(document.body, ".modal .o_form_view");
await doAction(webClient, 5); // target 'new'
assert.containsN(document.body, ".modal .o_form_view", 2);
}
);
QUnit.test("bootstrap tooltip in dialog action auto destroy", async (assert) => {
assert.expect(2);
const mockRPC = (route, args) => {
if (route === "/web/dataset/call_button") {
return false;
}
};
serverData.views["partner,3,form"] = /*xml*/ `
<form>
<field name="display_name" />
<footer>
<button name="echoes" type="object" string="Echoes" help="echoes"/>
</footer>
</form>
`;
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 25);
const tooltipProm = makeDeferred();
$(webClient.el).one("shown.bs.tooltip", () => {
tooltipProm.resolve();
});
triggerEvents(webClient.el, ".modal footer button", ["mouseover", "focusin"]);
await tooltipProm;
// check on webClient dom
assert.containsOnce(webClient.el, ".tooltip");
await doAction(webClient, {
type: "ir.actions.act_window_close",
});
// check on the whole DOM
assert.containsNone(document.body, ".tooltip");
});
QUnit.test("bootstrap tooltip destroyed on click", async (assert) => {
assert.expect(2);
const mockRPC = (route, args) => {
if (route === "/web/dataset/call_button") {
return false;
}
};
serverData.views["partner,666,form"] = /*xml*/ `
<form>
<header>
<button name="echoes" type="object" string="Echoes" help="echoes"/>
</header>
<field name="display_name" />
</form>
`;
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 24);
const tooltipProm = makeDeferred();
$(webClient.el).one("shown.bs.tooltip", () => {
tooltipProm.resolve();
});
triggerEvents(webClient.el, ".o_form_statusbar button", ["mouseover", "focusin"]);
await tooltipProm;
// check on webClient DOM
assert.containsOnce(webClient.el, ".tooltip");
await click(webClient.el, ".o_content");
// check on the whole DOM
assert.containsNone(document.body, ".tooltip");
});
});