/** @odoo-module **/ import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { click, clickDiscard, clickSave, editInput, getFixture, makeDeferred, nextTick, patchWithCleanup, triggerEvent, } from "@web/../tests/helpers/utils"; import { createWebClient, doAction } from "@web/../tests/webclient/helpers"; let serverData; let target; QUnit.module("Fields", (hooks) => { hooks.beforeEach(() => { target = getFixture(); serverData = { models: { partner: { fields: { date: { string: "A date", type: "date" }, display_name: { string: "Displayed name", type: "char" }, foo: { string: "Foo", type: "char", default: "My little Foo Value", }, bar: { string: "Bar", type: "boolean", default: true }, int_field: { string: "int_field", type: "integer" }, image: { string: "Picture", type: "binary", searchable: true }, }, records: [ { id: 1, date: "2017-02-03", display_name: "first record", bar: true, foo: "yop", int_field: 10, }, { id: 2, display_name: "second record", bar: true, foo: "blip", int_field: 0, }, { id: 4, display_name: "aaa", foo: "abc", int_field: false, }, { id: 3, bar: true, foo: "gnap", int_field: 80 }, { id: 5, bar: false, foo: "blop", int_field: -4 }, ], onchanges: {}, }, product: { fields: { name: { string: "Product Name", type: "char", searchable: true }, }, records: [ { id: 37, display_name: "xphone", }, { id: 41, display_name: "xpad", }, ], }, partner_type: { fields: { name: { string: "Partner Type", type: "char", searchable: true }, color: { string: "Color index", type: "integer", searchable: true }, }, records: [ { id: 12, display_name: "gold", color: 2 }, { id: 14, display_name: "silver", color: 5 }, ], }, }, }; setupViewRegistries(); }); QUnit.module("DomainField"); QUnit.test( "The domain editor should not crash the view when given a dynamic filter", async function (assert) { // dynamic filters (containing variables, such as uid, parent or today) // are not handled by the domain editor, but it shouldn't crash the view serverData.models.partner.records[0].foo = `[("int_field", "=", uid)]`; await makeView({ type: "form", resModel: "partner", resId: 1, serverData, arch: `
`, }); assert.strictEqual( target.querySelector(".o_edit_mode").textContent, " This domain is not supported. Reset domain", "The widget should not crash the view, but gracefully admit its failure." ); } ); QUnit.test( "The domain editor should not crash the view when given a dynamic filter ( datetime )", async function (assert) { // dynamic filters (containing variables, such as uid, parent or today) // are not handled by the domain editor, but it shouldn't crash the view serverData.models.partner.records[0].foo = `[("datetime", "=", context_today())]`; serverData.models.partner.fields.datetime = { string: "A date", type: "datetime" }; await makeView({ type: "form", resModel: "partner", resId: 1, serverData, arch: `
`, }); // The input field should display that the date is invalid assert.equal(target.querySelector(".o_datepicker_input").value, "Invalid DateTime"); // Change the date in the datepicker await click(target, ".o_datepicker_input"); // Select a date in the datepicker await click( document.body.querySelector( `.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]` ) ); // Close the datepicker await click( document.body.querySelector( `.bootstrap-datetimepicker-widget a[data-action="close"]` ) ); await clickDiscard(target); // Open the datepicker again await click(target, ".o_datepicker_input"); } ); QUnit.test("basic domain field usage is ok", async function (assert) { serverData.models.partner.records[0].foo = "[]"; await makeView({ type: "form", resModel: "partner", resId: 1, serverData, arch: `
`, }); // As the domain is empty, there should be a button to add the first // domain part assert.containsOnce( target, ".o_domain_add_first_node_button", "there should be a button to create first domain element" ); // Clicking on the button should add the [["id", "=", "1"]] domain, so // there should be a field selector in the DOM await click(target, ".o_domain_add_first_node_button"); assert.containsOnce(target, ".o_field_selector", "there should be a field selector"); // Focusing the field selector input should open the field selector // popover await click(target, ".o_field_selector"); assert.containsOnce( document.body, ".o_field_selector_popover", "field selector popover should be visible" ); assert.containsOnce( document.body, ".o_field_selector_search input", "field selector popover should contain a search input" ); // The popover should contain the list of partner_type fields and so // there should be the "Color index" field assert.strictEqual( document.body.querySelector(".o_field_selector_item").textContent, "Color index", "field selector popover should contain 'Color index' field" ); // Clicking on this field should close the popover, then changing the // associated value should reveal one matched record await click(document.body.querySelector(".o_field_selector_item")); const input = target.querySelector(".o_domain_leaf_value_input"); input.value = 2; await triggerEvent(input, null, "change"); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim().substr(0, 2), "1 ", "changing color value to 2 should reveal only one record" ); // Saving the form view should show a readonly domain containing the // "color" field await clickSave(target); assert.ok( target.querySelector(".o_field_domain").textContent.includes("Color index"), "field selector readonly value should now contain 'Color index'" ); }); QUnit.test("using binary field in domain widget", async function (assert) { assert.expect(0); serverData.models.partner.records[0].foo = "[]"; await makeView({ type: "form", resModel: "partner", resId: 1, serverData, arch: `
`, }); await click(target, ".o_domain_add_first_node_button"); await click(target, ".o_field_selector"); await click(document.body.querySelector(".o_field_selector_item[data-name='image']")); }); QUnit.test("domain field is correctly reset on every view change", async function (assert) { serverData.models.partner.records[0].foo = `[("id", "=", 1)]`; serverData.models.partner.fields.bar.type = "char"; serverData.models.partner.records[0].bar = "product"; await makeView({ type: "form", resModel: "partner", resId: 1, serverData, arch: `
`, }); // As the domain is equal to [["id", "=", 1]] there should be a field // selector to change this assert.containsOnce( target, ".o_field_domain .o_field_selector", "there should be a field selector" ); // Focusing its input should open the field selector popover await click(target.querySelector(".o_field_selector")); assert.containsOnce( document.body, ".o_field_selector_popover", "field selector popover should be visible" ); // As the value of the "bar" field is "product", the field selector // popover should contain the list of "product" fields assert.containsOnce( document.body, ".o_field_selector_item", "field selector popover should contain only one field" ); assert.strictEqual( document.body.querySelector(".o_field_selector_item").textContent, "Product Name", "field selector popover should contain 'Product Name' field" ); // Now change the value of the "bar" field to "partner_type" await editInput(target, ".o_field_widget[name='bar'] input", "partner_type"); // Refocusing the field selector input should open the popover again await click(target.querySelector(".o_field_selector")); assert.containsOnce( document.body, ".o_field_selector_popover", "field selector popover should be visible" ); // Now the list of fields should be the ones of the "partner_type" model assert.containsN( document.body, ".o_field_selector_item", 2, "field selector popover should contain two fields" ); assert.strictEqual( document.body.querySelector(".o_field_selector_item").textContent, "Color index", "field selector popover should contain 'Color index' field" ); }); QUnit.test( "domain field can be reset with a new domain (from onchange)", async function (assert) { serverData.models.partner.records[0].foo = "[]"; serverData.models.partner.onchanges = { display_name(obj) { obj.foo = `[("id", "=", 1)]`; }, }; await makeView({ type: "form", resModel: "partner", resId: 1, serverData, arch: `
`, }); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "5 record(s)", "the domain being empty, there should be 5 records" ); // update display_name to trigger the onchange and reset foo await editInput(target, ".o_field_widget[name='display_name'] input", "new value"); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "1 record(s)", "the domain has changed, there should be only 1 record" ); } ); QUnit.test("domain field: handle false domain as []", async function (assert) { assert.expect(3); serverData.models.partner.records[0].foo = false; serverData.models.partner.fields.bar.type = "char"; serverData.models.partner.records[0].bar = "product"; await makeView({ type: "form", resModel: "partner", resId: 1, serverData, arch: `
`, mockRPC(route, { args, method }) { if (method === "search_count") { assert.deepEqual(args[0], [], "should send a valid domain"); } }, }); assert.containsOnce( target, ".o_field_widget[name='foo']:not(.o_field_empty)", "there should be a domain field, not considered empty" ); assert.containsNone( target, ".o_field_widget[name='foo'] .text-warning", "should not display that the domain is invalid" ); }); QUnit.test("basic domain field: show the selection", async function (assert) { serverData.models.partner.records[0].foo = "[]"; serverData.views = { "partner_type,false,list": ``, "partner_type,false,search": ``, }; await makeView({ type: "form", resModel: "partner", resId: 1, serverData, arch: `
`, }); assert.equal( target.querySelector(".o_domain_show_selection_button").textContent.trim().substr(0, 2), "2 ", "selection should contain 2 records" ); // open the selection await click(target, ".o_domain_show_selection_button"); assert.strictEqual( target.querySelectorAll(".modal .o_list_view .o_data_row").length, 2, "should have open a list view with 2 records in a dialog" ); // click on a record -> should not open the record // we don't actually check that it doesn't open the record because even // if it tries to, it will crash as we don't define an arch in this test await click(target, ".modal .o_list_view .o_data_row .o_data_cell[data-tooltip='gold']"); }); QUnit.test("field context is propagated when opening selection", async function (assert) { serverData.models.partner.records[0].foo = "[]"; serverData.views = { "partner_type,false,list": ``, "partner_type,3,list": ``, "partner_type,false,search": ``, }; await makeView({ type: "form", resModel: "partner", resId: 1, serverData, arch: `
`, }); await click(target, ".o_domain_show_selection_button"); assert.deepEqual( [...target.querySelectorAll(".modal .o_data_row")].map((x) => x.textContent), ["12", "14"], "should have picked the correct list view" ); }); QUnit.test("domain field: manually edit domain with textarea", async function (assert) { patchWithCleanup(odoo, { debug: true }); serverData.models.partner.records[0].foo = false; serverData.models.partner.fields.bar.type = "char"; serverData.models.partner.records[0].bar = "product"; serverData.views = { "partner,false,form": `
`, "partner,false,search": ``, }; serverData.actions = { 1: { id: 1, name: "test", res_id: 1, res_model: "partner", type: "ir.actions.act_window", views: [[false, "form"]], }, }; const webClient = await createWebClient({ serverData, mockRPC(route, { method, args }) { if (method === "search_count") { assert.step(JSON.stringify(args[0])); } if (route === "/web/domain/validate") { return true; } }, }); await doAction(webClient, 1); assert.verifySteps(["[]"]); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "2 record(s)" ); await editInput(target, ".o_domain_debug_input", "[['id', '<', 40]]"); // the count should not be re-computed when editing with the textarea assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "2 record(s)" ); assert.verifySteps([]); await clickSave(target); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "1 record(s)" ); assert.verifySteps(['[["id","<",40]]']); }); QUnit.test( "domain field: manually set an invalid domain with textarea", async function (assert) { patchWithCleanup(odoo, { debug: true }); serverData.models.partner.records[0].foo = false; serverData.models.partner.fields.bar.type = "char"; serverData.models.partner.records[0].bar = "product"; serverData.views = { "partner,false,form": `
`, "partner,false,search": ``, }; serverData.actions = { 1: { id: 1, name: "test", res_id: 1, res_model: "partner", type: "ir.actions.act_window", views: [[false, "form"]], }, }; const webClient = await createWebClient({ serverData, mockRPC(route, { method, args }) { if (method === "search_count") { assert.step(JSON.stringify(args[0])); } if (method === "write") { throw new Error("should not save"); } if (route === "/web/domain/validate") { return false; } }, }); await doAction(webClient, 1); assert.verifySteps(["[]"]); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "2 record(s)" ); await editInput(target, ".o_domain_debug_input", "[['abc']]"); // the count should not be re-computed when editing with the textarea assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "2 record(s)" ); assert.verifySteps([]); await clickSave(target); assert.hasClass( target.querySelector(".o_field_domain"), "o_field_invalid", "the field is marked as invalid" ); assert.containsOnce( target, ".o_form_view .o_form_editable", "the view is still in edit mode" ); assert.verifySteps([]); } ); QUnit.test( "domain field: reload count by clicking on the refresh button", async function (assert) { patchWithCleanup(odoo, { debug: true }); serverData.models.partner.records[0].foo = "[]"; serverData.models.partner.fields.bar.type = "char"; serverData.models.partner.records[0].bar = "product"; serverData.views = { "partner,false,form": `
`, "partner,false,search": ``, }; serverData.actions = { 1: { id: 1, name: "test", res_id: 1, res_model: "partner", type: "ir.actions.act_window", views: [[false, "form"]], }, }; const webClient = await createWebClient({ serverData, mockRPC(route, { method, args }) { if (method === "search_count") { assert.step(JSON.stringify(args[0])); } }, }); await doAction(webClient, 1); assert.verifySteps(["[]"]); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "2 record(s)" ); await editInput(target, ".o_domain_debug_input", "[['id', '<', 40]]"); // the count should not be re-computed when editing with the textarea assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "2 record(s)" ); // click on the refresh button await click(target, ".o_refresh_count"); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "1 record(s)" ); assert.verifySteps(['[["id","<",40]]']); } ); QUnit.test("domain field: does not wait for the count to render", async function (assert) { serverData.models.partner.records[0].foo = "[]"; serverData.models.partner.fields.bar.type = "char"; serverData.models.partner.records[0].bar = "product"; const def = makeDeferred(); await makeView({ type: "form", resModel: "partner", resId: 1, serverData, arch: `
`, async mockRPC(route, { method }) { if (method === "search_count") { await def; } }, }); assert.containsOnce(target, ".o_field_domain_panel .fa-circle-o-notch.fa-spin"); assert.containsNone(target, ".o_field_domain_panel .o_domain_show_selection_button"); def.resolve(); await nextTick(); assert.containsNone(target, ".o_field_domain_panel .fa-circle-o-notch .fa-spin"); assert.containsOnce(target, ".o_field_domain_panel .o_domain_show_selection_button"); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "2 record(s)" ); }); QUnit.test("domain field: edit domain with dynamic content", async function (assert) { assert.expect(3); patchWithCleanup(odoo, { debug: true }); let rawDomain = ` [ ["date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -365), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S")] ] `; serverData.models.partner.records[0].foo = rawDomain; serverData.models.partner.fields.bar.type = "char"; serverData.models.partner.records[0].bar = "partner"; serverData.views = { "partner,false,form": `
`, "partner,false,search": ``, }; serverData.actions = { 1: { id: 1, name: "test", res_id: 1, res_model: "partner", type: "ir.actions.act_window", views: [[false, "form"]], }, }; const webClient = await createWebClient({ serverData, mockRPC(route, { method, args }) { if (method === "write") { assert.strictEqual(args[1].foo, rawDomain); } if (route === "/web/domain/validate") { return true; } }, }); await doAction(webClient, 1); assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain); rawDomain = ` [ ["date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -1), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S")] ] `; await editInput(target, ".o_domain_debug_input", rawDomain); assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain); await clickSave(target); }); QUnit.test("domain field: edit through selector (dynamic content)", async function (assert) { patchWithCleanup(odoo, { debug: true }); let rawDomain = `[("date", ">=", context_today())]`; serverData.models.partner.records[0].foo = rawDomain; serverData.models.partner.fields.bar.type = "char"; serverData.models.partner.records[0].bar = "partner"; serverData.views = { "partner,false,form": `
`, "partner,false,search": ``, }; serverData.actions = { 1: { id: 1, name: "test", res_id: 1, res_model: "partner", type: "ir.actions.act_window", views: [[false, "form"]], }, }; const webClient = await createWebClient({ serverData, mockRPC(route, { method }) { assert.step(method || route); }, }); assert.verifySteps(["/web/webclient/load_menus"]); await doAction(webClient, 1); assert.verifySteps(["/web/action/load", "get_views", "read", "search_count", "fields_get"]); assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain); assert.containsOnce(target, ".o_datepicker", "there should be a datepicker"); // Open and close the datepicker await click(target, ".o_datepicker_input"); assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget"); await triggerEvent(window, null, "scroll"); assert.containsNone(document.body, ".bootstrap-datetimepicker-widget"); assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain); assert.verifySteps([]); // Manually input a date rawDomain = `[("date", ">=", "2020-09-09")]`; await editInput(target, ".o_datepicker_input", "09/09/2020"); assert.verifySteps(["search_count"]); assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain); // Save await clickSave(target); assert.verifySteps(["write", "read", "search_count"]); assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain); }); QUnit.test("domain field without model", async function (assert) { serverData.models.partner.fields.model_name = { string: "Model name", type: "char" }; await makeView({ type: "form", resModel: "partner", serverData, arch: `
`, mockRPC(route, args) { if (args.method === "search_count") { assert.step(args.model); } }, }); assert.strictEqual( target.querySelector('.o_field_widget[name="display_name"]').innerText, "Select a model to add a filter.", "should contain an error message saying the model is missing" ); assert.verifySteps([]); await editInput(target, ".o_field_widget[name=model_name] input", "test"); assert.notStrictEqual( target.querySelector('.o_field_widget[name="display_name"]').innerText, "Select a model to add a filter.", "should not contain an error message anymore" ); assert.verifySteps(["test"]); }); QUnit.test("domain field in kanban view", async function (assert) { serverData.models.partner.records[0].foo = "[]"; serverData.views = { "partner_type,false,list": ``, "partner_type,false,search": ``, }; await makeView({ type: "kanban", resModel: "partner", resId: 1, serverData, arch: `
`, selectRecord: (resId) => { assert.step(`open record ${resId}`); }, }); assert.strictEqual(target.querySelector(".o_read_mode").textContent, "Match all records"); await click(target.querySelector(".o_domain_show_selection_button")); assert.containsOnce( target, ".o_dialog .o_list_view", "selected records are listed in a dialog" ); await click(target.querySelector(".o_domain_selector")); assert.verifySteps( ["open record 1"], "record should not open when clicked on the 'N record(s)' button" ); }); QUnit.test("domain field with 'inDialog' options", async function (assert) { await makeView({ type: "form", resModel: "partner", serverData, arch: `
`, }); assert.containsNone(target, ".o_domain_leaf"); assert.containsNone(target, ".modal"); await click(target, ".o_field_domain_dialog_button"); assert.containsOnce(target, ".modal"); await click(target, ".modal .o_domain_add_first_node_button"); await click(target, ".modal-footer .btn-primary"); assert.containsOnce(target, ".o_domain_leaf"); assert.strictEqual(target.querySelector(".o_domain_leaf").textContent, "ID = 1"); }); QUnit.test("invalid value in domain field with 'inDialog' options", async function (assert) { serverData.models.partner.fields.display_name.default = "[]"; await makeView({ type: "form", resModel: "partner", serverData, arch: `
`, mockRPC: (route, args) => { if (args.method === "search_count") { const domain = args.args[0]; if (domain.length && domain[0][0] === "id" && domain[0][2] === "01/01/2002") { throw new Error("Invalid Domain"); } } }, }); assert.containsNone(target, ".o_domain_leaf"); assert.containsNone(target, ".modal"); assert.containsNone(target, ".o_field_domain .text-warning"); await click(target, ".o_field_domain_dialog_button"); assert.containsOnce(target, ".modal"); await click(target, ".modal .o_domain_add_first_node_button"); await editInput(target, ".o_domain_leaf_value_input", "01/01/2002"); await click(target, ".modal-footer .btn-primary"); assert.containsOnce(target, ".o_domain_leaf"); assert.strictEqual(target.querySelector(".o_domain_leaf").textContent, 'ID = "01/01/2002"'); assert.strictEqual( target.querySelector(".o_field_domain .text-warning").textContent.trim(), "Invalid domain" ); }); QUnit.test( "quick check on save if domain has been edited via the debug input", async function (assert) { patchWithCleanup(odoo, { debug: true }); serverData.models.partner.fields.display_name.default = "[['id', '=', False]]"; await makeView({ type: "form", resModel: "partner", serverData, arch: `
`, mockRPC: (route, args) => { if (route === "/web/domain/validate") { assert.step(route); assert.deepEqual(args, { domain: [["id", "!=", false]], model: "partner", }); return true; } }, }); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "0 record(s)" ); await editInput(target, ".o_domain_debug_input", "[['id', '!=', False]]"); await click(target, "button.o_form_button_save"); assert.verifySteps(["/web/domain/validate"]); assert.strictEqual( target.querySelector(".o_domain_show_selection_button").textContent.trim(), "6 record(s)" ); } ); });