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 */ `
`, }); // 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 */ ` `, }); 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 */ ` `, }); }); test("clickable statusbar widget on many2one field", async () => { await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); 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 */ ` `, }); 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 */ ` `, }); 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 */ ` `, }); 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 */ ` `, }); 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 */ ` `, }); 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 */ ` `, }); 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 */ ` `, }); 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 */ ` `, }); 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 */ ` `, }); 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 */ ` `, 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 */ ` `, 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 */ ` `, 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 */ ` `, }); 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": '