import { expect, getFixture, test } from "@odoo/hoot"; import { getNextFocusableElement, press, queryAll, queryAllTexts, queryFirst, queryOne, } from "@odoo/hoot-dom"; import { Deferred, animationFrame, mockTimeZone, runAllTimers } from "@odoo/hoot-mock"; import { onWillDestroy, onWillStart, reactive, useState } from "@odoo/owl"; import { getPickerCell } from "@web/../tests/core/datetime/datetime_test_helpers"; import { clickFieldDropdown, clickFieldDropdownItem, clickSave, contains, defineModels, fields, getService, makeServerError, mockService, models, mountView, mountViewInDialog, mountWithCleanup, onRpc, patchWithCleanup, selectFieldDropdownItem, serverState, } from "@web/../tests/web_test_helpers"; import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; import { pick } from "@web/core/utils/objects"; import { Record } from "@web/model/relational_model/record"; import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field"; import { WebClient } from "@web/webclient/webclient"; class Partner extends models.Model { name = fields.Char(); foo = fields.Char({ default: "My little Foo Value" }); bar = fields.Boolean({ default: true }); int_field = fields.Integer(); qux = fields.Float({ string: "Qux", digits: [16, 1] }); 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({ relation: "partner" }); timmy = fields.Many2many({ relation: "partner.type", string: "pokemon" }); product_id = fields.Many2one({ relation: "product" }); color = fields.Selection({ selection: [ ["red", "Red"], ["black", "Black"], ], default: "red", }); date = fields.Date(); datetime = fields.Datetime(); user_id = fields.Many2one({ relation: "res.users" }); reference = fields.Reference({ selection: [ ["product.product", "Product"], ["partner.type", "Partner Type"], ["partner", "Partner"], ], }); _records = [ { id: 1, name: "first record", bar: true, foo: "yop", int_field: 10, qux: 0.44, p: [], turtles: [2], timmy: [], trululu: 4, user_id: 17, }, { id: 2, name: "second record", bar: true, foo: "blip", int_field: 9, qux: 13, p: [], timmy: [], trululu: 1, product_id: 37, date: "2017-01-25", datetime: "2016-12-12 10:55:05", user_id: 17, }, { id: 4, name: "aaa", bar: false, }, ]; } class Product extends models.Model { _name = "product"; name = fields.Char(); _records = [ { id: 37, name: "xphone", }, { id: 41, name: "xpad", }, ]; } class PartnerType extends models.Model { color = fields.Integer({ string: "Color index" }); name = fields.Char(); _records = [ { id: 12, name: "gold", color: 2, }, { id: 14, name: "silver", color: 5, }, ]; } class Turtle extends models.Model { name = fields.Char(); turtle_foo = fields.Char(); turtle_bar = fields.Boolean({ default: true }); turtle_int = fields.Integer(); turtle_qux = fields.Float({ string: "Qux", digits: [16, 1], required: true, default: 1.5, }); turtle_description = fields.Text({ string: "Description" }); turtle_trululu = fields.Many2one({ relation: "partner" }); turtle_ref = fields.Reference({ selection: [ ["product", "Product"], ["partner", "Partner"], ], }); product_id = fields.Many2one({ relation: "product", required: true }); partner_ids = fields.Many2many({ 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", turtle_int: 9, partner_ids: [2, 4], }, { id: 3, name: "raphael", product_id: 37, turtle_bar: false, turtle_foo: "kawa", turtle_int: 21, turtle_qux: 9.8, partner_ids: [], turtle_ref: "product,37", }, ]; } 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: 17, name: "Aline", partner_ids: [1, 2], }, { id: 19, name: "Christine", }, ]; } defineModels([Partner, PartnerType, Product, Turtle, Users]); test("New record with a o2m also with 2 new records, ordered, and resequenced", async () => { // Needed to have two new records in a single stroke Partner._onChanges = { foo: function (obj) { obj.p = [ [0, 0, { trululu: false }], [0, 0, { trululu: false }], ]; }, }; let startAssert = false; onRpc((args) => { if (startAssert) { expect.step(args.method + " " + args.model); } }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); startAssert = true; await contains(".o_control_panel_main_buttons .o_form_button_create").click(); // change the int_field through drag and drop // that way, we'll trigger the sorting and the name read // of the lines of "p" await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop("tbody tr"); expect.verifySteps(["onchange partner"]); }); test.tags("desktop"); test("resequence with NULL value", async () => { mockService("action", { doActionButton(params) { params.onClose(); }, }); Partner._records.push( { id: 10, int_field: 1 }, { id: 11, int_field: 2 }, { id: 12, int_field: 3 }, { id: 13 } ); Partner._records[0].p = [10, 11, 12, 13]; const serverValues = { 10: 1, 11: 2, 12: 3, 13: false, }; onRpc("web_read", function ({ parent }) { const res = parent(); const getServerValue = (record) => serverValues[record.id] === false ? Number.MAX_SAFE_INTEGER : serverValues[record.id]; // when sorted, NULL values are last res[0].p.sort((a, b) => getServerValue(a) - getServerValue(b)); return res; }); onRpc("web_save", ({ args }) => { args[1].p.forEach(([cmd, id, values]) => { serverValues[id] = values.int_field; }); }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(queryAllTexts(".o_field_cell[name=id]")).toEqual(["10", "11", "12", "13"]); await contains("tbody tr:nth-child(4) .o_handle_cell").dragAndDrop("tbody tr:nth-child(3)"); expect(queryAllTexts(".o_field_cell[name=id]")).toEqual(["10", "11", "13", "12"]); await contains("button.reload").click(); expect(queryAllTexts(".o_field_cell[name=id]")).toEqual(["10", "11", "13", "12"]); }); test.tags("desktop"); test("one2many in a list x2many editable use the right context", async () => { onRpc("name_create", (args) => { expect.step(`name_create ${args.kwargs.context.my_context}`); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_field_x2many_list .o_field_x2many_list_row_add a").click(); await contains("[name='trululu'] input").edit("new partner"); await selectFieldDropdownItem("trululu", 'Create "new partner"'); expect.verifySteps(["name_create list"]); }); test.tags("desktop"); test("one2many in a list x2many non-editable use the right context", async () => { onRpc("name_create", (args) => { expect.step(`name_create ${args.kwargs.context.my_context}`); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_field_x2many_list .o_field_x2many_list_row_add a").click(); await contains("[name='trululu'] input").edit("new partner"); await selectFieldDropdownItem("trululu", 'Create "new partner"'); expect.verifySteps(["name_create form"]); }); test("O2M field without relation_field", async () => { delete Partner._fields.p.relation_field; Partner._records[0].p = [2, 4]; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_field_x2many_list .o_field_x2many_list_row_add a").click(); expect(".o_dialog").toHaveCount(1); }); 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: {}, p: {}, }); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); }); test("O2M List with pager, decoration and default_order: add and cancel adding", async () => { // The decoration on the list implies that its condition will be evaluated // against the data of the field (actual records *displayed*) // If one data is wrongly formed, it will crash // This test adds then cancels a record in a paged, ordered, and decorated list // That implies prefetching of records for sorting // and evaluation of the decoration against *visible records* Partner._records[0].p = [2, 4]; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_field_x2many_list .o_field_x2many_list_row_add a").click(); expect(".o_field_x2many_list .o_data_row").toHaveCount(2); expect(queryOne(".o_selected_row")).toBe(queryOne(".o_field_x2many_list .o_data_row:eq(1)"), { message: "The selected row should be the new one", }); // Cancel Creation await press("escape"); await animationFrame(); expect(".o_field_x2many_list .o_data_row").toHaveCount(1); }); test.tags("desktop"); test("O2M with parented m2o and domain on parent.m2o", async () => { expect.assertions(4); // Records in an o2m can have a m2o pointing to themselves. // In that case, a domain evaluation on that field followed by name_search // shouldn't send virtual_ids to the server. Turtle._fields.parent_id = fields.Many2one({ string: "Parent", relation: "turtle", }); Turtle._views = { form: `
`, }; onRpc("name_search", ({ kwargs }) => { expect(kwargs.args).toEqual([["id", "in", []]]); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_x2many_list_row_add a").click(); await clickFieldDropdown("parent_id"); await contains(".o_field_widget[name=parent_id] input").edit("ABC", { confirm: false }); await runAllTimers(); await clickFieldDropdownItem("parent_id", "Create and edit..."); await contains(".o_dialog:not(.o_inactive_modal) .modal-footer .o_form_button_save").click(); await contains(".o_dialog:not(.o_inactive_modal) .o_form_button_save_new").click(); expect(".o_data_row").toHaveCount(1); await contains(".o_field_many2one input").click(); }); test.tags("desktop"); test('O2M with buttons with attr "special" in dialog close the dialog', async () => { await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_x2many_list_row_add a").click(); expect(".o_dialog").toHaveCount(1); expect(".modal .btn").toHaveText("Cancel"); await contains(".modal .btn").click(); expect(".o_dialog").toHaveCount(0); }); test.tags("desktop"); test("O2M modal buttons are disabled on click", async () => { // Records in an o2m can have a m2o pointing to themselves. // In that case, a domain evaluation on that field followed by name_search // shouldn't send virtual_ids to the server Turtle._fields.parent_id = fields.Many2one({ string: "Parent", relation: "turtle", }); Turtle._views = { form: `
`, }; const def = new Deferred(); onRpc("web_save", () => def); await mountView({ type: "form", resModel: "partner", arch: `
`, }); await contains(".o_field_x2many_list_row_add a").click(); await clickFieldDropdown("parent_id"); await contains(".o_field_widget[name=parent_id] input").edit("ABC", { confirm: false }); await runAllTimers(); await clickFieldDropdownItem("parent_id", "Create and edit..."); await contains(".o_dialog:not(.o_inactive_modal) .modal-footer .o_form_button_save").click(); expect(".o_dialog:not(.o_inactive_modal) .modal-footer .o_form_button_save").not.toBeEnabled(); def.resolve(); await animationFrame(); // close all dialogs await contains(".o_dialog:not(.o_inactive_modal) .modal-footer .o_form_button_save").click(); await animationFrame(); expect(".o_dialog .o_form_view").toHaveCount(0); }); test.tags("desktop"); test("clicking twice on a record in a one2many will open it once", async () => { Turtle._views = { form: `
`, }; const def = new Deferred(); let firstRead = true; onRpc("turtle", "web_read", async ({ model }) => { expect.step("web_read turtle"); if (!firstRead) { await def; } firstRead = false; }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await contains(".o_data_cell").click(); await contains(".o_data_cell").click(); def.resolve(); await animationFrame(); expect(".modal").toHaveCount(1); await contains(".modal .btn-close").click(); expect(".modal").toHaveCount(0); await contains(".o_data_cell").click(); expect(".modal").toHaveCount(1); expect.verifySteps(["web_read turtle"]); }); test("resequence a x2m in a form view dialog from another x2m", async () => { onRpc((args) => { expect.step(args.method); }); onRpc("write", (args) => { expect(Object.keys(args.args[1])).toEqual(["turtles"]); expect(args.args[1].turtles).toHaveLength(1); expect(args.args[1].turtles[0]).toEqual([ 1, 2, { partner_ids: [ [1, 2, { int_field: 0 }], [1, 4, { int_field: 1 }], ], }, ]); }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect.verifySteps(["get_views", "web_read"]); await contains(".o_data_cell").click(); expect(".modal").toHaveCount(1); expect(queryAllTexts(".modal [name='name']")).toEqual(["aaa", "second record"]); expect.verifySteps(["web_read"]); await contains(".modal tr:eq(2) .o_handle_cell").dragAndDrop(".modal [name='name']:eq(0)"); expect(queryAllTexts(".modal [name='name']")).toEqual(["second record", "aaa"]); expect.verifySteps([]); await contains(".modal .o_form_button_save").click(); await clickSave(); expect.verifySteps(["web_save"]); }); test("one2many list editable with cell readonly modifier", async () => { expect.assertions(3); Partner._records[0].p = [2]; Partner._records[1].turtles = [1, 2]; onRpc("web_save", (args) => { expect(args.args[1].p[0][2]).toEqual( { foo: "ff", qux: 99, turtles: [] }, { message: "The right values should be written" } ); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_field_x2many_list_row_add a").click(); expect(".o_selected_row [name=foo] input").toBeFocused({ message: "The first input of the line should have the focus", }); // Simulating hitting the 'f' key twice await contains(".o_selected_row [name=foo] input").edit("f", { confirm: false }); await contains(".o_selected_row [name=foo] input").edit("ff", { confirm: false }); expect(".o_selected_row [name=foo] input").toBeFocused({ message: "The first input of the line should still have the focus", }); // Simulating a TAB key await press("Tab"); await animationFrame(); await contains(".o_selected_row [name=qux] input").edit(9, { confirm: false }); await contains(".o_selected_row [name=qux] input").edit(99); await clickSave(); }); test("one2many wait for the onchange of the resequenced finish before save", async () => { expect.assertions(2); Partner._records[0].p = [1, 2]; Partner._onChanges = { p: function (obj) { obj.p = [[1, 2, { qux: 99 }]]; }, }; const def = new Deferred(); onRpc("onchange", async () => { await def; expect.step("onchange"); }); onRpc("web_save", (args) => { expect.step("web_save"); expect(args.args[1].p).toEqual([ [1, 1, { int_field: 9 }], [1, 2, { int_field: 10, qux: 99 }], ]); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); // Drag and drop the second line in first position await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop("tbody tr"); await clickSave(); // resolve the onchange promise def.resolve(); await animationFrame(); expect.verifySteps(["onchange", "web_save"]); }); test("one2many basic properties", async () => { Partner._records[0].p = [2]; onRpc((args) => { expect.step(args.method); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect.verifySteps(["get_views", "web_read"]); expect(".o_field_x2many_list_row_add").toHaveCount(1); expect(".o_field_x2many_list_row_add").toHaveAttribute("colspan", "2"); expect("td.o_list_record_remove").toHaveCount(1); }); test("transferring class attributes in one2many sub fields", async () => { await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect("td.hey").toHaveCount(1); await contains("td.o_data_cell").click(); expect('td.hey div[name="turtle_foo"] input').toHaveCount(1); // WOWL to check! hey on input? }); test("one2many with date and datetime", async () => { mockTimeZone(+2); Partner._records[0].p = [2]; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect("td:eq(0)").toHaveText("01/25/2017"); expect("td:eq(1)").toHaveText("12/12/2016 12:55:05"); }); test("rendering with embedded one2many", async () => { Partner._records[0].p = [2]; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect("thead th:eq(0)").toHaveText("Foo"); expect("tbody td:eq(0)").toHaveText("blip"); }); test("use the limit attribute in arch (in field o2m inline list view)", async () => { Partner._records[0].turtles = [1, 2, 3]; onRpc("turtle", (args) => { expect(args.args[0]).toEqual([1, 2]); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_data_row").toHaveCount(2); }); test.tags("desktop"); test("nested x2manys with inline form, but not list", async () => { Turtle._views = { list: `` }; Partner._views = { list: ``, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_form_view").toHaveCount(1); expect(".o_data_row").toHaveCount(1); await contains(".o_data_row .o_data_cell").click(); expect(".o_dialog").toHaveCount(1); expect(".o_dialog .o_data_row").toHaveCount(2); }); test.tags("desktop"); test("use the limit attribute in arch (in field o2m non inline list view)", async () => { Partner._records[0].turtles = [1, 2, 3]; Turtle._views = { list: `` }; onRpc((args) => { expect.step(args.method); }); onRpc("web_read", (args) => { expect(args.kwargs.specification).toEqual({ display_name: {}, turtles: { fields: { turtle_foo: {}, }, limit: 2, order: "", }, }); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_data_row").toHaveCount(2); expect.verifySteps(["get_views", "get_views", "web_read"]); }); test.tags("desktop"); test("one2many with default_order on view not inline", async () => { Partner._records[0].turtles = [1, 2, 3]; Turtle._views = { list: ` `, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(queryAllTexts(".o_field_one2many .o_data_cell")).toEqual([ "9", "blip", "21", "kawa", "0", "yop", ]); }); test.tags("desktop"); test("embedded one2many with widget", async () => { Partner._records[0].p = [2]; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect("span.o_row_handle").toHaveCount(1); }); test.tags("desktop"); test("embedded one2many with handle widget", async () => { Partner._records[0].turtles = [1, 2, 3]; Partner._onChanges = { turtles: function () {}, }; onRpc("onchange", () => { expect.step("onchange"); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["yop", "blip", "kawa"]); // Drag and drop the second line in first position await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop("tbody tr"); expect.verifySteps(["onchange"]); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blip", "yop", "kawa"]); await clickSave(); expect( Turtle._records.map((r) => { return { id: r.id, turtle_foo: r.turtle_foo, turtle_int: r.turtle_int, }; }) ).toEqual([ { id: 1, turtle_foo: "yop", turtle_int: 1 }, { id: 2, turtle_foo: "blip", turtle_int: 0 }, { id: 3, turtle_foo: "kawa", turtle_int: 21 }, ]); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blip", "yop", "kawa"]); }); test.tags("desktop"); test("onchange for embedded one2many in a one2many", async () => { expect.assertions(3); Turtle._fields.partner_ids = fields.One2many({ relation: "partner" }); Turtle._records[0].partner_ids = [1]; Partner._records[0].turtles = [1]; Partner._onChanges = { turtles: function (obj) { obj.turtles = [ [ 1, 1, { partner_ids: [[4, 2]], }, ], ]; }, }; onRpc("web_save", (args) => { expect(args.args[1].turtles).toEqual([ [1, 1, { turtle_foo: "hop", partner_ids: [[4, 2]] }], ]); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_field_many2many_tags").toHaveText("first record"); await contains(".o_data_cell:eq(1)").click(); await contains(".o_selected_row .o_field_widget[name=turtle_foo] input").edit("hop", { confirm: "blur", }); expect(".o_field_many2many_tags").toHaveText("first record\nsecond record"); await clickSave(); }); test("onchange for embedded one2many in a one2many with a second page", async () => { Turtle._fields.partner_ids = fields.One2many({ relation: "partner" }); Turtle._records[0].partner_ids = [1]; // we need a second page, so we set two records and only display one per page Partner._records[0].turtles = [1, 2]; Partner._onChanges = { turtles: function (obj) { obj.turtles = [ [ 1, 1, { partner_ids: [[4, 2]], }, ], [ 1, 2, { turtle_foo: "blip", partner_ids: [[4, 1]], }, ], ]; }, }; onRpc("web_save", (args) => { expect(args.args[1].turtles).toEqual([ [1, 1, { turtle_foo: "hop", partner_ids: [[4, 2]] }], [ 1, 2, { partner_ids: [[4, 1]], turtle_foo: "blip", }, ], ]); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_data_cell:eq(1)").click(); await contains(".o_selected_row .o_field_widget[name=turtle_foo] input").edit("hop", { confirm: "blur", }); await clickSave(); }); test("onchange for embedded one2many in a one2many updated by server", async () => { // here we test that after an onchange, the embedded one2many field has // been updated by a new list of ids by the server response, to this new // list should be correctly sent back at save time expect.assertions(3); Turtle._fields.partner_ids = fields.One2many({ relation: "partner" }); Partner._records[0].turtles = [2]; Turtle._records[1].partner_ids = [2]; Partner._onChanges = { turtles: function (obj) { obj.turtles = [ [ 1, 2, { partner_ids: [[4, 4]], }, ], ]; }, }; onRpc("web_save", (args) => { expect(args.args[1].turtles).toEqual( [ [ 1, 2, { partner_ids: [[4, 4]], turtle_foo: "hop", }, ], ], { message: "The right values should be written", } ); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(queryAllTexts(".o_data_cell.o_many2many_tags_cell .o_tag_badge_text")).toEqual([ "second record", ]); await contains(".o_data_cell:eq(1)").click(); await contains(".o_selected_row [name=turtle_foo] input").edit("hop", { confirm: "blur", }); await clickSave(); expect(queryAllTexts(".o_data_cell.o_many2many_tags_cell .o_tag_badge_text")).toEqual([ "second record", "aaa", ]); }); test("onchange for embedded one2many with handle widget", async () => { Partner._records[0].turtles = [1, 2, 3]; let partnerOnchange = 0; Partner._onChanges = { turtles: function () { partnerOnchange++; }, }; let turtleOnchange = 0; Turtle._onChanges = { turtle_int: function () { turtleOnchange++; }, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["yop", "blip", "kawa"]); // Drag and drop the second line in first position await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop("tbody tr"); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blip", "yop", "kawa"]); expect(turtleOnchange).toBe(2, { message: "should trigger one onchange per line updated" }); expect(partnerOnchange).toBe(1, { message: "should trigger only one onchange on the parent" }); }); test("onchange for embedded one2many with handle widget using same sequence", async () => { Turtle._records[0].turtle_int = 1; Turtle._records[1].turtle_int = 1; Turtle._records[2].turtle_int = 1; Partner._records[0].turtles = [1, 2, 3]; let turtleOnchange = 0; Turtle._onChanges = { turtle_int: function () { turtleOnchange++; }, }; onRpc("write", (args) => { expect(args.args[1].turtles).toEqual( [ [1, 2, { turtle_int: 1 }], [1, 1, { turtle_int: 2 }], [1, 3, { turtle_int: 3 }], ], { message: "should change all lines that have changed (the first one doesn't change because it has the same sequence)", } ); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["yop", "blip", "kawa"]); // Drag and drop the second line in first position await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop("tbody tr"); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blip", "yop", "kawa"]); expect(turtleOnchange).toBe(3, { message: "should update all lines" }); await clickSave(); }); test("onchange for embedded one2many with handle widget (more records)", async () => { const ids = []; for (let i = 10; i < 50; i++) { const id = 10 + i; ids.push(id); Turtle._records.push({ id: id, turtle_int: 0, turtle_foo: "#" + id, }); } ids.push(1, 2, 3); Partner._records[0].turtles = ids; Partner._onChanges = { turtles: function (obj) {}, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains("div[name=turtles] .o_pager_next").click(); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["yop", "blip", "kawa"]); await contains(".o_data_cell.o_list_char").click(); await contains('.o_list_renderer div[name="turtle_foo"] input').edit("blurp"); // Drag and drop the third line in second position await contains("tbody tr:eq(2) .o_handle_cell").dragAndDrop("tbody tr:eq(1)"); // need to unselect row... expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blurp", "kawa", "blip"]); await clickSave(); await contains('div[name="turtles"] .o_pager_next').click(); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blurp", "kawa", "blip"]); }); test("onchange with modifiers for embedded one2many on the second page", async () => { const ids = []; for (let i = 10; i < 60; i++) { const id = 10 + i; ids.push(id); Turtle._records.push({ id: id, turtle_int: 0, turtle_foo: "#" + id, }); } ids.push(1, 2, 3); Partner._records[0].turtles = ids; Partner._onChanges = { turtles: function (obj) {}, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); const getTurtleFooValues = () => { return queryAllTexts(".o_data_cell.o_list_char").join(""); }; expect(getTurtleFooValues()).toBe("#20#21#22#23#24#25#26#27#28#29"); await contains(".o_data_cell.o_list_char").click(); await contains("div[name=turtle_foo] input").edit("blurp"); // click outside of the one2many to unselect the row await contains(".o_form_view").click(); expect(getTurtleFooValues()).toBe("blurp#21#22#23#24#25#26#27#28#29"); // the domain fail if the widget does not use the already loaded data. await contains(".o_form_button_cancel").click(); expect(".modal").toHaveCount(0); expect(getTurtleFooValues()).toBe("#20#21#22#23#24#25#26#27#28#29"); // Drag and drop the third line in second position await contains("tbody tr:eq(2) .o_handle_cell").dragAndDrop("tbody tr:eq(1)"); expect(getTurtleFooValues()).toBe("#20#30#31#32#33#34#35#36#37#38"); // Drag and drop the third line in second position await contains("tbody tr:eq(2) .o_handle_cell").dragAndDrop("tbody tr:eq(1)"); expect(getTurtleFooValues()).toBe("#20#39#40#41#42#43#44#45#46#47"); await contains(".o_form_view").click(); expect(getTurtleFooValues()).toBe("#20#39#40#41#42#43#44#45#46#47"); await contains(".o_form_button_cancel").click(); expect(".modal").toHaveCount(0); expect(getTurtleFooValues()).toBe("#20#21#22#23#24#25#26#27#28#29"); }); test("onchange followed by edition on the second page", async () => { const ids = []; for (let i = 1; i < 85; i++) { const id = 10 + i; ids.push(id); Turtle._records.push({ id: id, turtle_int: (id / 3) | 0, turtle_foo: "#" + i, }); } ids.splice(41, 0, 1, 2, 3); Partner._records[0].turtles = ids; Partner._onChanges = { turtles: function (obj) {}, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_field_widget[name=turtles] .o_pager_next").click(); await contains(".o_field_one2many .o_list_renderer tbody tr td.o_handle_cell:eq(1)").click(); await contains('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"] input').edit( "value 1" ); await contains(".o_field_one2many .o_list_renderer tbody tr td.o_handle_cell:eq(2)").click(); await contains('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"] input').edit( "value 2" ); expect(".o_data_row").toHaveCount(40); expect(".o_field_one2many .o_list_renderer .o_data_cell.o_list_char:eq(0)").toHaveText("#39", { message: "should display '#39' at the first line", }); await contains(".o_field_x2many_list_row_add a").click(); expect(".o_data_row").toHaveCount(40, { message: "should display 39 records and the create line", }); expect(".o_data_row:eq(0)").toHaveClass("o_selected_row", { message: "should display the create line in first position", }); expect('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"]').toHaveText("", { message: "should be an empty input", }); expect(".o_field_one2many .o_list_renderer .o_data_cell.o_list_char:eq(1)").toHaveText("#39"); await contains(".o_data_row input").edit("value 3", { confirm: "blur" }); expect(".o_data_row:eq(0)").toHaveClass(["o_data_row", "o_row_draggable"]); expect(".o_field_one2many .o_list_renderer .o_data_cell.o_list_char:eq(1)").toHaveText("#39"); await contains(".o_field_x2many_list_row_add a").click(); expect(".o_data_row").toHaveCount(40, { message: "should display 39 records and the create line", }); expect(".o_field_one2many .o_list_renderer .o_data_cell.o_list_char:eq(1)").toHaveText( "value 3" ); expect(".o_field_one2many .o_list_renderer .o_data_cell.o_list_char:eq(2)").toHaveText("#39"); }); test("onchange followed by edition on the second page (part 2)", async () => { const ids = []; for (let i = 1; i < 85; i++) { const id = 10 + i; ids.push(id); Turtle._records.push({ id: id, turtle_int: (id / 3) | 0, turtle_foo: "#" + i, }); } ids.splice(41, 0, 1, 2, 3); Partner._records[0].turtles = ids; Partner._onChanges = { turtles: function (obj) {}, }; // bottom order await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_field_widget[name=turtles] .o_pager_next").click(); await contains(".o_field_one2many .o_list_renderer tbody tr td.o_handle_cell:eq(1)").click(); await contains('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"] input').edit( "value 1", { confirm: "blur" } ); await contains(".o_field_one2many .o_list_renderer tbody tr td.o_handle_cell:eq(2)").click(); await contains('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"] input').edit( "value 2", { confirm: "blur" } ); expect(".o_data_row").toHaveCount(40, { message: "should display 40 records" }); expect(".o_field_one2many .o_list_renderer tbody .o_data_cell.o_list_char:eq(0)").toHaveText( "#39", { message: "should display '#39' at the first line", } ); expect(".o_field_one2many .o_list_renderer tbody .o_data_cell.o_list_char:eq(39)").toHaveText( "#77", { message: "should display '#77' at the last line" } ); await contains(".o_field_x2many_list_row_add a").click(); expect(".o_data_row").toHaveCount(41, { message: "should display 41 records and the create line", }); expect(".o_field_one2many .o_list_renderer tbody .o_data_cell.o_list_char:eq(39)").toHaveText( "#77", { message: "should display '#77' at the penultimate line" } ); expect(".o_data_row:eq(40)").toHaveClass("o_selected_row", { message: "should display the create line in first position", }); await contains('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"] input').edit( "value 3", { confirm: "blur" } ); await contains(".o_field_x2many_list_row_add a").click(); expect(".o_data_row").toHaveCount(42, { message: "should display 42 records and the create line", }); expect(".o_field_one2many .o_list_renderer tbody .o_data_cell.o_list_char:eq(40)").toHaveText( "value 3" ); expect(".o_field_one2many .o_list_renderer tbody .o_data_cell.o_list_char:eq(41)").toHaveText( "" ); expect(".o_data_row:eq(41)").toHaveClass("o_selected_row", { message: "should display the create line in first position", }); }); test("onchange returning a commands 4 for an x2many", async () => { Partner._onChanges = { foo(obj) { obj.turtles = [ [4, 1], [4, 3], ]; }, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_data_row").toHaveCount(1); // change the value of foo to trigger the onchange await contains(".o_field_widget[name=foo] input").edit("some value"); expect(".o_data_row").toHaveCount(3); }); test("x2many fields inside x2manys are fetched after an onchange", async () => { expect.assertions(5); Turtle._records[0].partner_ids = [1]; Partner._onChanges = { foo: function (obj) { obj.turtles = [ [3, 2], [4, 1], [4, 2], [4, 3], ]; }, }; onRpc("onchange", (args) => { expect(args.args[3]).toEqual({ // spec display_name: {}, foo: {}, turtles: { fields: { partner_ids: { fields: { display_name: {}, }, }, turtle_foo: {}, }, limit: 40, order: "", }, }); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_data_row").toHaveCount(1); expect(".o_data_row .o_field_widget[name=partner_ids]").toHaveText("second record\naaa"); // change the value of foo to trigger the onchange await contains(".o_field_widget[name=foo] input").edit("some value"); expect(".o_data_row").toHaveCount(3, { message: "there should be three records in the relation", }); expect(".o_data_row .o_field_widget[name=partner_ids]:eq(0)").toHaveText("first record"); }); test("reference fields inside x2manys are fetched after an onchange", async () => { expect.assertions(4); Turtle._records[1].turtle_ref = "product,41"; Partner._onChanges = { foo: function (obj) { obj.turtles = [ [4, 1], [4, 3], ]; }, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_data_row").toHaveCount(1); expect(queryAllTexts(".ref_field")).toEqual(["xpad"]); // change the value of foo to trigger the onchange await contains(".o_field_widget[name=foo] input").edit("some value"); expect(".o_data_row").toHaveCount(3); expect(queryAllTexts(".ref_field")).toEqual(["xpad", "", "xphone"]); }); test.tags("desktop"); test("onchange on one2many containing x2many in form view", async () => { Partner._onChanges = { foo: function (obj) { obj.turtles = [[0, false, { turtle_foo: "new record" }]]; }, }; Partner._views = { list: '', search: "" }; await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect(".o_data_row").toHaveCount(1, { message: "the onchange should have created one record in the relation", }); // open the created o2m record in a form view, and add a m2m subrecord // in its relation await contains(".o_data_row .o_data_cell").click(); expect(".modal").toHaveCount(1); expect(".modal .o_data_row").toHaveCount(0); // add a many2many subrecord await contains(".modal .o_field_x2many_list_row_add a").click(); expect(".modal").toHaveCount(2, { message: "should have opened a second dialog" }); // select a many2many subrecord await contains(".modal:eq(1) .o_list_view .o_data_cell").click(); expect(".modal").toHaveCount(1); expect(".modal .o_data_row").toHaveCount(1); expect(".modal .o_x2m_control_panel .o_pager").toHaveCount(0, { message: "m2m pager should be hidden", }); // click on 'Save & Close' await contains(".modal-footer .btn-primary").click(); expect(".modal").toHaveCount(0, { message: "dialog should be closed" }); // reopen o2m record, and another m2m subrecord in its relation, but // discard the changes await contains(".o_data_row .o_data_cell").click(); expect(".modal").toHaveCount(1); expect(".modal .o_data_row").toHaveCount(1); // add another m2m subrecord await contains(".modal .o_field_x2many_list_row_add a").click(); expect(".modal").toHaveCount(2, { message: "should have opened a second dialog" }); await contains(".modal:eq(1) .o_list_view .o_data_cell").click(); expect(".modal").toHaveCount(1, { message: "second dialog should be closed" }); expect(".modal .o_data_row").toHaveCount(2, { message: "there should be two records in the one2many in the dialog", }); // click on 'Discard' await contains(".modal-footer .btn-secondary").click(); expect(".modal").toHaveCount(0, { message: "dialog should be closed" }); // reopen o2m record to check that second changes have properly been discarded await contains(".o_data_row .o_data_cell").click(); expect(".modal").toHaveCount(1); expect(".modal .o_data_row").toHaveCount(1); }); test.tags("desktop"); test("onchange on one2many with x2many in list (no widget) and form view (list)", async () => { expect.assertions(7); Turtle._fields.turtle_foo = fields.Char({ default: "a default value" }); Partner._onChanges = { foo: function (obj) { obj.p = [[0, false, { turtles: [[0, false, { turtle_foo: "hello" }]] }]]; }, }; onRpc("partner", "onchange", ({ args }) => { expect(args[3]).toEqual({ display_name: {}, foo: {}, p: { fields: { turtles: { fields: { turtle_foo: {}, }, }, }, limit: 40, order: "", }, }); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect(".o_data_row").toHaveCount(1, { message: "the onchange should have created one record in the relation", }); // open the created o2m record in a form view await contains(".o_data_row .o_data_cell").click(); expect(".modal").toHaveCount(1); expect(".modal .o_data_row").toHaveCount(1); expect(".modal .o_data_row").toHaveText("hello"); // add a one2many subrecord and check if the default value is correctly applied await contains(".modal .o_field_x2many_list_row_add a").click(); expect(".modal .o_data_row").toHaveCount(2); expect(".modal .o_data_row .o_field_widget[name=turtle_foo] input").toHaveValue( "a default value" ); }); test("save an o2m dialog form view and discard main form view", async () => { await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(".o_data_row").toHaveCount(1); expect(".o_data_row [name='name']").toHaveText("donatello"); await contains(".o_data_row .o_data_cell").click(); expect(".modal [name='name'] input").toHaveValue("donatello"); await contains(".modal [name='name'] input").edit("leonardo"); await contains(".modal .o_form_button_save").click(); expect(".modal").toHaveCount(0); expect(".o_data_row [name='name']").toHaveText("leonardo"); await contains(".o_data_row .o_data_cell").click(); await contains(".modal .o_form_button_cancel").click(); expect(".o_data_row [name='name']").toHaveText("leonardo"); await contains(".o_form_button_cancel").click(); expect(".o_data_row [name='name']").toHaveText("donatello"); await contains(".o_data_row .o_data_cell").click(); expect(".modal [name='name'] input").toHaveValue("donatello"); }); test("discard with nested o2m form view dialog", async () => { Partner._records[0].p = [2]; Partner._records[1].p = [4]; await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(".o_data_row").toHaveCount(1); expect(".o_data_row [name='name']").toHaveText("second record"); await contains(".o_data_row .o_data_cell").click(); expect("#dialog_0 [name='name'] input").toHaveValue("second record"); await contains("#dialog_0 .o_data_row .o_data_cell").click(); expect("#dialog_1 [name='name'] input").toHaveValue("aaa"); await contains("#dialog_1 [name='name'] input").edit("leonardo"); await contains("#dialog_1 .o_form_button_save").click(); expect("#dialog_1").toHaveCount(0); expect("#dialog_0 .o_data_row [name='name']").toHaveText("leonardo"); await contains("#dialog_0 .o_data_row .o_data_cell").click(); expect("#dialog_2 [name='name'] input").toHaveValue("leonardo"); await contains("#dialog_2 .o_form_button_cancel").click(); await contains("#dialog_0 .o_form_button_cancel").click(); await contains(".o_data_row .o_data_cell").click(); expect(".modal .o_data_row [name='name']").toHaveText("aaa"); }); test("discard a form dialog view and then reopen it with a domain based on a text field", async () => { Turtle._records[1].turtle_foo = "yop"; Turtle._views = { form: `
`, }; await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(".o_data_row").toHaveCount(1); expect(".o_data_row [name='name']").toHaveText("donatello"); await contains(".o_data_row .o_data_cell").click(); expect(".modal [name='name']").toHaveCount(0); expect(".modal [name='turtle_foo'] input").toHaveValue("yop"); await contains(".modal [name='turtle_foo'] input").edit("display"); expect(".modal [name='name'] input").toHaveValue("donatello"); expect(".modal [name='turtle_foo'] input").toHaveValue("display"); await contains(".modal .o_form_button_save").click(); await contains(".o_form_button_cancel").click(); await contains(".o_data_row .o_data_cell").click(); expect(".modal [name='name']").toHaveCount(0); expect(".modal [name='turtle_foo'] input").toHaveValue("yop"); }); test("onchange on one2many with x2many in list (many2many_tags) and form view (list)", async () => { expect.assertions(7); Turtle._fields.turtle_foo = fields.Char({ default: "a default value" }); Partner._onChanges = { foo: function (obj) { obj.p = [[0, false, { turtles: [[0, false, { turtle_foo: "hello" }]] }]]; }, }; onRpc("partner", "onchange", ({ args }) => { expect(args[3]).toEqual({ display_name: {}, foo: {}, p: { fields: { turtles: { fields: { display_name: {}, turtle_foo: {}, }, }, }, limit: 40, order: "", }, }); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect(".o_data_row").toHaveCount(1, { message: "the onchange should have created one record in the relation", }); // open the created o2m record in a form view await contains(".o_data_row .o_data_cell").click(); expect(".modal").toHaveCount(1); expect(".modal .o_data_row").toHaveCount(1); expect(".modal .o_data_row").toHaveText("hello"); // add a one2many subrecord and check if the default value is correctly applied await contains(".modal .o_field_x2many_list_row_add a").click(); expect(".modal .o_data_row").toHaveCount(2); expect(".modal .o_data_row .o_field_widget[name=turtle_foo] input").toHaveValue( "a default value" ); }); test("embedded one2many with handle widget with minimum setValue calls", async () => { Turtle._records[0].turtle_int = 6; Turtle._records.push( { id: 4, turtle_int: 20, turtle_foo: "a1", }, { id: 5, turtle_int: 9, turtle_foo: "a2", }, { id: 6, turtle_int: 2, turtle_foo: "a3", }, { id: 7, turtle_int: 11, turtle_foo: "a4", } ); Partner._records[0].turtles = [1, 2, 3, 4, 5, 6, 7]; patchWithCleanup(Record.prototype, { _update() { if (this.resModel === "turtle") { expect.step(`${this.resId}`); } return super._update(...arguments); }, }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(queryAllTexts(".o_data_row [name='turtle_foo']")).toEqual([ "a3", "yop", "blip", "a2", "a4", "a1", "kawa", ]); const positions = [ [6, 0, ["3", "6", "1", "2", "5", "7", "4"]], // move the last to the first line [5, 1, ["7", "6", "1", "2", "5"]], // move the penultimate to the second line [2, 5, ["1", "2", "5", "6"]], // move the third to the penultimate line ]; for (const [sourceIndex, targetIndex, steps] of positions) { await contains(`tbody tr:eq(${sourceIndex}) .o_handle_cell`).dragAndDrop( `tbody tr:eq(${targetIndex})` ); expect.verifySteps(steps); } expect(queryAllTexts(".o_data_row [name='turtle_foo']")).toEqual([ "kawa", "a4", "yop", "blip", "a2", "a3", "a1", ]); }); test("embedded one2many (editable list) with handle widget", async () => { Partner._records[0].p = [1, 2, 4]; onRpc("web_save", (args) => { expect.step(args.method); expect(args.args[1].p).toEqual([ [1, 2, { int_field: 0 }], [1, 4, { int_field: 1 }], ]); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([ "My little Foo Value", "blip", "yop", ]); expect.verifySteps([]); // Drag and drop the second line in first position await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop(".o_field_one2many tbody tr:eq(0)"); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([ "blip", "My little Foo Value", "yop", ]); await contains(".o_data_cell.o_list_char").click(); expect(".o_field_widget[name=foo] input").toHaveValue("blip"); expect.verifySteps([]); await clickSave(); expect.verifySteps(["web_save"]); expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([ "blip", "My little Foo Value", "yop", ]); }); test("one2many list order with handle widget", async () => { onRpc("web_read", (args) => { expect.step(`web_read`); expect(args.kwargs.specification.p.order).toBe("int_field ASC, id ASC"); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect.verifySteps(["web_read"]); }); test("one2many field when using the pager", async () => { const ids = []; for (let i = 0; i < 45; i++) { const id = 10 + i; ids.push(id); Partner._records.push({ id, name: `relational record ${id}`, }); } Partner._records[0].p = ids.slice(0, 42); Partner._records[1].p = ids.slice(42); onRpc("web_read", (args) => { expect.step(`unity read ${args.args[0]}`); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, resIds: [1, 2], }); expect.verifySteps(["unity read 1"]); expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(40); // move to record 2, which has 3 related records (and shouldn't contain the // related records of record 1 anymore) await contains(".o_form_view .o_control_panel .o_pager_next").click(); expect.verifySteps(["unity read 2"]); expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(3); // move back to record 1, which should contain again its first 40 related // records await contains(".o_form_view .o_control_panel .o_pager_previous").click(); expect.verifySteps(["unity read 1"]); expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(40); // move to the second page of the o2m: 1 RPC should have been done to fetch // the 2 subrecords of page 2, and those records should now be displayed await contains(".o_x2m_control_panel .o_pager_next").click(); expect.verifySteps(["unity read 50,51"]); expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2); // move to record 2 again and check that everything is correctly updated await contains(".o_form_view .o_control_panel .o_pager_next").click(); expect.verifySteps(["unity read 2"]); expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(3); // move back to record 1 and move to page 2 again: all data should have // been correctly reloaded await contains(".o_form_view .o_control_panel .o_pager_previous").click(); expect.verifySteps(["unity read 1"]); await contains(".o_x2m_control_panel .o_pager_next").click(); expect.verifySteps(["unity read 50,51"]); expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2); }); test("edition of one2many field with pager", async () => { const ids = []; for (let i = 0; i < 45; i++) { const id = 10 + i; ids.push(id); Partner._records.push({ id: id, name: "relational record " + id, }); } Partner._records[0].p = ids; Partner._views = { form: '
' }; let saveCount = 0; let checkRead = false; let readIDs; onRpc("web_read", (args) => { if (checkRead) { readIDs = args.args[0]; checkRead = false; } }); onRpc("web_save", (args) => { expect.step("web_save"); saveCount++; const commands = args.args[1].p; switch (saveCount) { case 1: expect(commands).toEqual([[0, commands[0][1], { name: "new record" }]]); break; case 2: expect(commands).toEqual([[2, 10]]); break; case 3: expect(commands).toEqual([ [0, commands[0][1], { name: "new record page 1" }], [2, 11], [2, 52], [0, commands[3][1], { name: "new record page 2" }], ]); break; } }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(40); // add a record on page one checkRead = true; await contains(".o-kanban-button-new").click(); await contains(".modal input").edit("new record"); await contains(".modal .modal-footer .btn-primary").click(); // checks expect(readIDs).toBe(undefined, { message: "should not have read any record" }); expect(".o_kanban_record:not(.o_kanban_ghost):contains('new record')").toHaveCount(0); expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(40); // save await clickSave(); // delete a record on page one checkRead = true; expect(".o_kanban_record:not(.o_kanban_ghost):eq(0)").toHaveText("relational record 10"); await contains(".delete_icon").click(); // should remove record!!! // checks expect(readIDs).toEqual([50], { message: "should have read a record (to display 40 records on page 1)", }); expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(40); // save await clickSave(); // add and delete records in both pages checkRead = true; readIDs = undefined; // add and delete a record in page 1 await contains(".o-kanban-button-new").click(); await contains(".modal input").edit("new record page 1"); await contains(".modal .modal-footer .btn-primary").click(); expect(".o_kanban_record:not(.o_kanban_ghost):eq(0)").toHaveText("relational record 11", { message: "first record should be the one with id 11 (next checks rely on that)", }); await contains(".delete_icon").click(); // should remove record!!! expect(readIDs).toEqual([51], { message: "should have read a record (to display 40 records on page 1)", }); // add and delete a record in page 2 await contains(".o_x2m_control_panel .o_pager_next").click(); expect(".o_kanban_record:not(.o_kanban_ghost):eq(0)").toHaveText("relational record 52", { message: "first record should be the one with id 52 (next checks rely on that)", }); checkRead = true; readIDs = undefined; await contains(".delete_icon").click(); // should remove record!!! await contains(".o-kanban-button-new").click(); await contains(".modal input").edit("new record page 2"); await contains(".modal .modal-footer .btn-primary").click(); expect(readIDs).toBe(undefined, { message: "should not have read any record" }); // checks expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(5); expect(".o_kanban_record:not(.o_kanban_ghost):contains('new record page 1')").toHaveCount(1); expect(".o_kanban_record:not(.o_kanban_ghost):contains('new record page 2')").toHaveCount(1); // save await clickSave(); expect.verifySteps(["web_save", "web_save", "web_save"]); }); test.tags("desktop"); test("edition of one2many field with pager on desktop", async () => { const ids = []; for (let i = 0; i < 45; i++) { const id = 10 + i; ids.push(id); Partner._records.push({ id: id, name: "relational record " + id, }); } Partner._records[0].p = ids; Partner._views = { form: '
' }; let saveCount = 0; let checkRead = false; onRpc("web_read", (args) => { if (checkRead) { checkRead = false; } }); onRpc("web_save", (args) => { expect.step("web_save"); saveCount++; const commands = args.args[1].p; switch (saveCount) { case 1: expect(commands).toEqual([[0, commands[0][1], { name: "new record" }]]); break; case 2: expect(commands).toEqual([[2, 10]]); break; case 3: expect(commands).toEqual([ [0, commands[0][1], { name: "new record page 1" }], [2, 11], [2, 52], [0, commands[3][1], { name: "new record page 2" }], ]); break; } }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_x2m_control_panel .o_pager_counter").toHaveText("1-40 / 45"); // add a record on page one checkRead = true; await contains(".o-kanban-button-new").click(); await contains(".modal input").edit("new record"); await contains(".modal .modal-footer .btn-primary").click(); // checks expect(".o_x2m_control_panel .o_pager_counter").toHaveText("1-40 / 46"); // save await clickSave(); // delete a record on page one checkRead = true; await contains(".delete_icon").click(); // should remove record!!! // checks expect(".o_x2m_control_panel .o_pager_counter").toHaveText("1-40 / 45"); // save await clickSave(); // add and delete records in both pages checkRead = true; // add and delete a record in page 1 await contains(".o-kanban-button-new").click(); await contains(".modal input").edit("new record page 1"); await contains(".modal .modal-footer .btn-primary").click(); await contains(".delete_icon").click(); // should remove record!!! // add and delete a record in page 2 await contains(".o_x2m_control_panel .o_pager_next").click(); checkRead = true; await contains(".delete_icon").click(); // should remove record!!! await contains(".o-kanban-button-new").click(); await contains(".modal input").edit("new record page 2"); await contains(".modal .modal-footer .btn-primary").click(); // checks expect(".o_x2m_control_panel .o_pager_counter").toHaveText("41-45 / 45"); // save await clickSave(); expect.verifySteps(["web_save", "web_save", "web_save"]); }); test("When viewing one2many records in an embedded kanban, the delete button should say 'Delete' and not 'Remove'", async () => { expect.assertions(1); Turtle._views = { form: `

Data

`, }; await mountView({ type: "form", resModel: "partner", arch: `

Record 1

`, resId: 1, }); // Opening the record to see the footer buttons await contains(".o_kanban_record").click(); expect(".o_btn_remove").toHaveText("Delete"); }); test("open a record in a one2many kanban (mode 'readonly')", async () => { Turtle._views = { form: `
`, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_kanban_record:eq(0)").toHaveText("donatello"); await contains(".o_kanban_record").click(); expect(".modal").toHaveCount(1); expect(".modal div[name=name] span").toHaveText("donatello"); }); test("open a record in a one2many kanban (mode 'edit')", async () => { Turtle._views = { form: `
`, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect(".o_kanban_record:eq(0)").toHaveText("donatello"); await contains(".o_kanban_record").click(); expect(".modal").toHaveCount(1); expect(".modal div[name=name] input").toHaveValue("donatello"); }); test("open a record in an one2many readonly", async () => { Turtle._views = { form: `
`, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_data_row .o_data_cell").click(); expect(".modal").toHaveCount(1); expect(".modal div[name=name] span").toHaveText("donatello"); await contains(".modal .o_form_button_cancel").click(); await contains(".o_data_row .o_data_cell").click(); expect(".modal").toHaveCount(1); expect(".modal div[name=name] span").toHaveText("donatello"); }); test("open a record in a one2many kanban with an x2m in the form", async () => { Partner._records[0].p = [2]; Partner._records[1].p = [4]; Partner._views = { form: `
`, }; const def = new Deferred(); onRpc("web_read", async (args) => { if (args.args[0][0] === 2) { expect.step("web_read: 2"); await def; } }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_kanban_record").click(); def.resolve(); await animationFrame(); expect(".modal").toHaveCount(1); expect(".modal [name=name] input").toHaveValue("second record"); expect(queryAllTexts(".modal .o_data_row")).toEqual(["aaa"]); expect.verifySteps(["web_read: 2"]); }); test("one2many in kanban: add a line custom control create editable", async () => { Turtle._views = { form: `
`, }; await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); const createButtons = queryAll(".o_x2m_control_panel .o_cp_buttons button"); expect(queryAllTexts(createButtons)).toEqual(["Add food", "Add pizza", "Add pasta"]); await contains(createButtons[0]).click(); expect(".modal").toHaveCount(1); expect(".modal div[name=name] input").toHaveValue(""); await contains(".modal .o_form_button_cancel").click(); await contains(createButtons[1]).click(); expect(".modal").toHaveCount(1); expect(".modal div[name=name] input").toHaveValue("pizza"); await contains(".modal .o_form_button_cancel").click(); await contains(createButtons[2]).click(); expect(".modal").toHaveCount(1); expect(".modal div[name=name] input").toHaveValue("pasta"); }); test("one2many in kanban: add a line custom control create editable (2)", async () => { Turtle._views = { form: `
`, }; onRpc("do_something", (args) => { expect.step("do_something"); expect(args.kwargs.context.parent_id).toBe(2); return true; }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_data_row td[name=name]").click(); expect(".modal-footer .my_button").toHaveCount(1); expect(".modal-footer button").toHaveCount(3); await contains(".modal-header .btn-close").click(); expect(".modal").toHaveCount(0); // open it again await contains(".o_data_row td[name=name]").click(); expect(".modal-footer .my_button").toHaveCount(1); expect(".modal-footer button").toHaveCount(3); }); test('Add a line, click on "Save & New" with an invalid form', async () => { mockService("notification", { add: (message, params) => { expect.step(params.type); expect(params.title).toBe("Invalid fields: "); expect(message.toString()).toBe("
  • Name
"); }, }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect(".o_data_row").toHaveCount(0); // Add a new record await contains(".o_field_x2many_list_row_add a").click(); expect(".o_dialog .o_form_view").toHaveCount(1); // Click on "Save & New" with an invalid form await contains(".o_dialog .o_form_button_save_new").click(); expect(".o_dialog .o_form_view").toHaveCount(1); expect.verifySteps(["danger"]); // Check that no buttons are disabled expect(".o_dialog .o_form_button_save_new").toBeEnabled(); expect(".o_dialog .o_form_button_cancel").toBeEnabled(); }); test("field in list but not in fetched form", async () => { Partner._fields.o2m = fields.One2many({ relation: "partner.type", relation_field: "p_id", }); PartnerType._onChanges = { name: (rec) => { if (rec.name === "changed") { rec.color = 5; } }, }; PartnerType._fields.p_id = fields.Many2one({ relation: "partner" }); PartnerType._views = { form: `
` }; onRpc((args) => { expect.step(`${args.method}: ${args.model}`); }); await mountView({ type: "form", resModel: "partner", arch: `
`, }); expect.verifySteps(["get_views: partner", "onchange: partner"]); await contains(".o_field_x2many_list_row_add a").click(); expect.verifySteps(["get_views: partner.type", "onchange: partner.type"]); await contains(".modal .o_field_widget[name='name'] input").edit("changed", { confirm: "blur", }); expect.verifySteps(["onchange: partner.type"]); await contains(".modal .o_form_button_save").click(); expect(".o_data_row").toHaveText("changed 5"); await contains(".o_form_button_save").click(); expect.verifySteps(["web_save: partner"]); expect(".o_data_row").toHaveText("changed 5"); }); test("pressing tab before an onchange is resolved", async () => { const onchangeGetPromise = new Deferred(); Partner._onChanges = { name: (obj) => { obj.name = "test"; }, }; onRpc("product", "onchange", async (args) => { if (args.args[2] === "name") { await onchangeGetPromise; } }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); await contains(".o_field_x2many_list_row_add a").click(); // This is not how it should happen but non trusted event listeners are called sooner than // trusted ones so the update is called after the list's tab listener in which case the field is // not dirty when we press tab, therefore we need to set it dirty through onChange before pressing tab // so in practice we could only run the following line but it wont work since the tab keydown event is not trusted // await contains(".o_field_widget[name='name'] input").edit("gold", { confirm: false }); await contains(".o_field_widget[name='name'] input").edit("gold", { confirm: "blur" }); await contains(".o_data_cell[name='name']").click(); // focus the input again await press("Tab"); onchangeGetPromise.resolve(); await animationFrame(); expect(".o_data_row").toHaveCount(2); }); test("add a row to an x2many and ask canBeRemoved twice", async () => { // This test simulates that the view is asked twice to save its changes because the user // is leaving. Before the corresponding fix, the changes in the x2many field weren't // removed after the save, and as a consequence they were saved twice (i.e. the row was // created twice). const def = new Deferred(); Partner._views = { list: ``, search: ``, form: `
`, }; onRpc("web_save", (args) => { expect.step("web_save"); expect(args.args[1]).toEqual({ p: [[0, args.args[1].p[0][1], { name: "a name" }]], }); }); onRpc("web_search_read", () => { return def; }); const actions = [ { id: 1, name: "test", res_model: "partner", res_id: 1, type: "ir.actions.act_window", views: [[false, "form"]], }, { id: 2, name: "another action", res_model: "partner", type: "ir.actions.act_window", views: [[false, "list"]], }, ]; await mountWithCleanup(WebClient); await getService("action").doAction(actions[0]); expect(".o_form_view").toHaveCount(1); // add a row in the x2many await contains(".o_field_x2many_list_row_add a").click(); await contains(".o_field_widget[name=name] input").edit("a name", { confirm: false }); expect(".o_data_row").toHaveCount(1); getService("action").doAction(actions[1]); await animationFrame(); getService("action").doAction(actions[1]); await animationFrame(); expect.verifySteps(["web_save"]); def.resolve(); await animationFrame(); expect(".o_list_view").toHaveCount(1); expect.verifySteps([]); }); test("one2many: save a record before the onchange is complete in a form dialog", async () => { Turtle._onChanges = { name: function () {}, }; Turtle._views = { form: `
`, }; const def = new Deferred(); onRpc("onchange", async (args) => { if (args.args[2].length === 1 && args.args[2][0] === "name") { await def; } }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); await contains(".o_field_x2many_list_row_add a").click(); expect(".modal").toHaveCount(1); await contains(".o_field_widget[name=name] input").edit("new name", { confirm: false }); await contains(".modal .o_form_button_save").click(); expect(".modal").toHaveCount(1); def.resolve(); await animationFrame(); expect(".modal").toHaveCount(0); expect(".o_data_row").toHaveCount(2); expect(queryAllTexts(".o_data_row [name='name']")).toEqual(["donatello", "new name"]); }); test("onchange create a record in an invisible x2many", async () => { Partner._onChanges = { foo: function () {}, }; Partner._records[0].p = [2]; onRpc("onchange", () => { return { value: { p: [ [ 1, 2, { name: "plop", p: [[0, false, {}]], }, ], ], }, }; }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: `
`, }); expect(queryAllTexts(".o_data_row")).toEqual(["second record"]); await contains(".o_field_widget[name=foo] input").edit("new foo value", { confirm: "blur" }); expect(queryAllTexts(".o_data_row")).toEqual(["plop"]); }); test("forget command for nested x2manys in form, not in list", async () => { expect.assertions(8); Partner._records[0].p = [1, 2]; Partner._records[1].turtles = [2]; Partner._onChanges = { int_field: function (obj) { obj.p = [ [ 1, 1, { foo: "new foo value (1)", turtles: [ [ 1, 2, { turtle_foo: "new turtle foo value (1)", partner_ids: [[3, 4]], }, ], ], }, ], [ 1, 2, { foo: "new foo value (2)", turtles: [ [ 1, 2, { turtle_foo: "new turtle foo value (2)", partner_ids: [[3, 2]], }, ], ], }, ], ]; }, }; onRpc("web_save", (args) => { expect(args.args[1]).toEqual({ int_field: 16, p: [ [ 1, 1, { foo: "new foo value (1)", turtles: [ [ 1, 2, { turtle_foo: "new turtle foo value (1)", partner_ids: [[3, 4]], }, ], ], }, ], [ 1, 2, { foo: "new foo value (2)", turtles: [ [ 1, 2, { turtle_foo: "new turtle foo value (2)", partner_ids: [[3, 2]], }, ], ], }, ], ], }); }); await mountView({ type: "form", resModel: "partner", arch: `
`, resId: 1, }); expect("[name=int_field] input").toHaveValue("10"); // trigger the onchange await contains("[name=int_field] input").edit("16", { confirm: "blur" }); expect("[name=foo]:eq(0)").toHaveText("new foo value (1)"); expect("[name=foo]:eq(1)").toHaveText("new foo value (2)"); // open the second x2many record await contains(".o_data_row:eq(1) td").click(); expect(".o_dialog .o_data_row").toHaveCount(1); expect(".o_dialog .o_data_cell[name=turtle_foo]").toHaveText("new turtle foo value (2)"); expect(".o_dialog .o_data_cell[name=partner_ids] .o_tag").toHaveCount(1); expect(".o_dialog .o_data_cell[name=partner_ids] .o_tag").toHaveText("aaa"); await contains(".o_dialog .o_form_button_save").click(); await clickSave(); }); test("modifiers based on x2many", async () => { await mountView({ type: "form", resModel: "partner", arch: `