import { describe, expect, getFixture, test } from "@odoo/hoot"; import { click, press, queryAll, queryAllTexts, queryOne, scroll } from "@odoo/hoot-dom"; import { Deferred, animationFrame, runAllTimers } from "@odoo/hoot-mock"; import { Component, xml } from "@odoo/owl"; import { clickFieldDropdown, clickFieldDropdownItem, clickSave, contains, defineModels, editSearch, fields, getDropdownMenu, getFacetTexts, getService, makeServerError, mockService, models, mountView, mountViewInDialog, mountWithCleanup, onRpc, selectFieldDropdownItem, serverState, toggleMenuItem, toggleSearchBarMenu, validateSearch, } from "@web/../tests/web_test_helpers"; import { user } from "@web/core/user"; import { Record } from "@web/model/record"; import { Field } from "@web/views/fields/field"; import { WebClient } from "@web/webclient/webclient"; describe.current.tags("desktop"); class ResPartner extends models.Model { name = fields.Char({ string: "Res Partner Name" }); _records = [ { id: 1, name: "res partner", }, ]; } class Partner extends models.Model { name = fields.Char({ string: "Displayed name" }); foo = fields.Char({ default: "My little Foo Value" }); bar = fields.Boolean({ default: true }); int_field = fields.Integer({ sortable: true }); p = fields.One2many({ string: "one2many field", relation: "partner", relation_field: "trululu", }); turtles = fields.One2many({ string: "one2many turtle field", relation: "turtle", relation_field: "turtle_trululu", }); trululu = fields.Many2one({ string: "Trululu", relation: "partner" }); res_trululu = fields.Many2one({ string: "Res Trululu", relation: "res.partner" }); timmy = fields.Many2many({ string: "pokemon", relation: "partner.type" }); product_id = fields.Many2one({ string: "Product", relation: "product" }); date = fields.Date({ string: "Some Date" }); datetime = fields.Datetime({ string: "Datetime Field" }); user_id = fields.Many2one({ string: "Users", relation: "res.users" }); _records = [ { id: 1, name: "first record", bar: true, foo: "yop", int_field: 10, p: [], turtles: [2], timmy: [], trululu: 4, res_trululu: 1, user_id: 1, }, { id: 2, name: "second record", bar: true, foo: "blip", int_field: 9, p: [], timmy: [], trululu: 1, product_id: 37, date: "2017-01-25", datetime: "2016-12-12 10:55:05", user_id: 1, }, { id: 4, name: "aaa", bar: false, }, ]; } class Product extends models.Model { name = fields.Char(); _records = [ { id: 37, name: "xphone", }, { id: 41, name: "xpad", }, ]; } class PartnerType extends models.Model { name = fields.Char(); _records = [ { id: 12, name: "gold" }, { id: 14, name: "silver" }, ]; } class Turtle extends models.Model { name = fields.Char({ string: "Displayed name" }); turtle_foo = fields.Char({ string: "Foo" }); turtle_bar = fields.Boolean({ string: "Bar", default: true }); turtle_trululu = fields.Many2one({ string: "Trululu", relation: "partner", }); product_id = fields.Many2one({ string: "Product", relation: "product", required: true, }); partner_ids = fields.Many2many({ string: "Partner", relation: "partner" }); _records = [ { id: 1, name: "leonardo", turtle_bar: true, turtle_foo: "yop", partner_ids: [], }, { id: 2, name: "donatello", turtle_bar: true, turtle_foo: "blip", partner_ids: [2, 4], }, { id: 3, name: "raphael", product_id: 37, turtle_bar: false, turtle_foo: "kawa", partner_ids: [], }, ]; } class Users extends models.Model { _name = "res.users"; name = fields.Char(); partner_ids = fields.One2many({ relation: "partner", relation_field: "user_id" }); has_group() { return true; } _records = [ { id: 1, name: "Aline", partner_ids: [1, 2], }, { id: 2, name: "Christine", }, ]; } defineModels([ResPartner, Partner, Product, PartnerType, Turtle, Users]); test("many2ones in form views", async () => { expect.assertions(2); mockService("action", { doAction(params) { expect(params.res_id).toBe(17); }, loadState() {}, }); Partner._views = { form: `
`, }; onRpc("get_formview_action", function ({ args }) { expect(args[0]).toEqual([4]); return { res_id: 17, type: "ir.actions.act_window", target: "current", res_model: "res.partner", }; }); onRpc("get_formview_id", function ({ args }) { expect(args[0]).toEqual([4]); return false; }); await mountViewInDialog({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await contains(".o_external_button:enabled", { visible: false }).click(); expect(".o_dialog:not(.o_inactive_modal) .modal-title").toHaveText("Open: custom label"); // TODO: test that we can edit the record in the dialog, and that // the value is correctly updated on close }); test("editing a many2one, but not changing anything", async () => { expect.assertions(1); Partner._views = { form: `
`, }; onRpc("get_formview_id", function ({ args }) { expect(args[0]).toEqual([4]); return false; }); onRpc("web_save", () => { throw new Error("should not call write"); }); await mountViewInDialog({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); // click on the external button (should do an RPC) await contains(".o_external_button", { visible: false }).click(); // save and close modal await contains(".modal:eq(1) .o_form_button_save").click(); // save form await clickSave(); }); test("context in many2one and default get", async () => { expect.assertions(2); Partner._fields.int_field.default = 14; Partner._fields.trululu.default = 2; onRpc("onchange", ({ args }) => { const context = args[3].trululu.context; expect(context.blip).toBe(undefined); expect(context.blop).toBe(3); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); }); test("do not send context in unity spec if field is invisible", async () => { expect.assertions(1); onRpc("web_read", ({ kwargs }) => { expect(kwargs.specification).toEqual({ display_name: {}, int_field: {}, trululu: { fields: {}, }, }); }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); }); test("editing a many2one (with form view opened with external button)", async () => { expect.assertions(4); Partner._views = { form: `
`, }; onRpc("get_formview_id", () => { return false; }); onRpc("web_save", () => { expect.step("web_save"); }); onRpc("read", ({ args, model, kwargs }) => { if (model === "partner" && args[0][0] === 4) { expect.step(`read partner: ${args[1]}`); expect(kwargs.context.blip).toBe(10); expect(kwargs.context.blop).toBe(3); } }); await mountViewInDialog({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); // click on the external button (should do an RPC) await contains(".o_external_button", { visible: false }).click(); await contains(".o_dialog:not(.o_inactive_modal) .o_field_widget[name='foo'] input").edit( "brandon" ); // save and close modal await contains(".modal:eq(1) .o_form_button_save").click(); expect.verifySteps(["web_save", "read partner: display_name"]); // save form await clickSave(); expect.verifySteps([]); }); test("many2ones in form views with show_address", async () => { onRpc("web_read", ({ kwargs }) => { if (kwargs.specification.trululu.context.show_address) { return [ { name: "", trululu: { display_name: "aaa\nStreet\nCity ZIP" }, }, ]; } }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect("input.o_input").toHaveValue("aaa"); expect(".o_field_many2one_extra").toHaveInnerHTML( "Street
City ZIP", { type: "html" } ); expect("button.o_external_button").toHaveCount(1); }); test("many2one show_address in edit", async () => { const namegets = { 1: "first record\nFirst\nRecord", 2: "second record\nSecond\nRecord", 4: "aaa\nAAA\nRecord", }; onRpc("web_read", async ({ kwargs, parent }) => { if (kwargs.specification.trululu.context.show_address) { const result = await parent(); result[0].trululu = { id: result[0].trululu.id, display_name: namegets[result[0].trululu.id], }; return result; } }); onRpc("name_search", async ({ parent }) => { const result = await parent(); return result.map(([id]) => [id, namegets[id]]); }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(".o_field_widget input").toHaveValue("aaa"); expect(".o_field_many2one_extra").toHaveInnerHTML("AAA
Record", { type: "html", }); await contains(".o_field_widget input").edit("first record", { confirm: false }); await runAllTimers(); await contains(".dropdown-menu li").click(); expect(".o_field_widget input").toHaveValue("first record"); expect(".o_field_many2one_extra").toHaveInnerHTML("First
Record", { type: "html", }); await contains(".o_field_widget input").edit("second record", { confirm: false }); await runAllTimers(); await contains(".dropdown-menu li").click(); expect(".o_field_widget input").toHaveValue("second record"); expect(".o_field_many2one_extra").toHaveInnerHTML( "Second
Record", { type: "html" } ); }); test("show_address works in a view embedded in a view of another type", async () => { expect.assertions(2); Turtle._records[1].turtle_trululu = 2; Turtle._views = { form: `
`, list: ` `, }; onRpc("turtle", "web_read", ({ kwargs }) => { expect(kwargs.specification.turtle_trululu.context).toEqual({ show_address: 1, }); return [ { id: 1, name: "donatello", turtle_trululu: { id: 2, display_name: "second record\nrue morgue\nparis 75013", }, }, ]; }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); // click the turtle field, opens a modal with the turtle form view await contains(".o_data_row td.o_data_cell").click(); expect('[name="turtle_trululu"]').toHaveText("second record\nrue morgue\nparis 75013"); }); test("many2ones in form views with search more", async () => { for (let i = 5; i < 11; i++) { Partner._records.push({ id: i, name: `Partner ${i}` }); } Partner._fields.datetime.searchable = true; Partner._views = { search: ` `, list: ` `, }; await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await selectFieldDropdownItem("trululu", "Search More..."); expect("tr.o_data_row").toHaveCount(9); expect(".o_field_widget[name=trululu] input").toHaveValue("aaa"); await toggleSearchBarMenu(".modal"); await toggleMenuItem("Filter"); expect("tr.o_data_row").toHaveCount(0); }); test("many2ones: Open the selection dialog several times using the 'Search More...' button with a context containing 'search_default_...'", async () => { for (let i = 5; i < 11; i++) { Partner._records.push({ id: i, name: `Partner ${i}` }); } Partner._fields.name.searchable = true; Partner._views = { search: ` `, list: ` `, }; await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await selectFieldDropdownItem("trululu", "Search More..."); expect(".modal .o_data_row").toHaveCount(1); expect(getFacetTexts(".modal")).toEqual(["Displayed name\nPartner 10"]); await contains(".modal .btn-close").click(); expect(".modal").toHaveCount(0); await selectFieldDropdownItem("trululu", "Search More..."); expect(".modal .o_data_row").toHaveCount(1); expect(getFacetTexts(".modal")).toEqual(["Displayed name\nPartner 10"]); }); test("many2ones in list views: create in dialog keeps the input", async () => { Partner._views = { form: `
`, }; onRpc("web_save", ({ args }) => { expect.step(`web_save: ${JSON.stringify(args)}`); }); await mountView({ type: "list", resModel: "partner", arch: ` `, }); await contains(".o_data_cell:eq(0)").click(); await contains(".o_field_widget[name=trululu] input").edit("yy", { confirm: false }); await runAllTimers(); await clickFieldDropdownItem("trululu", "Create and edit..."); await clickSave(); expect.verifySteps([`web_save: [[],{"name":"yy"}]`]); expect(".o_field_widget[name=trululu] input").toHaveValue("yy"); await contains(getFixture()).click(); expect.verifySteps([`web_save: [[1],{"trululu":5}]`]); expect(".o_data_cell[name=trululu]:eq(0)").toHaveText("yy"); }); test("many2ones in list views: create a new record with a context", async () => { expect.assertions(6); onRpc("res.users", "name_create", ({ kwargs }) => { const { context } = kwargs; expect.step("name_create"); expect(context.default_test).toBe(1); expect(context.test).toBe(2); expect(context).not.toInclude("default_yop"); expect(context.yop).toBe(4); }); await mountView({ type: "list", resModel: "partner", arch: ` `, context: { default_yop: 3, yop: 4, }, }); await contains(".o_data_cell:eq(0)").click(); await contains(".o_field_widget[name=user_id] input").edit("yy", { confirm: false }); await runAllTimers(); await clickFieldDropdownItem("user_id", 'Create "yy"'); expect(".o_external_button").toHaveCount(1); expect.verifySteps(["name_create"]); }); test("using a many2one widget must take into account the decorations", async () => { await mountView({ type: "list", resModel: "partner", arch: ` `, }); expect(".o_list_many2one a.text-danger").toHaveCount(1); expect(".o_data_row").toHaveCount(3); }); test("onchanges on many2ones trigger when editing record in form view", async () => { expect.assertions(2); Partner._onChanges = { user_id: () => {}, }; Users._fields.other_field = fields.Char({ string: "Other Field" }); Users._views = { form: `
`, }; onRpc("get_formview_id", () => false); onRpc("onchange", ({ args }) => { expect(args[1].user_id).toBe(1); }); onRpc(({ method }) => { expect.step(method); }); await mountViewInDialog({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); // open the many2one in form view and change something await contains(".o_external_button", { visible: false }).click(); await contains( ".o_dialog:not(.o_inactive_modal) .o_field_widget[name='other_field'] input" ).edit("wood"); // TODISCUSS ? Same record, don't change the display name (opti ?) // save the modal and make sure an onchange is triggered await contains(".modal:eq(1) .o_form_button_save").click(); expect.verifySteps([ "get_views", "web_read", "get_formview_id", "get_views", "web_read", "web_save", "read", "onchange", ]); }); test("edit many2one before onchange is finished should not reset the value", async () => { Partner._onChanges = { name: function (obj) { obj.user_id = 19; }, }; onRpc("onchange", () => { expect.step("onchange"); return def; }); const def = new Deferred(); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await contains("[name='name'] input").edit("new name"); await contains("[name='user_id'] input").edit("Plop"); expect("[name='user_id'] input").toHaveValue("Plop"); def.resolve(); await animationFrame(); expect("[name='user_id'] input").toHaveValue("Plop"); expect.verifySteps(["onchange"]); }); test("many2one doesn't trigger field_change when being emptied", async () => { await mountView({ type: "list", resModel: "partner", arch: ` `, }); // Select two records await contains(".o_data_row:eq(0) .o_list_record_selector input").click(); await contains(".o_data_row:eq(1) .o_list_record_selector input").click(); await contains(".o_data_row .o_data_cell").click(); await contains(".o_field_widget[name=trululu] input").clear({ confirm: false }); await runAllTimers(); expect(".modal").toHaveCount(0); await contains(".o_field_widget[name=trululu] .ui-menu-item").click(); expect(".modal").toHaveCount(1); }); test("..._view_ref keys are removed from many2one context on create and edit", async () => { onRpc("get_views", ({ kwargs, method }) => { expect.step( JSON.stringify([method, kwargs.context.default_name, kwargs.context.form_view_ref]) ); }); await mountView({ type: "form", resModel: "partner", arch: `
`, context: { form_view_ref: "test_form_view", }, }); expect.verifySteps(['["get_views",null,"test_form_view"]']); await contains(".o_field_widget[name=trululu] input").edit("ABC", { confirm: false }); await runAllTimers(); await contains(".o_field_widget[name=trululu] .o_m2o_dropdown_option_create_edit").click(); expect.verifySteps(['["get_views",null,null]']); }); test("empty a many2one field in list view", async () => { onRpc("web_save", ({ args }) => { expect.step("web_save"); expect(args[1]).toEqual({ trululu: false }); }); await mountView({ type: "list", resModel: "partner", arch: ` `, }); await contains(".o_data_row .o_data_cell").click(); await contains(".o_field_widget[name=trululu] input").edit(""); expect(".o_data_row .o_field_widget[name=trululu] input").toHaveText(""); await contains(".o_list_view").click(); expect(".o_data_row:eq(0)").toHaveText(""); expect.verifySteps(["web_save"]); }); test("focus tracking on a many2one in a list", async () => { Partner._views = { form: `
`, }; await mountView({ type: "list", resModel: "partner", arch: ` `, }); // Select two records await contains(".o_data_row:eq(0) .o_list_record_selector input").click(); await contains(".o_data_row:eq(1) .o_list_record_selector input").click(); await contains(".o_data_row .o_data_cell").click(); expect(".o_data_row .o_data_cell input").toBeFocused(); await contains(".o_field_widget[name=trululu] input").edit("ABC", { confirm: false }); await runAllTimers(); await contains(".o_field_widget[name=trululu] .o_m2o_dropdown_option_create_edit").click(); // At this point, if the focus is correctly registered by the m2o, there // should be only one modal (the "Create" one) and none for saving changes. expect(".modal").toHaveCount(1); await contains(".o_form_button_cancel").click(); expect(".o_data_row .o_data_cell input").toBeFocused(); expect(".o_data_row .o_data_cell input").toHaveValue(""); }); test('many2one fields with option "no_open"', async () => { await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(".o_field_widget[name='trululu'] .o_external_button").toHaveCount(0); }); test("empty many2one field", async () => { await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one input").click(); expect(".dropdown-menu li.o_m2o_dropdown_option").toHaveCount(1); expect(".dropdown-menu li.o_m2o_start_typing").toHaveCount(1); await contains(".o_field_many2one[name='trululu'] input").edit("abc", { confirm: false }); await runAllTimers(); expect(".dropdown-menu li.o_m2o_dropdown_option").toHaveCount(2); expect(".dropdown-menu li.o_m2o_start_typing").toHaveCount(0); }); test("empty readonly many2one field", async () => { await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect("div.o_field_widget[name=trululu]").toHaveCount(1); expect(".o_field_widget[name=trululu]").toHaveInnerHTML(""); }); test("empty many2one field with node options", async () => { expect.assertions(2); await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one[name='trululu'] input").click(); expect(".o_field_many2one[name='trululu'] .dropdown-menu li.o_m2o_start_typing").toHaveCount(1); await contains(".o_field_many2one[name='product_id'] input").click(); expect(".o_field_many2one[name='product_id'] .dropdown-menu li.o_m2o_start_typing").toHaveCount( 0 ); }); test("many2one with no_create_edit and no_quick_create options should show no records when no result match", async () => { expect.assertions(2); await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one[name='product_id'] input").click(); expect(".o_field_many2one[name='product_id'] .dropdown-menu li.o_m2o_no_result").toHaveCount(0); await contains(".o_field_many2one[name='product_id'] input").edit("aze", { confirm: false }); await runAllTimers(); expect(".o_field_many2one[name='product_id'] .dropdown-menu li.o_m2o_no_result").toHaveCount(1); }); test("many2one in edit mode", async () => { expect.assertions(17); // create 10 partners to have the 'Search More' option in the autocomplete dropdown for (let i = 0; i < 10; i++) { const id = 20 + i; Partner._records.push({ id, name: `Partner ${id}` }); } Partner._views = { list: ` `, search: ` `, }; onRpc("partner", "web_save", ({ args }) => { expect(args[1].trululu).toBe(20); }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await clickFieldDropdown("trululu"); expect(".o_field_many2one[name='trululu'] .dropdown-menu").toBeVisible(); expect( ".o_field_many2one[name='trululu'] .dropdown-menu li:not(.o_m2o_dropdown_option)" ).toHaveCount(8); expect(".o_field_many2one[name='trululu'] .dropdown-menu li.o_m2o_dropdown_option").toHaveCount( 1 ); expect(".o_field_many2one[name='trululu'] .dropdown-menu li.o_m2o_start_typing").toHaveCount(0); await contains(".o_field_many2one[name='trululu'] input").click(); expect(".o_field_many2one[name='trululu'] .dropdown-menu").toHaveCount(0); // change the value of the m2o with a suggestion of the dropdown await selectFieldDropdownItem("trululu", "first record"); expect(".o_field_many2one[name='trululu'] .dropdown-menu").not.toBeVisible(); expect(".o_field_many2one input").toHaveValue("first record"); // change the value of the m2o with a record in the 'Search More' modal await clickFieldDropdown("trululu"); // click on 'Search More' (mouseenter required by ui-autocomplete) await contains( ".o_field_many2one[name='trululu'] .dropdown-menu .o_m2o_dropdown_option_search_more" ).click(); expect(".modal .o_list_view").toHaveCount(1); expect(".modal .o_list_view .o_list_record_selector").toHaveCount(0); expect(".modal .modal-footer .o_select_button").toHaveCount(0); expect(queryAll(".modal tbody tr").length).toBeGreaterThan(10, { message: "list should contain more than 10 records", }); await contains(".modal .o_searchview_input").edit("P", { confirm: false }); await runAllTimers(); await press("Enter"); await animationFrame(); expect(".modal tbody tr").toHaveCount(10); // choose a record await contains(".modal .o_data_cell[data-tooltip='Partner 20']").click(); expect(".modal").toHaveCount(0); expect(".o_field_many2one[name='trululu'] .dropdown-menu").not.toBeVisible(); expect(".o_field_many2one input").toHaveValue("Partner 20"); // save await clickSave(); expect(".o_field_many2one input").toHaveValue("Partner 20"); }); test("many2one in non edit mode (with value)", async () => { await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect("a.o_form_uri").toHaveCount(2); expect("div[name=res_trululu] a.o_form_uri").toHaveAttribute("href", "/odoo/res.partner/1"); expect("div[name=trululu] a.o_form_uri").toHaveAttribute("href", "/odoo/m-partner/4"); }); test("many2one in non edit mode (without value)", async () => { Partner._records[0].trululu = false; await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); // Remove value from many2one and then save, there should be no link anymore expect("a.o_form_uri").toHaveCount(0); }); test("many2one with co-model whose name field is a many2one", async () => { Product._fields.name = fields.Many2one({ string: "User Name", relation: "res.users", }); Product._records = [ { id: 37, name: 1, }, { id: 41, name: 2, }, ]; Product._views = { form: `
`, }; await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains("div[name=product_id] input").edit("ABC", { confirm: false }); await runAllTimers(); await contains("div[name=product_id] .o_m2o_dropdown_option_create_edit").click(); expect(".modal .o_form_view").toHaveCount(1); // quick create 'new value' await contains(".modal div[name=name] input").edit("new value", { confirm: false }); await runAllTimers(); await contains(".modal div[name=name] .o_m2o_dropdown_option").click(); expect(".modal div[name=name] input").toHaveValue("new value"); await contains(".modal .o_form_button_save").click(); expect(".modal .o_form_view").toHaveCount(0); expect("div[name=product_id] input").toHaveValue("new value"); }); test("many2one searches with correct value", async () => { onRpc("name_search", ({ kwargs }) => { expect.step(`search: ${kwargs.name}`); }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(".o_field_many2one input").toHaveValue("aaa"); await contains(".o_field_many2one input").click(); // unset the many2one -> should search again with '' await contains(".o_field_many2one input").clear({ confirm: false }); await runAllTimers(); await contains(".o_field_many2one input").edit("p", { confirm: false }); await runAllTimers(); // close and re-open the dropdown -> should search with 'p' again await contains(".o_field_many2one input").click(); await runAllTimers(); await contains(".o_field_many2one input").click(); await runAllTimers(); expect.verifySteps(["search: ", "search: ", "search: p", "search: p"]); }); test("many2one search with server returning multiple lines", async () => { const namegets = { 2: "fizz\nbuzz\nfizzbuzz", 4: "aaa\nAAA\nRecord", }; onRpc("web_read", async ({ parent }) => { expect.step("web_read"); const result = await parent(); result[0].trululu = { id: result[0].trululu.id, display_name: namegets[result[0].trululu.id], }; return result; }); onRpc("name_search", () => { expect.step("name_search"); return Object.keys(namegets).map((id) => [parseInt(id), namegets[id]]); }); onRpc("web_save", ({ args }) => { expect.step("web_save"); expect(args[1]).toEqual({ trululu: 2, }); }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect.verifySteps(["web_read"]); // Initial value expect(".o_field_widget input").toHaveValue("aaa"); expect(".o_field_many2one_extra").toHaveInnerHTML("AAA
Record", { type: "html", }); // Change the value await contains(".o_field_widget input").edit("fizz", { confirm: false }); await runAllTimers(); // should display only the first line of the returned value from the server expect.verifySteps(["name_search"]); expect(queryAllTexts(".dropdown-menu li:not(.o_m2o_dropdown_option")).toEqual(["fizz", "aaa"]); await contains(".dropdown-menu li").click(); expect.verifySteps([]); expect(".o_field_widget input").toHaveValue("fizz"); expect(".o_field_many2one_extra").toHaveInnerHTML( "buzz
fizzbuzz", { type: "html" } ); await clickSave(); expect.verifySteps(["web_save"]); }); test("many2one search with trailing and leading spaces", async () => { onRpc("name_search", ({ kwargs }) => { expect.step("search: " + kwargs.name); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); const input = ".o_field_many2one[name='trululu'] input"; await contains(input).click(); expect(".o_field_many2one[name='trululu'] .dropdown-menu").toBeVisible(); expect( ".o_field_many2one[name='trululu'] .dropdown-menu li:not(.o_m2o_dropdown_option)" ).toHaveCount(4); // search with leading spaces await contains(input).edit(" first", { confirm: false }); await runAllTimers(); expect( ".o_field_many2one[name='trululu'] .dropdown-menu li:not(.o_m2o_dropdown_option)" ).toHaveCount(1); // search with trailing spaces await contains(input).edit("first ", { confirm: false }); await runAllTimers(); expect( ".o_field_many2one[name='trululu'] .dropdown-menu li:not(.o_m2o_dropdown_option)" ).toHaveCount(1); // search with leading and trailing spaces await contains(input).edit(" first ", { confirm: false }); await runAllTimers(); expect( ".o_field_many2one[name='trululu'] .dropdown-menu li:not(.o_m2o_dropdown_option)" ).toHaveCount(1); expect.verifySteps(["search: ", "search: first", "search: first", "search: first"]); }); // Should be removed ? test("many2one field with option always_reload (edit)", async () => { onRpc("web_read", async ({ parent }) => { const result = await parent(); result[0].trululu = { ...result[0].trululu, display_name: "first record\nand some address", }; return result; }); await mountView({ type: "form", resModel: "partner", resId: 2, arch: `
`, }); expect(".o_field_widget[name='trululu'] input").toHaveValue("first record"); expect(".o_field_many2one_extra").toHaveCount(1); expect(".o_field_many2one_extra").toHaveText("and some address"); }); test("many2one field and list navigation", async () => { await mountView({ type: "list", resModel: "partner", arch: ` `, }); // edit first input, to trigger autocomplete await contains(".o_data_row .o_data_cell").click(); await contains(".o_data_cell input").clear(); // press keydown, to select first choice await press("arrowdown"); // we now check that the dropdown is open (and that the focus did not go // to the next line) expect(".o_field_many2one").toHaveCount(1); expect(".o_data_row:eq(0)").toHaveClass("o_selected_row"); expect(".o_data_row:eq(1)").not.toHaveClass("o_selected_row"); }); test("standalone many2one field", async () => { class Comp extends Component { static components = { Record, Field }; static template = xml` `; static props = ["*"]; setup() { this.fields = { partner_id: { display_name: "partner_id", type: "many2one", relation: "partner", }, }; this.values = { partner_id: [1, "first partner"], }; } } onRpc(({ method }) => { expect.step(method); }); await mountWithCleanup(Comp); await contains(".o_field_widget input").edit("xyzzrot", { confirm: false }); await runAllTimers(); await contains(".o_field_widget .ui-menu-item").click(); expect(".o_field_widget .o_external_button").toHaveCount(0); expect.verifySteps(["name_search", "name_create"]); }); test("form: quick create then save directly", async () => { expect.assertions(3); const def = new Deferred(); const newRecordId = 5; // with the current records, the created record will be assigned id 5 onRpc("name_create", async () => { expect.step("name_create"); await def; }); onRpc("web_save", ({ args }) => { expect.step("web_save"); expect(args[1].trululu).toBe(newRecordId); }); await mountView({ type: "form", resModel: "partner", arch: '
', }); await contains(".o_field_widget[name=trululu] input").edit("b", { confirm: false }); await runAllTimers(); await contains(".ui-menu-item").click(); await contains(".o_form_button_save").click(); // should wait for the name_create before creating the record expect.verifySteps(["name_create"]); def.resolve(); await animationFrame(); expect.verifySteps(["web_save"]); }); test("form: quick create for field that returns false after name_create call", async () => { onRpc("name_create", () => { expect.step("name_create"); // Resolve the name_create call to false. This is possible if // _rec_name for the model of the field is unassigned. return false; }); await mountView({ type: "form", resModel: "partner", arch: '
', }); await contains(".o_field_widget[name=trululu] input").edit("beam", { confirm: false }); await runAllTimers(); await contains(".ui-menu-item").click(); expect.verifySteps(["name_create"]); expect(".o_input_dropdown input").toHaveValue(""); }); test("list: quick create then save directly", async () => { expect.assertions(8); const def = new Deferred(); const newRecordId = 5; onRpc("name_create", async () => { expect.step("name_create"); await def; }); onRpc("web_save", ({ args }) => { expect.step("web_save"); const values = args[1]; expect(values.trululu).toBe(newRecordId); }); await mountView({ type: "list", resModel: "partner", arch: ` `, }); expect(".o_data_row").toHaveCount(3); await contains(".o_control_panel_main_buttons .o_list_button_add").click(); expect(".o_data_row").toHaveCount(4); await contains(".o_field_widget[name=trululu] input").edit("b", { confirm: false }); await runAllTimers(); await contains(".ui-menu-item").click(); await contains(".o_list_button_save").click(); // should wait for the name_create before creating the record expect.verifySteps(["name_create"]); expect(".o_data_row").toHaveCount(4); def.resolve(); await animationFrame(); expect.verifySteps(["web_save"]); expect(".o_data_row").toHaveCount(4); expect(".o_data_row .o_data_cell:eq(0)").toHaveText("b"); }); test("list in form: quick create then save directly", async () => { expect.assertions(4); const def = new Deferred(); const newRecordId = 5; // with the current records, the created record will be assigned id 5 onRpc("name_create", async () => { expect.step("name_create"); await def; }); onRpc("web_save", ({ args }) => { expect.step("web_save"); expect(args[1].p[0][2].trululu).toBe(newRecordId); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_x2many_list_row_add a").click(); await contains(".o_field_widget[name=trululu] input").edit("b", { confirm: false }); await runAllTimers(); await contains(".ui-menu-item").click(); await contains(".o_form_button_save").click(); // should wait for the name_create before creating the record expect.verifySteps(["name_create"]); await def.resolve(); await animationFrame(); expect.verifySteps(["web_save"]); expect(".o_data_row .o_data_cell").toHaveText("b"); }); test("name_create in form dialog", async () => { onRpc("name_create", () => { expect.step("name_create"); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_x2many_list_row_add a").click(); await contains(".modal .o_field_widget[name=product_id] input").edit("new record", { confirm: false, }); await runAllTimers(); await contains(".modal .o_field_widget[name=product_id] .ui-menu-item").click(); expect.verifySteps(["name_create"]); }); test("many2one inside one2many form view, with domain", async () => { expect.assertions(4); Partner._fields.trululu.domain = "[['id', '<', 1000]]"; Partner._records = [ { id: 1, name: "a1", p: [1] }, { id: 2, name: "a2" }, { id: 3, name: "a3" }, { id: 4, name: "a4" }, { id: 5, name: "a5" }, { id: 6, name: "a6" }, { id: 7, name: "a7" }, { id: 8, name: "a8" }, { id: 9, name: "a9" }, ]; Partner._views = { list: '', search: "", }; onRpc("name_search", ({ kwargs }) => { expect(kwargs.args).toEqual([["id", ">", 1]]); }); onRpc("web_search_read", ({ kwargs }) => { expect(kwargs.domain).toEqual([["id", ">", 1]]); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_data_row .o_data_cell").click(); await contains(".o_field_widget[name=trululu] input").click(); await runAllTimers(); await clickFieldDropdownItem("trululu", "Search More..."); expect(".modal .o_list_view").toHaveCount(1); expect(".modal .o_data_row").toHaveCount(8); }); test("list in form: quick create then add a new line directly", async () => { // required many2one inside a one2many list: directly after quick creating // a new many2one value (before the name_create returns), click on add an item: // at this moment, the many2one has still no value, and as it is required, // the row is discarded if a saveLine is requested. However, it should // wait for the name_create to return before trying to save the line. expect.assertions(8); Partner._onChanges = { trululu: () => {}, }; const def = new Deferred(); const newRecordId = 5; // with the current records, the created record will be assigned id 5 onRpc("name_create", async () => { await def; }); onRpc("web_save", ({ args }) => { expect(args[1].p[0][2].trululu).toBe(newRecordId); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_x2many_list_row_add a").click(); await contains(".o_field_widget[name=trululu] input").edit("b", { confirm: false }); await runAllTimers(); await contains(".ui-menu-item").click(); await contains(".o_field_x2many_list_row_add a").click(); expect(".o_data_row").toHaveCount(1); expect(".o_data_row").toHaveClass("o_selected_row"); def.resolve(); await animationFrame(); expect(".o_data_row .o_data_cell:eq(0)").toHaveText("b"); expect(".o_data_row").toHaveCount(2); expect(".o_data_row:eq(1)").toHaveClass("o_selected_row"); await clickSave(); expect(".o_data_row").toHaveCount(1); expect(".o_data_row .o_data_cell").toHaveText("b"); }); test("list in form: create with one2many with many2one", async () => { Partner._fields.p = fields.One2many({ string: "one2many field", relation: "partner", relation_field: "trululu", default: [[0, 0, { name: "new record", p: [] }]], }); onRpc("read", ({ args }) => { if (args[1].length === 1 && args[1][0] === "name") { throw new Error("read(['name']) should not be called"); } }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect("td.o_data_cell:eq(0)").toHaveText("new record"); }); test("list in form: create with one2many with many2one (version 2)", async () => { // This test simulates the exact same scenario as the previous one, // except that the value for the many2one is explicitely set to false, // which is stupid, but this happens, so we have to handle it Partner._fields.p = fields.One2many({ string: "one2many field", relation: "partner", relation_field: "trululu", default: [[0, 0, { name: "new record", trululu: false, p: [] }]], }); onRpc("read", ({ args }) => { if (args[1].length === 1 && args[1][0] === "name") { throw new Error("read(['name']) should not be called"); } }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect("td.o_data_cell:eq(0)").toHaveText("new record"); }); test("item not dropped on discard with empty required field (default_get)", async () => { // This test simulates discarding a record that has been created with // one of its required field that is empty. When we discard the changes // on this empty field, it should not assume that this record should be // abandonned, since it has been added (even though it is a new record). Partner._fields.p = fields.One2many({ string: "one2many field", relation: "partner", relation_field: "trululu", default: [[0, 0, { name: "new record", trululu: false, p: [] }]], }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect("tr.o_data_row").toHaveCount(1); expect("td.o_data_cell:eq(0)").toHaveText("new record"); expect("td.o_data_cell:eq(1)").toHaveText(""); await contains(".o_data_row .o_data_cell").click(); expect(".o_selected_row .o_data_cell:eq(1)").toHaveClass("o_required_modifier"); // discard by clicking on body await contains(getFixture()).click(); expect("tr.o_data_row").toHaveCount(1); expect("td.o_data_cell:eq(0)").toHaveText("new record"); expect("td.o_data_cell:eq(1)").toHaveText(""); await contains(".o_data_row .o_data_cell").click(); expect(".o_selected_row .o_data_cell:eq(1)").toHaveClass("o_required_modifier"); }); test("list in form: read with unique ids (default_get)", async () => { Partner._records[0].name = "MyTrululu"; Partner._fields.p = fields.One2many({ string: "one2many field", relation: "partner", relation_field: "trululu", default: [ [0, 0, { trululu: 1, p: [] }], [0, 0, { trululu: 1, p: [] }], ], }); onRpc("read", ({ args }) => { if (args[1].length === 1 && args[1][0] === "name") { throw new Error("read(['name']) should not called"); } }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect(queryAllTexts("td.o_data_cell")).toEqual(["MyTrululu", "MyTrululu"]); }); test("list in form: show name of many2one fields in multi-page (default_get)", async () => { Partner._fields.p = fields.One2many({ string: "one2many field", relation: "partner", relation_field: "trululu", default: [ [0, 0, { name: "record1", trululu: 1, p: [] }], [0, 0, { name: "record2", trululu: 2, p: [] }], ], }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect(queryAllTexts("td.o_data_cell")).toEqual([ "record1", "first record", "record2", "second record", ]); }); test("list in form: item not dropped on discard with empty required field (onchange in default_get)", async () => { // variant of the test "list in form: discard newly added element with // empty required field (default_get)", in which the `default_get` // performs an `onchange` at the same time. This `onchange` may create // some records, which should not be abandoned on discard, similarly // to records created directly by `default_get` Partner._fields.product_id = fields.Many2one({ string: "Product", relation: "product", default: 37, onChange: (obj) => { if (obj.product_id === 37) { obj.p = [[0, 0, { name: "entry", trululu: false }]]; } }, }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); // check that there is a record in the editable list with empty string as required field expect(".o_data_row").toHaveCount(1); expect("td.o_data_cell:eq(0)").toHaveText("entry"); expect("td.o_data_cell.o_required_modifier").toHaveCount(1); expect("td.o_data_cell.o_required_modifier").toHaveText(""); // click on empty required field in editable list record await contains("td.o_data_cell.o_required_modifier").click(); // click off so that the required field still stay empty await contains(getFixture()).click(); // record should not be dropped expect(".o_data_row").toHaveCount(1); expect("td.o_data_cell:eq(0)").toHaveText("entry"); expect("td.o_data_cell.o_required_modifier").toHaveText(""); }); test("list in form: item not dropped on discard with empty required field (onchange on list after default_get)", async () => { // discarding a record from an `onchange` in a `default_get` should not // abandon the record. This should not be the case for following // `onchange`, except if an onchange make some changes on the list: // in particular, if an onchange make changes on the list such that // a record is added, this record should not be dropped on discard Partner._onChanges = { product_id: (obj) => { if (obj.product_id === 37) { obj.p = [[0, 0, { name: "entry", trululu: false }]]; } }, }; await mountView({ type: "form", resModel: "partner", arch: `
`, }); // check no record in list expect(".o_data_row").toHaveCount(0); // select product_id to force on_change in editable list await contains("div[name=product_id] input").click(); await contains(".ui-menu-item").click(); // check that there is a record in the editable list with empty string as required field expect(".o_data_row").toHaveCount(1); expect("td.o_data_cell:eq(0)").toHaveText("entry"); expect("td.o_required_modifier").toHaveCount(1); expect("td.o_required_modifier").toHaveText(""); // click on empty required field in editable list record await contains("td.o_required_modifier").click(); // click off so that the required field still stay empty await contains(getFixture()).click(); // record should not be dropped expect(".o_data_row").toHaveCount(1); expect(queryAllTexts("td.o_data_cell")).toEqual(["entry", ""]); }); test('item dropped on discard with empty required field with "Add an item" (invalid on "ADD")', async () => { // when a record in a list is added with "Add an item", it should // always be dropped on discard if some required field are empty // at the record creation. await mountView({ type: "form", resModel: "partner", arch: `
`, }); // Click on "Add an item" await contains(".o_field_x2many_list_row_add a").click(); expect(".o_field_widget.o_required_modifier[name=trululu]").toHaveCount(1); expect(".o_field_widget.o_required_modifier[name=trululu] input").toHaveValue(""); // click on empty required field in editable list record await contains(".o_field_widget.o_required_modifier[name=trululu] input").click(); // click off so that the required field still stay empty await contains(getFixture()).click(); // record should be dropped expect(".o_data_row").toHaveCount(0); }); test('item not dropped on discard with empty required field with "Add an item" (invalid on "UPDATE")', async () => { // when a record in a list is added with "Add an item", it should // be temporarily added to the list when it is valid (e.g. required // fields are non-empty). If the record is updated so that the required // field is empty, and it is discarded, then the record should not be // dropped. await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect(".o_data_row").toHaveCount(0); // Click on "Add an item" await contains(".o_field_x2many_list_row_add a").click(); expect(".o_data_row").toHaveCount(1); expect(".o_field_widget.o_required_modifier[name=trululu] input").toHaveCount(1); expect(".o_field_widget.o_required_modifier[name=trululu] input").toHaveValue(""); // add something to required field and leave edit mode of the record await contains(".o_field_widget.o_required_modifier[name=trululu] input").click(); await contains("li.ui-menu-item").click(); await contains(getFixture()).click(); expect(".o_data_row").toHaveCount(1); expect(".o_data_cell:eq(1)").toHaveText("first record"); // leave edit mode of the record await contains(getFixture()).click(); expect(".o_data_row").toHaveCount(1); expect(".o_data_cell:eq(1)").toHaveText("first record"); }); // WARNING: this does not seem to be a many2one field test test("list in form: default_get with x2many create", async () => { expect.assertions(3); Partner._fields.timmy = fields.Many2many({ string: "pokemon", relation: "partner.type", default: [[0, 0, { name: "brandon is the new timmy" }]], onChange: (obj) => { obj.int_field = obj.timmy.length; }, }); onRpc("web_save", ({ args }) => { expect(args[1]).toEqual({ int_field: 1, timmy: [[0, args[1].timmy[0][1], { name: "new value" }]], }); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect("td.o_data_cell").toHaveText("brandon is the new timmy"); expect(".o_field_integer input").toHaveValue("1"); // edit the subrecord and save await contains(".o_data_cell").click(); await contains(".o_data_cell input").edit("new value", { confirm: false }); await clickSave(); }); // WARNING: this does not seem to be a many2one field test test("list in form: default_get with x2many create and onchange", async () => { expect.assertions(1); Partner._fields.turtles = fields.One2many({ string: "one2many turtle field", relation: "turtle", relation_field: "turtle_trululu", default: [ [4, 2], [4, 3], ], }); onRpc("web_save", ({ args }) => { expect(args[1].turtles).toEqual([ [4, 2], [4, 3], ]); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); await clickSave(); }); test("list in form: call button in sub view", async () => { Partner._records[0].p = [2]; Product._views = { form: `
`, }; const def = new Deferred(); mockService("action", { doActionButton(params) { const { name, resModel, resId, resIds } = params; expect.step(name); expect(resModel).toBe("product"); expect(resId).toBe(37); expect(resIds).toEqual([37]); return def.then(() => { params.onClose(); }); }, }); onRpc("get_formview_id", () => { return false; }); await mountViewInDialog({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(".modal").toHaveCount(1); await contains("td.o_data_cell").click(); await contains(".o_external_button", { visible: false }).click(); expect(".modal").toHaveCount(2); const buttons = queryAll(".o_dialog:not(.o_inactive_modal) .o_form_statusbar button"); await contains(buttons[0]).click(); expect.verifySteps(["action"]); expect(buttons[1]).not.toBeEnabled(); def.resolve(); await animationFrame(); await contains(".modal:eq(1) .o_form_button_cancel").click(); expect(".modal").toHaveCount(1); await contains(".o_external_button", { visible: false }).click(); expect(".modal").toHaveCount(2); await contains(".o_dialog:not(.o_inactive_modal) .o_form_statusbar button:eq(1)").click(); expect.verifySteps(["object"]); }); test("X2Many sequence list in modal", async () => { Partner._fields.sequence = fields.Integer({ string: "Sequence", type: "integer", onChange: (obj) => { if (obj.id === 2) { obj.sequence = 1; expect.step("onchange sequence"); } }, }); Partner._records[0].sequence = 1; Partner._records[1].sequence = 2; Product._fields.turtle_ids = fields.One2many({ string: "Turtles", relation: "turtle", }); Product._records[0].turtle_ids = [1]; Product._records[0].name = "leonardo"; Product._records[0].name = "xphone"; Turtle._fields.partnertypes_ids = fields.One2many({ string: "Partner", relation: "partner", }); Turtle._fields.type_id = fields.Many2one({ string: "Partner Type", relation: "partner.type", }); Turtle._records[0].type_id = 12; PartnerType._fields.partner_ids = fields.One2many({ string: "Partner", relation: "partner", }); PartnerType._records[0].partner_ids = [1, 2]; PartnerType._views = { form: `
`, }; Partner._views = { list: ` `, }; onRpc("partner.type", "get_formview_id", () => { return false; }); onRpc("partner.type", "web_save", () => { expect.step("partner.type web_save"); }); await mountViewInDialog({ type: "form", resModel: "product", resId: 37, arch: `
`, }); expect(".modal").toHaveCount(1); await contains(".o_data_cell").click(); await contains(".o_external_button", { visible: false }).click(); expect(".modal").toHaveCount(2); expect(".modal:eq(1) .ui-sortable-handle").toHaveCount(2); await contains( ".o_dialog:not(.o_inactive_modal) .o_data_row:nth-child(2) .ui-sortable-handle" ).dragAndDrop(".o_dialog:not(.o_inactive_modal) tbody tr"); // Saving the modal and then the original model await contains(".modal:eq(1) .o_form_button_save").click(); await clickSave(); expect.verifySteps(["onchange sequence", "partner.type web_save"]); }); test("autocompletion in a many2one, in form view with a domain", async () => { expect.assertions(1); onRpc("name_search", ({ kwargs }) => { expect(kwargs.args).toEqual([]); }); await mountView({ type: "form", resModel: "partner", resId: 1, domain: [["trululu", "=", 4]], arch: '
', }); await contains(".o_field_widget[name=product_id] input").click(); }); test("autocompletion in a many2one, in form view with a date field", async () => { expect.assertions(1); onRpc("name_search", ({ kwargs }) => { expect(kwargs.args).toEqual([["bar", "=", true]]); }); await mountView({ type: "form", resModel: "partner", resId: 2, arch: `
`, }); await contains(".o_field_widget[name='trululu'] input").click(); }); test("creating record with many2one with option always_reload", async () => { Partner._fields.trululu = fields.Many2one({ string: "Trululu", relation: "partner", default: 1, onChange: (obj) => { obj.trululu = 2; //[2, "second record"]; }, }); onRpc("onchange", async ({ parent }) => { const result = await parent(); result.value.trululu = { ...result.value.trululu, display_name: "hello world\nso much noise", }; return result; }); onRpc(({ method }) => { expect.step(method); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect(".o_field_widget[name='trululu'] input").toHaveValue("hello world"); expect.verifySteps(["get_views", "onchange"]); }); test("empty list with sample data and many2one with option always_reload", async () => { await mountView({ type: "list", resModel: "partner", arch: ` `, context: { search_default_empty: true }, searchViewArch: ` `, }); expect(".o_list_view .o_content").toHaveClass("o_view_sample_data"); expect(".o_list_table").toHaveCount(1); expect(".o_data_row").toHaveCount(10); expect("thead tr th").toHaveCount(2); }); test("selecting a many2one, then discarding", async () => { await mountView({ type: "form", resModel: "partner", resId: 1, arch: '
', }); expect(".o_field_widget[name='product_id'] input").toHaveValue(""); await contains(".o_field_widget[name='product_id'] input").click(); await contains(".o_field_widget[name='product_id'] .dropdown-item").click(); expect(".o_field_widget[name='product_id'] input").toHaveValue("xphone"); await contains(".o_form_button_cancel").click(); expect(".o_field_widget[name='product_id'] input").toHaveValue(""); }); test("domain and context are correctly used when doing a name_search in a m2o", async () => { expect.assertions(4); Partner._records[0].timmy = [12]; // Need to take into account the company service which populates context at startup const DEFAULT_USER_CTX = { ...user.context, allowed_company_ids: [1] }; serverState.userContext = { hey: "ho" }; onRpc("product", "name_search", ({ kwargs }) => { expect(kwargs.args).toEqual(["&", ["foo", "=", "bar"], ["foo", "=", "yop"]]); expect(kwargs.context).toEqual({ ...DEFAULT_USER_CTX, hey: "ho", hello: "world", test: "yop", }); return []; }); onRpc("partner", "name_search", ({ kwargs }) => { expect(kwargs.args).toEqual([["id", "in", [12]]]); expect(kwargs.context).toEqual({ ...DEFAULT_USER_CTX, hey: "ho", timmy: [12], }); return []; }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await contains(".o_field_widget[name='product_id'] input").click(); await contains(".o_field_widget[name='trululu'] input").click(); }); test("quick create on a many2one", async () => { expect.assertions(1); onRpc("name_create", ({ args }) => { expect(args[0]).toBe("new partner"); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one input").edit("new partner", { confirm: false }); await runAllTimers(); await press("tab"); }); test("failing quick create on a many2one because ValidationError", async () => { expect.assertions(5); Product._views = { form: '
', }; onRpc("name_create", () => { throw makeServerError({ type: "ValidationError" }); }); onRpc("web_save", ({ args }) => { expect(args[1]).toEqual({ name: "xyz" }); }); await mountView({ type: "form", resModel: "partner", arch: '
', }); await contains(".o_field_widget[name='product_id'] input").edit("abcd", { confirm: false }); await runAllTimers(); await contains(".o_field_widget[name='product_id'] .dropdown-item").click(); await animationFrame(); // wait for the error service to ensure that there's no error dialog expect(".o_error_dialog").toHaveCount(0); expect(".modal .o_form_view").toHaveCount(1); expect(".modal .o_field_widget[name='name'] input").toHaveValue("abcd"); await contains(".modal .o_field_widget[name='name'] input").edit("xyz"); await contains(".modal .o_form_button_save").click(); expect(".o_field_widget[name='product_id'] input").toHaveValue("xyz"); }); test("failing quick create on a many2one", async () => { expect.assertions(2); expect.errors(1); Product._views = { form: '
', }; onRpc("name_create", () => { throw makeServerError(); }); await mountView({ type: "form", resModel: "partner", arch: '
', }); await contains(".o_field_widget[name='product_id'] input").edit("abcd", { confirm: false }); await runAllTimers(); await contains(".o_field_widget[name='product_id'] .dropdown-item").click(); await animationFrame(); // wait for the error service expect(".o_error_dialog").toHaveCount(1); expect(".modal .o_form_view").toHaveCount(0); }); test("failing quick create on a many2one inside a one2many because ValidationError", async () => { expect.assertions(4); Partner._views = { list: ` `, }; Product._views = { form: '
', }; onRpc("name_create", () => { throw makeServerError({ type: "ValidationError" }); }); onRpc("web_save", ({ args }) => { expect(args[1]).toEqual({ name: "xyz" }); }); await mountView({ type: "form", resModel: "partner", arch: '
', }); await contains(".o_field_x2many_list_row_add a").click(); await contains(".o_field_widget[name='product_id'] input").edit("abcd", { confirm: false }); await runAllTimers(); await contains(".o_field_widget[name='product_id'] .dropdown-item").click(); expect(".modal .o_form_view").toHaveCount(1); expect(".modal .o_field_widget[name='name'] input").toHaveValue("abcd"); await contains(".modal .o_field_widget[name='name'] input").edit("xyz"); await contains(".modal .o_form_button_save").click(); expect(".o_field_widget[name='product_id'] input").toHaveValue("xyz"); }); test("slow create on a many2one", async () => { Product._views = { form: '
', }; await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one input").edit("new product", { confirm: false }); await runAllTimers(); await press("tab"); await animationFrame(); expect(".modal").toHaveCount(1); // cancel the many2one creation with Discard button await contains(".modal .modal-footer .btn:not(.btn-primary)").click(); expect(".modal").toHaveCount(0); expect(".o_field_many2one input").toHaveValue(""); // cancel the many2one creation with Close button await contains(".o_field_many2one input").edit("new product", { confirm: false }); await runAllTimers(); await press("tab"); await animationFrame(); expect(".modal").toHaveCount(1); await contains(".modal .modal-header button").click(); expect(".o_field_many2one input").toHaveValue(""); expect(".modal").toHaveCount(0); // select a new value then cancel the creation of the new one --> restore the previous await contains(".o_field_widget[name=product_id] input").click(); await contains(".ui-menu-item").click(); expect(".o_field_many2one input").toHaveValue("xphone"); await contains(".o_field_many2one input").edit("new product", { confirm: false }); await runAllTimers(); await press("tab"); await animationFrame(); expect(".modal").toHaveCount(1); await contains(".modal .modal-footer .btn:not(.btn-primary)").click(); expect(".o_field_many2one input").toHaveValue(""); // confirm the many2one creation await contains(".o_field_many2one input").edit("new product", { confirm: false }); await runAllTimers(); await press("tab"); await animationFrame(); expect(".modal .o_form_view").toHaveCount(1); await contains(".modal .o_form_button_cancel").click(); }); test("select a many2one value by pressing tab", async () => { await mountView({ type: "form", resModel: "partner", arch: '
', }); await contains(".o_field_many2one input").edit("xph", { confirm: false }); await runAllTimers(); await press("tab"); await animationFrame(); expect(".modal").toHaveCount(0); expect(".o_field_many2one input").toHaveValue("xphone"); expect(".o_external_button").toHaveCount(1); }); test("no_create option on a many2one", async () => { await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one input").edit("new partner", { confirm: false }); await runAllTimers(); await press("tab"); expect(".modal").toHaveCount(0); expect(".o_field_many2one input").toHaveValue(""); }); test("no_create option on a many2one when can_create is absent", async () => { Partner._fields.product_id.readonly = true; await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one input").edit("new partner", { confirm: false }); await runAllTimers(); await press("tab"); expect(".modal").toHaveCount(0); expect(".o_field_many2one input").toHaveValue(""); }); test("no_quick_create option on a many2one when can_create is absent", async () => { Partner._fields.product_id.readonly = true; await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one input").edit("new partner", { confirm: false }); await runAllTimers(); expect(".ui-autocomplete .o_m2o_dropdown_option").toHaveCount(1); expect(".ui-autocomplete .o_m2o_dropdown_option").toHaveClass( "o_m2o_dropdown_option_create_edit" ); }); test("can_create and can_write option on a many2one", async () => { Product.options = { can_create: "false", can_write: "false", }; Product._views = { form: `
`, }; onRpc("product", "get_formview_id", () => { return false; }); await mountViewInDialog({ type: "form", resModel: "partner", arch: `
`, }); expect(".modal").toHaveCount(1); await contains(".o_field_many2one input").click(); expect(".o_m2o_dropdown_option.o_m2o_dropdown_option_create").toHaveCount(0); await contains(".ui-menu-item:eq(1)").click(); expect(".o_field_many2one input").toHaveValue("xpad"); expect(".o_field_many2one .o_external_button").toHaveCount(1); await contains(".o_field_many2one .o_external_button", { visible: false }).click(); expect(".modal").toHaveCount(2); expect(".modal .o_form_view .o_form_readonly").toHaveCount(1); await contains(".o_dialog:not(.o_inactive_modal) .modal-footer .btn-primary").click(); await contains(".o_field_many2one input").edit("new product"); expect(".modal").toHaveCount(1); }); test("create_name_field option on a many2one", async () => { // when the 'create_name_field' option is set, the value entered in the // many2one input should be used to populate this specified field, // instead of the generic 'name' field. Partner._views = { form: `
`, }; await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_widget[name=trululu] input").edit("yz", { confirm: false }); await runAllTimers(); await contains(".o_field_widget[name=trululu] input").click(); await selectFieldDropdownItem("trululu", "Create and edit..."); expect(".o_field_widget[name=foo] input").toHaveValue("yz"); await contains(".o_form_button_cancel").click(); }); test("propagate can_create onto the search popup", async () => { Product._records = [ { id: 1, name: "Tromblon1" }, { id: 2, name: "Tromblon2" }, { id: 3, name: "Tromblon3" }, { id: 4, name: "Tromblon4" }, { id: 5, name: "Tromblon5" }, { id: 6, name: "Tromblon6" }, { id: 7, name: "Tromblon7" }, { id: 8, name: "Tromblon8" }, ...Product._records, ]; Product._views = { list: ` `, search: ` `, }; onRpc("get_formview_id", () => { return false; }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await contains(".o_field_widget[name=product_id] input").click(); expect(".o-autocomplete a:contains(Start typing...)").toHaveCount(0); await contains(".o_field_widget[name=product_id] input").edit("a", { confirm: false }); await runAllTimers(); expect(".ui-autocomplete a:contains(Create and Edit)").toHaveCount(0); await contains(".o_field_many2one[name=product_id] input").edit("", { confirm: false }); await runAllTimers(); await clickFieldDropdownItem("product_id", "Search More..."); expect(".modal-dialog.modal-lg").toHaveCount(1); expect(queryAllTexts(".modal-footer button")).toEqual(["Close"]); }); test("many2one with can_create=false shows no result item when searched something that doesn't exist", async () => { await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one input").click(); await contains(".o_field_many2one[name=product_id] input").edit("abc", { confirm: false }); await runAllTimers(); expect(".o_field_many2one[name=product_id] .o_m2o_dropdown_option_create").toHaveCount(0); expect(".o_field_many2one[name=product_id] .o_m2o_no_result").toHaveCount(1); await contains(getFixture()).click(); expect(".o_field_many2one[name=product_id] .o_m2o_no_result").toHaveCount(0); }); test("pressing enter in a m2o in an editable list", async () => { await mountView({ type: "list", resModel: "partner", arch: ` `, }); await contains("td.o_data_cell").click(); expect(".o_selected_row").toHaveCount(1); // we now write 'a' and press enter to check that the selection is // working, and prevent the navigation await contains("[name=product_id] input").edit("a", { confirm: false }); await runAllTimers(); expect("[name=product_id] .o-autocomplete--dropdown-menu").toHaveCount(1); // we now trigger ENTER to select first choice await press("Enter"); await animationFrame(); expect("[name=product_id] input").toBeFocused(); expect("[name=product_id] .o-autocomplete--dropdown-menu").toHaveCount(0); // we now trigger again ENTER to make sure we can move to next line await press("Enter"); await animationFrame(); expect("tr.o_data_row:nth-child(1) [name=product_id] input").toHaveCount(0); expect("tr.o_data_row:nth-child(2)").toHaveClass("o_selected_row"); // we now write again 'a' in the cell to select xpad. We will now // test with the tab key await contains("[name=product_id] input").edit("a", { confirm: false }); await runAllTimers(); expect( "tr.o_data_row:nth-child(2) [name=product_id] .o-autocomplete--dropdown-menu" ).toHaveCount(1); await press("Tab"); await animationFrame(); expect("tr.o_data_row:nth-child(2) [name=product_id] input").toHaveCount(0); expect("tr.o_data_row:nth-child(3)").toHaveClass("o_selected_row"); }); test("pressing ENTER on a 'no_quick_create' many2one should open a M2ODialog", async () => { Partner._views = { form: `
`, }; await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one input").edit("Something that does not exist", { confirm: false, }); await runAllTimers(); await press("Enter"); await animationFrame(); expect(".modal").toHaveCount(1); // Check that discarding clears $input await contains(".modal .o_form_button_cancel").click(); expect(".o_field_many2one input").toHaveValue(""); }); test("select a value by pressing TAB on a many2one with onchange", async () => { Partner._onChanges = { trululu: () => {}, }; const def = new Deferred(); onRpc("onchange", () => def); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await contains(".o_field_many2one input").edit("first", { confirm: "tab" }); // simulate a focusout (e.g. because the user clicks outside) // before the onchange returns await click(".o_field_char"); expect(".modal").toHaveCount(0); // unlock the onchange def.resolve(); await runAllTimers(); expect(".o_field_many2one input").toHaveValue("first record"); expect(".modal").toHaveCount(0); }); test("leaving a many2one by pressing tab", async () => { await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one input").click(); await runAllTimers(); await press("tab"); await animationFrame(); expect(".o_field_many2one input").toHaveValue(""); // open autocomplete dropdown and manually select item by UP/DOWN key and press TAB await contains(".o_field_many2one input").click(); await runAllTimers(); await press("arrowdown"); await press("tab"); await animationFrame(); expect(".o_field_many2one input").toHaveValue("second record"); // clear many2one and then open autocomplete, write something and press TAB await contains(".o_field_many2one input").edit("", { confirm: false }); await runAllTimers(); await contains(".o_field_many2one input").click(); await contains(".o_field_many2one input").edit("se", { confirm: "tab" }); await runAllTimers(); expect(".o_field_many2one input").toHaveValue("second record"); }); test("leaving an empty many2one by pressing tab (after backspace or delete)", async () => { expect.assertions(4); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(".o_field_many2one input").toHaveValue(); // simulate backspace to remove values and press TAB await contains(".o_field_many2one input").edit("", { confirm: false }); await runAllTimers(); await press("backspace"); await press("tab"); await animationFrame(); expect(".o_field_many2one input").toHaveValue(""); // reset a value await selectFieldDropdownItem("trululu", "first record"); expect(".o_field_many2one input").toHaveValue("first record"); // simulate delete to remove values and press TAB await contains(".o_field_many2one input").edit("", { confirm: false }); await runAllTimers(); await press("delete"); await press("tab"); // TODO: fix owl await animationFrame(); expect(".o_field_many2one input").toHaveValue(""); }); test("many2one in editable list + onchange, with enter", async () => { Partner._onChanges = { product_id: (obj) => { obj.int_field = obj.product_id || 0; }, }; const def = new Deferred(); onRpc("onchange", () => def); onRpc(({ method }) => { expect.step(method); }); await mountView({ type: "list", resModel: "partner", arch: ` `, }); await contains("td.o_data_cell").click(); await contains("td.o_data_cell input").edit("a", { confirm: false }); await runAllTimers(); await press("enter"); def.resolve(); await animationFrame(); await press("enter"); await animationFrame(); expect(".modal").toHaveCount(0); expect.verifySteps([ "get_views", "web_search_read", // to display results in the dialog "has_group", "name_search", "onchange", "web_save", ]); }); test("many2one in editable list + onchange, with enter, part 2", async () => { // this is the same test as the previous one, but the onchange is just // resolved slightly later Partner._onChanges = { product_id: (obj) => { obj.int_field = obj.product_id || 0; }, }; const def = new Deferred(); onRpc("onchange", () => def); onRpc(({ method }) => { expect.step(method); }); await mountView({ type: "list", resModel: "partner", arch: ` `, }); await contains("td.o_data_cell").click(); await contains("td.o_data_cell input").edit("a", { confirm: false }); await runAllTimers(); await press("enter"); await press("enter"); def.resolve(); await animationFrame(); expect(".modal").toHaveCount(0); expect.verifySteps([ "get_views", "web_search_read", // to display results in the dialog "has_group", "name_search", "onchange", "web_save", ]); }); test("many2one: dynamic domain set in the field's definition", async () => { expect.assertions(2); Partner._fields.trululu = fields.Many2one({ string: "Trululu", relation: "partner", domain: "[('foo' ,'=', foo)]", }); onRpc("name_search", ({ kwargs }) => { expect(kwargs.args).toEqual([["foo", "=", "yop"]]); }); await mountView({ type: "list", resModel: "partner", arch: ` `, }); await contains(".o_data_cell:eq(0)").click(); await contains(".o_field_many2one input").click(); expect(".o_field_many2one .o-autocomplete--dropdown-item").toHaveCount(2); }); test("many2one: domain set in view and on field", async () => { expect.assertions(2); Partner._fields.trululu = fields.Many2one({ string: "Trululu", relation: "partner", domain: "[('foo' ,'=', 'boum')]", }); onRpc("name_search", ({ kwargs }) => { // should only use the domain set in the view expect(kwargs.args).toEqual([["foo", "=", "blip"]]); }); await mountView({ type: "list", resModel: "partner", arch: ` `, }); await contains(".o_data_cell:eq(0)").click(); await contains(".o_field_many2one input").click(); expect(".o_field_many2one .o-autocomplete--dropdown-item").toHaveCount(2); }); test("many2one: domain updated by an onchange", async () => { expect.assertions(2); Partner._onChanges = { int_field: () => {}, }; let domain = []; onRpc("onchange", () => { domain = [["id", "in", [10]]]; return { domain: { trululu: domain, unexisting_field: domain, }, }; }); onRpc("name_search", ({ kwargs }) => { expect(kwargs.args).toEqual(domain); }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); // trigger a name_search (domain should be []) await contains(".o_field_widget[name=trululu] input").click(); // close the dropdown await contains(".o_field_widget[name=trululu] input").click(); // trigger an onchange that will update the domain // trigger a name_search (domain should be [['id', 'in', [10]]]) await contains(".o_field_widget[name='trululu'] input").click(); }); test("search more in many2one: no text in input", async () => { // when the user clicks on 'Search More...' in a many2one dropdown, and there is no text // in the input (i.e. no value to search on), we bypass the name_search that is meant to // return a list of preselected ids to filter on in the list view (opened in a dialog) expect.assertions(2); for (let i = 0; i < 8; i++) { Partner._records.push({ id: 100 + i, name: `test_${i}` }); } Partner._views = { list: ` `, search: ``, }; onRpc(({ method }) => { expect.step(method); }); onRpc("web_search_read", ({ kwargs }) => { expect(kwargs.domain).toEqual([]); }); await mountView({ type: "form", resModel: "partner", arch: '
', }); await contains(`.o_field_widget[name="trululu"] input`).clear(); await contains(`.o_field_widget[name="trululu"] input`).click(); await contains(`.o_field_widget[name="trululu"] .o_m2o_dropdown_option_search_more`).click(); expect.verifySteps([ "get_views", // main form view "onchange", "name_search", // to display results in the dropdown "get_views", // list view in dialog "has_group", "web_search_read", // to display results in the dialog ]); }); test("search more in many2one: text in input", async () => { // when the user clicks on 'Search More...' in a many2one dropdown, and there is some // text in the input, we perform a name_search to get a (limited) list of preselected // ids and we add a dynamic filter (with those ids) to the search view in the dialog, so // that the user can remove this filter to bypass the limit expect.assertions(5); for (let i = 0; i < 8; i++) { Partner._records.push({ id: 100 + i, name: `test_${i}` }); } Partner._views = { list: ` `, search: ``, }; let expectedDomain; onRpc(({ method }) => { expect.step(method); }); onRpc("web_search_read", ({ kwargs }) => { expect(kwargs.domain).toEqual(expectedDomain); }); await mountView({ type: "form", resModel: "partner", arch: '
', }); expectedDomain = [["id", "in", [100, 101, 102, 103, 104, 105, 106, 107]]]; await contains(`.o_field_widget[name="trululu"] input`).click(); await contains(".o_field_widget[name='trululu'] input").edit("test", { confirm: false }); await runAllTimers(); await contains(`.o_field_widget[name="trululu"] .o_m2o_dropdown_option_search_more`).click(); expect(".modal .o_list_view").toHaveCount(1); expect(".modal .o_cp_searchview .o_facet_values").toHaveCount(1); // remove the filter on ids expectedDomain = []; await contains(".modal .o_cp_searchview .o_facet_remove").click(); expect.verifySteps([ "get_views", // main form view "onchange", "name_search", // empty search, triggered when the user clicks in the input "name_search", // to display results in the dropdown "name_search", // to get preselected ids matching the search "get_views", // list view in dialog "has_group", "web_search_read", // to display results in the dialog "web_search_read", // after removal of dynamic filter ]); }); test("search more in many2one: dropdown click", async () => { for (let i = 0; i < 8; i++) { Partner._records.push({ id: 100 + i, name: `test_${i}` }); } Partner._views = { list: ` `, search: ``, }; await mountView({ type: "form", resModel: "partner", arch: '
', }); await contains(`.o_field_widget[name="trululu"] input`).click(); await contains(".o_field_widget[name='trululu'] input").edit("test", { confirm: false }); await runAllTimers(); await contains(`.o_field_widget[name="trululu"] .o_m2o_dropdown_option_search_more`).click(); // dropdown selector const searchDropdown = ".o_control_panel_actions .o-dropdown"; await contains(searchDropdown).click(); expect(searchDropdown).toHaveClass("show"); expect(getDropdownMenu(searchDropdown)).toBeVisible(); }); test("updating a many2one from a many2many", async () => { expect.assertions(5); Turtle._records[1].turtle_trululu = 1; Partner._views = { form: `
`, }; onRpc("get_formview_id", ({ args }) => { expect(args[0]).toEqual([1]); return false; }); await mountViewInDialog({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(".modal").toHaveCount(1); // Opening the modal await contains(".o_data_row td:eq(1)").click(); await contains(".o_external_button", { visible: false }).click(); expect(".modal").toHaveCount(2); // Changing the 'trululu' value await contains(".o_dialog:not(.o_inactive_modal) div[name=name] input").edit("test"); await contains(".modal:eq(1) .o_form_button_save").click(); expect(".modal").toHaveCount(1); // Test whether the value has changed expect(".o_dialog:not(.o_inactive_modal) div[name=turtle_trululu] input").toHaveValue("test"); }); test("search more in many2one: resequence inside dialog", async () => { // when the user clicks on 'Search More...' in a many2one dropdown, resequencing inside // the dialog works Partner._fields.sequence = fields.Integer(); for (let i = 0; i < 8; i++) { Partner._records.push({ id: 100 + i, name: `test_${i}` }); } Partner._views = { list: ` `, search: ``, }; onRpc("web_search_read", ({ kwargs }) => { expect(kwargs.domain).toEqual([]); }); onRpc("/web/dataset/resequence", () => { expect.step("/web/dataset/resequence"); }); onRpc(({ method }) => { expect.step(method); }); await mountView({ type: "form", resModel: "partner", arch: '
', }); await contains(".o_field_widget[name='trululu'] input").click(); await runAllTimers(); await contains(`.o_field_widget[name="trululu"] .o_m2o_dropdown_option_search_more`).click(); expect(".modal").toHaveCount(1); expect(".modal .ui-sortable-handle").toHaveCount(11); await contains(".modal .o_data_row:nth-child(2) .ui-sortable-handle").dragAndDrop( ".modal tbody tr" ); await animationFrame(); expect.verifySteps([ "get_views", "onchange", "name_search", // to display results in the dropdown "get_views", // list view in dialog "web_search_read", // to display results in the dialog "has_group", "/web/dataset/resequence", // resequencing lines "read", ]); }); test("many2one dropdown disappears on scroll", async () => { await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await contains(".o_field_many2one input").click(); expect(".o_field_many2one .dropdown-menu").toHaveCount(1); const dropdown = queryOne(".o_field_many2one .dropdown-menu"); dropdown.style = "max-height: 40px;"; await scroll(dropdown, { top: 50 }); expect(dropdown).toHaveProperty("scrollTop", 50); expect(".o_field_many2one .dropdown-menu").toHaveCount(1); await scroll(".o_content", { top: 50 }); await animationFrame(); expect(".o_field_many2one .dropdown-menu").toHaveCount(0); }); test("search more in many2one: group and use the pager", async () => { Partner._records.push( { id: 5, name: "Partner 4", }, { id: 6, name: "Partner 5", }, { id: 7, name: "Partner 6", }, { id: 8, name: "Partner 7", }, { id: 9, name: "Partner 8", }, { id: 10, name: "Partner 9", } ); Partner._views = { list: ` `, search: ` `, }; await mountView({ type: "form", resModel: "partner", resId: 1, arch: '
', }); await selectFieldDropdownItem("trululu", "Search More..."); await toggleSearchBarMenu(".modal"); await toggleMenuItem("Bar"); await contains(".modal .o_group_header:eq(1)").click(); expect(".modal .o_data_row").toHaveCount(7); await contains(".modal .o_group_header .o_pager_next").click(); expect(".modal .o_data_row").toHaveCount(1); }); test("focus when closing many2one modal in many2one modal", async () => { Partner._views = { form: '
', }; onRpc("get_formview_id", () => { return false; }); await mountViewInDialog({ type: "form", resModel: "partner", resId: 2, arch: '
', }); expect(".o_dialog").toHaveCount(1); expect(document.body).toHaveClass("modal-open"); // Open many2one modal await contains(".o_external_button", { visible: false }).click(); const originalModal = queryOne(".o_dialog:eq(1)"); expect(".o_dialog").toHaveCount(2); expect(originalModal).not.toHaveClass("o_inactive_modal"); expect(document.body).toHaveClass("modal-open"); // Open many2one modal of field in many2one modal await contains(".o_dialog:eq(1) .o_external_button", { visible: false }).click(); expect(".o_dialog").toHaveCount(3); expect(".o_dialog:eq(2)").not.toHaveClass("o_inactive_modal"); expect(document.body).toHaveClass("modal-open"); // Close second modal await contains(".o_dialog:eq(2) button[class='btn-close']").click(); expect(".o_dialog").toHaveCount(2); expect(originalModal).toBe(queryOne(".o_dialog:eq(1)"), { message: "First modal is still opened", }); expect(".o_dialog:eq(1)").not.toHaveClass("o_inactive_modal"); expect(document.body).toHaveClass("modal-open"); // Close first modal await contains(".o_dialog:eq(1) button[class='btn-close']").click(); expect(".o_dialog").toHaveCount(1); }); test("search more pager is reset when doing a new search", async () => { Partner._fields.datetime = fields.Datetime({ string: "Datetime Field", searchable: true }); Partner._records.push( ...new Array(170).fill().map((_, i) => ({ id: i + 10, name: "Partner " + i })) ); Partner._views = { list: ` `, search: ` `, }; await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await selectFieldDropdownItem("trululu", "Search More..."); await contains(".modal .o_pager_next").click(); expect(".modal .o_pager_limit").toHaveText("173"); expect(".modal .o_pager_value").toHaveText("81-160"); expect(".modal tr.o_data_row").toHaveCount(80); await editSearch("first"); await validateSearch(); expect(".modal .o_pager_limit").toHaveText("1"); expect(".modal .o_pager_value").toHaveText("1-1"); expect(".modal tr.o_data_row").toHaveCount(1); }); test("click on many2one link in list view", async () => { Turtle._records[1].product_id = 37; Partner._views = { form: '
', search: "", }; Turtle._views = { list: ` `, }; Product._views = { search: "", form: "
", }; onRpc("get_formview_action", (args) => { expect.step("get_formview_action"); expect(args.kwargs.context.field).toBe("Yes"); expect(args.kwargs.context).not.toInclude("global"); return { type: "ir.actions.act_window", res_model: "product", view_type: "form", view_mode: "form", views: [[false, "form"]], target: "current", res_id: args[0], }; }); await mountWithCleanup(WebClient); await getService("action").doAction({ name: "Partner", res_model: "partner", res_id: 1, type: "ir.actions.act_window", views: [[false, "form"]], context: { global: "No" }, }); expect("a.o_form_uri").toHaveCount(1); expect(".o_breadcrumb").toHaveCount(1); await contains("a.o_form_uri").click(); expect.verifySteps(["get_formview_action"]); expect(".breadcrumb-item").toHaveCount(1); expect(".o_breadcrumb").toHaveCount(1); }); test("Many2oneField with placeholder", async () => { await mountView({ type: "form", resModel: "partner", arch: '
', }); expect(".o_field_widget[name='trululu'] input").toHaveAttribute("placeholder", "Placeholder"); }); test("external_button performs a doAction by default", async () => { Partner._views = { form: '
', search: "", }; onRpc("get_formview_action", () => { expect.step("get_formview_action"); return { type: "ir.actions.act_window", res_model: "partner", view_type: "form", view_mode: "form", views: [[false, "form"]], target: "current", res_id: false, }; }); await mountWithCleanup(WebClient); await getService("action").doAction({ name: "Partner", res_model: "partner", res_id: 1, type: "ir.actions.act_window", views: [[false, "form"]], }); await selectFieldDropdownItem("trululu", "first record"); expect(".o_field_widget .o_external_button.oi-arrow-right").toHaveCount(1); await contains(".o_field_widget .o_external_button", { visible: false }).click(); expect.verifySteps(["get_formview_action"]); expect(".breadcrumb").toHaveText("first record"); }); test("external_button opens a FormViewDialog in dialogs", async () => { onRpc("get_formview_id", () => { expect.step("get_formview_id"); return false; }); await mountViewInDialog({ type: "form", resModel: "partner", arch: '
', }); expect(".modal").toHaveCount(1); await selectFieldDropdownItem("trululu", "first record"); expect(".o_field_widget .o_external_button.oi-launch").toHaveCount(1); await contains(".o_field_widget .o_external_button", { visible: false }).click(); expect.verifySteps(["get_formview_id"]); expect(".modal").toHaveCount(2); }); test("keep changes when editing related record in a dialog", async () => { Partner._views = { [["form", 98]]: '
', }; onRpc("get_formview_id", () => { return 98; }); onRpc("web_save", () => { expect.step("web_save"); }); await mountViewInDialog({ type: "form", resModel: "partner", arch: '
', }); expect(".modal").toHaveCount(1); await contains(".o_field_widget[name=foo] input").edit("some value", { confirm: false }); await runAllTimers(); await selectFieldDropdownItem("trululu", "first record"); expect(".o_field_widget .o_external_button.oi-launch").toHaveCount(1); await contains(".o_field_widget .o_external_button", { visible: false }).click(); expect(".modal").toHaveCount(2); await contains(".o_dialog:not(.o_inactive_modal) .o_field_widget[name=int_field] input").edit( "5464" ); await contains( ".o_dialog:not(.o_inactive_modal) .modal-footer .btn-primary:not(.d-none)" ).click(); expect(".modal").toHaveCount(1); expect(".o_field_widget[name=foo] input").toHaveValue("some value"); expect.verifySteps(["web_save"]); }); test("create and edit, save and then discard", async () => { Partner.views = { [[98, "form"]]: '
', }; onRpc("get_formview_id", () => { return 98; }); await mountView({ type: "form", resModel: "partner", arch: '
', resId: 1, }); expect(".o_field_widget[name=trululu] input").toHaveValue("aaa"); await contains(".o_field_widget[name=trululu] input").edit("new m2o", { confirm: false }); await runAllTimers(); await contains(".o_field_widget[name=trululu] input").click(); await selectFieldDropdownItem("trululu", "Create and edit..."); expect(".modal").toHaveCount(1); await contains(".modal-footer .btn-primary:not(.d-none)").click(); expect(".modal").toHaveCount(0); expect(".o_field_widget[name=trululu] input").toHaveValue("new m2o"); await contains(".o_form_button_cancel").click(); expect(".o_field_widget[name=trululu] input").toHaveValue("aaa"); }); test("external button must be displayed after the update caused by an onchange", async () => { Partner._onChanges = { name: (obj) => { if (obj.name) { obj.trululu = 1; } }, }; await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect(".o_external_button").toHaveCount(0); await contains("[name=name] input").edit("new value"); expect(".o_external_button").toHaveCount(1); }); test("many2one field with false as name", async () => { Partner._records[0].name = false; await mountView({ type: "form", resModel: "partner", resId: 2, arch: `
`, }); expect(".o_field_widget[name='trululu'] input").toHaveValue("Unnamed"); }); test("many2one search with false as name", async () => { onRpc("name_search", () => { return [[1, false]]; }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_many2one input").click(); expect(".o_field_many2one[name='trululu'] .dropdown-menu a.dropdown-item:eq(0)").toHaveText( "Unnamed" ); });