import { expect, test } from "@odoo/hoot"; import { queryAll, queryFirst } from "@odoo/hoot-dom"; import { Deferred, animationFrame } from "@odoo/hoot-mock"; import { clickSave, contains, defineModels, fieldInput, fields, models, mountView, onRpc, serverState, } from "@web/../tests/web_test_helpers"; class Currency extends models.Model { digits = fields.Integer(); symbol = fields.Char({ string: "Currency Symbol" }); position = fields.Char({ string: "Currency Position" }); _records = [ { id: 1, display_name: "$", symbol: "$", position: "before", }, { id: 2, display_name: "€", symbol: "€", position: "after", }, ]; } class Partner extends models.Model { _name = "res.partner"; _inherit = []; name = fields.Char({ string: "Name", default: "My little Name Value", trim: true, }); int_field = fields.Integer(); partner_ids = fields.One2many({ string: "one2many field", relation: "res.partner", }); product_id = fields.Many2one({ relation: "product" }); placeholder_name = fields.Char(); _records = [ { id: 1, display_name: "first record", name: "yop", int_field: 10, partner_ids: [], placeholder_name: "Placeholder Name", }, { id: 2, display_name: "second record", name: "blip", int_field: 0, partner_ids: [], }, { id: 3, name: "gnap", int_field: 80 }, { id: 4, display_name: "aaa", name: "abc", }, { id: 5, name: "blop", int_field: -4 }, ]; _views = { form: /* xml */ `
`, }; } class PartnerType extends models.Model { color = fields.Integer({ string: "Color index" }); name = fields.Char({ string: "Partner Type" }); _records = [ { id: 12, display_name: "gold", color: 2 }, { id: 14, display_name: "silver", color: 5 }, ]; } class Product extends models.Model { name = fields.Char({ string: "Product Name" }); _records = [ { id: 37, name: "xphone", }, { id: 41, name: "xpad", }, ]; } class Users extends models.Model { _name = "res.users"; name = fields.Char(); has_group() { return true; } _records = [ { id: 1, name: "Aline", }, { id: 2, name: "Christine", }, ]; } defineModels([Currency, Partner, PartnerType, Product, Users]); test("char field in form view", async () => { await mountView({ type: "form", resModel: "res.partner", resId: 1 }); expect(".o_field_widget input[type='text']").toHaveCount(1, { message: "should have an input for the char field", }); expect(".o_field_widget input[type='text']").toHaveValue("yop", { message: "input should contain field value in edit mode", }); await fieldInput("name").edit("limbo"); await clickSave(); expect(".o_field_widget input[type='text']").toHaveValue("limbo", { message: "the new value should be displayed", }); }); test("setting a char field to empty string is saved as a false value", async () => { expect.assertions(1); await mountView({ type: "form", resModel: "res.partner", resId: 1 }); onRpc("web_save", ({ args }) => { expect(args[1].name).toBe(false); }); await fieldInput("name").clear(); await clickSave(); }); test("char field with size attribute", async () => { Partner._fields.name.size = 5; await mountView({ type: "form", resModel: "res.partner", resId: 1 }); expect("input").toHaveAttribute("maxlength", "5", { message: "maxlength attribute should have been set correctly on the input", }); }); test.tags("desktop"); test("char field in editable list view", async () => { await mountView({ type: "list", resModel: "res.partner", arch: `first paragraph
second paragraph
"); await contains(".o_field_char .btn.o_field_translate").click(); expect(".modal").toHaveCount(1, { message: "a translate modal should be visible", }); expect(".modal .o_translation_dialog .translation").toHaveCount(4, { message: "four rows should be visible", }); const enField = queryFirst(".modal .o_translation_dialog .translation input"); expect(enField).toHaveValue("first paragraph", { message: "first part of english translation should be filled", }); await contains(enField).edit("first paragraph modified"); await contains(".modal button.btn-primary").click(); expect(".o_field_char input[type='text']").toHaveValue( "first paragraph
second paragraph
", { message: "the new partial translation should not be transfered", } ); }); test("char field translatable in create mode", async () => { Partner._fields.name.translate = true; serverState.multiLang = true; await mountView({ type: "form", resModel: "res.partner" }); expect(".o_field_char .btn.o_field_translate").toHaveCount(1, { message: "should have a translate button in create mode", }); }); test("char field does not allow html injections", async () => { await mountView({ type: "form", resModel: "res.partner", resId: 1 }); await fieldInput("name").edit(""); await clickSave(); expect(".o_field_widget input").toHaveValue("", { message: "the value should have been properly escaped", }); }); test("char field trim (or not) characters", async () => { Partner._fields.foo2 = fields.Char({ trim: false }); await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); await fieldInput("name").edit(" abc "); await fieldInput("foo2").edit(" def "); await clickSave(); expect(".o_field_widget[name='name'] input").toHaveValue("abc", { message: "Name value should have been trimmed", }); expect(".o_field_widget[name='foo2'] input:only").toHaveValue(" def "); }); test.tags("desktop"); test("input field: change value before pending onchange returns", async () => { await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); let def; onRpc("onchange", () => def); await contains(".o_field_x2many_list_row_add a").click(); expect(".o_field_widget[name='name'] input").toHaveValue("My little Name Value", { message: "should contain the default value", }); def = new Deferred(); await contains(".o-autocomplete--input").click(); await contains(".o-autocomplete--dropdown-item").click(); await fieldInput("name").edit("tralala", { confirm: false }); expect(".o_field_widget[name='name'] input").toHaveValue("tralala", { message: "should contain tralala", }); def.resolve(); await animationFrame(); expect(".o_field_widget[name='name'] input").toHaveValue("tralala", { message: "should contain the same value as before onchange", }); }); test("input field: change value before pending onchange returns (2)", async () => { Partner._onChanges.int_field = (obj) => { if (obj.int_field === 7) { obj.name = "blabla"; } else { obj.name = "tralala"; } }; const def = new Deferred(); await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); onRpc("onchange", () => def); expect(".o_field_widget[name='name'] input").toHaveValue("yop", { message: "should contain the correct value", }); // trigger a deferred onchange await fieldInput("int_field").edit("7"); await fieldInput("name").edit("test", { confirm: false }); def.resolve(); await animationFrame(); expect(".o_field_widget[name='name'] input").toHaveValue("test", { message: "The onchage value should not be applied because the input is in edition", }); await fieldInput("name").press("Enter"); await expect(".o_field_widget[name='name'] input").toHaveValue("test"); await fieldInput("int_field").edit("10"); await expect(".o_field_widget[name='name'] input").toHaveValue("tralala", { message: "The onchange value should be applied because the input is not in edition", }); }); test.tags("desktop"); test("input field: change value before pending onchange returns (with fieldDebounce)", async () => { // this test is exactly the same as the previous one, except that in // this scenario the onchange return *before* we validate the change // on the input field (before the "change" event is triggered). Partner._onChanges.product_id = (obj) => { obj.int_field = obj.product_id ? 7 : false; }; let def; await mountView({ type: "form", resModel: "res.partner", arch: ` `, }); onRpc("onchange", () => def); await contains(".o_field_x2many_list_row_add a").click(); expect(".o_field_widget[name='name'] input").toHaveValue("My little Name Value", { message: "should contain the default value", }); def = new Deferred(); await contains(".o-autocomplete--input").click(); await contains(".o-autocomplete--dropdown-item").click(); await fieldInput("name").edit("tralala", { confirm: false }); expect(".o_field_widget[name='name'] input").toHaveValue("tralala", { message: "should contain tralala", }); expect(".o_field_widget[name='int_field'] input").toHaveValue(""); def.resolve(); await animationFrame(); expect(".o_field_widget[name='name'] input").toHaveValue("tralala", { message: "should contain the same value as before onchange", }); expect(".o_field_widget[name='int_field'] input").toHaveValue("7", { message: "should contain the value returned by the onchange", }); }); test("onchange return value before editing input", async () => { Partner._onChanges.name = (obj) => { obj.name = "yop"; }; await mountView({ type: "form", resModel: "res.partner", resId: 1 }); expect(".o_field_widget[name='name'] input").toHaveValue("yop"); await fieldInput("name").edit("tralala"); await expect("[name='name'] input").toHaveValue("yop"); }); test.tags("desktop"); test("input field: change value before pending onchange renaming", async () => { Partner._onChanges.product_id = (obj) => { obj.name = "on change value"; }; await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); onRpc("onchange", () => def); const def = new Deferred(); expect(".o_field_widget[name='name'] input").toHaveValue("yop", { message: "should contain the correct value", }); await contains(".o-autocomplete--input").click(); await contains(".o-autocomplete--dropdown-item").click(); // set name before onchange await fieldInput("name").edit("tralala"); await expect(".o_field_widget[name='name'] input").toHaveValue("tralala", { message: "should contain tralala", }); // complete the onchange def.resolve(); await animationFrame(); expect(".o_field_widget[name='name'] input").toHaveValue("tralala", { message: "input should contain the same value as before onchange", }); }); test("support autocomplete attribute", async () => { await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); expect(".o_field_widget[name='name'] input").toHaveAttribute("autocomplete", "coucou", { message: "attribute autocomplete should be set", }); }); test("input autocomplete attribute set to none by default", async () => { await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); expect(".o_field_widget[name='name'] input").toHaveAttribute("autocomplete", "off", { message: "attribute autocomplete should be set to none by default", }); }); test("support password attribute", async () => { await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); expect(".o_field_widget[name='name'] input").toHaveValue("yop", { message: "input value should be the password", }); expect(".o_field_widget[name='name'] input").toHaveAttribute("type", "password", { message: "input should be of type password", }); }); test("input field: readonly password", async () => { await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); expect(".o_field_char").not.toHaveText("yop", { message: "password field value should be visible in read mode", }); expect(".o_field_char").toHaveText("***", { message: "password field value should be hidden with '*' in read mode", }); }); test("input field: change password value", async () => { await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); expect(".o_field_char input").toHaveAttribute("type", "password", { message: "password field input value should with type 'password' in edit mode", }); expect(".o_field_char input").toHaveValue("yop", { message: "password field input value should be the (hidden) password value", }); }); test("input field: empty password", async () => { Partner._records[0].name = false; await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); expect(".o_field_char input").toHaveAttribute("type", "password", { message: "password field input value should with type 'password' in edit mode", }); expect(".o_field_char input").toHaveValue("", { message: "password field input value should be the (non-hidden, empty) password value", }); }); test.tags("desktop"); test("input field: set and remove value, then wait for onchange", async () => { Partner._onChanges.product_id = (obj) => { obj.name = obj.product_id ? "onchange value" : false; }; await mountView({ type: "form", resModel: "res.partner", arch: ` `, }); await contains(".o_field_x2many_list_row_add a").click(); expect(".o_field_widget[name=name] input").toHaveValue(""); await fieldInput("name").edit("test", { confirm: false }); await fieldInput("name").clear({ confirm: false }); // trigger the onchange by setting a product await contains(".o-autocomplete--input").click(); await contains(".o-autocomplete--dropdown-item").click(); expect(".o_field_widget[name=name] input").toHaveValue("onchange value", { message: "input should contain correct value after onchange", }); }); test("char field with placeholder", async () => { Partner._fields.name.default = false; await mountView({ type: "form", resModel: "res.partner", arch: ` `, }); expect(".o_field_widget[name='name'] input").toHaveAttribute("placeholder", "Placeholder", { message: "placeholder attribute should be set", }); }); test("Form: placeholder_field shows as placeholder", async () => { Partner._records[0].name = false; await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); expect("input").toHaveValue("", { message: "should have no value in input", }); expect("input").toHaveAttribute("placeholder", "Placeholder Name", { message: "placeholder_field should be the placeholder", }); }); test("char field: correct value is used to evaluate the modifiers", async () => { Partner._records[0].name = false; Partner._records[0].display_name = false; Partner._onChanges.name = (obj) => { if (obj.name === "a") { obj.display_name = false; } else if (obj.name === "b") { obj.display_name = ""; } }; await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); expect("[name='display_name']").toHaveCount(1); await fieldInput("name").edit("a"); await animationFrame(); expect("[name='display_name']").toHaveCount(1); await fieldInput("name").edit("b"); await animationFrame(); expect("[name='display_name']").toHaveCount(0); }); test("edit a char field should display the status indicator buttons without flickering", async () => { Partner._records[0].partner_ids = [2]; Partner._onChanges.name = (obj) => { obj.display_name = "cc"; }; const def = new Deferred(); await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: ` `, }); onRpc("onchange", () => { expect.step("onchange"); return def; }); expect(".o_form_status_indicator_buttons").not.toBeVisible({ message: "form view is not dirty", }); await contains(".o_data_cell").click(); await fieldInput("name").edit("a"); expect(".o_form_status_indicator_buttons").toBeVisible({ message: "form view is dirty", }); def.resolve(); expect.verifySteps(["onchange"]); await animationFrame(); expect(".o_form_status_indicator_buttons").toBeVisible({ message: "form view is dirty", }); expect.verifySteps(["onchange"]); });