flectra.define('web.form_tests', function (require) { "use strict"; var concurrency = require('web.concurrency'); var config = require('web.config'); var core = require('web.core'); var fieldRegistry = require('web.field_registry'); var FormView = require('web.FormView'); var mixins = require('web.mixins'); var pyeval = require('web.pyeval'); var RainbowMan = require('web.rainbow_man'); var testUtils = require('web.test_utils'); var widgetRegistry = require('web.widget_registry'); var Widget = require('web.Widget'); var _t = core._t; var createView = testUtils.createView; var createAsyncView = testUtils.createAsyncView; 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"}, }, 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", }], onchanges: {}, }, product: { fields: { 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}, ] }, }; } }, function () { QUnit.module('FormView'); QUnit.test('simple form rendering', function (assert) { assert.expect(12); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
some htmlaa
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '
', res_id: 2, }); assert.strictEqual(form.$('div.test').length, 1, "should contain a div with some html"); assert.strictEqual(form.$('div.test').css('opacity'), "0.5", "should keep the inline style on html elements"); assert.strictEqual(form.$('label:contains(Foo)').length, 1, "should contain label Foo"); assert.strictEqual(form.$('span:contains(blip)').length, 1, "should contain span with field value"); assert.strictEqual(form.$('.o_group .o_group:first').attr('style'), 'background-color: red', "should apply style attribute on groups"); assert.strictEqual(form.$('.o_field_widget[name=foo]').attr('style'), 'color: blue', "should apply style attribute on fields"); assert.strictEqual(form.$('label:contains(something_id)').length, 0, "should not contain f3 string description"); assert.strictEqual(form.$('label:contains(f3_description)').length, 1, "should contain custom f3 string description"); assert.strictEqual(form.$('div.o_field_one2many table').length, 1, "should render a one2many relation"); assert.strictEqual(form.$('tbody td:not(.o_list_record_selector) .o_checkbox input:checked').length, 1, "1 checkboxes should be checked"); assert.strictEqual(form.get('title'), "second record", "title should be display_name of record"); assert.strictEqual(form.$('label.o_form_label_empty:contains(timmy)').length, 0, "the many2many label shouldn't be marked as empty"); form.destroy(); }); QUnit.test('attributes are transferred on async widgets', function (assert) { assert.expect(1); var def = $.Deferred(); var FieldChar = fieldRegistry.get('char'); fieldRegistry.add('asyncwidget', FieldChar.extend({ willStart: function () { return def; }, })); createAsyncView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '
', res_id: 2, }).then(function (form) { assert.strictEqual(form.$('.o_field_widget[name=foo]').attr('style'), 'color: blue', "should apply style attribute on fields"); form.destroy(); delete fieldRegistry.map.asyncwidget; }); def.resolve(); }); QUnit.test('only necessary fields are fetched with correct context', function (assert) { assert.expect(2); var form = 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', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('table.o_inner_group').length, 1, "should contain an inner group"); form.destroy(); }); QUnit.test('invisible fields are properly hidden', function (assert) { assert.expect(4); var form = 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.strictEqual(form.$('label.o_invisible_modifier:contains(Foo)').length, 1, "should not contain label Foo"); assert.strictEqual(form.$('span.o_invisible_modifier:contains(yop)').length, 1, "should not contain span with field value"); assert.strictEqual(form.$('.o_field_widget.o_invisible_modifier:contains(0.4)').length, 1, "field qux should be invisible"); assert.ok(form.$('.o_field_widget[name=p]').hasClass('o_invisible_modifier'), "field p should be invisible"); form.destroy(); }); QUnit.test('invisible elements are properly hidden', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('.o_form_statusbar.o_invisible_modifier button:contains(coucou)').length, 1, "should not display invisible header"); assert.strictEqual(form.$('.o_notebook li.o_invisible_modifier a:contains(invisible)').length, 1, "should not display tab invisible"); assert.strictEqual(form.$('table.o_inner_group.o_invisible_modifier td:contains(invgroup)').length, 1, "should not display invisible groups"); form.destroy(); }); QUnit.test('invisible attrs on fields are re-evaluated on field change', 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 = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); assert.ok(form.$('.foo_field').hasClass('o_invisible_modifier'), 'should not display foo field'); assert.ok(form.$('.bar_field').hasClass('o_invisible_modifier'), 'should not display bar field'); // set a value on the m2o var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); form.$('.o_field_many2one input').click(); $dropdown.find('li:first()').click(); assert.ok(!form.$('.foo_field').hasClass('o_invisible_modifier'), 'should display foo field'); form.destroy(); }); QUnit.test('asynchronous fields can be set invisible', function (assert) { assert.expect(1); var def = $.Deferred(); // 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; }, })); createAsyncView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '
', res_id: 1, }).then(function (form) { assert.ok(form.$('.o_field_widget[name="int_field"]').hasClass('o_invisible_modifier'), 'int_field is invisible'); form.destroy(); delete fieldRegistry.map.asyncwidget; }); def.resolve(); }); QUnit.test('properly handle modifiers and attributes on notebook tags', function (assert) { assert.expect(2); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.ok(form.$('.o_notebook').hasClass('o_invisible_modifier'), 'the notebook should handle modifiers (invisible)'); assert.ok(form.$('.o_notebook').hasClass('new_class'), 'the notebook should handle attributes'); form.destroy(); }); QUnit.test('invisible attrs on first notebook page', function (assert) { assert.expect(6); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); assert.ok(form.$('.o_notebook .nav li:first()').hasClass('active'), 'first tab should be active'); assert.ok(!form.$('.o_notebook .nav li:first()').hasClass('o_invisible_modifier'), 'first tab should be visible'); // set a value on the m2o var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); form.$('.o_field_many2one input').click(); $dropdown.find('li:first()').click(); assert.ok(!form.$('.o_notebook .nav li:first()').hasClass('active'), 'first tab should not be active'); assert.ok(form.$('.o_notebook .nav li:first()').hasClass('o_invisible_modifier'), 'first tab should be invisible'); assert.ok(form.$('.o_notebook .nav li:nth(1)').hasClass('active'), 'second tab should be active'); assert.ok(form.$('.o_notebook .tab-content .tab-pane:nth(1)').hasClass('active'), 'second page should be active'); form.destroy(); }); QUnit.test('first notebook page invisible', function (assert) { assert.expect(2); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.notOk(form.$('.o_notebook .nav li:first()').is(':visible'), 'first tab should be invisible'); assert.ok(form.$('.o_notebook .nav li:nth(1)').hasClass('active'), 'second tab should be active'); form.destroy(); }); QUnit.test('autofocus on second notebook page', function (assert) { assert.expect(2); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.notOk(form.$('.o_notebook .nav li:first()').hasClass('active'), 'first tab should not active'); assert.ok(form.$('.o_notebook .nav li:nth(1)').hasClass('active'), 'second tab should be active'); form.destroy(); }); QUnit.test('invisible attrs on group are re-evaluated on field change', function (assert) { assert.expect(2); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1 }); assert.strictEqual(form.$('div.o_group:visible').length, 1, "should display the group"); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_boolean input').click(); assert.strictEqual(form.$('div.o_group:hidden').length, 1, "should not display the group"); form.destroy(); }); QUnit.test('invisible attrs with zero value in domain and unset value in data', function (assert) { assert.expect(1); this.data.partner.fields.int_field.type = 'monetary'; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + 'this should be invisible' + '' + '' + '' + '
', }); assert.notOk(form.$('div.hello').is(':visible'), "attrs invisible should have been computed and applied"); form.destroy(); }); QUnit.test('rendering stat buttons', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '
' + '' + '' + '
' + '' + '' + '' + '
' + '
', res_id: 2, }); assert.strictEqual(form.$('button.oe_stat_button').length, 2, "should have 2 stat buttons"); assert.strictEqual(form.$('button.oe_stat_button.o_invisible_modifier').length, 1, "should have 1 invisible stat buttons"); var count = 0; testUtils.intercept(form, "execute_action", function () { count++; }); form.$('.oe_stat_button').first().click(); assert.strictEqual(count, 1, "should have triggered a execute action"); form.destroy(); }); QUnit.test('label uses the string attribute', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '
', res_id: 2, }); assert.strictEqual(form.$('label.o_form_label:contains(customstring)').length, 1, "should have 1 label with correct string"); form.destroy(); }); QUnit.test('readonly attrs on fields are re-evaluated on field change', function (assert) { assert.expect(4); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('span[name="foo"]').length, 1, "the foo field widget should be readonly"); form.$('.o_field_boolean input').click(); assert.strictEqual(form.$('input[name="foo"]').length, 1, "the foo field widget should have been rerendered to now be editable"); form.$('.o_field_boolean input').click(); assert.strictEqual(form.$('span[name="foo"]').length, 1, "the foo field widget should have been rerendered to now be readonly again"); form.$('.o_field_boolean input').click(); assert.strictEqual(form.$('input[name="foo"]').length, 1, "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', 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 = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, }); assert.strictEqual(form.$('.o_field_widget.o_field_empty').length, 2, "should have 2 empty fields with correct class"); assert.strictEqual(form.$('.o_form_label_empty').length, 2, "should have 2 muted labels (for the empty fieds) in readonly"); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('.o_field_empty').length, 1, "in edit mode, only empty readonly fields should have the o_field_empty class"); assert.strictEqual(form.$('.o_form_label_empty').length, 1, "in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class"); form.$('input[name="foo"]').val("test").trigger("input"); assert.strictEqual(form.$('.o_field_empty').length, 0, "after readonly modifier change, the o_field_empty class should have been removed"); assert.strictEqual(form.$('.o_form_label_empty').length, 0, "after readonly modifier change, the o_form_label_empty class should have been removed"); form.$('input[name="foo"]').val("hello").trigger("input"); assert.strictEqual(form.$('.o_field_empty').length, 1, "after value changed to false for a readonly field, the o_field_empty class should have been added"); assert.strictEqual(form.$('.o_form_label_empty').length, 1, "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', 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].int_field = false; // 1 is record with id=2 var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, }); assert.strictEqual(form.$('.o_field_widget.o_field_empty').length, 1, "should have 1 empty field with correct class"); assert.strictEqual(form.$('.o_form_label_empty').length, 1, "should have 1 muted label (for the empty fied) in readonly"); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('.o_field_empty').length, 0, "in edit mode, only empty readonly fields should have the o_field_empty class"); assert.strictEqual(form.$('.o_form_label_empty').length, 0, "in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class"); form.$('input[name="foo"]').val("readonly").trigger("input"); // int_field is now rerendered as readonly form.$('input[name="foo"]').val("edit").trigger("input"); // int_field is now rerendered as editable form.$('input[name="int_field"]').val('1').trigger("input"); // int_field is now set form.$('input[name="foo"]').val("readonly").trigger("input"); // int_field is now rerendered as readonly assert.strictEqual(form.$('.o_field_empty').length, 0, "there still should not be any empty class on fields as the readonly one is now set"); assert.strictEqual(form.$('.o_form_label_empty').length, 0, "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', function (assert) { assert.expect(2); this.data.partner.fields.product_id.readonly = true; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', }); assert.strictEqual(form.$('.o_form_label_empty').length, 0, "no empty class on label"); assert.strictEqual(form.$('.o_field_empty').length, 0, "no empty class on field"); form.destroy(); }); QUnit.test('form view can switch to edit mode', function (assert) { assert.expect(9); var form = 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.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'form view should be .o_form_readonly'); assert.ok(form.$buttons.find('.o_form_buttons_view').is(':visible'), 'readonly buttons should be visible'); assert.ok(!form.$buttons.find('.o_form_buttons_edit').is(':visible'), 'edit buttons should not be visible'); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode'); assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'form view should be .o_form_editable'); assert.ok(!form.$('.o_form_view').hasClass('o_form_readonly'), 'form view should not be .o_form_readonly'); assert.ok(!form.$buttons.find('.o_form_buttons_view').is(':visible'), 'readonly buttons should not be visible'); assert.ok(form.$buttons.find('.o_form_buttons_edit').is(':visible'), 'edit buttons should be visible'); form.destroy(); }); QUnit.test('required attrs on fields are re-evaluated on field change', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('input[name="foo"].o_required_modifier').length, 1, "the foo field widget should be required"); form.$('.o_field_boolean input').click(); assert.strictEqual(form.$('input[name="foo"]:not(.o_required_modifier)').length, 1, "the foo field widget should now have been marked as non-required"); form.$('.o_field_boolean input').click(); assert.strictEqual(form.$('input[name="foo"].o_required_modifier').length, 1, "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', function (assert) { assert.expect(2); this.data.partner.fields.foo.required = true; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('span.o_required_modifier').length, 1, "should have 1 span with o_required_modifier class"); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('input.o_required_modifier').length, 1, "in edit mode, should have 1 input with o_required_modifier"); form.destroy(); }); QUnit.test('required float fields works as expected', function (assert) { assert.expect(10); this.data.partner.fields.qux.required = true; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); assert.ok(form.$('input[name="qux"]').hasClass('o_required_modifier'), "qux input is flagged as required"); assert.strictEqual(form.$('input[name="qux"]').val(), "0.0", "qux input is 0 by default (float field)"); form.$buttons.find('.o_form_button_save').click(); assert.notOk(form.$('input[name="qux"]').hasClass('o_field_invalid'), "qux input is not displayed as invalid"); form.$buttons.find('.o_form_button_edit').click(); form.$('input[name="qux"]').val("1").trigger('input'); form.$buttons.find('.o_form_button_save').click(); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('input[name="qux"]').val(), "1.0", "qux input is properly formatted"); assert.verifySteps(['default_get', 'create', 'read', 'write', 'read']); form.destroy(); }); QUnit.test('separators', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('div.o_horizontal_separator').length, 1, "should contain a separator div"); form.destroy(); }); QUnit.test('invisible attrs on separators', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('div.o_horizontal_separator').hasClass('o_invisible_modifier'), true, "separator div should be hidden"); form.destroy(); }); QUnit.test('buttons in form view', function (assert) { assert.expect(7); var rpcCount = 0; var form = 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); }, }); form.$buttons.find('.o_form_button_edit').click(); var count = 0; testUtils.intercept(form, "execute_action", function (event) { event.stopPropagation(); count++; }); form.$('.oe_stat_button').first().click(); assert.strictEqual(count, 1, "should have triggered a execute action"); assert.strictEqual(form.mode, "edit", "form view should be in edit mode"); form.$('input').val("tralala").trigger('input'); form.$('.oe_stat_button').first().click(); 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', function (assert) { assert.expect(2); var form = 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.getTitle(), 'second record', "should have correct display_name"); form.$buttons.find('.o_form_button_edit').click(); form.$('input[name="name"]').val('some other name').trigger('input'); form.$('.oe_stat_button').first().click(); assert.strictEqual(form.getTitle(), 'GOLDORAK', "should have correct display_name"); form.destroy(); }); QUnit.test('buttons with attr "special" do not trigger a save', function (assert) { assert.expect(4); var executeActionCount = 0; var writeCount = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '', res_id: 2, }); // readonly mode assert.strictEqual(form.$('.oe_stat_button').length, 1, "button box should be displayed in readonly"); // edit mode form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('.oe_stat_button').length, 1, "button box should be displayed in edit on an existing record"); // create mode (leave edition first!) form.$buttons.find('.o_form_button_cancel').click(); form.$buttons.find('.o_form_button_create').click(); assert.strictEqual(form.$('.oe_stat_button').length, 1, "button box should be displayed when creating a new record as well"); form.destroy(); }); QUnit.test('properly apply onchange on one2many fields', 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 = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('.o_field_one2many .o_data_row').length, 1, "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 form.$buttons.find('.o_form_button_edit').click(); form.$('input').val('let us trigger an onchange').trigger('input'); 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('update many2many value in one2many after onchange', 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 = 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"); form.$buttons.find('.o_form_button_edit').click(); form.$('input').val("tralala").trigger('input'); 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', function (assert) { assert.expect(3); this.data.partner.records[0].p = [1, 2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_data_cell').first().click(); // edit first row form.$('input').val('').trigger('input'); form.$('.fa-trash-o').eq(1).click(); // delete second row assert.strictEqual($('.modal').find('.modal-title').first().text(), "Warning", "Clicking out of a dirty line while editing should trigger a warning modal."); $('.modal').find('.btn-primary').click(); // discard changes assert.strictEqual(form.$('.o_data_cell').first().text(), "first record", "Value should have been reset to what it was before editing began."); assert.strictEqual(form.$('.o_data_row').length, 1, "The other line should have been deleted."); form.destroy(); }); QUnit.test('properly apply onchange on many2many fields', function (assert) { assert.expect(14); this.data.partner.onchanges = { foo: function (obj) { obj.timmy = [ [5], [4, 12], [4, 14], ]; }, }; var form = 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.strictEqual(form.$('.o_field_many2many .o_data_row').length, 0, "there should be no many2many record linked at first"); // switch to edit mode form.$buttons.find('.o_form_button_edit').click(); form.$('input').val('let us trigger an onchange').trigger('input'); 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"); form.$buttons.find('.o_form_button_save').click(); assert.verifySteps(['read', 'onchange', 'read', 'write', 'read', 'read']); form.destroy(); }); QUnit.test('display_name not sent for onchanges if not in view', 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 = 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 form.$('.o_field_widget[name=foo]').val("coucou").trigger('input'); // open a subrecord and trigger an onchange readInModal = true; form.$('.o_data_row .o_data_cell:first').click(); $('.modal .o_field_widget[name=name]').val("new name").trigger('input'); form.destroy(); }); QUnit.test('onchanges on date(time) fields', 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 = 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"); form.$buttons.find('.o_form_button_edit').click(); 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 form.$('.o_field_widget[name="foo"]').val("coucou").trigger('input'); 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', 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 = $.Deferred(); var form = 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; }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('input').first().val("1").trigger('input'); assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet"); form.$('input').first().val("12").trigger('input'); assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet"); return waitForFinishedOnChange().then(function () { assert.strictEqual(onchangeNbr, 1, "one onchange has been called"); // add something in the input, then focus another input form.$('input').first().val("123").trigger('input'); form.$('input').first().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 = $.Deferred(); return concurrency.delay(0); }); } }); QUnit.test('onchanges are not sent for invalid values', function (assert) { assert.expect(6); this.data.partner.onchanges = { int_field: function (obj) { obj.foo = String(obj.int_field); }, }; var form = 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); }, }); form.$buttons.find('.o_form_button_edit').click(); // edit int_field, and check that an onchange has been applied form.$('input[name="int_field"]').val("123").trigger('input'); 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 form.$('input[name="int_field"]').val("123a").trigger('input'); 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 form.$buttons.find('.o_form_button_save').click(); assert.ok(form.$('input[name="int_field"]').hasClass('o_field_invalid'), "input int_field is marked as invalid"); assert.verifySteps(['read', 'onchange']); form.destroy(); }); QUnit.test('rpc complete after destroying parent', function (assert) { // We just test that there is no crash in this situation assert.expect(0); var form = 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"); }, }, }); form.$('.oe_stat_button').click(); form.destroy(); }); QUnit.test('clicking on a stat button with no context', function (assert) { assert.expect(1); var form = 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"); }, }, }); form.$('.oe_stat_button').click(); form.destroy(); }); QUnit.test('diplay a stat button outside a buttonbox', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '
', res_id: 2, }); assert.strictEqual(form.$('button .o_field_widget').length, 1, "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 than the field value"); form.destroy(); }); QUnit.test('diplay something else than a button in a buttonbox', function (assert) { assert.expect(3); var form = 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.strictEqual(form.$('.oe_button_box .oe_stat_button').length, 1, "button box should only contain one button"); assert.strictEqual(form.$('.oe_button_box label').length, 1, "button box should only contain one label"); form.destroy(); }); QUnit.test('one2many default value creation', function (assert) { assert.expect(1); this.data.partner.records[0].product_ids = [37]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'default_get') { return $.when({ product_ids: [[0, 0, { name: 'xdroid', partner_type_id: 12, }]] }); } 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); }, }); form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('many2manys inside one2manys are saved correctly', function (assert) { assert.expect(1); var form = 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 form.$('.o_field_x2many_list_row_add a').click(); form.$('.o_many2many_tags_cell').click(); form.$('.o_field_many2one input').click(); var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); $dropdown.find('li:first()').click(); // select the first tag form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('one2manys (list editable) inside one2manys are saved correctly', function (assert) { assert.expect(3); var form = 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 form.$('.o_field_x2many_list_row_add a').click(); $('.modal-body .o_field_one2many .o_field_x2many_list_row_add a').click(); $('.modal-body input').val('xtv').trigger('input'); $('.modal-footer button:first').click(); // save & close 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"); form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('*_view_ref in context are passed correctly', function (assert) { var done = assert.async(); assert.expect(3); createAsyncView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '
', res_id: 1, intercepts: { load_views: function (event) { var context = event.data.context.eval(); 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(function (form) { // reload to check that the record's context hasn't been modified form.reload(); form.destroy(); done(); }); }); QUnit.test('readonly fields with modifiers may be saved', 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 overriden 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 = 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 form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('input[name="foo"]').length, 1, "foo field should be editable"); form.$('input[name="foo"]').val('New foo value').trigger('input'); form.$buttons.find('.o_form_button_save').click(); 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('check if id and active_id are defined', function (assert) { assert.expect(2); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', archs: { "partner,false,form": '
' }, mockRPC: function (route, args) { if (args.method === 'default_get' && args.args[0][0] === 'trululu') { 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); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.destroy(); }); QUnit.test('modifiers are considered on multiple