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: ` `, }); expect("tbody td:not(.o_list_record_selector)").toHaveCount(5, { message: "should have 5 cells", }); expect("tbody td:not(.o_list_record_selector):first").toHaveText("yop", { message: "value should be displayed properly as text", }); const cellSelector = "tbody td:not(.o_list_record_selector)"; await contains(cellSelector).click(); expect(queryFirst(cellSelector).parentElement).toHaveClass("o_selected_row", { message: "should be set as edit mode", }); expect(`${cellSelector} input`).toHaveValue("yop", { message: "should have the corect value in internal input", }); await fieldInput("name").edit("brolo", { confirm: false }); await contains(".o_list_button_save").click(); expect(cellSelector).not.toHaveClass("o_selected_row", { message: "should not be in edit mode anymore", }); }); test("char field translatable", async () => { Partner._fields.name.translate = true; serverState.lang = "en_US"; serverState.multiLang = true; await mountView({ type: "form", resModel: "res.partner", resId: 1 }); let call_get_field_translations = 0; onRpc(async ({ args, method, model }) => { if (method === "get_installed" && model === "res.lang") { return [ ["en_US", "English"], ["fr_BE", "French (Belgium)"], ["es_ES", "Spanish"], ]; } if (method === "get_field_translations" && model === "res.partner") { if (call_get_field_translations === 0) { call_get_field_translations = 1; return [ [ { lang: "en_US", source: "yop", value: "yop" }, { lang: "fr_BE", source: "yop", value: "yop français" }, { lang: "es_ES", source: "yop", value: "yop español" }, ], { translation_type: "char", translation_show_source: false }, ]; } if (call_get_field_translations === 1) { return [ [ { lang: "en_US", source: "bar", value: "bar" }, { lang: "fr_BE", source: "bar", value: "yop français" }, { lang: "es_ES", source: "bar", value: "bar" }, ], { translation_type: "char", translation_show_source: false }, ]; } } if (method === "update_field_translations" && model === "res.partner") { expect(args[2]).toEqual( { en_US: "bar", es_ES: false }, { message: "the new translation value should be written and the value false voids the translation", } ); Partner._records[0].name = "bar"; return true; } }); expect("[name=name] input").toHaveClass("o_field_translate"); await contains("[name=name] input").click(); expect(".o_field_char .btn.o_field_translate").toHaveCount(1, { message: "should have a translate button", }); expect(".o_field_char .btn.o_field_translate").toHaveText("EN", { message: "the button should have as test the current language", }); 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(3, { message: "three rows should be visible", }); let translations = queryAll(".modal .o_translation_dialog .translation input"); expect(translations[0]).toHaveValue("yop", { message: "English translation should be filled", }); expect(translations[1]).toHaveValue("yop français", { message: "French translation should be filled", }); expect(translations[2]).toHaveValue("yop español", { message: "Spanish translation should be filled", }); await contains(translations[0]).edit("bar"); await contains(translations[2]).clear(); await contains("footer .btn.btn-primary").click(); expect(".o_field_widget.o_field_char input").toHaveValue("bar", { message: "the new translation should be transfered to modified record", }); await fieldInput("name").edit("baz"); await contains(".o_field_char .btn.o_field_translate").click(); translations = queryAll(".modal .o_translation_dialog .translation input"); expect(translations[0]).toHaveValue("baz", { message: "Modified value should be used instead of translation", }); expect(translations[1]).toHaveValue("yop français", { message: "French translation shouldn't be changed", }); expect(translations[2]).toHaveValue("bar", { message: "Spanish translation should fallback to the English translation", }); }); test("translation dialog should close if field is not there anymore", async () => { expect.assertions(4); // In this test, we simulate the case where the field is removed from the view // this can happen for example if the user click the back button of the browser. Partner._fields.name.translate = true; serverState.lang = "en_US"; serverState.multiLang = true; await mountView({ type: "form", resModel: "res.partner", resId: 1, arch: `
`, }); onRpc(async ({ method, model }) => { if (method === "get_installed" && model === "res.lang") { return [ ["en_US", "English"], ["fr_BE", "French (Belgium)"], ["es_ES", "Spanish"], ]; } if (method === "get_field_translations" && model === "res.partner") { return [ [ { lang: "en_US", source: "yop", value: "yop" }, { lang: "fr_BE", source: "yop", value: "valeur français" }, { lang: "es_ES", source: "yop", value: "yop español" }, ], { translation_type: "char", translation_show_source: false }, ]; } }); expect("[name=name] input").toHaveClass("o_field_translate"); await contains("[name=name] input").click(); await contains(".o_field_char .btn.o_field_translate").click(); expect(".modal").toHaveCount(1, { message: "a translate modal should be visible", }); await fieldInput("int_field").edit("9"); await animationFrame(); expect("[name=name] input").toHaveCount(0, { message: "the field name should be invisible", }); expect(".modal").toHaveCount(0, { message: "a translate modal should not be visible", }); }); test("html field translatable", async () => { expect.assertions(5); Partner._fields.name.translate = true; serverState.lang = "en_US"; serverState.multiLang = true; await mountView({ type: "form", resModel: "res.partner", resId: 1 }); onRpc(async ({ args, method, model }) => { if (method === "get_installed" && model === "res.lang") { return [ ["en_US", "English"], ["fr_BE", "French (Belgium)"], ]; } if (method === "get_field_translations" && model === "res.partner") { return [ [ { lang: "en_US", source: "first paragraph", value: "first paragraph", }, { lang: "en_US", source: "second paragraph", value: "second paragraph", }, { lang: "fr_BE", source: "first paragraph", value: "premier paragraphe", }, { lang: "fr_BE", source: "second paragraph", value: "deuxième paragraphe", }, ], { translation_type: "char", translation_show_source: true, }, ]; } if (method === "update_field_translations" && model === "res.partner") { expect(args[2]).toEqual( { en_US: { "first paragraph": "first paragraph modified" } }, { message: "the new translation value should be written", } ); return true; } }); // this will not affect the translate_fields effect until the record is // saved but is set for consistency of the test await fieldInput("name").edit("

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"]); });