Files
odoo_source/addons/web/static/tests/webclient/actions/concurrency_tests.js
T
+2 48ef812a63 [REF] web: kanban, list and form views in owl
This commit introduces the new kanban, list and form views written
in owl. Even though it contains the implementation of those 3 views,
only the kanban view is activated for now (the list and form views
aren't 100% ready yet, so they aren't added to the view registry).

Alongside the views, the fields (<field name="..."/> in archs) and
widgets (<widget name="..." in archs) of web/ have been implemented
in owl as well. Legacy ones remaining in other addons are supported
in our new views, thanks to a compatibility layer. The goal is to convert
them asap though.

Legacy views, fields and widgets are kept for now, which explains the
number of added lines in this PR (around half of them concern tests).
They are still extended by custom code in other addons, that still need
to be converted (a lot of them are already on the way). Moreover,
they are still used in Studio as well. The plan is to lazy load them in
the Studio bundle when all custom code extending them will be
converted. Studio will be converted for v17.

Part-of: odoo/odoo#92475
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>
Co-authored-by: luvi <luvi@odoo.com>
2022-06-30 09:29:44 +02:00

713 lines
27 KiB
JavaScript

/** @odoo-module **/
import {
click,
getFixture,
legacyExtraNextTick,
makeDeferred,
nextTick,
} from "@web/../tests/helpers/utils";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import {
isItemSelected,
toggleFilterMenu,
toggleMenuItem,
switchView,
} from "@web/../tests/search/helpers";
import { registry } from "@web/core/registry";
import { useSetupView } from "@web/views/view_hook";
import {
createWebClient,
doAction,
getActionManagerServerData,
loadState,
} from "@web/../tests/webclient/helpers";
const { Component, xml } = owl;
const actionRegistry = registry.category("actions");
let serverData;
let target;
QUnit.module("ActionManager", (hooks) => {
hooks.beforeEach(() => {
serverData = getActionManagerServerData();
target = getFixture();
});
QUnit.module("Concurrency management");
QUnit.test("drop previous actions if possible", async function (assert) {
assert.expect(7);
const def = makeDeferred();
const mockRPC = async function (route) {
assert.step(route);
if (route === "/web/action/load") {
await def;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
doAction(webClient, 4);
doAction(webClient, 8);
def.resolve();
await nextTick();
// action 4 loads a kanban view first, 6 loads a list view. We want a list
assert.containsOnce(target, ".o_list_view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"/web/action/load",
"/web/dataset/call_kw/pony/get_views",
"/web/dataset/call_kw/pony/web_search_read",
]);
});
QUnit.test("handle switching view and switching back on slow network", async function (assert) {
assert.expect(9);
const def = makeDeferred();
const defs = [Promise.resolve(), def, Promise.resolve()];
const mockRPC = async function (route, { method }) {
assert.step(route);
if (method === "web_search_read") {
await defs.shift();
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 4);
// kanban view is loaded, switch to list view
await switchView(target, "list");
// here, list view is not ready yet, because def is not resolved
// switch back to kanban view
await switchView(target, "kanban");
// here, we want the kanban view to reload itself, regardless of list view
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/web_search_read",
"/web/dataset/call_kw/partner/web_search_read",
]);
// we resolve def => list view is now ready (but we want to ignore it)
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_kanban_view", "there should be a kanban view in dom");
assert.containsNone(target, ".o_list_view", "there should not be a list view in dom");
});
QUnit.test("when an server action takes too much time...", async function (assert) {
assert.expect(1);
const def = makeDeferred();
const mockRPC = async function (route, args) {
if (route === "/web/action/run") {
await def;
return 1;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
doAction(webClient, 2);
doAction(webClient, 4);
def.resolve();
await nextTick();
await legacyExtraNextTick();
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item.active").text(),
"Partners Action 4",
"action 4 should be loaded"
);
});
QUnit.test("clicking quickly on breadcrumbs...", async function (assert) {
assert.expect(1);
let def;
const mockRPC = async function (route, args) {
if (args && args.method === "read") {
await def;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// create a situation with 3 breadcrumbs: kanban/form/list
await doAction(webClient, 4);
await click(target.querySelector(".o_kanban_record"));
await doAction(webClient, 8);
// now, the next read operations will be promise (this is the read
// operation for the form view reload)
def = makeDeferred();
// click on the breadcrumbs for the form view, then on the kanban view
// before the form view is fully reloaded
await click(target.querySelectorAll(".o_control_panel .breadcrumb-item")[1]);
await click(target.querySelector(".o_control_panel .breadcrumb-item"));
// resolve the form view read
def.resolve();
await nextTick();
assert.strictEqual(
$(target).find(".o_control_panel .breadcrumb-item.active").text(),
"Partners Action 4",
"action 4 should be loaded and visible"
);
});
QUnit.test(
"execute a new action while loading a lazy-loaded controller",
async function (assert) {
assert.expect(16);
let def;
const mockRPC = async function (route, { method, model }) {
assert.step(method || route);
if (method === "web_search_read" && model === "partner") {
await def;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await loadState(webClient, {
action: 4,
id: 2,
view_type: "form",
});
assert.containsOnce(target, ".o_form_view", "should display the form view of action 4");
// click to go back to Kanban (this request is blocked)
def = makeDeferred();
await click(target.querySelector(".o_control_panel .breadcrumb a"));
assert.containsOnce(
target,
".o_form_view",
"should still display the form view of action 4"
);
// execute another action meanwhile (don't block this request)
await doAction(webClient, 8, { clearBreadcrumbs: true });
assert.containsOnce(target, ".o_list_view", "should display action 8");
assert.containsNone(target, ".o_form_view", "should no longer display the form view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"read",
"web_search_read",
"/web/action/load",
"get_views",
"web_search_read",
]);
// unblock the switch to Kanban in action 4
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_list_view", "should still display action 8");
assert.containsNone(
target,
".o_kanban_view",
"should not display the kanban view of action 4"
);
assert.verifySteps([]);
}
);
QUnit.test("execute a new action while handling a call_button", async function (assert) {
assert.expect(17);
const def = makeDeferred();
const mockRPC = async function (route, args) {
assert.step((args && args.method) || route);
if (route === "/web/dataset/call_button") {
await def;
return serverData.actions[1];
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// execute action 3 and open a record in form view
await doAction(webClient, 3);
await click(target.querySelector(".o_list_view .o_data_cell"));
assert.containsOnce(target, ".o_form_view", "should display the form view of action 3");
// click on 'Call method' button (this request is blocked)
await click(target.querySelector('.o_form_view button[name="object"]'));
assert.containsOnce(
target,
".o_form_view",
"should still display the form view of action 3"
);
// execute another action
await doAction(webClient, 8, { clearBreadcrumbs: true });
assert.containsOnce(target, ".o_list_view", "should display the list view of action 8");
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",
"read",
"object",
"/web/action/load",
"get_views",
"web_search_read",
]);
// unblock the call_button request
def.resolve();
await nextTick();
assert.containsOnce(
target,
".o_list_view",
"should still display the list view of action 8"
);
assert.containsNone(target, ".o_kanban_view", "should not display action 1");
assert.verifySteps([]);
});
QUnit.test(
"execute a new action while switching to another controller",
async function (assert) {
assert.expect(16);
// This test's bottom line is that a doAction always has priority
// over a switch controller (clicking on a record row to go to form view).
// In general, the last actionManager's operation has priority because we want
// to allow the user to make mistakes, or to rapidly reconsider her next action.
// Here we assert that the actionManager's RPC are in order, but a 'read' operation
// is expected, with the current implementation, to take place when switching to the form view.
// Ultimately the form view's 'read' is superfluous, but can happen at any point of the flow,
// except at the very end, which should always be the final action's list's 'search_read'.
let def;
const mockRPC = async function (route, args) {
assert.step((args && args.method) || route);
if (args && args.method === "read") {
await def;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view", "should display the list view of action 3");
// switch to the form view (this request is blocked)
def = makeDeferred();
await click(target.querySelector(".o_list_view .o_data_cell"));
assert.containsOnce(
target,
".o_list_view",
"should still display the list view of action 3"
);
// execute another action meanwhile (don't block this request)
await doAction(webClient, 4, { clearBreadcrumbs: true });
assert.containsOnce(
target,
".o_kanban_view",
"should display the kanban view of action 8"
);
assert.containsNone(target, ".o_list_view", "should no longer display the list view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"read",
"/web/action/load",
"get_views",
"web_search_read",
]);
// unblock the switch to the form view in action 3
def.resolve();
await nextTick();
assert.containsOnce(
target,
".o_kanban_view",
"should still display the kanban view of action 8"
);
assert.containsNone(
target,
".o_form_view",
"should not display the form view of action 3"
);
assert.verifySteps([]);
}
);
QUnit.test("execute a new action while loading views", async function (assert) {
assert.expect(11);
const def = makeDeferred();
const mockRPC = async function (route, args) {
assert.step((args && args.method) || route);
if (args && args.method === "get_views") {
await def;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// execute a first action (its 'get_views' RPC is blocked)
doAction(webClient, 3);
await nextTick();
assert.containsNone(target, ".o_list_view", "should not display the list view of action 3");
// execute another action meanwhile (and unlock the RPC)
doAction(webClient, 4);
await nextTick();
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_kanban_view", "should display the kanban view of action 4");
assert.containsNone(target, ".o_list_view", "should not display the list view of action 3");
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should be one controller in the breadcrumbs"
);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"/web/action/load",
"get_views",
"web_search_read",
]);
});
QUnit.test("execute a new action while loading data of default view", async function (assert) {
assert.expect(12);
const def = makeDeferred();
const mockRPC = async function (route, { method }) {
assert.step(method || route);
if (method === "web_search_read") {
await def;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
// execute a first action (its 'search_read' RPC is blocked)
doAction(webClient, 3);
await nextTick();
assert.containsNone(target, ".o_list_view", "should not display the list view of action 3");
// execute another action meanwhile (and unlock the RPC)
doAction(webClient, 4);
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_kanban_view", "should display the kanban view of action 4");
assert.containsNone(target, ".o_list_view", "should not display the list view of action 3");
assert.containsOnce(
target,
".o_control_panel .breadcrumb-item",
"there should be one controller in the breadcrumbs"
);
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"/web/action/load",
"get_views",
"web_search_read",
]);
});
QUnit.test("open a record while reloading the list view", async function (assert) {
assert.expect(12);
let def;
const mockRPC = async function (route) {
if (route === "/web/dataset/search_read") {
await def;
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
assert.containsN(target, ".o_list_view .o_data_row", 5);
assert.containsOnce(target, ".o_control_panel .o_list_buttons");
// reload (the search_read RPC will be blocked)
def = makeDeferred();
await switchView(target, "list");
assert.containsN(target, ".o_list_view .o_data_row", 5);
assert.containsOnce(target, ".o_control_panel .o_list_buttons");
// open a record in form view
await click(target.querySelector(".o_list_view .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
assert.containsNone(target, ".o_control_panel .o_list_buttons");
assert.containsOnce(target, ".o_control_panel .o_form_buttons_view");
// unblock the search_read RPC
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_form_view");
assert.containsNone(target, ".o_list_view");
assert.containsNone(target, ".o_control_panel .o_list_buttons");
assert.containsOnce(target, ".o_control_panel .o_form_buttons_view");
});
QUnit.test(
"properly drop client actions after new action is initiated",
async function (assert) {
assert.expect(3);
const slowWillStartDef = makeDeferred();
class ClientAction extends Component {
setup() {
owl.onWillStart(() => slowWillStartDef);
}
}
ClientAction.template = xml`<div class="client_action">ClientAction</div>`;
actionRegistry.add("slowAction", ClientAction);
const webClient = await createWebClient({ serverData });
doAction(webClient, "slowAction");
await nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".client_action", "client action isn't ready yet");
doAction(webClient, 4);
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_kanban_view", "should have loaded a kanban view");
slowWillStartDef.resolve();
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_kanban_view", "should still display the kanban view");
}
);
QUnit.test(
"restoring a controller when doing an action -- load_action slow",
async function (assert) {
assert.expect(14);
let def;
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
if (route === "/web/action/load") {
return Promise.resolve(def);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
await click(target.querySelector(".o_list_view .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
def = makeDeferred();
doAction(webClient, 4, { clearBreadcrumbs: true });
await nextTick();
assert.containsOnce(target, ".o_form_view", "should still contain the form view");
await click(target.querySelector(".o_control_panel .breadcrumb-item a"));
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_list_view");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb-item").textContent,
"Partners"
);
assert.containsNone(target, ".o_form_view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"read",
"/web/action/load",
"web_search_read",
]);
}
);
QUnit.test("switching when doing an action -- load_action slow", async function (assert) {
assert.expect(12);
let def;
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
if (route === "/web/action/load") {
return Promise.resolve(def);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
def = makeDeferred();
doAction(webClient, 4, { clearBreadcrumbs: true });
await nextTick();
assert.containsOnce(target, ".o_list_view", "should still contain the list view");
await switchView(target, "kanban");
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_kanban_view");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb-item").textContent,
"Partners"
);
assert.containsNone(target, ".o_list_view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"/web/action/load",
"web_search_read",
]);
});
QUnit.test("switching when doing an action -- get_views slow", async function (assert) {
assert.expect(13);
let def;
const mockRPC = async (route, args) => {
assert.step((args && args.method) || route);
if (args && args.method === "get_views") {
return Promise.resolve(def);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
def = makeDeferred();
doAction(webClient, 4, { clearBreadcrumbs: true });
await nextTick();
assert.containsOnce(target, ".o_list_view", "should still contain the list view");
await switchView(target, "kanban");
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_kanban_view");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb-item").textContent,
"Partners"
);
assert.containsNone(target, ".o_list_view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"/web/action/load",
"get_views",
"web_search_read",
]);
});
QUnit.test("switching when doing an action -- search_read slow", async function (assert) {
assert.expect(13);
const def = makeDeferred();
const defs = [null, def, null];
const mockRPC = async (route, { method }) => {
assert.step(method || route);
if (method === "web_search_read") {
await Promise.resolve(defs.shift());
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
doAction(webClient, 4, { clearBreadcrumbs: true });
await nextTick();
await switchView(target, "kanban");
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_kanban_view");
assert.strictEqual(
target.querySelector(".o_control_panel .breadcrumb-item").textContent,
"Partners"
);
assert.containsNone(target, ".o_list_view");
assert.verifySteps([
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"web_search_read",
"/web/action/load",
"get_views",
"web_search_read",
"web_search_read",
]);
});
QUnit.test("click multiple times to open a record", async function (assert) {
const def = makeDeferred();
const defs = [null, def];
const mockRPC = async (route, args) => {
if (args.method === "read") {
await Promise.resolve(defs.shift());
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
await click(target.querySelector(".o_list_view .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
await click(target.querySelector(".o_back_button"));
assert.containsOnce(target, ".o_list_view");
const row1 = target.querySelectorAll(".o_list_view .o_data_row")[0];
const row2 = target.querySelectorAll(".o_list_view .o_data_row")[1];
await click(row1.querySelector(".o_data_cell"));
await click(row2.querySelector(".o_data_cell"));
assert.containsOnce(target, ".o_form_view");
assert.strictEqual(
target.querySelector(".breadcrumb-item.active").innerText,
"Second record"
);
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_form_view");
assert.strictEqual(
target.querySelector(".breadcrumb-item.active").innerText,
"Second record"
);
});
QUnit.test("local state, global state, and race conditions", async function (assert) {
serverData.views = {
"partner,false,toy": `<toy/>`,
"partner,false,list": `<list><field name="foo"/></list>`,
"partner,false,search": `
<search>
<filter name="foo" string="Foo" domain="[]"/>
</search>
`,
};
let def = Promise.resolve();
let id = 1;
class ToyController extends Component {
setup() {
this.id = id++;
assert.step(JSON.stringify(this.props.state || "no state"));
useSetupView({
getLocalState: () => {
return { fromId: this.id };
},
});
owl.onWillStart(() => def);
}
}
ToyController.template = xml`
<div class="o_toy_view">
<ControlPanel />
</div>`;
ToyController.components = { ControlPanel };
registry.category("views").add("toy", {
type: "toy",
display_name: "Toy",
icon: "fab fa-android",
multiRecord: true,
searchMenuTypes: ["filter"],
Controller: ToyController,
});
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
res_model: "partner",
type: "ir.actions.act_window",
// list (or something else) must be added to have the view switcher displayed
views: [
[false, "toy"],
[false, "list"],
],
});
await toggleFilterMenu(target);
await toggleMenuItem(target, "Foo");
assert.ok(isItemSelected(target, "Foo"));
// reload twice by clicking on toy view switcher
def = makeDeferred();
await click(target.querySelector(".o_control_panel .o_switch_view.o_toy"));
await click(target.querySelector(".o_control_panel .o_switch_view.o_toy"));
def.resolve();
await nextTick();
await toggleFilterMenu(target);
assert.ok(isItemSelected(target, "Foo"));
// this test is not able to detect that getGlobalState is put on the right place:
// currentController.action.globalState contains in any case the search state
// of the first instantiated toy view.
assert.verifySteps([
`"no state"`, // setup first view instantiated
`{"fromId":1}`, // setup second view instantiated
`{"fromId":1}`, // setup third view instantiated
]);
});
});