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