From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/web/static/tests/views/form_tests.js | 9907 +++++++++++++++++++++++++++ 1 file changed, 9907 insertions(+) create mode 100644 addons/web/static/tests/views/form_tests.js (limited to 'addons/web/static/tests/views/form_tests.js') diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js new file mode 100644 index 00000000..094d913f --- /dev/null +++ b/addons/web/static/tests/views/form_tests.js @@ -0,0 +1,9907 @@ +odoo.define('web.form_tests', function (require) { +"use strict"; + +const AbstractField = require("web.AbstractField"); +var AbstractStorageService = require('web.AbstractStorageService'); +var BasicModel = require('web.BasicModel'); +var concurrency = require('web.concurrency'); +var core = require('web.core'); +var fieldRegistry = require('web.field_registry'); +const fieldRegistryOwl = require('web.field_registry_owl'); +const FormRenderer = require('web.FormRenderer'); +var FormView = require('web.FormView'); +var mixins = require('web.mixins'); +var NotificationService = require('web.NotificationService'); +var pyUtils = require('web.py_utils'); +var RamStorage = require('web.RamStorage'); +var testUtils = require('web.test_utils'); +var widgetRegistry = require('web.widget_registry'); +var Widget = require('web.Widget'); + +var _t = core._t; +const cpHelpers = testUtils.controlPanel; +var createView = testUtils.createView; +var createActionManager = testUtils.createActionManager; + +QUnit.module('Views', { + 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"}, + 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'}, + trululu: {string: "Trululu", type: "many2one", relation: 'partner'}, + timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'}, + product_id: {string: "Product", type: "many2one", relation: 'product'}, + priority: { + string: "Priority", + type: "selection", + selection: [[1, "Low"], [2, "Medium"], [3, "High"]], + default: 1, + }, + state: {string: "State", type: "selection", selection: [["ab", "AB"], ["cd", "CD"], ["ef", "EF"]]}, + date: {string: "Some Date", type: "date"}, + datetime: {string: "Datetime Field", type: 'datetime'}, + product_ids: {string: "one2many product", type: "one2many", relation: "product"}, + 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: [], + timmy: [], + trululu: 4, + state: "ab", + date: "2017-01-25", + datetime: "2016-12-12 10:55:05", + }, { + id: 2, + display_name: "second record", + bar: true, + foo: "blip", + int_field: 9, + qux: 13, + p: [], + timmy: [], + trululu: 1, + state: "cd", + }, { + id: 4, + display_name: "aaa", + state: "ef", + }, { + id: 5, + display_name: "aaa", + foo:'', + bar:false, + state: "ef", + }], + onchanges: {}, + }, + product: { + fields: { + display_name: {string: "Product Name", type: "char"}, + name: {string: "Product Name", type: "char"}, + partner_type_id: {string: "Partner type", type: "many2one", relation: "partner_type"}, + }, + 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}, + ] + }, + "ir.translation": { + fields: { + lang_code: {type: "char"}, + value: {type: "char"}, + res_id: {type: "integer"} + }, + records: [{ + id: 99, + res_id: 12, + value: '', + lang_code: 'en_US' + }] + }, + 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], + }, { + id: 19, + name: "Christine", + }] + }, + "res.company": { + fields: { + name: { string: "Name", type: "char" }, + }, + }, + }; + this.actions = [{ + id: 1, + name: 'Partners Action 1', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'kanban'], [false, 'form']], + }]; + }, +}, function () { + + QUnit.module('FormView'); + + QUnit.test('simple form rendering', async function (assert) { + assert.expect(12); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '
some htmlaa
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '
', + res_id: 2, + }); + + + assert.containsOnce(form, 'div.test'); + assert.strictEqual(form.$('div.test').css('opacity'), '0.5', + "should keep the inline style on html elements"); + assert.containsOnce(form, 'label:contains(Foo)'); + assert.containsOnce(form, 'span:contains(blip)'); + assert.hasAttrValue(form.$('.o_group .o_group:first'), 'style', 'background-color: red', + "should apply style attribute on groups"); + assert.hasAttrValue(form.$('.o_field_widget[name=foo]'), 'style', 'color: blue', + "should apply style attribute on fields"); + assert.containsNone(form, 'label:contains(something_id)'); + assert.containsOnce(form, 'label:contains(f3_description)'); + assert.containsOnce(form, 'div.o_field_one2many table'); + assert.containsOnce(form, 'tbody td:not(.o_list_record_selector) .custom-checkbox input:checked'); + assert.containsOnce(form, '.o_control_panel .breadcrumb:contains(second record)'); + assert.containsNone(form, 'label.o_form_label_empty:contains(timmy)'); + + form.destroy(); + }); + + QUnit.test('duplicate fields rendered properly', async function (assert) { + assert.expect(6); + this.data.partner.records.push({ + id: 6, + bar: true, + foo: "blip", + int_field: 9, + }); + var form = await createView({ + View: FormView, + viewOptions: { mode: 'edit' }, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 6, + }); + + assert.hasClass(form.$('div.o_group input[name="foo"]:eq(0)'), 'o_invisible_modifier', 'first foo widget should be invisible'); + assert.containsOnce(form, 'div.o_group input[name="foo"]:eq(1):not(.o_invisible_modifier)', "second foo widget should be visible"); + assert.containsOnce(form, 'div.o_group input[name="foo"]:eq(2):not(.o_invisible_modifier)', "third foo widget should be visible"); + await testUtils.fields.editInput(form.$('div.o_group input[name="foo"]:eq(2)'), "hello"); + assert.strictEqual(form.$('div.o_group input[name="foo"]:eq(1)').val(), "hello", "second foo widget should be 'hello'"); + assert.containsOnce(form, 'div.o_group input[name="int_field"]:eq(0):not(.o_readonly_modifier)', "first int_field widget should not be readonly"); + assert.hasClass(form.$('div.o_group span[name="int_field"]:eq(0)'),'o_readonly_modifier', "second int_field widget should be readonly"); + form.destroy(); + }); + + QUnit.test('duplicate fields rendered properly (one2many)', async function (assert) { + assert.expect(7); + this.data.partner.records.push({ + id: 6, + p: [1], + }); + var form = await createView({ + View: FormView, + viewOptions: { mode: 'edit' }, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 6, + }); + assert.containsOnce(form, 'div.o_field_one2many:eq(0):not(.o_readonly_modifier)', "first one2many widget should not be readonly"); + assert.hasClass(form.$('div.o_field_one2many:eq(1)'),'o_readonly_modifier', "second one2many widget should be readonly"); + await testUtils.dom.click(form.$('div.tab-content table.o_list_table:eq(0) tr.o_data_row td.o_data_cell:eq(0)')); + assert.strictEqual(form.$('div.tab-content table.o_list_table tr.o_selected_row input[name="foo"]').val(), "yop", + "first line in one2many of first tab contains yop"); + assert.strictEqual(form.$('div.tab-content table.o_list_table:eq(1) tr.o_data_row td.o_data_cell:eq(0)').text(), + "yop", "first line in one2many of second tab contains yop"); + await testUtils.fields.editInput(form.$('div.tab-content table.o_list_table tr.o_selected_row input[name="foo"]'), "hello"); + assert.strictEqual(form.$('div.tab-content table.o_list_table:eq(1) tr.o_data_row td.o_data_cell:eq(0)').text(), "hello", + "first line in one2many of second tab contains hello"); + await testUtils.dom.click(form.$('div.tab-content table.o_list_table:eq(0) a:contains(Add a line)')); + assert.strictEqual(form.$('div.tab-content table.o_list_table tr.o_selected_row input[name="foo"]').val(), "My little Foo Value", + "second line in one2many of first tab contains 'My little Foo Value'"); + assert.strictEqual(form.$('div.tab-content table.o_list_table:eq(1) tr.o_data_row:eq(1) td.o_data_cell:eq(0)').text(), + "My little Foo Value", "first line in one2many of second tab contains hello"); + form.destroy(); + }); + + QUnit.test('attributes are transferred on async widgets', async function (assert) { + assert.expect(1); + var done = assert.async(); + + var def = testUtils.makeTestPromise(); + + var FieldChar = fieldRegistry.get('char'); + fieldRegistry.add('asyncwidget', FieldChar.extend({ + willStart: function () { + return def; + }, + })); + + createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '
', + res_id: 2, + }).then(function (form) { + assert.hasAttrValue(form.$('.o_field_widget[name=foo]'), 'style', 'color: blue', + "should apply style attribute on fields"); + form.destroy(); + delete fieldRegistry.map.asyncwidget; + done(); + }); + def.resolve(); + await testUtils.nextTick(); + }); + + QUnit.test('placeholder attribute on input', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '
', + res_id: 2, + }); + + assert.containsOnce(form, 'input[placeholder="chimay"]'); + form.destroy(); + }); + + QUnit.test('decoration works on widgets', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '', + res_id: 2, + }); + assert.doesNotHaveClass(form.$('span[name="display_name"]'), 'text-danger'); + assert.hasClass(form.$('span[name="foo"]'), 'text-danger'); + form.destroy(); + }); + + QUnit.test('decoration on widgets are reevaluated if necessary', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '', + res_id: 2, + viewOptions: {mode: 'edit'}, + }); + assert.doesNotHaveClass(form.$('input[name="display_name"]'), 'text-danger'); + await testUtils.fields.editInput(form.$('input[name=int_field]'), 3); + assert.hasClass(form.$('input[name="display_name"]'), 'text-danger'); + form.destroy(); + }); + + QUnit.test('decoration on widgets works on same widget', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '', + res_id: 2, + viewOptions: {mode: 'edit'}, + }); + assert.doesNotHaveClass(form.$('input[name="int_field"]'), 'text-danger'); + await testUtils.fields.editInput(form.$('input[name=int_field]'), 3); + assert.hasClass(form.$('input[name="int_field"]'), 'text-danger'); + form.destroy(); + }); + + QUnit.test('only necessary fields are fetched with correct context', 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) { + // NOTE: actually, the current web client always request the __last_update + // field, not sure why. Maybe this test should be modified. + assert.deepEqual(args.args[1], ["foo", "display_name"], + "should only fetch requested fields"); + assert.deepEqual(args.kwargs.context, {bin_size: true}, + "bin_size should always be in the context"); + return this._super(route, args); + } + }); + form.destroy(); + }); + + QUnit.test('group rendering', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.containsOnce(form, 'table.o_inner_group'); + + form.destroy(); + }); + + QUnit.test('group containing both a field and a group', async function (assert) { + // The purpose of this test is to check that classnames defined in a + // field widget and those added by the form renderer are correctly + // combined. For instance, the renderer adds className 'o_group_col_x' + // on outer group's children (an outer group being a group that contains + // at least a group). + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.containsOnce(form, '.o_group .o_field_widget[name=foo]'); + assert.containsOnce(form, '.o_group .o_inner_group .o_field_widget[name=int_field]'); + + assert.hasClass(form.$('.o_field_widget[name=foo]'), 'o_field_char'); + assert.hasClass(form.$('.o_field_widget[name=foo]'), 'o_group_col_6'); + + form.destroy(); + }); + + QUnit.test('Form and subview with _view_ref contexts', async function (assert) { + assert.expect(2); + + this.data.product.fields.partner_type_ids = {string: "one2many field", type: "one2many", relation: "partner_type"}, + this.data.product.records = [{id: 1, name: 'Tromblon', partner_type_ids: [12,14]}]; + this.data.partner.records[0].product_id = 1; + + var actionManager = await createActionManager({ + data: this.data, + archs: { + 'product,false,form': '
'+ + ''+ + '' + + '', + + 'partner_type,false,list': ''+ + ''+ + '', + 'product,false,search': '', + }, + mockRPC: function (route, args) { + if (args.method === 'load_views') { + var context = args.kwargs.context; + if (args.model === 'product') { + assert.deepEqual(context, {tree_view_ref: 'some_tree_view'}, + 'The correct _view_ref should have been sent to the server, first time'); + } + if (args.model === 'partner_type') { + assert.deepEqual(context, { + base_model_name: 'product', + tree_view_ref: 'some_other_tree_view', + }, 'The correct _view_ref should have been sent to the server for the subview'); + } + } + return this._super.apply(this, arguments); + }, + }); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '', + res_id: 1, + + mockRPC: function(route, args) { + if (args.method === 'get_formview_action') { + return Promise.resolve({ + res_id: 1, + type: 'ir.actions.act_window', + target: 'current', + res_model: args.model, + context: args.kwargs.context, + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + } + return this._super(route, args); + }, + + interceptsPropagate: { + do_action: function (ev) { + actionManager.doAction(ev.data.action); + }, + }, + }); + await testUtils.dom.click(form.$('.o_field_widget[name="product_id"]')); + form.destroy(); + actionManager.destroy(); + }); + + QUnit.test('invisible fields are properly hidden', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + // x2many field without inline view: as it is always invisible, the view + // should not be fetched. we don't specify any view in this test, so if it + // ever tries to fetch it, it will crash, indicating that this is wrong. + '' + + '' + + '
', + res_id: 1, + }); + + assert.containsNone(form, 'label:contains(Foo)'); + assert.containsNone(form, '.o_field_widget[name=foo]'); + assert.containsNone(form, '.o_field_widget[name=qux]'); + assert.containsNone(form, '.o_field_widget[name=p]'); + + form.destroy(); + }); + + QUnit.test('invisible elements are properly hidden', async function (assert) { + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + assert.containsOnce(form, '.o_form_statusbar.o_invisible_modifier button:contains(coucou)'); + assert.containsOnce(form, '.o_notebook li.o_invisible_modifier a:contains(invisible)'); + assert.containsOnce(form, 'table.o_inner_group.o_invisible_modifier td:contains(invgroup)'); + form.destroy(); + }); + + QUnit.test('invisible attrs on fields are re-evaluated on field change', async function (assert) { + assert.expect(3); + + // we set the value bar to simulate a falsy boolean value. + this.data.partner.records[0].bar = false; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + viewOptions: { + mode:'edit' + }, + }); + + assert.hasClass(form.$('.foo_field'), 'o_invisible_modifier'); + assert.hasClass(form.$('.bar_field'), 'o_invisible_modifier'); + + // set a value on the m2o + await testUtils.fields.many2one.searchAndClickItem('product_id'); + assert.doesNotHaveClass(form.$('.foo_field'), 'o_invisible_modifier'); + + form.destroy(); + }); + + QUnit.test('asynchronous fields can be set invisible', async function (assert) { + assert.expect(1); + var done = assert.async(); + + var def = testUtils.makeTestPromise(); + + // we choose this widget because it is a quite simple widget with a non + // empty qweb template + var PercentPieWidget = fieldRegistry.get('percentpie'); + fieldRegistry.add('asyncwidget', PercentPieWidget.extend({ + willStart: function () { + return def; + }, + })); + + createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }).then(function (form) { + assert.containsNone(form, '.o_field_widget[name="int_field"]'); + form.destroy(); + delete fieldRegistry.map.asyncwidget; + done(); + }); + def.resolve(); + }); + + QUnit.test('properly handle modifiers and attributes on notebook tags', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.hasClass(form.$('.o_notebook'), 'o_invisible_modifier'); + assert.hasClass(form.$('.o_notebook'), 'new_class'); + form.destroy(); + }); + + QUnit.test('empty notebook', async function (assert) { + assert.expect(2); + + const form = await createView({ + arch: ` +
+ + + +
`, + data: this.data, + model: 'partner', + res_id: 1, + View: FormView, + }); + + // Does not change when switching state + await testUtils.form.clickEdit(form); + + assert.containsNone(form, ':scope .o_notebook .nav'); + + // Does not change when coming back to initial state + await testUtils.form.clickSave(form); + + assert.containsNone(form, ':scope .o_notebook .nav'); + + form.destroy(); + }); + + QUnit.test('no visible page', async function (assert) { + assert.expect(4); + + const form = await createView({ + arch: ` +
+ + + + + + + + + + +
`, + data: this.data, + model: 'partner', + res_id: 1, + View: FormView, + }); + + // Does not change when switching state + await testUtils.form.clickEdit(form); + + for (const nav of form.el.querySelectorAll(':scope .o_notebook .nav')) { + assert.containsNone(nav, '.nav-link.active'); + assert.containsN(nav, '.nav-item.o_invisible_modifier', 2); + } + + // Does not change when coming back to initial state + await testUtils.form.clickSave(form); + + for (const nav of form.el.querySelectorAll(':scope .o_notebook .nav')) { + assert.containsNone(nav, '.nav-link.active'); + assert.containsN(nav, '.nav-item.o_invisible_modifier', 2); + } + + form.destroy(); + }); + + QUnit.test('notebook: pages with invisible modifiers', async function (assert) { + assert.expect(10); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: `
+ + + + + + + + + + + + + + +
`, + res_id: 1, + }); + + await testUtils.form.clickEdit(form); + + assert.containsOnce(form, ".o_notebook .nav .nav-link.active", + "There should be only one active tab" + ); + assert.isVisible(form.$(".o_notebook .nav .nav-item:first")); + assert.hasClass(form.$(".o_notebook .nav .nav-link:first"), "active"); + + assert.isNotVisible(form.$(".o_notebook .nav .nav-item:eq(1)")); + assert.doesNotHaveClass(form.$(".o_notebook .nav .nav-link:eq(1)"), "active"); + + await testUtils.dom.click(form.$(".o_field_widget[name=bar] input")); + + assert.containsOnce(form, ".o_notebook .nav .nav-link.active", + "There should be only one active tab" + ); + assert.isNotVisible(form.$(".o_notebook .nav .nav-item:first")); + assert.doesNotHaveClass(form.$(".o_notebook .nav .nav-link:first"), "active"); + + assert.isVisible(form.$(".o_notebook .nav .nav-item:eq(1)")); + assert.hasClass(form.$(".o_notebook .nav .nav-link:eq(1)"), "active"); + + form.destroy(); + }); + + QUnit.test('invisible attrs on first notebook page', async function (assert) { + assert.expect(6); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + await testUtils.form.clickEdit(form); + assert.hasClass(form.$('.o_notebook .nav .nav-link:first()'), 'active'); + assert.doesNotHaveClass(form.$('.o_notebook .nav .nav-item:first()'), 'o_invisible_modifier'); + + // set a value on the m2o + await testUtils.fields.many2one.searchAndClickItem('product_id'); + assert.doesNotHaveClass(form.$('.o_notebook .nav .nav-link:first()'), 'active'); + assert.hasClass(form.$('.o_notebook .nav .nav-item:first()'), 'o_invisible_modifier'); + assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(1)'), 'active'); + assert.hasClass(form.$('.o_notebook .tab-content .tab-pane:nth(1)'), 'active'); + form.destroy(); + }); + + QUnit.test('invisible attrs on notebook page which has only one page', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + assert.notOk(form.$('.o_notebook .nav .nav-link:first()').hasClass('active'), + 'first tab should not be active'); + assert.ok(form.$('.o_notebook .nav .nav-item:first()').hasClass('o_invisible_modifier'), + 'first tab should be invisible'); + + // enable checkbox + await testUtils.dom.click(form.$('.o_field_boolean input')); + assert.ok(form.$('.o_notebook .nav .nav-link:first()').hasClass('active'), + 'first tab should be active'); + assert.notOk(form.$('.o_notebook .nav .nav-item:first()').hasClass('o_invisible_modifier'), + 'first tab should be visible'); + + form.destroy(); + }); + + QUnit.test('first notebook page invisible', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.notOk(form.$('.o_notebook .nav .nav-item:first()').is(':visible'), + 'first tab should be invisible'); + assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(1)'), 'active'); + + form.destroy(); + }); + + QUnit.test('autofocus on second notebook page', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.doesNotHaveClass(form.$('.o_notebook .nav .nav-link:first()'), 'active'); + assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(1)'), 'active'); + + form.destroy(); + }); + + QUnit.test('invisible attrs on group are re-evaluated on field change', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + viewOptions: { + mode: 'edit' + }, + }); + + assert.containsOnce(form, 'div.o_group:visible'); + await testUtils.dom.click('.o_field_boolean input', form); + assert.containsOnce(form, 'div.o_group:hidden'); + form.destroy(); + }); + + QUnit.test('invisible attrs with zero value in domain and unset value in data', async function (assert) { + assert.expect(1); + + this.data.partner.fields.int_field.type = 'monetary'; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + 'this should be invisible' + + '' + + '' + + '' + + '
', + }); + + assert.isNotVisible(form.$('div.hello')); + form.destroy(); + }); + + QUnit.test('reset local state when switching to another view', async function (assert) { + assert.expect(3); + + const actionManager = await createActionManager({ + data: this.data, + archs: { + 'partner,false,form': `
+ + + + + + + + + + + +
`, + 'partner,false,list': '', + 'partner,false,search': '', + }, + actions: [{ + id: 1, + name: 'Partner', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'list'], [false, 'form']], + }], + }); + + await actionManager.doAction(1); + + await testUtils.dom.click(actionManager.$('.o_list_button_add')); + assert.containsOnce(actionManager, '.o_form_view'); + + // click on second page tab + await testUtils.dom.click(actionManager.$('.o_notebook .nav-link:eq(1)')); + + await testUtils.dom.click('.o_control_panel .o_form_button_cancel'); + assert.containsNone(actionManager, '.o_form_view'); + + await testUtils.dom.click(actionManager.$('.o_list_button_add')); + // check notebook active page is 0th page + assert.hasClass(actionManager.$('.o_notebook .nav-link:eq(0)'), 'active'); + + actionManager.destroy(); + }); + + QUnit.test('rendering stat buttons', async function (assert) { + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '
' + + '' + + '' + + '
' + + '' + + '' + + '' + + '
' + + '
', + res_id: 2, + }); + + assert.containsN(form, 'button.oe_stat_button', 2); + assert.containsOnce(form, 'button.oe_stat_button.o_invisible_modifier'); + + var count = 0; + await testUtils.mock.intercept(form, "execute_action", function () { + count++; + }); + await testUtils.dom.click('.oe_stat_button'); + assert.strictEqual(count, 1, "should have triggered a execute action"); + form.destroy(); + }); + + QUnit.test('label uses the string attribute', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '
', + res_id: 2, + }); + + assert.containsOnce(form, 'label.o_form_label:contains(customstring)'); + form.destroy(); + }); + + QUnit.test('input ids for multiple occurrences of fields in form view', async function (assert) { + // A same field can occur several times in the view, but its id must be + // unique by occurrence, otherwise there is a warning in the console (in + // edit mode) as we get several inputs with the same "id" attribute, and + // several labels the same "for" attribute. + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + +
`, + }); + + const fieldIdAttrs = [...form.$('.o_field_widget')].map(n => n.getAttribute('id')); + const labelForAttrs = [...form.$('.o_form_label')].map(n => n.getAttribute('for')); + + assert.strictEqual([...new Set(fieldIdAttrs)].length, 4, + "should have generated a unique id for each field occurrence"); + assert.deepEqual(fieldIdAttrs, labelForAttrs, + "the for attribute of labels must coincide with field ids"); + + form.destroy(); + }); + + QUnit.test('input ids for multiple occurrences of fields in sub form view (inline)', async function (assert) { + // A same field can occur several times in the view, but its id must be + // unique by occurrence, otherwise there is a warning in the console (in + // edit mode) as we get several inputs with the same "id" attribute, and + // several labels the same "for" attribute. + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + + + + + +
+ `, + }); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + + assert.containsOnce(document.body, '.modal .o_form_view'); + + const fieldIdAttrs = [...$('.modal .o_form_view .o_field_widget')].map(n => n.getAttribute('id')); + const labelForAttrs = [...$('.modal .o_form_view .o_form_label')].map(n => n.getAttribute('for')); + + assert.strictEqual([...new Set(fieldIdAttrs)].length, 4, + "should have generated a unique id for each field occurrence"); + assert.deepEqual(fieldIdAttrs, labelForAttrs, + "the for attribute of labels must coincide with field ids"); + + form.destroy(); + }); + + QUnit.test('input ids for multiple occurrences of fields in sub form view (not inline)', async function (assert) { + // A same field can occur several times in the view, but its id must be + // unique by occurrence, otherwise there is a warning in the console (in + // edit mode) as we get several inputs with the same "id" attribute, and + // several labels the same "for" attribute. + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
', + archs: { + 'partner,false,list': '', + 'partner,false,form': ` +
+ + + + + + +
` + }, + }); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + + assert.containsOnce(document.body, '.modal .o_form_view'); + + const fieldIdAttrs = [...$('.modal .o_form_view .o_field_widget')].map(n => n.getAttribute('id')); + const labelForAttrs = [...$('.modal .o_form_view .o_form_label')].map(n => n.getAttribute('for')); + + assert.strictEqual([...new Set(fieldIdAttrs)].length, 4, + "should have generated a unique id for each field occurrence"); + assert.deepEqual(fieldIdAttrs, labelForAttrs, + "the for attribute of labels must coincide with field ids"); + + form.destroy(); + }); + + QUnit.test('two occurrences of invalid field in form view', async function (assert) { + assert.expect(2); + + this.data.partner.fields.trululu.required = true; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + +
`, + }); + + await testUtils.form.clickSave(form); + + assert.containsN(form, '.o_form_label.o_field_invalid', 2); + assert.containsN(form, '.o_field_many2one.o_field_invalid', 2); + + form.destroy(); + }); + + QUnit.test('tooltips on multiple occurrences of fields and labels', async function (assert) { + assert.expect(4); + + const initialDebugMode = odoo.debug; + odoo.debug = false; + + this.data.partner.fields.foo.help = 'foo tooltip'; + this.data.partner.fields.bar.help = 'bar tooltip'; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + +
`, + }); + + const $fooLabel1 = form.$('.o_form_label:nth(0)'); + $fooLabel1.tooltip('show', false); + $fooLabel1.trigger($.Event('mouseenter')); + assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "foo tooltip"); + $fooLabel1.trigger($.Event('mouseleave')); + + const $fooLabel2 = form.$('.o_form_label:nth(2)'); + $fooLabel2.tooltip('show', false); + $fooLabel2.trigger($.Event('mouseenter')); + assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "foo tooltip"); + $fooLabel2.trigger($.Event('mouseleave')); + + const $barLabel1 = form.$('.o_form_label:nth(1)'); + $barLabel1.tooltip('show', false); + $barLabel1.trigger($.Event('mouseenter')); + assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "bar tooltip"); + $barLabel1.trigger($.Event('mouseleave')); + + const $barLabel2 = form.$('.o_form_label:nth(3)'); + $barLabel2.tooltip('show', false); + $barLabel2.trigger($.Event('mouseenter')); + assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "bar tooltip"); + $barLabel2.trigger($.Event('mouseleave')); + + odoo.debug = initialDebugMode; + form.destroy(); + }); + + QUnit.test('readonly attrs on fields are re-evaluated on field change', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + await testUtils.form.clickEdit(form); + + assert.containsOnce(form, 'span[name="foo"]', + "the foo field widget should be readonly"); + await testUtils.dom.click(form.$('.o_field_boolean input')); + assert.containsOnce(form, 'input[name="foo"]', + "the foo field widget should have been rerendered to now be editable"); + await testUtils.dom.click(form.$('.o_field_boolean input')); + assert.containsOnce(form, 'span[name="foo"]', + "the foo field widget should have been rerendered to now be readonly again"); + await testUtils.dom.click(form.$('.o_field_boolean input')); + assert.containsOnce(form, 'input[name="foo"]', + "the foo field widget should have been rerendered to now be editable again"); + + form.destroy(); + }); + + QUnit.test('empty fields have o_form_empty class in readonly mode', async function (assert) { + assert.expect(8); + + this.data.partner.fields.foo.default = false; // no default value for this test + this.data.partner.records[1].foo = false; // 1 is record with id=2 + this.data.partner.records[1].trululu = false; // 1 is record with id=2 + this.data.partner.fields.int_field.readonly = true; + this.data.partner.onchanges.foo = function (obj) { + if (obj.foo === "hello") { + obj.int_field = false; + } + }; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 2, + }); + + assert.containsN(form, '.o_field_widget.o_field_empty', 2, + "should have 2 empty fields with correct class"); + assert.containsN(form, '.o_form_label_empty', 2, + "should have 2 muted labels (for the empty fieds) in readonly"); + + await testUtils.form.clickEdit(form); + + assert.containsOnce(form, '.o_field_empty', + "in edit mode, only empty readonly fields should have the o_field_empty class"); + assert.containsOnce(form, '.o_form_label_empty', + "in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class"); + + await testUtils.fields.editInput(form.$('input[name=foo]'), 'test'); + + assert.containsNone(form, '.o_field_empty', + "after readonly modifier change, the o_field_empty class should have been removed"); + assert.containsNone(form, '.o_form_label_empty', + "after readonly modifier change, the o_form_label_empty class should have been removed"); + + await testUtils.fields.editInput(form.$('input[name=foo]'), 'hello'); + + assert.containsOnce(form, '.o_field_empty', + "after value changed to false for a readonly field, the o_field_empty class should have been added"); + assert.containsOnce(form, '.o_form_label_empty', + "after value changed to false for a readonly field, the o_form_label_empty class should have been added"); + + form.destroy(); + }); + + QUnit.test('empty fields\' labels still get the empty class after widget rerender', async function (assert) { + assert.expect(6); + + this.data.partner.fields.foo.default = false; // no default value for this test + this.data.partner.records[1].foo = false; // 1 is record with id=2 + this.data.partner.records[1].display_name = false; // 1 is record with id=2 + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '
', + res_id: 2, + }); + + assert.containsN(form, '.o_field_widget.o_field_empty', 2); + assert.containsN(form, '.o_form_label_empty', 2, + "should have 1 muted label (for the empty fied) in readonly"); + + await testUtils.form.clickEdit(form); + + assert.containsNone(form, '.o_field_empty', + "in edit mode, only empty readonly fields should have the o_field_empty class"); + assert.containsNone(form, '.o_form_label_empty', + "in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class"); + + await testUtils.fields.editInput(form.$('input[name=foo]'), 'readonly'); + await testUtils.fields.editInput(form.$('input[name=foo]'), 'edit'); + await testUtils.fields.editInput(form.$('input[name=display_name]'), 'some name'); + await testUtils.fields.editInput(form.$('input[name=foo]'), 'readonly'); + + assert.containsNone(form, '.o_field_empty', + "there still should not be any empty class on fields as the readonly one is now set"); + assert.containsNone(form, '.o_form_label_empty', + "there still should not be any empty class on labels as the associated readonly field is now set"); + + form.destroy(); + }); + + QUnit.test('empty inner readonly fields don\'t have o_form_empty class in "create" mode', async function (assert) { + assert.expect(2); + + this.data.partner.fields.product_id.readonly = true; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + }); + assert.containsNone(form, '.o_form_label_empty', + "no empty class on label"); + assert.containsNone(form, '.o_field_empty', + "no empty class on field"); + form.destroy(); + }); + + QUnit.test('form view can switch to edit mode', async function (assert) { + assert.expect(9); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '', + res_id: 1, + }); + + assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode'); + assert.hasClass(form.$('.o_form_view'), 'o_form_readonly'); + assert.isVisible(form.$buttons.find('.o_form_buttons_view')); + assert.isNotVisible(form.$buttons.find('.o_form_buttons_edit')); + + await testUtils.form.clickEdit(form); + + assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode'); + assert.hasClass(form.$('.o_form_view'), 'o_form_editable'); + assert.doesNotHaveClass(form.$('.o_form_view'), 'o_form_readonly'); + assert.isNotVisible(form.$buttons.find('.o_form_buttons_view')); + assert.isVisible(form.$buttons.find('.o_form_buttons_edit')); + form.destroy(); + }); + + QUnit.test('required attrs on fields are re-evaluated on field change', async function (assert) { + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + await testUtils.form.clickEdit(form); + + assert.containsOnce(form, 'input[name="foo"].o_required_modifier', + "the foo field widget should be required"); + await testUtils.dom.click('.o_field_boolean input'); + assert.containsOnce(form, 'input[name="foo"]:not(.o_required_modifier)', + "the foo field widget should now have been marked as non-required"); + await testUtils.dom.click('.o_field_boolean input'); + assert.containsOnce(form, 'input[name="foo"].o_required_modifier', + "the foo field widget should now have been marked as required again"); + + form.destroy(); + }); + + QUnit.test('required fields should have o_required_modifier in readonly mode', async function (assert) { + assert.expect(2); + + this.data.partner.fields.foo.required = true; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.containsOnce(form, 'span.o_required_modifier', form); + + await testUtils.form.clickEdit(form); + assert.containsOnce(form, 'input.o_required_modifier', + "in edit mode, should have 1 input with o_required_modifier"); + form.destroy(); + }); + + QUnit.test('required float fields works as expected', async function (assert) { + assert.expect(10); + + this.data.partner.fields.qux.required = true; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + }); + + assert.hasClass(form.$('input[name="qux"]'), 'o_required_modifier'); + assert.strictEqual(form.$('input[name="qux"]').val(), "0.0", + "qux input is 0 by default (float field)"); + + await testUtils.form.clickSave(form); + + assert.containsNone(form.$('input[name="qux"]'), "should have switched to readonly"); + + await testUtils.form.clickEdit(form); + + await testUtils.fields.editInput(form.$('input[name=qux]'), '1'); + + await testUtils.form.clickSave(form); + + await testUtils.form.clickEdit(form); + + assert.strictEqual(form.$('input[name="qux"]').val(), "1.0", + "qux input is properly formatted"); + + assert.verifySteps(['onchange', 'create', 'read', 'write', 'read']); + form.destroy(); + }); + + QUnit.test('separators', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.containsOnce(form, 'div.o_horizontal_separator'); + form.destroy(); + }); + + QUnit.test('invisible attrs on separators', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.hasClass(form.$('div.o_horizontal_separator'), 'o_invisible_modifier'); + + form.destroy(); + }); + + QUnit.test('buttons in form view', async function (assert) { + assert.expect(8); + + var rpcCount = 0; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + res_id: 2, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].foo, "tralala", "should have saved the changes"); + } + assert.step(args.method); + return this._super(route, args); + }, + }); + + await testUtils.form.clickEdit(form); + + var count = 0; + await testUtils.mock.intercept(form, "execute_action", function (event) { + event.stopPropagation(); + count++; + }); + await testUtils.dom.click('.oe_stat_button'); + assert.strictEqual(count, 1, "should have triggered a execute action"); + assert.strictEqual(form.mode, "edit", "form view should be in edit mode"); + + await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala'); + await testUtils.dom.click('.oe_stat_button:first'); + + assert.strictEqual(form.mode, "edit", "form view should be in edit mode"); + assert.strictEqual(count, 2, "should have triggered a execute action"); + assert.verifySteps(['read', 'write', 'read']); + form.destroy(); + }); + + QUnit.test('clicking on stat buttons save and reload in edit mode', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '
' + + '' + + '
' + + '' + + '' + + '' + + '
' + + '
', + res_id: 2, + mockRPC: function (route, args) { + if (args.method === 'write') { + // simulate an override of the model... + args.args[1].display_name = "GOLDORAK"; + args.args[1].name = "GOLDORAK"; + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'second record', + "should have correct display_name"); + await testUtils.form.clickEdit(form); + await testUtils.fields.editInput(form.$('input[name=name]'), 'some other name'); + + await testUtils.dom.click('.oe_stat_button'); + assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'GOLDORAK', + "should have correct display_name"); + + form.destroy(); + }); + + QUnit.test('buttons with attr "special" do not trigger a save', async function (assert) { + assert.expect(4); + + var executeActionCount = 0; + var writeCount = 0; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '', + res_id: 2, + }); + + // readonly mode + assert.containsOnce(form, '.oe_stat_button', + "button box should be displayed in readonly"); + + // edit mode + await testUtils.form.clickEdit(form); + + assert.containsOnce(form, '.oe_stat_button', + "button box should be displayed in edit on an existing record"); + + // create mode (leave edition first!) + await testUtils.form.clickDiscard(form); + await testUtils.form.clickCreate(form); + assert.containsOnce(form, '.oe_stat_button', + "button box should be displayed when creating a new record as well"); + + form.destroy(); + }); + + QUnit.test('properly apply onchange on one2many fields', async function (assert) { + assert.expect(5); + + this.data.partner.records[0].p = [4]; + this.data.partner.onchanges = { + foo: function (obj) { + obj.p = [ + [5], + [1, 4, {display_name: "updated record"}], + [0, null, {display_name: "created record"}], + ]; + }, + }; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.containsOnce(form, '.o_field_one2many .o_data_row', + "there should be one one2many record linked at first"); + assert.strictEqual(form.$('.o_field_one2many .o_data_row td:first').text(), 'aaa', + "the 'display_name' of the one2many record should be correct"); + + // switch to edit mode + await testUtils.form.clickEdit(form); + await testUtils.fields.editInput(form.$('input[name=foo]'), 'let us trigger an onchange'); + var $o2m = form.$('.o_field_one2many'); + assert.strictEqual($o2m.find('.o_data_row').length, 2, + "there should be two linked record"); + assert.strictEqual($o2m.find('.o_data_row:first td:first').text(), 'updated record', + "the 'display_name' of the first one2many record should have been updated"); + assert.strictEqual($o2m.find('.o_data_row:nth(1) td:first').text(), 'created record', + "the 'display_name' of the second one2many record should be correct"); + + form.destroy(); + }); + + QUnit.test('properly apply onchange on one2many fields direct click', async function (assert) { + assert.expect(3); + + var def = testUtils.makeTestPromise(); + + this.data.partner.records[0].p = [2, 4]; + this.data.partner.onchanges = { + int_field: function (obj) { + obj.p = [ + [5], + [1, 2, {display_name: "updated record 1", int_field: obj.int_field}], + [1, 4, {display_name: "updated record 2", int_field: obj.int_field * 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 self = this; + var my_args = arguments; + var my_super = this._super; + return def.then(() => { + return my_super.apply(self, my_args) + }); + } + return this._super.apply(this, arguments); + }, + archs: { + 'partner,false,form': '
' + }, + viewOptions: { + mode: 'edit', + }, + }); + // Trigger the onchange + await testUtils.fields.editInput(form.$('input[name=int_field]'), '2'); + + // Open first record in one2many + await testUtils.dom.click(form.$('.o_data_row:first')); + + assert.containsNone(document.body, '.modal'); + + def.resolve(); + await testUtils.nextTick(); + + assert.containsOnce(document.body, '.modal'); + assert.strictEqual($('.modal').find('input[name=int_field]').val(), '2'); + + form.destroy(); + }); + + QUnit.test('update many2many value in one2many after onchange', async function (assert) { + assert.expect(2); + + this.data.partner.records[1].p = [4]; + this.data.partner.onchanges = { + foo: function (obj) { + obj.p = [ + [5], + [1, 4, { + display_name: "gold", + timmy: [[5]], + }], + ]; + }, + }; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + res_id: 2, + }); + assert.strictEqual($('div[name="p"] .o_data_row td').text().trim(), "aaaNo records", + "should have proper initial content"); + await testUtils.form.clickEdit(form); + + await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala'); + + assert.strictEqual($('div[name="p"] .o_data_row td').text().trim(), "goldNo records", + "should have proper initial content"); + form.destroy(); + }); + + QUnit.test('delete a line in a one2many while editing another line triggers a warning', async function (assert) { + assert.expect(3); + + this.data.partner.records[0].p = [1, 2]; + + 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_data_cell').first()); + await testUtils.fields.editInput(form.$('input[name=display_name]'), ''); + await testUtils.dom.click(form.$('.fa-trash-o').eq(1)); + + assert.strictEqual($('.modal').find('.modal-title').first().text(), "Warning", + "Clicking out of a dirty line while editing should trigger a warning modal."); + + await testUtils.dom.click($('.modal').find('.btn-primary')); + // use of owlCompatibilityNextTick because there are two sequential updates of the + // control panel (which is written in owl): each of them waits for the next animation frame + // to complete + await testUtils.owlCompatibilityNextTick(); + assert.strictEqual(form.$('.o_data_cell').first().text(), "first record", + "Value should have been reset to what it was before editing began."); + assert.containsOnce(form, '.o_data_row', + "The other line should have been deleted."); + form.destroy(); + }); + + QUnit.test('properly apply onchange on many2many fields', async function (assert) { + assert.expect(14); + + this.data.partner.onchanges = { + foo: function (obj) { + obj.timmy = [ + [5], + [4, 12], + [4, 14], + ]; + }, + }; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + assert.step(args.method); + if (args.method === 'read' && args.model === 'partner_type') { + assert.deepEqual(args.args[0], [12, 14], + "should read both m2m with one RPC"); + } + if (args.method === 'write') { + assert.deepEqual(args.args[1].timmy, [[6, false, [12, 14]]], + "should correctly save the changed m2m values"); + + } + return this._super.apply(this, arguments); + }, + res_id: 2, + }); + + assert.containsNone(form, '.o_field_many2many .o_data_row', + "there should be no many2many record linked at first"); + + // switch to edit mode + await testUtils.form.clickEdit(form); + await testUtils.fields.editInput(form.$('input[name=foo]'), 'let us trigger an onchange'); + var $m2m = form.$('.o_field_many2many'); + assert.strictEqual($m2m.find('.o_data_row').length, 2, + "there should be two linked records"); + assert.strictEqual($m2m.find('.o_data_row:first td:first').text(), 'gold', + "the 'display_name' of the first m2m record should be correctly displayed"); + assert.strictEqual($m2m.find('.o_data_row:nth(1) td:first').text(), 'silver', + "the 'display_name' of the second m2m record should be correctly displayed"); + + await testUtils.form.clickSave(form); + + assert.verifySteps(['read', 'onchange', 'read', 'write', 'read', 'read']); + + form.destroy(); + }); + + QUnit.test('display_name not sent for onchanges if not in view', async function (assert) { + assert.expect(7); + + this.data.partner.records[0].timmy = [12]; + this.data.partner.onchanges = { + foo: function () {}, + }; + this.data.partner_type.onchanges = { + name: function () {}, + }; + var readInModal = false; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '', + mockRPC: function (route, args) { + if (args.method === 'read' && args.model === 'partner') { + assert.deepEqual(args.args[1], ['foo', 'timmy', 'display_name'], + "should read display_name even if not in the view"); + } + if (args.method === 'read' && args.model === 'partner_type') { + if (!readInModal) { + assert.deepEqual(args.args[1], ['name'], + "should not read display_name for records in the list"); + } else { + assert.deepEqual(args.args[1], ['name', 'color', 'display_name'], + "should read display_name when opening the subrecord"); + } + } + if (args.method === 'onchange' && args.model === 'partner') { + assert.deepEqual(args.args[1], { + id: 1, + foo: 'coucou', + timmy: [[6, false, [12]]], + }, "should only send the value of fields in the view (+ id)"); + assert.deepEqual(args.args[3], { + foo: '1', + timmy: '', + 'timmy.name': '1', + 'timmy.color': '', + }, "only the fields in the view should be in the onchange spec"); + } + if (args.method === 'onchange' && args.model === 'partner_type') { + assert.deepEqual(args.args[1], { + id: 12, + name: 'new name', + color: 2, + }, "should only send the value of fields in the view (+ id)"); + assert.deepEqual(args.args[3], { + name: '1', + color: '', + }, "only the fields in the view should be in the onchange spec"); + } + return this._super.apply(this, arguments); + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + // trigger the onchange + await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), "coucou"); + + // open a subrecord and trigger an onchange + readInModal = true; + await testUtils.dom.click(form.$('.o_data_row .o_data_cell:first')); + await testUtils.fields.editInput($('.modal .o_field_widget[name=name]'), "new name"); + + form.destroy(); + }); + + QUnit.test('onchanges on date(time) fields', async function (assert) { + assert.expect(6); + + this.data.partner.onchanges = { + foo: function (obj) { + obj.date = '2021-12-12'; + obj.datetime = '2021-12-12 10:55:05'; + }, + }; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + session: { + getTZOffset: function () { + return 120; + }, + }, + }); + + assert.strictEqual(form.$('.o_field_widget[name=date]').text(), + '01/25/2017', "the initial date should be correct"); + assert.strictEqual(form.$('.o_field_widget[name=datetime]').text(), + '12/12/2016 12:55:05', "the initial datetime should be correct"); + + await testUtils.form.clickEdit(form); + + assert.strictEqual(form.$('.o_field_widget[name=date] input').val(), + '01/25/2017', "the initial date should be correct in edit"); + assert.strictEqual(form.$('.o_field_widget[name=datetime] input').val(), + '12/12/2016 12:55:05', "the initial datetime should be correct in edit"); + + // trigger the onchange + await testUtils.fields.editInput(form.$('.o_field_widget[name="foo"]'), "coucou"); + + assert.strictEqual(form.$('.o_field_widget[name=date] input').val(), + '12/12/2021', "the initial date should be correct in edit"); + assert.strictEqual(form.$('.o_field_widget[name=datetime] input').val(), + '12/12/2021 12:55:05', "the initial datetime should be correct in edit"); + + form.destroy(); + }); + + QUnit.test('onchanges are not sent for each keystrokes', async function (assert) { + var done = assert.async(); + assert.expect(5); + + var onchangeNbr = 0; + + this.data.partner.onchanges = { + foo: function (obj) { + obj.int_field = obj.foo.length + 1000; + }, + }; + var def = testUtils.makeTestPromise(); + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '
', + res_id: 2, + fieldDebounce: 3, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'onchange') { + onchangeNbr++; + return concurrency.delay(3).then(function () { + def.resolve(); + return result; + }); + } + return result; + }, + }); + + await testUtils.form.clickEdit(form); + + testUtils.fields.editInput(form.$('input[name=foo]'), '1'); + assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet"); + testUtils.fields.editInput(form.$('input[name=foo]'), '12'); + assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet"); + + return waitForFinishedOnChange().then(async function () { + assert.strictEqual(onchangeNbr, 1, "one onchange has been called"); + + // add something in the input, then focus another input + await testUtils.fields.editAndTrigger(form.$('input[name=foo]'), '123', ['change']); + assert.strictEqual(onchangeNbr, 2, "one onchange has been called immediately"); + + return waitForFinishedOnChange(); + }).then(function () { + assert.strictEqual(onchangeNbr, 2, "no extra onchange should have been called"); + + form.destroy(); + done(); + }); + + function waitForFinishedOnChange() { + return def.then(function () { + def = testUtils.makeTestPromise(); + return concurrency.delay(0); + }); + } + }); + + QUnit.test('onchanges are not sent for invalid values', async function (assert) { + assert.expect(6); + + this.data.partner.onchanges = { + int_field: function (obj) { + obj.foo = String(obj.int_field); + }, + }; + 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); + + // edit int_field, and check that an onchange has been applied + await testUtils.fields.editInput(form.$('input[name="int_field"]'), "123"); + assert.strictEqual(form.$('input[name="foo"]').val(), "123", + "the onchange has been applied"); + + // enter an invalid value in a float, and check that no onchange has + // been applied + await testUtils.fields.editInput(form.$('input[name="int_field"]'), "123a"); + assert.strictEqual(form.$('input[name="foo"]').val(), "123", + "the onchange has not been applied"); + + // save, and check that the int_field input is marked as invalid + await testUtils.form.clickSave(form); + assert.hasClass(form.$('input[name="int_field"]'),'o_field_invalid', + "input int_field is marked as invalid"); + + assert.verifySteps(['read', 'onchange']); + form.destroy(); + }); + + QUnit.test('rpc complete after destroying parent', async function (assert) { + // We just test that there is no crash in this situation + assert.expect(0); + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '
' + + '' + + '' + + '', + res_id: 2, + viewOptions: { + context: {some_context: true}, + }, + intercepts: { + execute_action: function (e) { + assert.deepEqual(e.data.action_data.context, { + 'test': 2 + }, "button context should have been evaluated and given to the action, with magicc without previous context"); + }, + }, + }); + + await testUtils.dom.click(form.$('.oe_stat_button')); + + form.destroy(); + }); + + QUnit.test('clicking on a stat button with no context', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '
' + + '' + + '
' + + '' + + '
' + + '
' + + '
', + res_id: 2, + viewOptions: { + context: {some_context: true}, + }, + intercepts: { + execute_action: function (e) { + assert.deepEqual(e.data.action_data.context, { + }, "button context should have been evaluated and given to the action, with magic keys but without previous context"); + }, + }, + }); + + await testUtils.dom.click(form.$('.oe_stat_button')); + + form.destroy(); + }); + + QUnit.test('diplay a stat button outside a buttonbox', async function (assert) { + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '
' + + '' + + '' + + '' + + '
', + res_id: 2, + }); + + assert.containsOnce(form, 'button .o_field_widget', + "a field widget should be display inside the button"); + assert.strictEqual(form.$('button .o_field_widget').children().length, 2, + "the field widget should have 2 children, the text and the value"); + assert.strictEqual(parseInt(form.$('button .o_field_widget .o_stat_value').text()), 9, + "the value rendered should be the same as the field value"); + form.destroy(); + }); + + QUnit.test('diplay something else than a button in a buttonbox', async function (assert) { + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '
' + + '' + + '
' + + '
', + res_id: 2, + }); + + assert.strictEqual(form.$('.oe_button_box').children().length, 2, + "button box should contain two children"); + assert.containsOnce(form, '.oe_button_box > .oe_stat_button', + "button box should only contain one button"); + assert.containsOnce(form, '.oe_button_box > label', + "button box should only contain one label"); + + form.destroy(); + }); + + QUnit.test('invisible fields are not considered as visible in a buttonbox', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '
', + res_id: 2, + }); + + assert.strictEqual(form.$('.oe_button_box').children().length, 1, + "button box should contain only one child"); + assert.hasClass(form.$('.oe_button_box'), 'o_not_full', + "the buttonbox should not be full"); + + form.destroy(); + }); + + QUnit.test('display correctly buttonbox, in large size class', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '
' + + '' + + '' + + '
' + + '
', + res_id: 2, + config: { + device: {size_class: 5}, + }, + }); + + assert.strictEqual(form.$('.oe_button_box').children().length, 2, + "button box should contain two children"); + + form.destroy(); + }); + + QUnit.test('one2many default value creation', async function (assert) { + assert.expect(1); + + this.data.partner.records[0].product_ids = [37]; + this.data.partner.fields.product_ids.default = [ + [0, 0, { name: 'xdroid', partner_type_id: 12 }] + ]; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (args.method === 'create') { + var command = args.args[0].product_ids[0]; + assert.strictEqual(command[2].partner_type_id, 12, + "the default partner_type_id should be equal to 12"); + } + return this._super.apply(this, arguments); + }, + }); + await testUtils.form.clickSave(form); + form.destroy(); + }); + + QUnit.test('many2manys inside one2manys are saved correctly', 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 command = args.args[0].p; + assert.deepEqual(command, [[0, command[0][1], { + timmy: [[6, false, [12]]], + }]], "the default partner_type_id should be equal to 12"); + } + return this._super.apply(this, arguments); + }, + }); + + // add a o2m subrecord with a m2m tag + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + await testUtils.fields.many2one.clickOpenDropdown('timmy'); + await testUtils.fields.many2one.clickHighlightedItem('timmy'); + + await testUtils.form.clickSave(form); + + form.destroy(); + }); + + QUnit.test('one2manys (list editable) inside one2manys are saved correctly', async function (assert) { + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + archs: { + "partner,false,form": '
' + + '' + + '' + + '' + + '' + + '' + + '
' + }, + mockRPC: function (route, args) { + if (args.method === 'create') { + assert.deepEqual(args.args[0].p, + [[0, args.args[0].p[0][1], { + p: [[0, args.args[0].p[0][2].p[0][1], {display_name: "xtv"}]], + }]], + "create should be called with the correct arguments"); + } + return this._super.apply(this, arguments); + }, + }); + + // add a o2m subrecord + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + await testUtils.dom.click($('.modal-body .o_field_one2many .o_field_x2many_list_row_add a')); + await testUtils.fields.editInput($('.modal-body input'), 'xtv'); + await testUtils.dom.click($('.modal-footer button:first')); + assert.strictEqual($('.modal').length, 0, + "dialog should be closed"); + + var row = form.$('.o_field_one2many .o_list_view .o_data_row'); + assert.strictEqual(row.children()[0].textContent, '1 record', + "the cell should contains the number of record: 1"); + + await testUtils.form.clickSave(form); + + form.destroy(); + }); + + QUnit.test('oe_read_only and oe_edit_only classNames on fields inside groups', async function (assert) { + assert.expect(10); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + +
`, + res_id: 1, + }); + + assert.hasClass(form.$('.o_form_view'), 'o_form_readonly', + 'form should be in readonly mode'); + assert.isVisible(form.$('.o_field_widget[name=foo]')); + assert.isVisible(form.$('label:contains(Foo)')); + assert.isNotVisible(form.$('.o_field_widget[name=bar]')); + assert.isNotVisible(form.$('label:contains(Bar)')); + + await testUtils.form.clickEdit(form); + assert.hasClass(form.$('.o_form_view'), 'o_form_editable', + 'form should be in readonly mode'); + assert.isNotVisible(form.$('.o_field_widget[name=foo]')); + assert.isNotVisible(form.$('label:contains(Foo)')); + assert.isVisible(form.$('.o_field_widget[name=bar]')); + assert.isVisible(form.$('label:contains(Bar)')); + + form.destroy(); + }); + + QUnit.test('oe_read_only className is handled in list views', async function (assert) { + assert.expect(7); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.hasClass(form.$('.o_form_view'), 'o_form_readonly', + 'form should be in readonly mode'); + assert.isVisible(form.$('.o_field_one2many .o_list_view thead th[data-name="display_name"]'), + 'display_name cell should be visible in readonly mode'); + + await testUtils.form.clickEdit(form); + + assert.strictEqual(form.el.querySelector('th[data-name="foo"]').style.width, '100%', + 'As the only visible char field, "foo" should take 100% of the remaining space'); + assert.strictEqual(form.el.querySelector('th.oe_read_only').style.width, '0px', + '"oe_read_only" in edit mode should have a 0px width'); + + assert.hasClass(form.$('.o_form_view'), 'o_form_editable', + 'form should be in edit mode'); + assert.isNotVisible(form.$('.o_field_one2many .o_list_view thead th[data-name="display_name"]'), + 'display_name cell should not be visible in edit mode'); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + await testUtils.owlCompatibilityNextTick(); + assert.hasClass(form.$('.o_form_view .o_list_view tbody tr:first input[name="display_name"]'), + 'oe_read_only', 'display_name input should have oe_read_only class'); + + form.destroy(); + }); + + QUnit.test('oe_edit_only className is handled in list views', async function (assert) { + assert.expect(5); + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + assert.hasClass(form.$('.o_form_view'), 'o_form_readonly', + 'form should be in readonly mode'); + assert.isNotVisible(form.$('.o_field_one2many .o_list_view thead th[data-name="display_name"]'), + 'display_name cell should not be visible in readonly mode'); + + await testUtils.form.clickEdit(form); + assert.hasClass(form.$('.o_form_view'), 'o_form_editable', + 'form should be in edit mode'); + assert.isVisible(form.$('.o_field_one2many .o_list_view thead th[data-name="display_name"]'), + 'display_name cell should be visible in edit mode'); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + await testUtils.owlCompatibilityNextTick(); + assert.hasClass(form.$('.o_form_view .o_list_view tbody tr:first input[name="display_name"]'), + 'oe_edit_only', 'display_name input should have oe_edit_only class'); + + form.destroy(); + }); + + QUnit.test('*_view_ref in context are passed correctly', async function (assert) { + var done = assert.async(); + assert.expect(3); + + createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '
', + res_id: 1, + intercepts: { + load_views: function (event) { + var context = event.data.context; + assert.strictEqual(context.tree_view_ref, 'module.tree_view_ref', + "context should contain tree_view_ref"); + event.data.on_success(); + } + }, + viewOptions: { + context: {some_context: false}, + }, + mockRPC: function (route, args) { + if (args.method === 'read') { + assert.strictEqual('some_context' in args.kwargs.context && !args.kwargs.context.some_context, true, + "the context should have been set"); + } + return this._super.apply(this, arguments); + }, + }).then(async function (form) { + // reload to check that the record's context hasn't been modified + await form.reload(); + form.destroy(); + done(); + }); + }); + + QUnit.test('non inline subview and create=0 in action context', async function (assert) { + // the create=0 should apply on the main view (form), but not on subviews + assert.expect(2); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
', + archs: { + "product,false,kanban": ` + +
+
+
`, + }, + res_id: 1, + viewOptions: { + context: {create: false}, + mode: 'edit', + }, + }); + + assert.containsNone(form, '.o_form_button_create'); + assert.containsOnce(form, '.o-kanban-button-new'); + + form.destroy(); + }); + + QUnit.test('readonly fields with modifiers may be saved', async function (assert) { + // the readonly property on the field description only applies on view, + // this is not a DB constraint. It should be seen as a default value, + // that may be overridden in views, for example with modifiers. So + // basically, a field defined as readonly may be edited. + assert.expect(3); + + this.data.partner.fields.foo.readonly = true; + 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], {foo: 'New foo value'}, + "the new value should be saved"); + } + return this._super.apply(this, arguments); + }, + }); + + // bar being set to true, foo shouldn't be readonly and thus its value + // could be saved, even if in its field description it is readonly + await testUtils.form.clickEdit(form); + + assert.containsOnce(form, 'input[name="foo"]', + "foo field should be editable"); + await testUtils.fields.editInput(form.$('input[name="foo"]'), 'New foo value'); + + await testUtils.form.clickSave(form); + + assert.strictEqual(form.$('.o_field_widget[name=foo]').text(), 'New foo value', + "new value for foo field should have been saved"); + + form.destroy(); + }); + + QUnit.test('readonly set by modifier do not break many2many_tags', async function (assert) { + assert.expect(0); + + this.data.partner.onchanges = { + bar: function (obj) { + obj.timmy = [[6, false, [12]]]; + }, + }; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '
', + res_id: 5, + }); + + await testUtils.form.clickEdit(form); + await testUtils.dom.click(form.$('.o_field_widget[name=bar] input')); + + form.destroy(); + }); + + QUnit.test('check if id and active_id are defined', async function (assert) { + assert.expect(2); + + let checkOnchange = false; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + archs: { + "partner,false,form": '
' + }, + mockRPC: function (route, args) { + if (args.method === 'onchange' && checkOnchange) { + assert.strictEqual(args.kwargs.context.current_id, false, + "current_id should be false"); + assert.strictEqual(args.kwargs.context.default_trululu, false, + "default_trululu should be false"); + } + return this._super.apply(this, arguments); + }, + }); + + checkOnchange = true; + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + form.destroy(); + }); + + QUnit.test('modifiers are considered on multiple