Odoo18-Base/addons/web/static/tests/views/fields/statusbar_field.test.js
2025-01-06 10:57:38 +07:00

778 lines
23 KiB
JavaScript

import { expect, test } from "@odoo/hoot";
import { click, edit, press, queryAllTexts, queryAttribute, queryFirst } from "@odoo/hoot-dom";
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
import {
clickSave,
defineActions,
defineModels,
fields,
getDropdownMenu,
getService,
models,
mockService,
mountView,
mountWithCleanup,
onRpc,
serverState,
} from "@web/../tests/web_test_helpers";
import { EventBus } from "@odoo/owl";
import { WebClient } from "@web/webclient/webclient";
class Partner extends models.Model {
name = fields.Char();
foo = fields.Char({ default: "My little Foo Value" });
bar = fields.Boolean({ default: true });
int_field = fields.Integer();
qux = fields.Float({ digits: [16, 1] });
p = fields.One2many({
relation: "partner",
relation_field: "trululu",
});
trululu = fields.Many2one({ relation: "partner" });
product_id = fields.Many2one({ relation: "product" });
color = fields.Selection({
selection: [
["red", "Red"],
["black", "Black"],
],
default: "red",
});
user_id = fields.Many2one({ relation: "users" });
_records = [
{
id: 1,
name: "first record",
bar: true,
foo: "yop",
int_field: 10,
qux: 0.44,
p: [],
trululu: 4,
user_id: 17,
},
{
id: 2,
name: "second record",
bar: true,
foo: "blip",
int_field: 9,
qux: 13,
p: [],
trululu: 1,
product_id: 37,
user_id: 17,
},
{ id: 4, name: "aaa", bar: false },
];
}
class Product extends models.Model {
name = fields.Char();
_records = [
{ id: 37, name: "xphone" },
{ id: 41, name: "xpad" },
];
}
class Users extends models.Model {
name = fields.Char();
partner_ids = fields.One2many({
relation: "partner",
relation_field: "user_id",
});
_records = [
{ id: 17, name: "Aline", partner_ids: [1, 2] },
{ id: 19, name: "Christine" },
];
}
defineModels([Partner, Product, Users]);
test("static statusbar widget on many2one field", async () => {
Partner._fields.trululu = fields.Many2one({
relation: "partner",
domain: "[('bar', '=', True)]",
});
Partner._records[1].bar = false;
onRpc("search_read", ({ kwargs }) => expect.step(kwargs.fields.toString()));
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" />
</header>
</form>
`,
});
// search_read should only fetch field display_name
expect.verifySteps(["display_name"]);
expect(".o_statusbar_status button:not(.dropdown-toggle)").toHaveCount(2);
expect(".o_statusbar_status button:disabled").toHaveCount(5);
expect('.o_statusbar_status button[data-value="4"]').toHaveClass("o_arrow_button_current");
});
test("folded statusbar widget on selection field has selected value in the toggler", async () => {
mockService("ui", (env) => {
Object.defineProperty(env, "isSmall", {
value: true,
});
return {
bus: new EventBus(),
size: 0,
isSmall: true,
};
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="color" widget="statusbar" />
</header>
</form>
`,
});
expect(".o_statusbar_status button.dropdown-toggle:contains(Red)").toHaveCount(1);
});
test("static statusbar widget on many2one field with domain", async () => {
expect.assertions(1);
serverState.userId = 17;
onRpc("search_read", ({ kwargs }) => {
expect(kwargs.domain).toEqual(["|", ["id", "=", 4], ["user_id", "=", 17]], {
message: "search_read should sent the correct domain",
});
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" domain="[('user_id', '=', uid)]" />
</header>
</form>
`,
});
});
test("clickable statusbar widget on many2one field", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
</header>
</form>
`,
});
expect(".o_statusbar_status button[data-value='4']").toHaveClass("o_arrow_button_current");
expect(".o_statusbar_status button[data-value='4']").not.toBeEnabled();
expect(
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current)"
).toHaveCount(2);
await click(
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current):eq(1)"
);
await animationFrame();
expect(".o_statusbar_status button[data-value='1']").toHaveClass("o_arrow_button_current");
expect(".o_statusbar_status button[data-value='1']").not.toBeEnabled();
});
test("statusbar with no status", async () => {
Partner._records[1].product_id = false;
Product._records = [];
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="product_id" widget="statusbar" />
</header>
</form>
`,
});
expect(".o_statusbar_status").not.toHaveClass("o_field_empty");
expect(".o_statusbar_status > :not(.d-none)").toHaveCount(0, {
message: "statusbar widget should be empty",
});
});
test("statusbar with tooltip for help text", async () => {
Partner._fields.product_id = fields.Many2one({
relation: "product",
help: "some info about the field",
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="product_id" widget="statusbar" />
</header>
</form>
`,
});
expect(".o_statusbar_status").not.toHaveClass("o_field_empty");
expect(".o_field_statusbar").toHaveAttribute("data-tooltip-info");
const tooltipInfo = JSON.parse(queryAttribute(".o_field_statusbar", "data-tooltip-info"));
expect(tooltipInfo.field.help).toBe("some info about the field", {
message: "tooltip text is present on the field",
});
});
test("statusbar with required modifier", async () => {
mockService("notification", {
add() {
expect.step("Show error message");
return () => {};
},
});
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<header>
<field name="product_id" widget="statusbar" required="1"/>
</header>
</form>
`,
});
await click(".o_form_button_save");
await animationFrame();
expect(".o_form_editable").toHaveCount(1, { message: "view should still be in edit" });
// should display an 'invalid fields' notificationaveCount(1, { message: "view should still be in edit" });
expect.verifySteps(["Show error message"]);
});
test("statusbar with no value in readonly", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="product_id" widget="statusbar" />
</header>
</form>
`,
});
expect(".o_statusbar_status").not.toHaveClass("o_field_empty");
expect(".o_statusbar_status button:visible").toHaveCount(2);
});
test("statusbar with domain but no value (create mode)", async () => {
Partner._fields.trululu = fields.Many2one({
relation: "partner",
domain: "[('bar', '=', True)]",
});
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" />
</header>
</form>
`,
});
expect(".o_statusbar_status button:disabled").toHaveCount(5);
});
test("clickable statusbar should change m2o fetching domain in edit mode", async () => {
Partner._fields.trululu = fields.Many2one({
relation: "partner",
domain: "[('bar', '=', True)]",
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
</header>
</form>
`,
});
expect(".o_statusbar_status button:not(.dropdown-toggle)").toHaveCount(3);
await click(".o_statusbar_status button:not(.dropdown-toggle):eq(-1)");
await animationFrame();
expect(".o_statusbar_status button:not(.dropdown-toggle)").toHaveCount(2);
});
test("statusbar fold_field option and statusbar_visible attribute", async () => {
Partner._records[0].bar = false;
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'fold_field': 'bar'}" />
<field name="color" widget="statusbar" statusbar_visible="red" />
</header>
</form>
`,
});
await click(".o_statusbar_status .dropdown-toggle:not(.d-none)");
await animationFrame();
expect(".o_statusbar_status:first button:visible").toHaveCount(3);
expect(".o_statusbar_status:last button:visible").toHaveCount(1);
expect(".o_statusbar_status button").not.toBeEnabled({
message: "no status bar buttons should be enabled",
});
});
test("statusbar: choose an item from the folded menu", async () => {
Partner._records[0].bar = false;
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'clickable': '1', 'fold_field': 'bar'}" />
</header>
</form>
`,
});
expect("[aria-checked='true']").toHaveText("aaa", {
message: "default status is 'aaa'",
});
expect(".o_statusbar_status .dropdown-toggle.o_arrow_button").toHaveText("...", {
message: "button has the correct text",
});
await click(".o_statusbar_status .dropdown-toggle:not(.d-none)");
await animationFrame();
await click(".o-dropdown--menu .dropdown-item");
await animationFrame();
expect("[aria-checked='true']").toHaveText("second record", {
message: "status has changed to the selected dropdown item",
});
});
test("statusbar with dynamic domain", async () => {
Partner._fields.trululu = fields.Many2one({
relation: "partner",
domain: "[('int_field', '>', qux)]",
});
Partner._records[2].int_field = 0;
onRpc("search_read", () => {
rpcCount++;
});
let rpcCount = 0;
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" />
</header>
<field name="qux" />
<field name="foo" />
</form>
`,
});
expect(".o_statusbar_status button:disabled").toHaveCount(6);
expect(rpcCount).toBe(1, { message: "should have done 1 search_read rpc" });
await click(".o_field_widget[name='qux'] input");
await edit(9.5, { confirm: "enter" });
await runAllTimers();
await animationFrame();
expect(".o_statusbar_status button:disabled").toHaveCount(5);
expect(rpcCount).toBe(2, { message: "should have done 1 more search_read rpc" });
await edit("hey", { confirm: "enter" });
await animationFrame();
expect(rpcCount).toBe(2, { message: "should not have done 1 more search_read rpc" });
});
test(`statusbar edited by the smart action "Move to stage..."`, async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'clickable': '1'}"/>
</header>
</form>
`,
resId: 1,
});
expect(".o_field_widget").toHaveCount(1);
await press(["control", "k"]);
await animationFrame();
await click(`.o_command:contains("Move to Trululu")`);
await animationFrame();
expect(queryAllTexts(".o_command")).toEqual(["first record", "second record", "aaa"]);
await click("#o_command_2");
await animationFrame();
});
test("smart actions are unavailable if readonly", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" readonly="1"/>
</header>
</form>
`,
resId: 1,
});
expect(".o_field_widget").toHaveCount(1);
await press(["control", "k"]);
await animationFrame();
const moveStages = queryAllTexts(".o_command");
expect(moveStages).not.toInclude("Move to Trululu\nALT + SHIFT + X");
expect(moveStages).not.toInclude("Move to next\nALT + X");
});
test("hotkeys are unavailable if readonly", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" readonly="1"/>
</header>
</form>
`,
resId: 1,
});
expect(".o_field_widget").toHaveCount(1);
await press(["alt", "shift", "x"]); // Move to stage...
await animationFrame();
expect(".modal").toHaveCount(0, { message: "command palette should not open" });
await press(["alt", "x"]); // Move to next
await animationFrame();
expect(".modal").toHaveCount(0, { message: "command palette should not open" });
});
test("auto save record when field toggled", async () => {
onRpc("web_save", () => expect.step("web_save"));
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
</header>
</form>
`,
});
await click(
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current):eq(-1)"
);
await animationFrame();
expect.verifySteps(["web_save"]);
});
test("For the same record, a single rpc is done to recover the specialData", async () => {
Partner._views = {
"list,3": '<list><field name="display_name"/></list>',
"search,9": `<search></search>`,
"form,false": `
<form>
<header>
<field name="trululu" widget="statusbar" readonly="1"/>
</header>
</form>
`,
};
defineActions([
{
id: 1,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
},
]);
onRpc("has_group", () => true);
onRpc("search_read", () => expect.step("search_read"));
await mountWithCleanup(WebClient);
await getService("action").doAction(1);
await click(".o_data_row .o_data_cell");
await animationFrame();
expect.verifySteps(["search_read"]);
await click(".o_back_button");
await animationFrame();
await click(".o_data_row .o_data_cell");
await animationFrame();
expect.verifySteps([]);
});
test("open form with statusbar, leave and come back to another one with other domain", async () => {
Partner._views = {
"list,3": '<list><field name="display_name"/></list>',
"search,9": `<search/>`,
"form,false": `
<form>
<header>
<field name="trululu" widget="statusbar" domain="[['id', '>', id]]" readonly="1"/>
</header>
</form>
`,
};
defineActions([
{
id: 1,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
},
]);
onRpc("has_group", () => true);
onRpc("search_read", () => expect.step("search_read"));
await mountWithCleanup(WebClient);
await getService("action").doAction(1);
// open first record
await click(".o_data_row .o_data_cell");
await animationFrame();
expect.verifySteps(["search_read"]);
// go back and open second record
await click(".o_back_button");
await animationFrame();
await click(".o_data_row:eq(1) .o_data_cell");
await animationFrame();
expect.verifySteps(["search_read"]);
});
test("clickable statusbar with readonly modifier set to false is editable", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 2,
arch: /* xml */ `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': true}" readonly="False"/>
</header>
</form>
`,
});
expect(".o_statusbar_status button:visible").toHaveCount(2);
expect(".o_statusbar_status button[disabled][aria-checked='false']:visible").toHaveCount(0);
});
test("clickable statusbar with readonly modifier set to true is not editable", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 2,
arch: /* xml */ `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': true}" readonly="True"/>
</header>
</form>
`,
});
expect(".o_statusbar_status button[disabled]:visible").toHaveCount(2);
});
test("non-clickable statusbar with readonly modifier set to false is not editable", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 2,
arch: /* xml */ `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': false}" readonly="False"/>
</header>
</form>
`,
});
expect(".o_statusbar_status button[disabled]:visible").toHaveCount(2);
});
test("last status bar button have a border radius (no arrow shape) on the right side when a prior folded stage gets selected", async () => {
class Stage extends models.Model {
name = fields.Char();
folded = fields.Boolean({ default: false });
_records = [
{ id: 1, name: "New" },
{ id: 2, name: "In Progress", folded: true },
{ id: 3, name: "Done" },
];
}
class Task extends models.Model {
status = fields.Many2one({ relation: "stage" });
_records = [
{ id: 1, status: 1 },
{ id: 2, status: 2 },
{ id: 3, status: 3 },
];
}
defineModels([Stage, Task]);
await mountView({
type: "form",
resModel: "task",
resId: 3,
arch: /* xml */ `
<form>
<header>
<field name="status" widget="statusbar" options="{'clickable': true, 'fold_field': 'folded'}" />
</header>
</form>
`,
});
await click(".o_statusbar_status .dropdown-toggle:not(.d-none)");
await animationFrame();
await click(
queryFirst(".dropdown-item", {
root: getDropdownMenu(".o_statusbar_status .dropdown-toggle:not(.d-none)"),
})
);
await animationFrame();
expect(".o_statusbar_status button[data-value='3']").not.toHaveStyle({
borderTopRightRadius: "0px",
});
expect(".o_statusbar_status button[data-value='3']").toHaveClass("o_first");
});
test.tags("desktop");
test("correctly load statusbar when dynamic domain changes", async () => {
class Stage extends models.Model {
name = fields.Char();
folded = fields.Boolean({ default: false });
project_ids = fields.Many2many({ relation: "project" });
_records = [
{ id: 1, name: "Stage Project 1", project_ids: [1] },
{ id: 2, name: "Stage Project 2", project_ids: [2] },
];
}
class Project extends models.Model {
display_name = fields.Char();
_records = [
{ id: 1, display_name: "Project 1" },
{ id: 2, display_name: "Project 2" },
];
}
class Task extends models.Model {
status = fields.Many2one({ relation: "stage" });
project_id = fields.Many2one({ relation: "project" });
_records = [{ id: 1, project_id: 1, status: 1 }];
}
Task._onChanges.project_id = (obj) => {
obj.status = obj.project_id === 1 ? 1 : 2;
};
defineModels([Stage, Project, Task]);
onRpc("search_read", ({ kwargs }) => expect.step(JSON.stringify(kwargs.domain)));
await mountView({
type: "form",
resModel: "task",
resId: 1,
arch: /* xml */ `
<form>
<header>
<field name="status" widget="statusbar" domain="[('project_ids', 'in', project_id)]" />
</header>
<field name="project_id"/>
</form>
`,
});
expect(queryAllTexts(".o_statusbar_status button:not(.d-none)")).toEqual(["Stage Project 1"]);
expect.verifySteps(['["|",["id","=",1],["project_ids","in",1]]']);
await click(`[name="project_id"] .dropdown input`);
await animationFrame();
await click(`[name="project_id"] .dropdown .dropdown-menu .ui-menu-item:contains("Project 2")`);
await animationFrame();
expect(queryAllTexts(".o_statusbar_status button:not(.d-none)")).toEqual(["Stage Project 2"]);
expect.verifySteps(['["|",["id","=",2],["project_ids","in",2]]']);
await clickSave();
expect(queryAllTexts(".o_statusbar_status button:not(.d-none)")).toEqual(["Stage Project 2"]);
expect.verifySteps([]);
});