odoo.define('web.field_one_to_many_tests', function (require) { "use strict"; var AbstractField = require('web.AbstractField'); var AbstractStorageService = require('web.AbstractStorageService'); const ControlPanel = require('web.ControlPanel'); const fieldRegistry = require('web.field_registry'); var FormView = require('web.FormView'); var KanbanRecord = require('web.KanbanRecord'); var ListRenderer = require('web.ListRenderer'); var NotificationService = require('web.NotificationService'); var RamStorage = require('web.RamStorage'); var relationalFields = require('web.relational_fields'); var testUtils = require('web.test_utils'); var fieldUtils = require('web.field_utils'); const cpHelpers = testUtils.controlPanel; var createView = testUtils.createView; const { FieldOne2Many } = relationalFields; QUnit.module('fields', {}, function () { QUnit.module('relational_fields', { beforeEach: function () { this.data = { partner: { fields: { display_name: { string: "Displayed name", type: "char" }, foo: { string: "Foo", type: "char", default: "My little Foo Value" }, bar: { string: "Bar", type: "boolean", default: true }, int_field: { string: "int_field", type: "integer", sortable: true }, qux: { string: "Qux", type: "float", digits: [16, 1] }, p: { string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu' }, turtles: { string: "one2many turtle field", type: "one2many", relation: 'turtle', relation_field: 'turtle_trululu' }, trululu: { string: "Trululu", type: "many2one", relation: 'partner' }, timmy: { string: "pokemon", type: "many2many", relation: 'partner_type' }, product_id: { string: "Product", type: "many2one", relation: 'product' }, color: { type: "selection", selection: [['red', "Red"], ['black', "Black"]], default: 'red', string: "Color", }, date: { string: "Some Date", type: "date" }, datetime: { string: "Datetime Field", type: 'datetime' }, user_id: { string: "User", type: 'many2one', relation: 'user' }, reference: { string: "Reference Field", type: 'reference', selection: [ ["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]] }, }, records: [{ id: 1, display_name: "first record", bar: true, foo: "yop", int_field: 10, qux: 0.44, p: [], turtles: [2], timmy: [], trululu: 4, user_id: 17, reference: 'product,37', }, { id: 2, display_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, display_name: "aaa", bar: false, }], onchanges: {}, }, product: { fields: { name: { string: "Product Name", type: "char" } }, records: [{ id: 37, display_name: "xphone", }, { id: 41, display_name: "xpad", }] }, partner_type: { fields: { name: { string: "Partner Type", type: "char" }, color: { string: "Color index", type: "integer" }, }, records: [ { id: 12, display_name: "gold", color: 2 }, { id: 14, display_name: "silver", color: 5 }, ] }, turtle: { fields: { display_name: { string: "Displayed name", type: "char" }, turtle_foo: { string: "Foo", type: "char" }, turtle_bar: { string: "Bar", type: "boolean", default: true }, turtle_int: { string: "int", type: "integer", sortable: true }, turtle_qux: { string: "Qux", type: "float", digits: [16, 1], required: true, default: 1.5 }, turtle_description: { string: "Description", type: "text" }, turtle_trululu: { string: "Trululu", type: "many2one", relation: 'partner' }, turtle_ref: { string: "Reference", type: 'reference', selection: [ ["product", "Product"], ["partner", "Partner"]] }, product_id: { string: "Product", type: "many2one", relation: 'product', required: true }, partner_ids: { string: "Partner", type: "many2many", relation: 'partner' }, }, records: [{ id: 1, display_name: "leonardo", turtle_bar: true, turtle_foo: "yop", partner_ids: [], }, { id: 2, display_name: "donatello", turtle_bar: true, turtle_foo: "blip", turtle_int: 9, partner_ids: [2, 4], }, { id: 3, display_name: "raphael", product_id: 37, turtle_bar: false, turtle_foo: "kawa", turtle_int: 21, turtle_qux: 9.8, partner_ids: [], turtle_ref: 'product,37', }], onchanges: {}, }, user: { fields: { name: { string: "Name", type: "char" }, partner_ids: { string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id' }, }, records: [{ id: 17, name: "Aline", partner_ids: [1, 2], }, { id: 19, name: "Christine", }] }, }; } }, function () { QUnit.module('FieldOne2Many'); QUnit.test('New record with a o2m also with 2 new records, ordered, and resequenced', async function (assert) { assert.expect(2); // Needed to have two new records in a single stroke this.data.partner.onchanges = { foo: function (obj) { obj.p = [ [5], [0, 0, { trululu: false }], [0, 0, { trululu: false }], ]; } }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '', viewOptions: { mode: 'create', }, mockRPC: function (route, args) { assert.step(args.method + ' ' + args.model); return this._super(route, args); }, }); // change the int_field through drag and drop // that way, we'll trigger the sorting and the name_get // of the lines of "p" await testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), { position: 'top' } ); assert.verifySteps(['onchange partner']); form.destroy(); }); QUnit.test('O2M List with pager, decoration and default_order: add and cancel adding', async function (assert) { assert.expect(3); // 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* this.data.partner.records[0].p = [2, 4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, viewOptions: { mode: 'edit', }, }); await testUtils.dom.click(form.$('.o_field_x2many_list .o_field_x2many_list_row_add a')); assert.containsN(form, '.o_field_x2many_list .o_data_row', 2, 'There should be 2 rows'); var $expectedSelectedRow = form.$('.o_field_x2many_list .o_data_row').eq(1); var $actualSelectedRow = form.$('.o_selected_row'); assert.equal($actualSelectedRow[0], $expectedSelectedRow[0], 'The selected row should be the new one'); // Cancel Creation await testUtils.fields.triggerKeydown($actualSelectedRow.find('input'), 'escape'); assert.containsOnce(form, '.o_field_x2many_list .o_data_row', 'There should be 1 row'); form.destroy(); }); QUnit.test('O2M with parented m2o and domain on parent.m2o', async function (assert) { assert.expect(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 */ this.data.turtle.fields.parent_id = { string: "Parent", type: "many2one", relation: 'turtle' }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', archs: { 'turtle,false,form': '
', }, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/turtle/name_search') { // We are going to pass twice here // First time, we really have nothing // Second time, a virtual_id has been created assert.deepEqual(args.kwargs.args, [['id', 'in', []]]); } return this._super(route, args); }, }); await testUtils.dom.click(form.$('.o_field_x2many_list[name=turtles] .o_field_x2many_list_row_add a')); await testUtils.fields.many2one.createAndEdit('parent_id'); var $modal = $('.modal-content'); await testUtils.dom.click($modal.eq(1).find('.modal-footer .btn-primary').eq(0)); await testUtils.dom.click($modal.eq(0).find('.modal-footer .btn-primary').eq(1)); assert.containsOnce(form, '.o_data_row', 'The main record should have the new record in its o2m'); $modal = $('.modal-content'); await testUtils.dom.click($modal.find('.o_field_many2one input')); form.destroy(); }); QUnit.test('one2many list editable with cell readonly modifier', async function (assert) { assert.expect(4); this.data.partner.records[0].p = [2]; this.data.partner.records[1].turtles = [1, 2]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { assert.deepEqual(args.args[1].p[1][2], { foo: 'ff', qux: 99 }, 'The right values should be written'); } return this._super(route, args); } }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); var $targetInput = $('.o_selected_row .o_input[name=foo]'); assert.equal($targetInput[0], document.activeElement, 'The first input of the line should have the focus'); // Simulating hitting the 'f' key twice await testUtils.fields.editInput($targetInput, 'f'); await testUtils.fields.editInput($targetInput, $targetInput.val() + 'f'); assert.equal($targetInput[0], document.activeElement, 'The first input of the line should still have the focus'); // Simulating a TAB key await testUtils.fields.triggerKeydown($targetInput, 'tab'); var $secondTarget = $('.o_selected_row .o_input[name=qux]'); assert.equal($secondTarget[0], document.activeElement, 'The second input of the line should have the focus after the TAB press'); await testUtils.fields.editInput($secondTarget, 9); await testUtils.fields.editInput($secondTarget, $secondTarget.val() + 9); await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('one2many basic properties', async function (assert) { assert.expect(6); this.data.partner.records[0].p = [2]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, intercepts: { load_filters: function (event) { throw new Error('Should not load filters'); }, }, }); assert.containsNone(form, 'td.o_list_record_selector', "embedded one2many should not have a selector"); assert.ok(!form.$('.o_field_x2many_list_row_add').length, "embedded one2many should not be editable"); assert.ok(!form.$('td.o_list_record_remove').length, "embedded one2many records should not have a remove icon"); await testUtils.form.clickEdit(form); assert.ok(form.$('.o_field_x2many_list_row_add').length, "embedded one2many should now be editable"); assert.hasAttrValue(form.$('.o_field_x2many_list_row_add'), 'colspan', "2", "should have colspan 2 (one for field foo, one for being below remove icon)"); assert.ok(form.$('td.o_list_record_remove').length, "embedded one2many records should have a remove icon"); form.destroy(); }); QUnit.test('transferring class attributes in one2many sub fields', async function (assert) { assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.containsOnce(form, 'td.hey', 'should have a td with the desired class'); await testUtils.form.clickEdit(form); assert.containsOnce(form, 'td.hey', 'should have a td with the desired class'); await testUtils.dom.click(form.$('td.o_data_cell')); assert.containsOnce(form, 'input[name="turtle_foo"].hey', 'should have an input with the desired class'); form.destroy(); }); QUnit.test('one2many with date and datetime', async function (assert) { assert.expect(2); this.data.partner.records[0].p = [2]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, session: { getTZOffset: function () { return 120; }, }, }); assert.strictEqual(form.$('td:eq(0)').text(), "01/25/2017", "should have formatted the date"); assert.strictEqual(form.$('td:eq(1)').text(), "12/12/2016 12:55:05", "should have formatted the datetime"); form.destroy(); }); QUnit.test('rendering with embedded one2many', async function (assert) { assert.expect(2); this.data.partner.records[0].p = [2]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('th:contains(Foo)').length, 1, "embedded one2many should have a column titled according to foo"); assert.strictEqual(form.$('td:contains(blip)').length, 1, "embedded one2many should have a cell with relational value"); form.destroy(); }); QUnit.test('use the limit attribute in arch (in field o2m inline tree view)', async function (assert) { assert.expect(2); this.data.partner.records[0].turtles = [1, 2, 3]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.model === 'turtle') { assert.deepEqual(args.args[0], [1, 2], 'should only load first 2 records'); } return this._super.apply(this, arguments); }, }); assert.containsN(form, '.o_data_row', 2, 'should display 2 data rows'); form.destroy(); }); QUnit.test('use the limit attribute in arch (in field o2m non inline tree view)', async function (assert) { assert.expect(2); this.data.partner.records[0].turtles = [1, 2, 3]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', archs: { 'turtle,false,list': '', }, res_id: 1, mockRPC: function (route, args) { if (args.model === 'turtle' && args.method === 'read') { assert.deepEqual(args.args[0], [1, 2], 'should only load first 2 records'); } return this._super.apply(this, arguments); }, }); assert.containsN(form, '.o_data_row', 2, 'should display 2 data rows'); form.destroy(); }); QUnit.test('one2many with default_order on view not inline', async function (assert) { assert.expect(1); this.data.partner.records[0].turtles = [1, 2, 3]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', archs: { 'turtle,false,list': '' + '' + '' + '', }, res_id: 1, }); assert.strictEqual(form.$('.o_field_one2many .o_list_view .o_data_row').text(), "9blip21kawa0yop", "the default order should be correctly applied"); form.destroy(); }); QUnit.test('embedded one2many with widget', async function (assert) { assert.expect(1); this.data.partner.records[0].p = [2]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.containsOnce(form, 'span.o_row_handle', "should have 1 handles"); form.destroy(); }); QUnit.test('embedded one2many with handle widget', async function (assert) { assert.expect(10); var nbConfirmChange = 0; testUtils.mock.patch(ListRenderer, { confirmChange: function () { nbConfirmChange++; return this._super.apply(this, arguments); }, }); this.data.partner.records[0].turtles = [1, 2, 3]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); testUtils.mock.intercept(form, "field_changed", function (event) { assert.step(event.data.changes.turtles.data.turtle_int.toString()); }, true); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should have the 3 rows in the correct order"); await testUtils.form.clickEdit(form); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should still have the 3 rows in the correct order"); assert.strictEqual(nbConfirmChange, 0, "should not have confirmed any change yet"); // Drag and drop the second line in first position await testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), { position: 'top' } ); assert.strictEqual(nbConfirmChange, 1, "should have confirmed changes only once"); assert.verifySteps(["0", "1"], "sequences values should be incremental starting from the previous minimum one"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should have the 3 rows in the new order"); await testUtils.form.clickSave(form); assert.deepEqual(_.map(this.data.turtle.records, function (turtle) { return _.pick(turtle, 'id', 'turtle_foo', 'turtle_int'); }), [ { id: 1, turtle_foo: "yop", turtle_int: 1 }, { id: 2, turtle_foo: "blip", turtle_int: 0 }, { id: 3, turtle_foo: "kawa", turtle_int: 21 } ], "should have save the changed sequence"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should still have the 3 rows in the new order"); testUtils.mock.unpatch(ListRenderer); form.destroy(); }); QUnit.test('onchange for embedded one2many in a one2many with a second page', async function (assert) { assert.expect(1); this.data.turtle.fields.partner_ids.type = 'one2many'; this.data.turtle.records[0].partner_ids = [1]; // we need a second page, so we set two records and only display one per page this.data.partner.records[0].turtles = [1, 2]; this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [ [5], [1, 1, { turtle_foo: "hop", partner_ids: [[5], [4, 1]], }], [1, 2, { turtle_foo: "blip", partner_ids: [[5], [4, 2], [4, 4]], }], ]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { var expectedResultTurtles = [ [1, 1, { turtle_foo: "hop", }], [1, 2, { partner_ids: [[4, 2, false], [4, 4, false]], turtle_foo: "blip", }], ]; assert.deepEqual(args.args[1].turtles, expectedResultTurtles, "the right values should be written"); } return this._super.apply(this, arguments); } }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_data_cell').eq(1)); var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]'); await testUtils.fields.editSelect($cell, "hop"); await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('onchange for embedded one2many in a one2many updated by server', async function (assert) { // 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 assert.expect(3); this.data.turtle.fields.partner_ids.type = 'one2many'; this.data.partner.records[0].turtles = [2]; this.data.turtle.records[1].partner_ids = [2]; this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [ [5], [1, 2, { turtle_foo: "hop", partner_ids: [[5], [4, 2], [4, 4]], }], ]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { var expectedResultTurtles = [ [1, 2, { partner_ids: [[4, 2, false], [4, 4, false]], turtle_foo: "hop", }], ]; assert.deepEqual(args.args[1].turtles, expectedResultTurtles, 'The right values should be written'); } return this._super.apply(this, arguments); }, }); assert.deepEqual(form.$('.o_data_cell.o_many2many_tags_cell').text().trim(), "second record", "the partner_ids should be as specified at initialization"); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_data_cell').eq(1)); var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]'); await testUtils.fields.editSelect($cell, "hop"); await testUtils.form.clickSave(form); assert.deepEqual(form.$('.o_data_cell.o_many2many_tags_cell').text().trim().split(/\s+/), ["second", "record", "aaa"], 'The partner_ids should have been updated'); form.destroy(); }); QUnit.test('onchange for embedded one2many with handle widget', async function (assert) { assert.expect(2); this.data.partner.records[0].turtles = [1, 2, 3]; var partnerOnchange = 0; this.data.partner.onchanges = { turtles: function () { partnerOnchange++; }, }; var turtleOnchange = 0; this.data.turtle.onchanges = { turtle_int: function () { turtleOnchange++; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); // Drag and drop the second line in first position await testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), { position: 'top' } ); assert.strictEqual(turtleOnchange, 2, "should trigger one onchange per line updated"); assert.strictEqual(partnerOnchange, 1, "should trigger only one onchange on the parent"); form.destroy(); }); QUnit.test('onchange for embedded one2many with handle widget using same sequence', async function (assert) { assert.expect(4); this.data.turtle.records[0].turtle_int = 1; this.data.turtle.records[1].turtle_int = 1; this.data.turtle.records[2].turtle_int = 1; this.data.partner.records[0].turtles = [1, 2, 3]; var turtleOnchange = 0; this.data.turtle.onchanges = { turtle_int: function () { turtleOnchange++; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[1].turtles, [[1, 2, { "turtle_int": 1 }], [1, 1, { "turtle_int": 2 }], [1, 3, { "turtle_int": 3 }]], "should change all lines that have changed (the first one doesn't change because it has the same sequence)"); } return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should have the 3 rows in the correct order"); // Drag and drop the second line in first position await testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), { position: 'top' } ); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should still have the 3 rows in the correct order"); assert.strictEqual(turtleOnchange, 3, "should update all lines"); await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('onchange (with command 5) for embedded one2many with handle widget', async function (assert) { assert.expect(3); var ids = []; for (var i = 10; i < 50; i++) { var id = 10 + i; ids.push(id); this.data.turtle.records.push({ id: id, turtle_int: 0, turtle_foo: "#" + id, }); } ids.push(1, 2, 3); this.data.partner.records[0].turtles = ids; this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [[5]].concat(obj.turtles); }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should have the 3 rows in the correct order"); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first')); await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp'); // Drag and drop the third line in second position await testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(2), form.$('.o_field_one2many tbody tr').eq(1), { position: 'top' } ); assert.strictEqual(form.$('.o_data_cell').text(), "blurpkawablip", "should display to record in 'turtle_int' order"); await testUtils.form.clickSave(form); await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); assert.strictEqual(form.$('.o_data_cell:not(.o_handle_cell)').text(), "blurpkawablip", "should display to record in 'turtle_int' order"); form.destroy(); }); QUnit.test('onchange with modifiers for embedded one2many on the second page', async function (assert) { assert.expect(7); var data = this.data; var ids = []; for (var i = 10; i < 60; i++) { var id = 10 + i; ids.push(id); data.turtle.records.push({ id: id, turtle_int: 0, turtle_foo: "#" + id, }); } ids.push(1, 2, 3); data.partner.records[0].turtles = ids; data.partner.onchanges = { turtles: function (obj) { // TODO: make this test more 'difficult' // For now, the server only returns UPDATE commands (no LINK TO) // even though it should do it (for performance reasons) // var turtles = obj.turtles.splice(0, 20); var turtles = []; turtles.unshift([5]); // create UPDATE commands for each records (this is the server // usual answer for onchange) for (var k in obj.turtles) { var change = obj.turtles[k]; var record = _.findWhere(data.turtle.records, { id: change[1] }); if (change[0] === 1) { _.extend(record, change[2]); } turtles.push([1, record.id, record]); } obj.turtles = turtles; }, }; var form = await createView({ View: FormView, model: 'partner', data: data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#21#22#23#24#25#26#27#28#29", "should display the records in order"); await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first')); await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp'); // the domain fail if the widget does not use the allready loaded data. await testUtils.form.clickDiscard(form); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "blurp#21#22#23#24#25#26#27#28#29", "should display the records in order with the changes"); await testUtils.dom.click($('.modal .modal-footer button:first')); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#21#22#23#24#25#26#27#28#29", "should cancel changes and display the records in order"); await testUtils.form.clickEdit(form); // Drag and drop the third line in second position await testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(2), form.$('.o_field_one2many tbody tr').eq(1), { position: 'top' } ); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#30#31#32#33#34#35#36#37#38", "should display the records in order after resequence (display record with turtle_int=0)"); // Drag and drop the third line in second position await testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(2), form.$('.o_field_one2many tbody tr').eq(1), { position: 'top' } ); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#39#40#41#42#43#44#45#46#47", "should display the records in order after resequence (display record with turtle_int=0)"); await testUtils.form.clickDiscard(form); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#39#40#41#42#43#44#45#46#47", "should display the records in order after resequence"); await testUtils.dom.click($('.modal .modal-footer button:first')); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#21#22#23#24#25#26#27#28#29", "should cancel changes and display the records in order"); form.destroy(); }); QUnit.test('onchange followed by edition on the second page', async function (assert) { assert.expect(12); var ids = []; for (var i = 1; i < 85; i++) { var id = 10 + i; ids.push(id); this.data.turtle.records.push({ id: id, turtle_int: id / 3 | 0, turtle_foo: "#" + i, }); } ids.splice(41, 0, 1, 2, 3); this.data.partner.records[0].turtles = ids; this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [[5]].concat(obj.turtles); }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first')); await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first'), 'value 1'); await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first')); await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first'), 'value 2'); assert.containsN(form, '.o_data_row', 40, "should display 40 records"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 0, "should display '#39' at the first line"); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, '.o_data_row', 40, "should display 39 records and the create line"); assert.containsOnce(form, '.o_data_row:first .o_field_char', "should display the create line in first position"); assert.strictEqual(form.$('.o_data_row:first .o_field_char').val(), "", "should an empty input"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line"); await testUtils.fields.editInput(form.$('.o_data_row input:first'), 'value 3'); assert.containsOnce(form, '.o_data_row:first .o_field_char', "should display the create line in first position after onchange"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line after onchange"); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, '.o_data_row', 40, "should display 39 records and the create line"); assert.containsOnce(form, '.o_data_row:first .o_field_char', "should display the create line in first position"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(value 3))').index(), 1, "should display the created line at the second position"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 2, "should display '#39' at the third line"); form.destroy(); }); QUnit.test('onchange followed by edition on the second page (part 2)', async function (assert) { assert.expect(8); var ids = []; for (var i = 1; i < 85; i++) { var id = 10 + i; ids.push(id); this.data.turtle.records.push({ id: id, turtle_int: id / 3 | 0, turtle_foo: "#" + i, }); } ids.splice(41, 0, 1, 2, 3); this.data.partner.records[0].turtles = ids; this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [[5]].concat(obj.turtles); }, }; // bottom order var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first')); await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first'), 'value 1'); await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first')); await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first'), 'value 2'); assert.containsN(form, '.o_data_row', 40, "should display 40 records"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#77))').index(), 39, "should display '#77' at the last line"); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, '.o_data_row', 41, "should display 41 records and the create line"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line"); assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 40, "should display the create line at the last position"); await testUtils.fields.editInput(form.$('.o_data_row input:first'), 'value 3'); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, '.o_data_row', 42, "should display 42 records and the create line"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line"); assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 41, "should display the create line at the last position"); form.destroy(); }); QUnit.test('onchange returning a command 6 for an x2many', async function (assert) { assert.expect(2); this.data.partner.onchanges = { foo: function (obj) { obj.turtles = [[6, false, [1, 2, 3]]]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.containsOnce(form, '.o_data_row', "there should be one record in the relation"); // change the value of foo to trigger the onchange await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value'); assert.containsN(form, '.o_data_row', 3, "there should be three records in the relation"); form.destroy(); }); QUnit.test('x2many fields inside x2manys are fetched after an onchange', async function (assert) { assert.expect(6); this.data.turtle.records[0].partner_ids = [1]; this.data.partner.onchanges = { foo: function (obj) { obj.turtles = [[5], [4, 1], [4, 2], [4, 3]]; }, }; var checkRPC = false; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (checkRPC && args.method === 'read' && args.model === 'partner') { assert.deepEqual(args.args[1], ['display_name'], "should only read the display_name for the m2m tags"); assert.deepEqual(args.args[0], [1], "should only read the display_name of the unknown record"); } return this._super.apply(this, arguments); }, res_id: 1, viewOptions: { mode: 'edit', }, }); assert.containsOnce(form, '.o_data_row', "there should be one record in the relation"); assert.strictEqual(form.$('.o_data_row .o_field_widget[name=partner_ids]').text().replace(/\s/g, ''), 'secondrecordaaa', "many2many_tags should be correctly displayed"); // change the value of foo to trigger the onchange checkRPC = true; // enable flag to check read RPC for the m2m field await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value'); assert.containsN(form, '.o_data_row', 3, "there should be three records in the relation"); assert.strictEqual(form.$('.o_data_row:first .o_field_widget[name=partner_ids]').text().trim(), 'first record', "many2many_tags should be correctly displayed"); form.destroy(); }); QUnit.test('reference fields inside x2manys are fetched after an onchange', async function (assert) { assert.expect(5); this.data.turtle.records[1].turtle_ref = 'product,41'; this.data.partner.onchanges = { foo: function (obj) { obj.turtles = [[5], [4, 1], [4, 2], [4, 3]]; }, }; var checkRPC = false; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (checkRPC && args.method === 'name_get') { assert.deepEqual(args.args[0], [37], "should only fetch the name_get of the unknown record"); } return this._super.apply(this, arguments); }, res_id: 1, viewOptions: { mode: 'edit', }, }); assert.containsOnce(form, '.o_data_row', "there should be one record in the relation"); assert.strictEqual(form.$('.ref_field').text().trim(), 'xpad', "reference field should be correctly displayed"); // change the value of foo to trigger the onchange checkRPC = true; // enable flag to check read RPC for reference field await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value'); assert.containsN(form, '.o_data_row', 3, "there should be three records in the relation"); assert.strictEqual(form.$('.ref_field').text().trim(), 'xpadxphone', "reference fields should be correctly displayed"); form.destroy(); }); QUnit.test('onchange on one2many containing x2many in form view', async function (assert) { assert.expect(16); this.data.partner.onchanges = { foo: function (obj) { obj.turtles = [[0, false, { turtle_foo: 'new record' }]]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', archs: { 'partner,false,list': '', 'partner,false,search': '', }, }); assert.containsOnce(form, '.o_data_row', "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 testUtils.dom.click(form.$('.o_data_row')); assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); assert.strictEqual($('.modal .o_data_row').length, 0, "there should be no record in the one2many in the dialog"); // add a many2many subrecord await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a')); assert.strictEqual($('.modal').length, 2, "should have opened a second dialog"); // select a many2many subrecord await testUtils.dom.click($('.modal:nth(1) .o_list_view .o_data_cell:first')); assert.strictEqual($('.modal').length, 1, "second dialog should be closed"); assert.strictEqual($('.modal .o_data_row').length, 1, "there should be one record in the one2many in the dialog"); assert.containsNone($('.modal'), '.o_x2m_control_panel .o_pager', 'm2m pager should be hidden'); // click on 'Save & Close' await testUtils.dom.click($('.modal-footer .btn-primary:first')); assert.strictEqual($('.modal').length, 0, "dialog should be closed"); // reopen o2m record, and another m2m subrecord in its relation, but // discard the changes await testUtils.dom.click(form.$('.o_data_row')); assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); assert.strictEqual($('.modal .o_data_row').length, 1, "there should be one record in the one2many in the dialog"); // add another m2m subrecord await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a')); assert.strictEqual($('.modal').length, 2, "should have opened a second dialog"); await testUtils.dom.click($('.modal:nth(1) .o_list_view .o_data_cell:first')); assert.strictEqual($('.modal').length, 1, "second dialog should be closed"); assert.strictEqual($('.modal .o_data_row').length, 2, "there should be two records in the one2many in the dialog"); // click on 'Discard' await testUtils.dom.click($('.modal-footer .btn-secondary')); assert.strictEqual($('.modal').length, 0, "dialog should be closed"); // reopen o2m record to check that second changes have properly been discarded await testUtils.dom.click(form.$('.o_data_row')); assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); assert.strictEqual($('.modal .o_data_row').length, 1, "there should be one record in the one2many in the dialog"); form.destroy(); }); QUnit.test('onchange on one2many with x2many in list (no widget) and form view (list)', async function (assert) { assert.expect(6); this.data.turtle.fields.turtle_foo.default = "a default value"; this.data.partner.onchanges = { foo: function (obj) { obj.p = [[0, false, { turtles: [[0, false, { turtle_foo: 'hello'}]] }]]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', }); assert.containsOnce(form, '.o_data_row', "the onchange should have created one record in the relation"); // open the created o2m record in a form view await testUtils.dom.click(form.$('.o_data_row')); assert.containsOnce(document.body, '.modal', "should have opened a dialog"); assert.containsOnce(document.body, '.modal .o_data_row'); assert.strictEqual($('.modal .o_data_row').text(), 'hello'); // add a one2many subrecord and check if the default value is correctly applied await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a')); assert.containsN(document.body, '.modal .o_data_row', 2); assert.strictEqual($('.modal .o_data_row:first .o_field_widget[name=turtle_foo]').val(), 'a default value'); form.destroy(); }); QUnit.test('onchange on one2many with x2many in list (many2many_tags) and form view (list)', async function (assert) { assert.expect(6); this.data.turtle.fields.turtle_foo.default = "a default value"; this.data.partner.onchanges = { foo: function (obj) { obj.p = [[0, false, { turtles: [[0, false, { turtle_foo: 'hello'}]] }]]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', }); assert.containsOnce(form, '.o_data_row', "the onchange should have created one record in the relation"); // open the created o2m record in a form view await testUtils.dom.click(form.$('.o_data_row')); assert.containsOnce(document.body, '.modal', "should have opened a dialog"); assert.containsOnce(document.body, '.modal .o_data_row'); assert.strictEqual($('.modal .o_data_row').text(), 'hello'); // add a one2many subrecord and check if the default value is correctly applied await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a')); assert.containsN(document.body, '.modal .o_data_row', 2); assert.strictEqual($('.modal .o_data_row:first .o_field_widget[name=turtle_foo]').val(), 'a default value'); form.destroy(); }); QUnit.test('embedded one2many with handle widget with minimum setValue calls', async function (assert) { var done = assert.async(); assert.expect(20); this.data.turtle.records[0].turtle_int = 6; this.data.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", }); this.data.partner.records[0].turtles = [1, 2, 3, 4, 5, 6, 7]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); testUtils.mock.intercept(form, "field_changed", function (event) { assert.step(String(form.model.get(event.data.changes.turtles.id).res_id)); }, true); await testUtils.form.clickEdit(form); var positions = [ [6, 0, 'top', ['3', '6', '1', '2', '5', '7', '4']], // move the last to the first line [5, 1, 'top', ['7', '6', '1', '2', '5']], // move the penultimate to the second line [2, 5, 'bottom', ['1', '2', '5', '6']], // move the third to the penultimate line ]; async function dragAndDrop() { var pos = positions.shift(); await testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(pos[0]), form.$('tbody tr').eq(pos[1]), { position: pos[2] } ); assert.verifySteps(pos[3], "sequences values should be apply from the begin index to the drop index"); if (positions.length) { setTimeout(dragAndDrop, 10); } else { assert.deepEqual(_.pluck(form.model.get(form.handle).data.turtles.data, 'data'), [ { id: 3, turtle_foo: "kawa", turtle_int: 2 }, { id: 7, turtle_foo: "a4", turtle_int: 3 }, { id: 1, turtle_foo: "yop", turtle_int: 4 }, { id: 2, turtle_foo: "blip", turtle_int: 5 }, { id: 5, turtle_foo: "a2", turtle_int: 6 }, { id: 6, turtle_foo: "a3", turtle_int: 7 }, { id: 4, turtle_foo: "a1", turtle_int: 8 } ], "sequences must be apply correctly"); form.destroy(); done(); } } dragAndDrop(); }); QUnit.test('embedded one2many (editable list) with handle widget', async function (assert) { assert.expect(8); this.data.partner.records[0].p = [1, 2, 4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); testUtils.mock.intercept(form, "field_changed", function (event) { assert.step(event.data.changes.p.data.int_field.toString()); }, true); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop", "should have the 3 rows in the correct order"); await testUtils.form.clickEdit(form); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop", "should still have the 3 rows in the correct order"); // Drag and drop the second line in first position await testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), { position: 'top' } ); assert.verifySteps(["0", "1"], "sequences values should be incremental starting from the previous minimum one"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", "should have the 3 rows in the new order"); await testUtils.dom.click(form.$('tbody tr:first td:first')); assert.strictEqual(form.$('tbody tr:first td.o_data_cell:not(.o_handle_cell) input').val(), "blip", "should edit the correct row"); await testUtils.form.clickSave(form); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", "should still have the 3 rows in the new order"); form.destroy(); }); QUnit.test('one2many field when using the pager', async function (assert) { assert.expect(13); var ids = []; for (var i = 0; i < 45; i++) { var id = 10 + i; ids.push(id); this.data.partner.records.push({ id: id, display_name: "relational record " + id, }); } this.data.partner.records[0].p = ids.slice(0, 42); this.data.partner.records[1].p = ids.slice(42); var count = 0; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
' + '
' + '
' + '
' + '
' + '
', viewOptions: { ids: [1, 2], index: 0, }, mockRPC: function () { count++; return this._super.apply(this, arguments); }, res_id: 1, }); // we are on record 1, which has 90 related record (first 40 should be // displayed), 2 RPCs (read) should have been done, one on the main record // and one for the O2M assert.strictEqual(count, 2, 'two RPCs should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'one2many kanban should contain 40 cards for record 1'); // move to record 2, which has 3 related records (and shouldn't contain the // related records of record 1 anymore). Two additional RPCs should have // been done await cpHelpers.pagerNext(form); assert.strictEqual(count, 4, 'two RPCs should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 3, 'one2many kanban should contain 3 cards for record 2'); // move back to record 1, which should contain again its first 40 related // records await cpHelpers.pagerPrevious(form); assert.strictEqual(count, 6, 'two RPCs should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'one2many kanban should contain 40 cards for record 1'); // 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 testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next')); assert.strictEqual(count, 7, 'one RPC should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 2, 'one2many kanban should contain 2 cards for record 1 at page 2'); // move to record 2 again and check that everything is correctly updated await cpHelpers.pagerNext(form); assert.strictEqual(count, 9, 'two RPCs should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 3, 'one2many kanban should contain 3 cards for record 2'); // move back to record 1 and move to page 2 again: all data should have // been correctly reloaded await cpHelpers.pagerPrevious(form); assert.strictEqual(count, 11, 'two RPCs should have been done'); await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next')); assert.strictEqual(count, 12, 'one RPC should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 2, 'one2many kanban should contain 2 cards for record 1 at page 2'); form.destroy(); }); QUnit.test('edition of one2many field with pager', async function (assert) { assert.expect(31); var ids = []; for (var i = 0; i < 45; i++) { var id = 10 + i; ids.push(id); this.data.partner.records.push({ id: id, display_name: "relational record " + id, }); } this.data.partner.records[0].p = ids; var saveCount = 0; var checkRead = false; var readIDs; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', archs: { 'partner,false,form': '
', }, mockRPC: function (route, args) { if (args.method === 'read' && checkRead) { readIDs = args.args[0]; checkRead = false; } if (args.method === 'write') { saveCount++; var nbCommands = args.args[1].p.length; var nbLinkCommands = _.filter(args.args[1].p, function (command) { return command[0] === 4; }).length; switch (saveCount) { case 1: assert.strictEqual(nbCommands, 46, "should send 46 commands (one for each record)"); assert.strictEqual(nbLinkCommands, 45, "should send a LINK_TO command for each existing record"); assert.deepEqual(args.args[1].p[45], [0, args.args[1].p[45][1], { display_name: 'new record', }], "should sent a CREATE command for the new record"); break; case 2: assert.strictEqual(nbCommands, 46, "should send 46 commands"); assert.strictEqual(nbLinkCommands, 45, "should send a LINK_TO command for each existing record"); assert.deepEqual(args.args[1].p[45], [2, 10, false], "should sent a DELETE command for the deleted record"); break; case 3: assert.strictEqual(nbCommands, 47, "should send 47 commands"); assert.strictEqual(nbLinkCommands, 43, "should send a LINK_TO command for each existing record"); assert.deepEqual(args.args[1].p[43], [0, args.args[1].p[43][1], { display_name: 'new record page 1' }], "should sent correct CREATE command"); assert.deepEqual(args.args[1].p[44], [0, args.args[1].p[44][1], { display_name: 'new record page 2' }], "should sent correct CREATE command"); assert.deepEqual(args.args[1].p[45], [2, 11, false], "should sent correct DELETE command"); assert.deepEqual(args.args[1].p[46], [2, 52, false], "should sent correct DELETE command"); break; } } return this._super.apply(this, arguments); }, res_id: 1, }); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'there should be 40 records on page 1'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '1-40 / 45', "pager range should be correct"); // add a record on page one checkRead = true; await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o-kanban-button-new')); await testUtils.fields.editInput($('.modal input'), 'new record'); await testUtils.dom.click($('.modal .modal-footer .btn-primary:first')); // checks assert.strictEqual(readIDs, undefined, "should not have read any record"); assert.strictEqual(form.$('span:contains(new record)').length, 0, "new record should be on page 2"); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'there should be 40 records on page 1'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '1-40 / 46', "pager range should be correct"); assert.strictEqual(form.$('.o_kanban_record:first span:contains(new record)').length, 0, 'new record should not be on page 1'); // save await testUtils.form.clickSave(form); // delete a record on page one checkRead = true; await testUtils.form.clickEdit(form); assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 10)').length, 1, 'first record should be the one with id 10 (next checks rely on that)'); await testUtils.dom.click(form.$('.delete_icon:first')); // checks assert.deepEqual(readIDs, [50], "should have read a record (to display 40 records on page 1)"); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'there should be 40 records on page 1'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '1-40 / 45', "pager range should be correct"); // save await testUtils.form.clickSave(form); // add and delete records in both pages await testUtils.form.clickEdit(form); checkRead = true; readIDs = undefined; // add and delete a record in page 1 await testUtils.dom.click(form.$('.o-kanban-button-new')); await testUtils.fields.editInput($('.modal input'), 'new record page 1'); await testUtils.dom.click($('.modal .modal-footer .btn-primary:first')); assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 11)').length, 1, 'first record should be the one with id 11 (next checks rely on that)'); await testUtils.dom.click(form.$('.delete_icon:first')); assert.deepEqual(readIDs, [51], "should have read a record (to display 40 records on page 1)"); // add and delete a record in page 2 await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next')); assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 52)').length, 1, 'first record should be the one with id 52 (next checks rely on that)'); checkRead = true; readIDs = undefined; await testUtils.dom.click(form.$('.delete_icon:first')); await testUtils.dom.click(form.$('.o-kanban-button-new')); await testUtils.fields.editInput($('.modal input'), 'new record page 2'); await testUtils.dom.click($('.modal .modal-footer .btn-primary:first')); assert.strictEqual(readIDs, undefined, "should not have read any record"); // checks assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5, 'there should be 5 records on page 2'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '41-45 / 45', "pager range should be correct"); assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 1)').length, 1, 'new records should be on page 2'); assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 2)').length, 1, 'new records should be on page 2'); // save await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('edition of one2many field, with onchange and not inline sub view', async function (assert) { assert.expect(2); this.data.turtle.onchanges.turtle_int = function (obj) { obj.turtle_foo = String(obj.turtle_int); }; this.data.partner.onchanges.turtles = function () { }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', archs: { 'turtle,false,list': '', 'turtle,false,form': '
', }, mockRPC: function () { return this._super.apply(this, arguments); }, res_id: 1, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.editInput($('input[name="turtle_int"]'), '5'); await testUtils.dom.click($('.modal-footer button.btn-primary').first()); assert.strictEqual(form.$('tbody tr:eq(1) td.o_data_cell').text(), '5', 'should display 5 in the foo field'); await testUtils.dom.click(form.$('tbody tr:eq(1) td.o_data_cell')); await testUtils.fields.editInput($('input[name="turtle_int"]'), '3'); await testUtils.dom.click($('.modal-footer button.btn-primary').first()); assert.strictEqual(form.$('tbody tr:eq(1) td.o_data_cell').text(), '3', 'should now display 3 in the foo field'); form.destroy(); }); QUnit.test('sorting one2many fields', async function (assert) { assert.expect(4); this.data.partner.fields.foo.sortable = true; this.data.partner.records.push({ id: 23, foo: "abc" }); this.data.partner.records.push({ id: 24, foo: "xyz" }); this.data.partner.records.push({ id: 25, foo: "def" }); this.data.partner.records[0].p = [23, 24, 25]; var rpcCount = 0; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function () { rpcCount++; return this._super.apply(this, arguments); }, }); rpcCount = 0; assert.ok(form.$('table tbody tr:eq(2) td:contains(def)').length, "the 3rd record is the one with 'def' value"); form.renderer._render = function () { throw "should not render the whole form"; }; await testUtils.dom.click(form.$('table thead th:contains(Foo)')); assert.strictEqual(rpcCount, 0, 'sort should be in memory, no extra RPCs should have been done'); assert.ok(form.$('table tbody tr:eq(2) td:contains(xyz)').length, "the 3rd record is the one with 'xyz' value"); await testUtils.dom.click(form.$('table thead th:contains(Foo)')); assert.ok(form.$('table tbody tr:eq(2) td:contains(abc)').length, "the 3rd record is the one with 'abc' value"); form.destroy(); }); QUnit.test('one2many list field edition', async function (assert) { assert.expect(6); this.data.partner.records.push({ id: 3, display_name: "relational record 1", }); this.data.partner.records[1].p = [3]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 2, }); // edit the first line of the o2m assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1', "display name of first record in o2m list should be 'relational record 1'"); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_one2many tbody td').first()); assert.hasClass(form.$('.o_field_one2many tbody td').first().parent(),'o_selected_row', "first row of o2m should be in edition"); await testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value"); assert.hasClass(form.$('.o_field_one2many tbody td').first().parent(),'o_selected_row', "first row of o2m should still be in edition"); // // leave o2m edition await testUtils.dom.click(form.$el); assert.doesNotHaveClass(form.$('.o_field_one2many tbody td').first().parent(), 'o_selected_row', "first row of o2m should be readonly again"); // discard changes await testUtils.form.clickDiscard(form); assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value', "changes shouldn't have been discarded yet, waiting for user confirmation"); await testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1', "display name of first record in o2m list should be 'relational record 1'"); // edit again and save await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_one2many tbody td').first()); await testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value"); await testUtils.dom.click(form.$el); await testUtils.form.clickSave(form); // FIXME: this next test doesn't pass as the save of updates of // relational data is temporarily disabled // assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value', // "display name of first record in o2m list should be 'new value'"); form.destroy(); }); QUnit.test('one2many list: create action disabled', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.ok(!form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should not be available in readonly'); await testUtils.form.clickEdit(form); assert.ok(!form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should not be available in readonly'); form.destroy(); }); QUnit.test('one2many list: conditional create/delete actions', async function (assert) { assert.expect(4); this.data.partner.records[0].p = [2, 4]; const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, res_id: 1, viewOptions: { mode: 'edit', }, }); // bar is true -> create and delete action are available assert.containsOnce(form, '.o_field_x2many_list_row_add', '"Add an item" link should be available'); assert.hasClass(form.$('td.o_list_record_remove button').first(), 'fa fa-trash-o', "should have trash bin icons"); // set bar to false -> create and delete action are no longer available await testUtils.dom.click(form.$('.o_field_widget[name="bar"] input').first()); assert.containsNone(form, '.o_field_x2many_list_row_add', '"Add an item" link should not be available if bar field is False'); assert.containsNone(form, 'td.o_list_record_remove button', "should not have trash bin icons if bar field is False"); form.destroy(); }); QUnit.test('one2many list: unlink two records', async function (assert) { assert.expect(8); this.data.partner.records[0].p = [1, 2, 4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { var commands = args.args[1].p; assert.strictEqual(commands.length, 3, 'should have generated three commands'); assert.ok(commands[0][0] === 4 && commands[0][1] === 2, 'should have generated the command 4 (LINK_TO) with id 4'); assert.ok(commands[1][0] === 4 && commands[1][1] === 4, 'should have generated the command 4 (LINK_TO) with id 4'); assert.ok(commands[2][0] === 3 && commands[2][1] === 1, 'should have generated the command 3 (UNLINK) with id 1'); } return this._super.apply(this, arguments); }, archs: { 'partner,false,form': '
', }, }); await testUtils.form.clickEdit(form); assert.containsN(form, 'td.o_list_record_remove button', 3, "should have 3 remove buttons"); assert.hasClass(form.$('td.o_list_record_remove button').first(),'fa fa-times', "should have X icons to remove (unlink) records"); await testUtils.dom.click(form.$('td.o_list_record_remove button').first()); assert.containsN(form, 'td.o_list_record_remove button', 2, "should have 2 remove buttons (a record is supposed to have been unlinked)"); await testUtils.dom.click(form.$('tr.o_data_row').first()); assert.containsNone($('.modal .modal-footer .o_btn_remove'), 'there should not be a modal having Remove Button'); await testUtils.dom.click($('.modal .btn-secondary')) await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('one2many list: deleting one records', async function (assert) { assert.expect(7); this.data.partner.records[0].p = [1, 2, 4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { var commands = args.args[1].p; assert.strictEqual(commands.length, 3, 'should have generated three commands'); assert.ok(commands[0][0] === 4 && commands[0][1] === 2, 'should have generated the command 4 (LINK_TO) with id 2'); assert.ok(commands[1][0] === 4 && commands[1][1] === 4, 'should have generated the command 2 (LINK_TO) with id 1'); assert.ok(commands[2][0] === 2 && commands[2][1] === 1, 'should have generated the command 2 (DELETE) with id 2'); } return this._super.apply(this, arguments); }, archs: { 'partner,false,form': '
', }, }); await testUtils.form.clickEdit(form); assert.containsN(form, 'td.o_list_record_remove button', 3, "should have 3 remove buttons"); assert.hasClass(form.$('td.o_list_record_remove button').first(),'fa fa-trash-o', "should have trash bin icons to remove (delete) records"); await testUtils.dom.click(form.$('td.o_list_record_remove button').first()); assert.containsN(form, 'td.o_list_record_remove button', 2, "should have 2 remove buttons"); // save and check that the correct command has been generated await testUtils.form.clickSave(form); // FIXME: it would be nice to test that the view is re-rendered correctly, // but as the relational data isn't re-fetched, the rendering is ok even // if the changes haven't been saved form.destroy(); }); QUnit.test('one2many kanban: edition', async function (assert) { assert.expect(23); this.data.partner.records[0].p = [2]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + // color will be in the kanban but not in the form '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + // foo will be in the form but not in the kanban '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { var commands = args.args[1].p; assert.strictEqual(commands.length, 2, 'should have generated two commands'); assert.strictEqual(commands[0][0], 0, 'generated command should be ADD WITH VALUES'); assert.strictEqual(commands[0][2].display_name, "new subrecord 3", 'value of newly created subrecord should be "new subrecord 3"'); assert.strictEqual(commands[1][0], 2, 'generated command should be REMOVE AND DELETE'); assert.strictEqual(commands[1][1], 2, 'deleted record id should be 2'); } return this._super.apply(this, arguments); }, }); assert.ok(!form.$('.o_kanban_view .delete_icon').length, 'delete icon should not be visible in readonly'); assert.ok(!form.$('.o_field_one2many .o-kanban-button-new').length, '"Create" button should not be visible in readonly'); await testUtils.form.clickEdit(form); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1, 'should contain 1 record'); assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'second record', 'display_name of subrecord should be the one in DB'); assert.strictEqual(form.$('.o_kanban_record span:nth(1)').text(), 'Red', 'color of subrecord should be the one in DB'); assert.ok(form.$('.o_kanban_view .delete_icon').length, 'delete icon should be visible in edit'); assert.ok(form.$('.o_field_one2many .o-kanban-button-new').length, '"Create" button should be visible in edit'); assert.hasClass(form.$('.o_field_one2many .o-kanban-button-new'),'btn-secondary', "'Create' button should have className 'btn-secondary'"); assert.strictEqual(form.$('.o_field_one2many .o-kanban-button-new').text().trim(), "Add", 'Create button should have "Add" label'); // edit existing subrecord await testUtils.dom.click(form.$('.oe_kanban_global_click')); await testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new name'); await testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'new name', 'value of subrecord should have been updated'); // create a new subrecord await testUtils.dom.click(form.$('.o-kanban-button-new')); await testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new subrecord 1'); await testUtils.dom.clickFirst($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2, 'should contain 2 records'); assert.strictEqual(form.$('.o_kanban_record:nth(1) span').text(), 'new subrecord 1Red', 'value of newly created subrecord should be "new subrecord 1"'); // create two new subrecords await testUtils.dom.click(form.$('.o-kanban-button-new')); await testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new subrecord 2'); await testUtils.dom.click($('.modal .modal-footer .btn-primary:nth(1)')); await testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new subrecord 3'); await testUtils.dom.clickFirst($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 4, 'should contain 4 records'); // delete subrecords await testUtils.dom.click(form.$('.oe_kanban_global_click').first()); assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1, 'There should be a modal having Remove Button'); await testUtils.dom.click($('.modal .modal-footer .o_btn_remove')); assert.containsNone($('.o_modal'), "modal should have been closed"); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 3, 'should contain 3 records'); await testUtils.dom.click(form.$('.o_kanban_view .delete_icon:first()')); await testUtils.dom.click(form.$('.o_kanban_view .delete_icon:first()')); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1, 'should contain 1 records'); assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'new subrecord 3', 'the remaining subrecord should be "new subrecord 3"'); // save and check that the correct command has been generated await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('one2many kanban (editable): properly handle create_text node option', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '
' + '' + '
' + '
' + '
' + '
' + '
' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o-kanban-button-new').text().trim(), "Add turtle", "In O2M Kanban, Add button should have 'Add turtle' label"); form.destroy(); }); QUnit.test('one2many kanban: create action disabled', async function (assert) { assert.expect(3); this.data.partner.records[0].p = [4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, }); assert.ok(!form.$('.o-kanban-button-new').length, '"Add" button should not be available in readonly'); await testUtils.form.clickEdit(form); assert.ok(!form.$('.o-kanban-button-new').length, '"Add" button should not be available in edit'); assert.ok(form.$('.o_kanban_view .delete_icon').length, 'delete icon should be visible in edit'); form.destroy(); }); QUnit.test('one2many kanban: conditional create/delete actions', async function (assert) { assert.expect(4); this.data.partner.records[0].p = [2, 4]; const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, res_id: 1, viewOptions: { mode: 'edit', }, }); // bar is initially true -> create and delete actions are available assert.containsOnce(form, '.o-kanban-button-new', '"Add" button should be available'); await testUtils.dom.click(form.$('.oe_kanban_global_click').first()); assert.containsOnce(document.body, '.modal .modal-footer .o_btn_remove', 'There should be a Remove Button inside modal'); await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); // set bar false -> create and delete actions are no longer available await testUtils.dom.click(form.$('.o_field_widget[name="bar"] input').first()); assert.containsNone(form, '.o-kanban-button-new', '"Add" button should not be available as bar is False'); await testUtils.dom.click(form.$('.oe_kanban_global_click').first()); assert.containsNone(document.body, '.modal .modal-footer .o_btn_remove', 'There should not be a Remove Button as bar field is False'); form.destroy(); }); QUnit.test('editable one2many list, pager is updated', async function (assert) { assert.expect(1); this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' }); this.data.partner.records[0].turtles = [1, 2, 3, 4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a record, then click in form view to confirm it await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.dom.click(form.$el); assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5', "pager should display the correct total"); form.destroy(); }); QUnit.test('one2many list (non editable): edition', async function (assert) { assert.expect(12); var nbWrite = 0; this.data.partner.records[0].p = [2, 4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { nbWrite++; assert.deepEqual(args.args[1], { p: [[1, 2, { display_name: 'new name' }], [2, 4, false]] }, "should have sent the correct commands"); } return this._super.apply(this, arguments); }, }); assert.ok(!form.$('.o_list_record_remove').length, 'remove icon should not be visible in readonly'); assert.ok(!form.$('.o_field_x2many_list_row_add').length, '"Add an item" should not be visible in readonly'); await testUtils.form.clickEdit(form); assert.containsN(form, '.o_list_view td.o_list_number', 2, 'should contain 2 records'); assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'second record', 'display_name of first subrecord should be the one in DB'); assert.ok(form.$('.o_list_record_remove').length, 'remove icon should be visible in edit'); assert.ok(form.$('.o_field_x2many_list_row_add').length, '"Add an item" should not visible in edit'); // edit existing subrecord await testUtils.dom.click(form.$('.o_list_view tbody tr:first() td:eq(1)')); await testUtils.fields.editInput($('.modal .o_form_view input'), 'new name'); await testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name', 'value of subrecord should have been updated'); assert.strictEqual(nbWrite, 0, "should not have write anything in DB"); // create new subrecords // TODO when 'Add an item' will be implemented // remove subrecords await testUtils.dom.click(form.$('.o_list_record_remove:nth(1)')); assert.containsOnce(form, '.o_list_view td.o_list_number', 'should contain 1 subrecord'); assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name', 'the remaining subrecord should be "new name"'); await testUtils.form.clickSave(form); // save the record assert.strictEqual(nbWrite, 1, "should have write the changes in DB"); form.destroy(); }); QUnit.test('one2many list (editable): edition', async function (assert) { assert.expect(7); this.data.partner.records[0].p = [2, 4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, }); assert.ok(!form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should not be available in readonly'); await testUtils.dom.click(form.$('.o_list_view tbody td:first()')); assert.ok($('.modal .o_form_readonly').length, 'in readonly, clicking on a subrecord should open it in readonly in a dialog'); await testUtils.dom.click($('.modal .o_form_button_cancel')); await testUtils.form.clickEdit(form); assert.ok(form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should be available in edit'); // edit existing subrecord await testUtils.dom.click(form.$('.o_list_view tbody td:first()')); assert.strictEqual($('.modal').length, 0, 'in edit, clicking on a subrecord should not open a dialog'); assert.hasClass(form.$('.o_list_view tbody tr:first()'),'o_selected_row', 'first row should be in edition'); await testUtils.fields.editInput(form.$('.o_list_view input:first()'), 'new name'); await testUtils.dom.click(form.$('.o_list_view tbody tr:nth(1) td:first')); assert.doesNotHaveClass(form.$('.o_list_view tbody tr:first'), 'o_selected_row', 'first row should not be in edition anymore'); assert.strictEqual(form.$('.o_list_view tbody td:first').text(), 'new name', 'value of subrecord should have been updated'); // create new subrecords // TODO when 'Add an item' will be implemented form.destroy(); }); QUnit.test('one2many list (editable): edition, part 2', async function (assert) { assert.expect(8); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { assert.strictEqual(args.args[1].p[0][0], 0, "should send a 0 command for field p"); assert.strictEqual(args.args[1].p[1][0], 0, "should send a second 0 command for field p"); } return this._super.apply(this, arguments); }, }); // edit mode, then click on Add an item and enter a value await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.editInput(form.$('.o_selected_row > td input'), 'kartoffel'); // click again on Add an item await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.strictEqual(form.$('td:contains(kartoffel)').length, 1, "should have one td with the new value"); assert.containsOnce(form, '.o_selected_row > td input', "should have one other new td"); assert.containsN(form, 'tr.o_data_row', 2, "should have 2 data rows"); // enter another value and save await testUtils.fields.editInput(form.$('.o_selected_row > td input'), 'gemuse'); await testUtils.form.clickSave(form); assert.containsN(form, 'tr.o_data_row', 2, "should have 2 data rows"); assert.strictEqual(form.$('td:contains(kartoffel)').length, 1, "should have one td with the new value"); assert.strictEqual(form.$('td:contains(gemuse)').length, 1, "should have one td with the new value"); form.destroy(); }); QUnit.test('one2many list (editable): edition, part 3', async function (assert) { assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // edit mode, then click on Add an item 2 times assert.containsOnce(form, 'tr.o_data_row', "should have 1 data rows"); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, 'tr.o_data_row', 3, "should have 3 data rows"); // cancel the edition await testUtils.form.clickDiscard(form); await testUtils.dom.click($('.modal-footer button.btn-primary').first()); assert.containsOnce(form, 'tr.o_data_row', "should have 1 data rows"); form.destroy(); }); QUnit.test('one2many list (editable): edition, part 4', async function (assert) { assert.expect(3); var i = 0; this.data.turtle.onchanges = { turtle_trululu: function (obj) { if (i) { obj.turtle_description = "Some Description"; } i++; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, }); // edit mode, then click on Add an item assert.containsNone(form, 'tr.o_data_row', "should have 0 data rows"); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.strictEqual(form.$('textarea').val(), "", "field turtle_description should be empty"); // add a value in the turtle_trululu field to trigger an onchange await testUtils.fields.many2one.clickOpenDropdown('turtle_trululu'); await testUtils.fields.many2one.clickHighlightedItem('turtle_trululu'); assert.strictEqual(form.$('textarea').val(), "Some Description", "field turtle_description should be set to the result of the onchange"); form.destroy(); }); QUnit.test('one2many list (editable): discarding required empty data', async function (assert) { assert.expect(7); this.data.turtle.fields.turtle_foo.required = true; delete this.data.turtle.fields.turtle_foo.default; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { if (args.method) { assert.step(args.method); } return this._super.apply(this, arguments); }, }); // edit mode, then click on Add an item, then click elsewhere assert.containsNone(form, 'tr.o_data_row', "should have 0 data rows"); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.dom.click(form.$('label.o_form_label').first()); assert.containsNone(form, 'tr.o_data_row', "should still have 0 data rows"); // click on Add an item again, then click on save await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.form.clickSave(form); assert.containsNone(form, 'tr.o_data_row', "should still have 0 data rows"); assert.verifySteps(['read', 'onchange', 'onchange']); form.destroy(); }); QUnit.test('editable one2many list, adding line when only one page', async function (assert) { assert.expect(5); this.data.partner.records[0].turtles = [1, 2, 3]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a record, to reach the page size limit await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); // the record currently being added should not count in the pager assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager'); // unselect the row await testUtils.dom.click(form.$el); assert.containsNone(form, '.o_selected_row'); assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager'); await testUtils.form.clickSave(form); assert.containsOnce(form, '.o_field_widget[name=turtles] .o_pager'); assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text(), "1-3 / 4"); form.destroy(); }); QUnit.test('editable one2many list, adding line, then discarding', async function (assert) { assert.expect(2); this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' }); this.data.partner.records[0].turtles = [1, 2, 3, 4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a record, then discard await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.form.clickDiscard(form); // confirm the discard operation await testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.isVisible(form.$('.o_field_widget[name=turtles] .o_pager')); assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-3 / 4', "pager should display correct values"); form.destroy(); }); QUnit.test('editable one2many list, required field and pager', async function (assert) { assert.expect(1); this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' }); this.data.turtle.fields.turtle_foo.required = true; this.data.partner.records[0].turtles = [1, 2, 3, 4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a (empty) record await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); // go on next page. The new record is not valid and should be discarded await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); assert.containsOnce(form, 'tr.o_data_row'); form.destroy(); }); QUnit.test('editable one2many list, required field, pager and confirm discard', async function (assert) { assert.expect(3); this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' }); this.data.turtle.fields.turtle_foo.required = true; this.data.partner.records[0].turtles = [1, 2, 3, 4]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a record with a dirty state, but not valid await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.editInput(form.$('input[name="turtle_int"]'), 4321); // go to next page. The new record is not valid, but dirty. we should // see a confirm dialog await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5', "pager should still display the correct total"); // click on cancel await testUtils.dom.click($('.modal .modal-footer .btn-secondary')); assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5', "pager should again display the correct total"); assert.containsOnce(form, '.o_field_one2many input.o_field_invalid', "there should be an invalid input in the one2many"); form.destroy(); }); QUnit.test('editable one2many list, adding, discarding, and pager', async function (assert) { assert.expect(4); this.data.partner.records[0].turtles = [1]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add 4 records (to have more records than the limit) await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, 'tr.o_data_row', 5); assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager'); // discard await testUtils.form.clickDiscard(form); await testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.containsOnce(form, 'tr.o_data_row'); assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager'); form.destroy(); }); QUnit.test('unselecting a line with missing required data', async function (assert) { assert.expect(5); this.data.turtle.fields.turtle_foo.required = true; delete this.data.turtle.fields.turtle_foo.default; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, }); // edit mode, then click on Add an item, then click elsewhere assert.containsNone(form, 'tr.o_data_row', "should have 0 data rows"); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsOnce(form, 'tr.o_data_row', "should have 1 data rows"); // adding a value in the non required field, so it is dirty, but with // a missing required field await testUtils.fields.editInput(form.$('input[name="turtle_int"]'), '12345'); // click elsewhere, await testUtils.dom.click(form.$('label.o_form_label')); assert.strictEqual($('.modal').length, 1, 'a confirmation model should be opened'); // click on cancel, the line should still be selected await testUtils.dom.click($('.modal .modal-footer button.btn-secondary')); assert.containsOnce(form, 'tr.o_data_row.o_selected_row', "should still have 1 selected data row"); // click elsewhere, and click on ok (on the confirmation dialog) await testUtils.dom.click(form.$('label.o_form_label')); await testUtils.dom.click($('.modal .modal-footer button.btn-primary')); assert.containsNone(form, 'tr.o_data_row', "should have 0 data rows (invalid line has been discarded"); form.destroy(); }); QUnit.test('pressing enter in a o2m with a required empty field', async function (assert) { assert.expect(4); this.data.turtle.fields.turtle_foo.required = true; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); // edit mode, then click on Add an item, then press enter await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'enter'); assert.hasClass(form.$('input[name="turtle_foo"]'), 'o_field_invalid', "input should be marked invalid"); assert.verifySteps(['read', 'onchange']); form.destroy(); }); QUnit.test('editing a o2m, with required field and onchange', async function (assert) { assert.expect(11); this.data.turtle.fields.turtle_foo.required = true; delete this.data.turtle.fields.turtle_foo.default; this.data.turtle.onchanges = { turtle_foo: function (obj) { obj.turtle_int = obj.turtle_foo.length; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { if (args.method) { assert.step(args.method); } return this._super.apply(this, arguments); }, }); // edit mode, then click on Add an item assert.containsNone(form, 'tr.o_data_row', "should have 0 data rows"); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); // input some text in required turtle_foo field await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'aubergine'); assert.strictEqual(form.$('input[name="turtle_int"]').val(), "9", "onchange should have been triggered"); // save and check everything is fine await testUtils.form.clickSave(form); assert.strictEqual(form.$('.o_data_row td:contains(aubergine)').length, 1, "should have one row with turtle_foo value"); assert.strictEqual(form.$('.o_data_row td:contains(9)').length, 1, "should have one row with turtle_int value"); assert.verifySteps(['read', 'onchange', 'onchange', 'write', 'read', 'read']); form.destroy(); }); QUnit.test('editable o2m, pressing ESC discard current changes', async function (assert) { assert.expect(5); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsOnce(form, 'tr.o_data_row', "there should be one data row"); await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'escape'); assert.containsNone(form, 'tr.o_data_row', "data row should have been discarded"); assert.verifySteps(['read', 'onchange']); form.destroy(); }); QUnit.test('editable o2m with required field, pressing ESC discard current changes', async function (assert) { assert.expect(5); this.data.turtle.fields.turtle_foo.required = true; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsOnce(form, 'tr.o_data_row', "there should be one data row"); await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'escape'); assert.containsNone(form, 'tr.o_data_row', "data row should have been discarded"); assert.verifySteps(['read', 'onchange']); form.destroy(); }); QUnit.test('pressing escape in editable o2m list in dialog', async function (assert) { assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { "partner,false,form": '
' + '' + '' + '' + '' + '' + '
', }, viewOptions: { mode: 'edit', }, }); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a')); assert.strictEqual($('.modal .o_data_row.o_selected_row').length, 1, "there should be a row in edition in the dialog"); await testUtils.fields.triggerKeydown($('.modal .o_data_cell input'), 'escape'); assert.strictEqual($('.modal').length, 1, "dialog should still be open"); assert.strictEqual($('.modal .o_data_row').length, 0, "the row should have been removed"); form.destroy(); }); QUnit.test('editable o2m with onchange and required field: delete an invalid line', async function (assert) { assert.expect(5); this.data.partner.onchanges = { turtles: function () { }, }; this.data.partner.records[0].turtles = [1]; this.data.turtle.records[0].product_id = 37; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, viewOptions: { mode: 'edit', }, }); await testUtils.dom.click(form.$('.o_data_cell:first')); form.$('.o_field_widget[name="product_id"] input').val('').trigger('keyup'); assert.verifySteps(['read', 'read'], 'no onchange should be done as line is invalid'); await testUtils.dom.click(form.$('.o_list_record_remove')); assert.verifySteps(['onchange'], 'onchange should have been done'); form.destroy(); }); QUnit.test('onchange in a one2many', async function (assert) { assert.expect(1); this.data.partner.records.push({ id: 3, foo: "relational record 1", }); this.data.partner.records[1].p = [3]; this.data.partner.onchanges = { p: true }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { if (args.method === 'onchange') { return Promise.resolve({ value: { p: [ [5], // delete all [0, 0, { foo: "from onchange" }], // create new ] } }); } return this._super(route, args); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_one2many tbody td').first()); await testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value"); await testUtils.form.clickSave(form); assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'from onchange', "display name of first record in o2m list should be 'new value'"); form.destroy(); }); QUnit.test('one2many, default_get and onchange (basic)', async function (assert) { assert.expect(1); this.data.partner.fields.p.default = [ [6, 0, []], // replace with zero ids ]; this.data.partner.onchanges = { p: true }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'onchange') { return Promise.resolve({ value: { p: [ [5], // delete all [0, 0, { foo: "from onchange" }], // create new ] } }); } return this._super(route, args); }, }); assert.ok(form.$('td:contains(from onchange)').length, "should have 'from onchange' value in one2many"); form.destroy(); }); QUnit.test('one2many and default_get (with date)', async function (assert) { assert.expect(1); this.data.partner.fields.p.default = [ [0, false, { date: '2017-10-08', p: [] }], ]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', }); assert.strictEqual(form.$('.o_data_cell').text(), '10/08/2017', "should correctly display the date"); form.destroy(); }); QUnit.test('one2many and onchange (with integer)', async function (assert) { assert.expect(4); this.data.turtle.onchanges = { turtle_int: function () { } }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('td:contains(9)')); await testUtils.fields.editInput(form.$('td input[name="turtle_int"]'), "3"); // the 'change' event is triggered on the input when we focus somewhere // else, for example by clicking in the body. However, if we try to // programmatically click in the body, it does not trigger a change // event, so we simply trigger it directly instead. form.$('td input[name="turtle_int"]').trigger('change'); assert.verifySteps(['read', 'read', 'onchange']); form.destroy(); }); QUnit.test('one2many and onchange (with date)', async function (assert) { assert.expect(7); this.data.partner.onchanges = { date: function () { } }; this.data.partner.records[0].p = [2]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('td:contains(01/25/2017)')); await testUtils.dom.click(form.$('.o_datepicker_input')); await testUtils.nextTick(); await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first()); await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)')); await testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)')); await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(1)); await testUtils.dom.click($('.day:contains(22)')); await testUtils.form.clickSave(form); assert.verifySteps(['read', 'read', 'onchange', 'write', 'read', 'read']); form.destroy(); }); QUnit.test('one2many and onchange (with command DELETE_ALL)', async function (assert) { assert.expect(5); this.data.partner.onchanges = { foo: function (obj) { obj.p = [[5]]; }, p: function () { }, // dummy onchange on the o2m to execute _isX2ManyValid() }; this.data.partner.records[0].p = [2]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', mockRPC: function (method, args) { if (args.method === 'write') { assert.deepEqual(args.args[1].p, [ [0, args.args[1].p[0][1], { display_name: 'z' }], [2, 2, false], ], "correct commands should be sent"); } return this._super.apply(this, arguments); }, res_id: 1, viewOptions: { mode: 'edit', }, }); assert.containsOnce(form, '.o_data_row', "o2m should contain one row"); // empty o2m by triggering the onchange await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange'); assert.containsNone(form, '.o_data_row', "rows of the o2m should have been deleted"); // add two new subrecords await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'x'); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'y'); assert.containsN(form, '.o_data_row', 2, "o2m should contain two rows"); // empty o2m by triggering the onchange await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange again'); assert.containsNone(form, '.o_data_row', "rows of the o2m should have been deleted"); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'z'); await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('one2many and onchange only write modified field', async function (assert) { assert.expect(2); this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [ [5], // delete all [1, 3, { // the server returns all fields display_name: "coucou", product_id: [37, "xphone"], turtle_bar: false, turtle_foo: "has changed", turtle_int: 42, turtle_qux: 9.8, partner_ids: [], turtle_ref: 'product,37', }], ]; }, }; this.data.partner.records[0].turtles = [3]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', mockRPC: function (method, args) { if (args.method === 'write') { assert.deepEqual(args.args[1].turtles, [ [1, 3, { display_name: 'coucou', turtle_foo: 'has changed', turtle_int: 42 }], ], "correct commands should be sent (only send changed values)"); } return this._super.apply(this, arguments); }, res_id: 1, viewOptions: { mode: 'edit', }, }); assert.containsOnce(form, '.o_data_row', "o2m should contain one row"); await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first')); await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp'); await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('one2many with CREATE onchanges correctly refreshed', async function (assert) { assert.expect(5); var delta = 0; testUtils.mock.patch(AbstractField, { init: function () { delta++; this._super.apply(this, arguments); }, destroy: function () { delta--; this._super.apply(this, arguments); }, }); var deactiveOnchange = true; this.data.partner.records[0].turtles = []; this.data.partner.onchanges = { turtles: function (obj) { if (deactiveOnchange) { return; } // the onchange will either: // - create a second line if there is only one line // - edit the second line if there are two lines if (obj.turtles.length === 1) { obj.turtles = [ [5], // delete all [0, obj.turtles[0][1], { display_name: "first", turtle_int: obj.turtles[0][2].turtle_int, }], [0, 0, { display_name: "second", turtle_int: -obj.turtles[0][2].turtle_int, }], ]; } else if (obj.turtles.length === 2) { obj.turtles = [ [5], // delete all [0, obj.turtles[0][1], { display_name: "first", turtle_int: obj.turtles[0][2].turtle_int, }], [0, obj.turtles[1][1], { display_name: "second", turtle_int: -obj.turtles[0][2].turtle_int, }], ]; } }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.containsNone(form, '.o_data_row', "o2m shouldn't contain any row"); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); // trigger the first onchange deactiveOnchange = false; await testUtils.fields.editInput(form.$('input[name="turtle_int"]'), '10'); // put the list back in non edit mode await testUtils.dom.click(form.$('input[name="foo"]')); assert.strictEqual(form.$('.o_data_row').text(), "first10second-10", "should correctly refresh the records"); // trigger the second onchange await testUtils.dom.click(form.$('.o_field_x2many_list tbody tr:first td:first')); await testUtils.fields.editInput(form.$('input[name="turtle_int"]'), '20'); await testUtils.dom.click(form.$('input[name="foo"]')); assert.strictEqual(form.$('.o_data_row').text(), "first20second-20", "should correctly refresh the records"); assert.containsN(form, '.o_field_widget', delta, "all (non visible) field widgets should have been destroyed"); await testUtils.form.clickSave(form); assert.strictEqual(form.$('.o_data_row').text(), "first20second-20", "should correctly refresh the records after save"); form.destroy(); testUtils.mock.unpatch(AbstractField); }); QUnit.test('editable one2many with sub widgets are rendered in readonly', async function (assert) { assert.expect(2); var editableWidgets = 0; testUtils.mock.patch(AbstractField, { init: function () { this._super.apply(this, arguments); if (this.mode === 'edit') { editableWidgets++; } }, }); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual(editableWidgets, 1, "o2m is only widget in edit mode"); await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); assert.strictEqual(editableWidgets, 3, "3 widgets currently in edit mode"); form.destroy(); testUtils.mock.unpatch(AbstractField); }); QUnit.test('one2many editable list with onchange keeps the order', async function (assert) { assert.expect(2); this.data.partner.records[0].p = [1, 2, 4]; this.data.partner.onchanges = { p: function () { }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual(form.$('.o_data_cell').text(), 'first recordsecond recordaaa', "records should be display in the correct order"); await testUtils.dom.click(form.$('.o_data_row:first .o_data_cell')); await testUtils.fields.editInput(form.$('.o_selected_row .o_field_widget[name=display_name]'), 'new'); await testUtils.dom.click(form.$el); assert.strictEqual(form.$('.o_data_cell').text(), 'newsecond recordaaa', "records should be display in the correct order"); form.destroy(); }); QUnit.test('one2many list (editable): readonly domain is evaluated', async function (assert) { assert.expect(2); this.data.partner.records[0].p = [2, 4]; this.data.partner.records[1].product_id = false; this.data.partner.records[2].product_id = 37; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); assert.hasClass(form.$('.o_list_view tbody tr:eq(0) td:first'),'o_readonly_modifier', "first record should have display_name in readonly mode"); assert.doesNotHaveClass(form.$('.o_list_view tbody tr:eq(1) td:first'), 'o_readonly_modifier', "second record should not have display_name in readonly mode"); form.destroy(); }); QUnit.test('pager of one2many field in new record', async function (assert) { assert.expect(3); this.data.partner.records[0].p = []; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', }); assert.containsNone(form, '.o_x2m_control_panel .o_pager', 'o2m pager should be hidden'); // click to create a subrecord await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); assert.containsOnce(form, 'tr.o_data_row'); assert.containsNone(form, '.o_x2m_control_panel .o_pager', 'o2m pager should be hidden'); form.destroy(); }); QUnit.test('one2many list with a many2one', async function (assert) { assert.expect(5); let checkOnchange = false; this.data.partner.records[0].p = [2]; this.data.partner.records[1].product_id = 37; this.data.partner.onchanges.p = function (obj) { obj.p = [ [5], // delete all [1, 2, { product_id: [37, "xphone"] }], // update existing record [0, 0, { product_id: [41, "xpad"] }] ]; // }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner,false,form': '
', }, mockRPC: function (route, args) { if (args.method === 'onchange' && checkOnchange) { assert.deepEqual(args.args[1].p, [[4, 2, false], [0, args.args[1].p[1][1], { product_id: 41 }]], "should trigger onchange with correct parameters"); } return this._super.apply(this, arguments); }, }); assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1, "should have properly fetched the many2one nameget"); assert.strictEqual(form.$('tbody td:contains(xpad)').length, 0, "should not display 'xpad' anywhere"); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); checkOnchange = true; await testUtils.fields.many2one.clickOpenDropdown('product_id'); testUtils.fields.many2one.clickItem('product_id', 'xpad'); await testUtils.dom.click($('.modal .modal-footer button:eq(0)')); assert.strictEqual(form.$('tbody td:contains(xpad)').length, 1, "should display 'xpad' on a td"); assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1, "should still display xphone"); form.destroy(); }); QUnit.test('one2many list with inline form view', async function (assert) { assert.expect(5); this.data.partner.records[0].p = []; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + // don't remove this, it is // useful to make sure the foo fieldwidget // does not crash because the foo field // is not in the form view '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[1].p, [[0, args.args[1].p[0][1], { foo: "My little Foo Value", int_field: 123, product_id: 41, }]]); } return this._super(route, args); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); // write in the many2one field, value = 37 (xphone) await testUtils.fields.many2one.clickOpenDropdown('product_id'); await testUtils.fields.many2one.clickHighlightedItem('product_id'); // write in the integer field await testUtils.fields.editInput($('.modal .modal-body input.o_field_widget'), '123'); // save and close await testUtils.dom.click($('.modal .modal-footer button:eq(0)')); assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1, "should display 'xphone' in a td"); // reopen the record in form view await testUtils.dom.click(form.$('tbody td:contains(xphone)')); assert.strictEqual($('.modal .modal-body input').val(), "xphone", "should display 'xphone' in an input"); await testUtils.fields.editInput($('.modal .modal-body input.o_field_widget'), '456'); // discard await testUtils.dom.click($('.modal .modal-footer span:contains(Discard)')); // reopen the record in form view await testUtils.dom.click(form.$('tbody td:contains(xphone)')); assert.strictEqual($('.modal .modal-body input.o_field_widget').val(), "123", "should display 123 (previous change has been discarded)"); // write in the many2one field, value = 41 (xpad) await testUtils.fields.many2one.clickOpenDropdown('product_id'); testUtils.fields.many2one.clickItem('product_id', 'xpad'); // save and close await testUtils.dom.click($('.modal .modal-footer button:eq(0)')); assert.strictEqual(form.$('tbody td:contains(xpad)').length, 1, "should display 'xpad' in a td"); // save the record await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('one2many list with inline form view with context with parent key', async function (assert) { assert.expect(2); this.data.partner.records[0].p = [2]; this.data.partner.records[0].product_id = 41; this.data.partner.records[1].product_id = 37; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'name_search') { assert.strictEqual(args.kwargs.context.partner_foo, "yop", "should have correctly evaluated parent foo field"); assert.strictEqual(args.kwargs.context.lalala, 41, "should have correctly evaluated parent product_id field"); } return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); // open a modal await testUtils.dom.click(form.$('tr.o_data_row:eq(0) td:contains(xphone)')); // write in the many2one field await testUtils.dom.click($('.modal .o_field_many2one input')); form.destroy(); }); QUnit.test('value of invisible x2many fields is correctly evaluated in context', async function (assert) { assert.expect(1); this.data.partner.records[0].timmy = [12]; this.data.partner.records[0].p = [2, 3]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'name_search') { assert.deepEqual( args.kwargs.context, { p: [[4, 2, false], [4, 3, false]], timmy: [[6, false, [12]]], }, 'values of x2manys should have been correctly evaluated in context'); } return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_widget[name=product_id] input')); form.destroy(); }); QUnit.test('one2many list, editable, with many2one and with context with parent key', async function (assert) { assert.expect(1); this.data.partner.records[0].p = [2]; this.data.partner.records[1].product_id = 37; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'name_search') { assert.strictEqual(args.kwargs.context.partner_foo, "yop", "should have correctly evaluated parent foo field"); } return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('tr.o_data_row:eq(0) td:contains(xphone)')); // trigger a name search await testUtils.dom.click(form.$('table td input')); form.destroy(); }); QUnit.test('one2many list, editable, with a date in the context', async function (assert) { assert.expect(1); this.data.partner.records[0].p = [2]; this.data.partner.records[1].product_id = 37; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { if (args.method === 'onchange') { assert.strictEqual(args.kwargs.context.date, '2017-01-25', "should have properly evaluated date key in context"); } return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); form.destroy(); }); QUnit.test('one2many field with context', async function (assert) { assert.expect(2); var counter = 0; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'onchange') { var expected = counter === 0 ? [[4, 2, false]] : [[4, 2, false], [0, args.kwargs.context.turtles[1][1], { turtle_foo: 'hammer' }]]; assert.deepEqual(args.kwargs.context.turtles, expected, "should have properly evaluated turtles key in context"); counter++; } return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'hammer'); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); form.destroy(); }); QUnit.test('one2many list edition, some basic functionality', async function (assert) { assert.expect(3); this.data.partner.fields.foo.default = false; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); assert.containsOnce(form, 'td input.o_field_widget', "should have created a row in edit mode"); await testUtils.fields.editInput(form.$('td input.o_field_widget'), 'a'); assert.containsOnce(form, 'td input.o_field_widget', "should not have unselected the row after edition"); await testUtils.fields.editInput(form.$('td input.o_field_widget'), 'abc'); await testUtils.form.clickSave(form); assert.strictEqual(form.$('td:contains(abc)').length, 1, "should have a row with the correct value"); form.destroy(); }); QUnit.test('one2many list, the context is properly evaluated and sent', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'onchange') { var context = args.kwargs.context; assert.strictEqual(context.hello, "world"); assert.strictEqual(context.abc, 10); } return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); form.destroy(); }); QUnit.test('one2many with many2many widget: create', async function (assert) { assert.expect(10); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '', archs: { 'turtle,false,list': '', 'turtle,false,search': '', }, session: {}, res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/turtle/create') { assert.ok(args.args, "should write on the turtle record"); } if (route === '/web/dataset/call_kw/partner/write') { assert.strictEqual(args.args[0][0], 1, "should write on the partner record 1"); assert.strictEqual(args.args[1].turtles[0][0], 6, "should send only a 'replace with' command"); } return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.strictEqual($('.modal .o_data_row').length, 2, "should have 2 records in the select view (the last one is not displayed because it is already selected)"); await testUtils.dom.click($('.modal .o_data_row:first .o_list_record_selector input')); await testUtils.dom.click($('.modal .o_select_button')); await testUtils.dom.click($('.o_form_button_save')); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.strictEqual($('.modal .o_data_row').length, 1, "should have 1 record in the select view"); await testUtils.dom.click($('.modal-footer button:eq(1)')); await testUtils.fields.editInput($('.modal input.o_field_widget[name="turtle_foo"]'), 'tototo'); await testUtils.fields.editInput($('.modal input.o_field_widget[name="turtle_int"]'), 50); await testUtils.fields.many2one.clickOpenDropdown('product_id'); await testUtils.fields.many2one.clickHighlightedItem('product_id'); await testUtils.dom.click($('.modal-footer button:contains(&):first')); assert.strictEqual($('.modal').length, 0, "should close the modals"); assert.containsN(form, '.o_data_row', 3, "should have 3 records in one2many list"); assert.strictEqual(form.$('.o_data_row').text(), "blip1.59yop1.50tototo1.550xphone", "should display the record values in one2many list"); await testUtils.dom.click($('.o_form_button_save')); form.destroy(); }); QUnit.test('one2many with many2many widget: edition', async function (assert) { assert.expect(7); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '', archs: { 'turtle,false,list': '', 'turtle,false,search': '', }, session: {}, res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/turtle/write') { assert.strictEqual(args.args[0].length, 1, "should write on the turtle record"); assert.deepEqual(args.args[1], { "product_id": 37 }, "should write only the product_id on the turtle record"); } if (route === '/web/dataset/call_kw/partner/write') { assert.strictEqual(args.args[0][0], 1, "should write on the partner record 1"); assert.strictEqual(args.args[1].turtles[0][0], 6, "should send only a 'replace with' command"); } return this._super.apply(this, arguments); }, }); await testUtils.dom.click(form.$('.o_data_row:first')); assert.strictEqual($('.modal .modal-title').first().text().trim(), 'Open: one2many turtle field', "modal should use the python field string as title"); await testUtils.dom.click($('.modal .o_form_button_cancel')); await testUtils.form.clickEdit(form); // edit the first one2many record await testUtils.dom.click(form.$('.o_data_row:first')); await testUtils.fields.many2one.clickOpenDropdown('product_id'); await testUtils.fields.many2one.clickHighlightedItem('product_id'); await testUtils.dom.click($('.modal-footer button:first')); await testUtils.dom.click($('.o_form_button_save')); // add a one2many record await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.dom.click($('.modal .o_data_row:first .o_list_record_selector input')); await testUtils.dom.click($('.modal .o_select_button')); // edit the second one2many record await testUtils.dom.click(form.$('.o_data_row:eq(1)')); await testUtils.fields.many2one.clickOpenDropdown('product_id'); await testUtils.fields.many2one.clickHighlightedItem('product_id'); await testUtils.dom.click($('.modal-footer button:first')); await testUtils.dom.click($('.o_form_button_save')); form.destroy(); }); QUnit.test('new record, the context is properly evaluated and sent', async function (assert) { assert.expect(2); this.data.partner.fields.int_field.default = 17; var n = 0; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', mockRPC: function (route, args) { if (args.method === 'onchange') { n++; if (n === 2) { var context = args.kwargs.context; assert.strictEqual(context.hello, "world"); assert.strictEqual(context.abc, 17); } } return this._super.apply(this, arguments); }, }); await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); form.destroy(); }); QUnit.test('parent data is properly sent on an onchange rpc', async function (assert) { assert.expect(1); this.data.partner.onchanges = { bar: function () { } }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'onchange') { var fieldValues = args.args[1]; assert.strictEqual(fieldValues.trululu.foo, "yop", "should have properly sent the parent foo value"); } return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); // use of owlCompatibilityNextTick because we have an x2many field with a boolean field // (written in owl), so when we add a line, we sequentially render the list itself // (including the boolean field), so we have to wait for the next animation frame, and // then we render the control panel (also in owl), so we have to wait again for the // next animation frame await testUtils.owlCompatibilityNextTick(); form.destroy(); }); QUnit.test('parent data is properly sent on an onchange rpc (existing x2many record)', async function (assert) { assert.expect(4); this.data.partner.onchanges = { display_name: function () {}, }; this.data.partner.records[0].p = [1]; this.data.partner.records[0].turtles = [2]; const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, res_id: 1, mockRPC(route, args) { if (args.method === 'onchange') { const fieldValues = args.args[1]; assert.strictEqual(fieldValues.trululu.foo, "yop"); // we only send fields that changed inside the reverse many2one assert.deepEqual(fieldValues.trululu.p, [ [1, 1, { display_name: 'new val' }], ]); } return this._super(...arguments); }, viewOptions: { mode: 'edit', }, }); assert.containsOnce(form, '.o_data_row'); await testUtils.dom.click(form.$('.o_data_row .o_data_cell:first')); assert.containsOnce(form, '.o_data_row.o_selected_row'); await testUtils.fields.editInput(form.$('.o_selected_row .o_field_widget[name=display_name]'), "new val"); form.destroy(); }); QUnit.test('parent data is properly sent on an onchange rpc, new record', async function (assert) { assert.expect(4); this.data.turtle.onchanges = { turtle_bar: function () { } }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', mockRPC: function (route, args) { assert.step(args.method); if (args.method === 'onchange' && args.model === 'turtle') { var fieldValues = args.args[1]; assert.strictEqual(fieldValues.turtle_trululu.foo, "My little Foo Value", "should have properly sent the parent foo value"); } return this._super.apply(this, arguments); }, }); await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); // use of owlCompatibilityNextTick because we have an x2many field with a boolean field // (written in owl), so when we add a line, we sequentially render the list itself // (including the boolean field), so we have to wait for the next animation frame, and // then we render the control panel (also in owl), so we have to wait again for the // next animation frame await testUtils.owlCompatibilityNextTick(); assert.verifySteps(['onchange', 'onchange']); form.destroy(); }); QUnit.test('id in one2many obtained in onchange is properly set', async function (assert) { assert.expect(1); this.data.partner.onchanges.turtles = function (obj) { obj.turtles = [ [5], [1, 3, { turtle_foo: "kawa" }] ]; }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', }); assert.strictEqual(form.$('tr.o_data_row').text(), '3kawa', "should have properly displayed id and foo field"); form.destroy(); }); QUnit.test('id field in one2many in a new record', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'create') { var virtualID = args.args[0].turtles[0][1]; assert.deepEqual(args.args[0].turtles, [[0, virtualID, { turtle_foo: "cat" }]], 'should send proper commands'); } return this._super.apply(this, arguments); }, }); await testUtils.dom.click(form.$('td.o_field_x2many_list_row_add a')); await testUtils.fields.editInput(form.$('td input[name="turtle_foo"]'), 'cat'); await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('sub form view with a required field', async function (assert) { assert.expect(2); this.data.partner.fields.foo.required = true; this.data.partner.fields.foo.default = null; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
' + '', res_id: 1, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); await testUtils.dom.click($('.modal-footer button.btn-primary').first()); assert.strictEqual($('.modal').length, 1, "should still have an open modal"); assert.strictEqual($('.modal tbody label.o_field_invalid').length, 1, "should have displayed invalid fields"); form.destroy(); }); QUnit.test('one2many list with action button', async function (assert) { assert.expect(4); this.data.partner.records[0].p = [2]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '