Files
odoo_source/addons/web/static/tests/webclient/actions/misc_tests.js
T
+1 0573acae23 [REF] web: rewrite the webclient in OWL (phase 1)
This commit is the first phase of the conversion of the web/ JS
codebase to the owl framework. The impact of this commit is two-fold.

First, it rewrites the framework part of web with a new system of
services and registries. Services allow to execute code (e.g. do rpcs,
setup things) before launching the application. They can also expose
an API to be used by other parts of the application (e.g. a notification
service would expose a function to display notifications). Services are
often a good extension point for external modules that want to execute
code at webclient startup. Registries offer another way to extend the
application. They provide well designed extension points to add
elements/behaviors from the outside (for instance, to add a systray item,
an error handler...).

Second, this commit initiates the conversion of the webclient to owl
with a top-down approach, around those notions of services and registries.
The root of the web application is now an owl application. Among others,
the WebClient, ActionManager, Navbar, UserMenu, DebugManager, Dialogs,
services (e.g. notification, ajax...) have been converted to the new
framework/architecture.

Legacy views and client actions are still supported (and used). They
will be converted in the next months, and at some point, the support
will be dropped.

Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Bruno Boi <boi@odoo.com>
Co-authored-by: Géry Debongnie <ged@odoo.com>
Co-authored-by: Samuel Degueldre <sad@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Co-authored-by: Simon Genin (ges) <ges@odoo.com>
Co-authored-by: Francois (fge) <fge@odoo.com>
Co-authored-by: Michael Mattiello (mcm) <mcm@odoo.com>
Co-authored-by: Julien Mougenot <jum@odoo.com>
Co-authored-by: Lucas Perais (lpe) <lpe@odoo.com>
Co-authored-by: Jorge Pinna Puissant <jpp@odoo.com>
2021-06-18 21:31:27 +02:00

527 lines
21 KiB
JavaScript

/** @odoo-module **/
import { registry } from "@web/core/registry";
import testUtils from "web.test_utils";
import core from "web.core";
import Widget from "web.Widget";
import AbstractAction from "web.AbstractAction";
import { makeTestEnv } from "../../helpers/mock_env";
import { legacyExtraNextTick } from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerTestConfig } from "./helpers";
let testConfig;
// legacy stuff
let cpHelpers;
const actionRegistry = registry.category("actions");
const actionHandlersRegistry = registry.category("action_handlers");
QUnit.module("ActionManager", (hooks) => {
hooks.before(() => {
cpHelpers = testUtils.controlPanel;
});
hooks.beforeEach(() => {
testConfig = getActionManagerTestConfig();
});
QUnit.module("Misc");
QUnit.test("can execute actions from id, xmlid and tag", async (assert) => {
assert.expect(6);
testConfig.serverData.actions[1] = {
tag: "client_action_by_db_id",
target: "main",
type: "ir.actions.client",
};
testConfig.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"));
const env = await makeTestEnv(testConfig);
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);
const env = await makeTestEnv(testConfig);
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);
const env = await makeTestEnv(testConfig);
actionHandlersRegistry.add("ir.action_in_handler_registry", ({ env, action, options }) =>
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("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));
}
};
const env = await makeTestEnv({ ...testConfig, 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, args) => {
if (route === "/web/action/load") {
assert.step("server loaded");
}
};
const env = await makeTestEnv({ ...testConfig, 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 = {
allowed_company_ids: [1],
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({ testConfig });
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({ testConfig });
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({ testConfig, 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({ testConfig, 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, args) {
if (route === "/web/dataset/search_read") {
await Promise.resolve(def);
}
};
const webClient = await createWebClient({ testConfig, 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);
testConfig.serverData.actions[4].context = { no_breadcrumbs: true };
const webClient = await createWebClient({
testConfig,
});
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({ testConfig });
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({ testConfig });
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({ testConfig });
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++) {
testConfig.serverData.models.partner.records.push({ id: 100 + i, foo: `Record ${i}` });
}
const webClient = await createWebClient({ testConfig });
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);
testConfig.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({ testConfig });
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);
testConfig.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({ testConfig });
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);
}
);
});