odoo.define('web.relational_fields_tests', function (require) { "use strict"; var concurrency = require('web.concurrency'); var FormView = require('web.FormView'); var ListView = require('web.ListView'); var relationalFields = require('web.relational_fields'); var testUtils = require('web.test_utils'); var createView = testUtils.createView; QUnit.module('fields', {}, function () { QUnit.module('relational_fields', { beforeEach: function () { this.data = { partner: { fields: { display_name: { string: "Displayed name", type: "char" }, foo: {string: "Foo", type: "char", default: "My little Foo Value"}, bar: {string: "Bar", type: "boolean", default: true}, int_field: {string: "int_field", type: "integer", sortable: true}, qux: {string: "Qux", type: "float", digits: [16,1] }, p: {string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu'}, turtles: {string: "one2many turtle field", type: "one2many", relation: 'turtle', relation_field: 'turtle_trululu'}, trululu: {string: "Trululu", type: "many2one", relation: 'partner'}, timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'}, product_id: {string: "Product", type: "many2one", relation: 'product'}, color: { type: "selection", selection: [['red', "Red"], ['black', "Black"]], default: 'red', string: "Color", }, date: {string: "Some Date", type: "date"}, datetime: {string: "Datetime Field", type: 'datetime'}, user_id: {string: "User", type: 'many2one', relation: 'user'}, reference: {string: "Reference Field", type: 'reference', selection: [ ["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]}, }, records: [{ id: 1, display_name: "first record", bar: true, foo: "yop", int_field: 10, qux: 0.44, p: [], turtles: [2], timmy: [], trululu: 4, user_id: 17, reference: 'product,37', }, { id: 2, display_name: "second record", bar: true, foo: "blip", int_field: 9, qux: 13, p: [], timmy: [], trululu: 1, product_id: 37, date: "2017-01-25", datetime: "2016-12-12 10:55:05", user_id: 17, }, { id: 4, display_name: "aaa", bar: false, }], onchanges: {}, }, product: { fields: { name: {string: "Product Name", type: "char"} }, records: [{ id: 37, display_name: "xphone", }, { id: 41, display_name: "xpad", }] }, partner_type: { fields: { name: {string: "Partner Type", type: "char"}, color: {string: "Color index", type: "integer"}, }, records: [ {id: 12, display_name: "gold", color: 2}, {id: 14, display_name: "silver", color: 5}, ] }, turtle: { fields: { display_name: { string: "Displayed name", type: "char" }, turtle_foo: {string: "Foo", type: "char"}, turtle_bar: {string: "Bar", type: "boolean", default: true}, turtle_int: {string: "int", type: "integer", sortable: true}, turtle_description: {string: "Description", type: "text"}, turtle_trululu: {string: "Trululu", type: "many2one", relation: 'partner'}, turtle_ref: {string: "Reference", type: 'reference', selection: [ ["product", "Product"], ["partner", "Partner"]]}, product_id: {string: "Product", type: "many2one", relation: 'product', required: true}, partner_ids: {string: "Partner", type: "many2many", relation: 'partner'}, }, records: [{ id: 1, display_name: "leonardo", turtle_bar: true, turtle_foo: "yop", partner_ids: [], }, { id: 2, display_name: "donatello", turtle_bar: true, turtle_foo: "blip", turtle_int: 9, partner_ids: [2,4], }, { id: 3, display_name: "raphael", product_id: 37, turtle_bar: false, turtle_foo: "kawa", turtle_int: 21, partner_ids: [], turtle_ref: 'product,37', }], onchanges: {}, }, user: { fields: { name: {string: "Name", type: "char"}, partner_ids: {string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id'}, }, records: [{ id: 17, name: "Aline", partner_ids: [1, 2], }, { id: 19, name: "Christine", }] }, }; } }, function () { QUnit.test('onchange on unloaded record clearing posterious change', function (assert) { // when we got onchange result for fields of record that were not // already available because they were in a inline view not already // opened, in a given configuration the change were applied ignoring // posteriously changed data, thus an added/removed/modified line could // be reset to the original onchange data assert.expect(5); var numUserOnchange = 0; this.data.user.onchanges = { partner_ids: function (obj) { // simulate actual server onchange after save of modal with new record if (numUserOnchange === 0) { obj.partner_ids = _.clone(obj.partner_ids); obj.partner_ids.unshift([5]); obj.partner_ids[1][2].turtles.unshift([5]); obj.partner_ids[2] = [1, 2, { display_name: 'second record', trululu: 1, turtles: [[5]], }]; } else if (numUserOnchange === 1) { obj.partner_ids = _.clone(obj.partner_ids); obj.partner_ids.unshift([5]); obj.partner_ids[1][2].turtles.unshift([5]); obj.partner_ids[2][2].turtles.unshift([5]); } numUserOnchange++; }, }; var form = createView({ View: FormView, model: 'user', data: this.data, arch: '
' + '' + ''+ '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 17, }); // open first partner and change turtle name form.$buttons.find('.o_form_button_edit').click(); form.$('.o_data_row:eq(0)').click(); $('.modal .o_data_cell:eq(0)').click(); $('.modal input[name="display_name"]').val('Donatello').change(); $('.modal .btn-primary').click(); // open second partner and add a turtle form.$('.o_data_row:eq(1)').click(); $('.modal .o_field_x2many_list_row_add a').click(); $('.modal input[name="display_name"]').val('Michelangelo').change(); $('.modal .btn-primary').click(); assert.strictEqual(numUserOnchange, 2, 'there should 2 and only 2 onchange from closing the partner modal'); // check first record still has change form.$('.o_data_row:eq(0)').click(); assert.strictEqual($('.modal .o_data_row').length, 1, 'only 1 turtle for first partner'); assert.strictEqual($('.modal .o_data_row').text(), 'Donatello', 'first partner turtle is Donatello'); $('.modal .o_form_button_cancel').click(); // check second record still has changes form.$('.o_data_row:eq(1)').click(); assert.strictEqual($('.modal .o_data_row').length, 1, 'only 1 turtle for second partner'); assert.strictEqual($('.modal .o_data_row').text(), 'Michelangelo', 'second partner turtle is Michelangelo'); $('.modal .o_form_button_cancel').click(); form.destroy(); }); QUnit.module('FieldStatus'); QUnit.test('static statusbar widget on many2one field', function (assert) { assert.expect(5); this.data.partner.fields.trululu.domain = "[('bar', '=', True)]"; this.data.partner.records[1].bar = false; var count = 0; var nb_fields_fetched; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '
' + // the following field seem useless, but its presence was the // cause of a crash when evaluating the field domain. '' + '', mockRPC: function (route, args) { if (args.method === 'search_read') { count++; nb_fields_fetched = args.kwargs.fields.length; } return this._super.apply(this, arguments); }, res_id: 1, config: {device: {isMobile: false}}, }); assert.strictEqual(count, 1, 'once search_read should have been done to fetch the relational values'); assert.strictEqual(nb_fields_fetched, 1, 'search_read should only fetch field id'); assert.containsN(form, '.o_statusbar_status button:not(.dropdown-toggle)', 2); assert.containsN(form, '.o_statusbar_status button:disabled', 2); assert.hasClass(form.$('.o_statusbar_status button[data-value="4"]'), 'btn-primary'); form.destroy(); }); QUnit.test('static statusbar widget on many2one field with domain', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '
' + '
', mockRPC: function (route, args) { if (args.method === 'search_read') { assert.deepEqual(args.kwargs.domain, ['|', ['id', '=', 4], ['user_id', '=', 17]], "search_read should sent the correct domain"); } return this._super.apply(this, arguments); }, res_id: 1, session: {user_context: {uid: 17}}, }); form.destroy(); }); QUnit.test('clickable statusbar widget on many2one field', function (assert) { assert.expect(5); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '
' + '
', res_id: 1, config: {device: {isMobile: false}}, }); assert.hasClass(form.$('.o_statusbar_status button[data-value="4"]'), 'btn-primary'); assert.hasClass(form.$('.o_statusbar_status button[data-value="4"]'), 'disabled'); assert.containsN(form, '.o_statusbar_status button.btn-secondary:not(.dropdown-toggle):not(:disabled)', 2); var $clickable = form.$('.o_statusbar_status button.btn-secondary:not(.dropdown-toggle):not(:disabled)'); testUtils.dom.click($clickable.last()); // (last is visually the first here (css)) assert.hasClass(form.$('.o_statusbar_status button[data-value="1"]'), "btn-primary"); assert.hasClass(form.$('.o_statusbar_status button[data-value="1"]'), "disabled"); form.destroy(); }); QUnit.test('statusbar with no status', function (assert) { assert.expect(2); this.data.product.records = []; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '
' + '
', res_id: 1, config: {device: {isMobile: false}}, }); assert.hasClass(form.$('.o_statusbar_status'), 'o_field_empty'); assert.strictEqual(form.$('.o_statusbar_status').children().length, 0, 'statusbar widget should be empty'); form.destroy(); }); QUnit.test('statusbar with domain but no value (create mode)', function (assert) { assert.expect(1); this.data.partner.fields.trululu.domain = "[('bar', '=', True)]"; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
' + '
', config: {device: {isMobile: false}}, }); assert.containsN(form, '.o_statusbar_status button:disabled', 2); form.destroy(); }); QUnit.test('clickable statusbar should change m2o fetching domain in edit mode', function (assert) { assert.expect(2); this.data.partner.fields.trululu.domain = "[('bar', '=', True)]"; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
' + '
', res_id: 1, config: {device: {isMobile: false}}, }); testUtils.form.clickEdit(form); assert.containsN(form, '.o_statusbar_status button:not(.dropdown-toggle)', 3); testUtils.dom.click(form.$('.o_statusbar_status button:not(.dropdown-toggle)').last()); assert.containsN(form, '.o_statusbar_status button:not(.dropdown-toggle)', 2); form.destroy(); }); QUnit.test('statusbar fold_field option and statusbar_visible attribute', function (assert) { assert.expect(2); this.data.partner.records[0].bar = false; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
' + '
' + '
', res_id: 1, config: {device: {isMobile: false}}, }); testUtils.form.clickEdit(form); assert.containsOnce(form, '.o_statusbar_status:first .dropdown-menu button.disabled'); assert.containsOnce(form, '.o_statusbar_status:last button.disabled'); form.destroy(); }); QUnit.test('statusbar with dynamic domain', function (assert) { assert.expect(5); this.data.partner.fields.trululu.domain = "[('int_field', '>', qux)]"; this.data.partner.records[2].int_field = 0; var rpcCount = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
' + '' + '' + '', mockRPC: function (route, args) { if (args.method === 'search_read') { rpcCount++; } return this._super.apply(this, arguments); }, res_id: 1, config: {device: {isMobile: false}}, }); testUtils.form.clickEdit(form); assert.containsN(form, '.o_statusbar_status button.disabled', 3); assert.strictEqual(rpcCount, 1, "should have done 1 search_read rpc"); testUtils.fields.editInput(form.$('input[name=qux]'), 9.5); assert.containsN(form, '.o_statusbar_status button.disabled', 2); assert.strictEqual(rpcCount, 2, "should have done 1 more search_read rpc"); testUtils.fields.editInput(form.$('input[name=qux]'), "hey"); assert.strictEqual(rpcCount, 2, "should not have done 1 more search_read rpc"); form.destroy(); }); QUnit.module('FieldSelection'); QUnit.test('widget selection in a list view', function (assert) { assert.expect(3); this.data.partner.records.forEach(function (r) { r.color = 'red'; }); var list = createView({ View: ListView, model: 'partner', data: this.data, arch: '' + '' + '', }); assert.strictEqual(list.$('td:contains(Red)').length, 3, "should have 3 rows with correct value"); testUtils.dom.click(list.$('td:contains(Red):first')); var $td = list.$('tbody tr.o_selected_row td:not(.o_list_record_selector)'); assert.strictEqual($td.find('select').length, 1, "td should have a child 'select'"); assert.strictEqual($td.contents().length, 1, "select tag should be only child of td"); list.destroy(); }); QUnit.test('widget selection, edition and on many2one field', function (assert) { assert.expect(18); this.data.partner.onchanges = {product_id: function () {}}; this.data.partner.records[0].product_id = 37; this.data.partner.records[0].trululu = false; var count = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { count++; assert.step(args.method); return this._super(route, args); }, }); assert.ok(!form.$('select').length, "should not have a select tag in dom"); assert.strictEqual(form.$('.o_field_widget[name=product_id]').text(), 'xphone', "should have rendered the many2one field correctly"); assert.strictEqual(form.$('.o_field_widget[name=trululu]').text(), '', "should have rendered the unset many2one field correctly"); assert.strictEqual(form.$('.o_field_widget[name=color]').text(), 'Red', "should have rendered the selection field correctly"); testUtils.form.clickEdit(form); assert.containsN(form, 'select', 3, "should have 3 select tag in dom"); assert.strictEqual(form.$('select[name="product_id"] option:contains(xphone)').length, 1, "should have fetched xphone option"); assert.strictEqual(form.$('select[name="product_id"] option:contains(xpad)').length, 1, "should have fetched xpad option"); assert.strictEqual(form.$('select[name="product_id"]').val(), "37", "should have correct product_id value"); assert.strictEqual(form.$('select[name="trululu"]').val(), "false", "should not have any value in trululu field"); testUtils.fields.editSelect(form.$('select[name="product_id"]'), 41); assert.strictEqual(form.$('select[name="product_id"]').val(), "41", "should have a value of xphone"); assert.strictEqual(form.$('select[name="color"]').val(), "\"red\"", "should have correct value in color field"); assert.verifySteps(['read', 'name_search', 'name_search', 'onchange']); count = 0; form.reload(); assert.strictEqual(count, 1, "should not reload product_id relation"); form.destroy(); }); QUnit.test('unset selection field with 0 as key', function (assert) { // The server doesn't make a distinction between false value (the field // is unset), and selection 0, as in that case the value it returns is // false. So the client must convert false to value 0 if it exists. assert.expect(2); this.data.partner.fields.selection = { type: "selection", selection: [[0, "Value O"], [1, "Value 1"]], }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 1, }); assert.strictEqual(form.$('.o_field_widget').text(), 'Value O', "the displayed value should be 'Value O'"); assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty', "should not have class o_field_empty"); form.destroy(); }); QUnit.test('unset selection field with string keys', function (assert) { // The server doesn't make a distinction between false value (the field // is unset), and selection 0, as in that case the value it returns is // false. So the client must convert false to value 0 if it exists. In // this test, it doesn't exist as keys are strings. assert.expect(2); this.data.partner.fields.selection = { type: "selection", selection: [['0', "Value O"], ['1', "Value 1"]], }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 1, }); assert.strictEqual(form.$('.o_field_widget').text(), '', "there should be no displayed value"); assert.hasClass(form.$('.o_field_widget'),'o_field_empty', "should have class o_field_empty"); form.destroy(); }); QUnit.test('unset selection on a many2one field', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', mockRPC: function (route, args) { if (args.method === 'write') { assert.strictEqual(args.args[1].trululu, false, "should send 'false' as trululu value"); } return this._super.apply(this, arguments); }, res_id: 1, viewOptions: { mode: 'edit', }, }); testUtils.fields.editSelect(form.$('select'), "false"); testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('field selection with many2ones and special characters', function (assert) { assert.expect(1); // edit the partner with id=4 this.data.partner.records[2].display_name = 'hey'; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 1, viewOptions: {mode: 'edit'}, }); assert.strictEqual(form.$('select option[value="4"]').text(), 'hey'); form.destroy(); }); QUnit.test('widget selection on a many2one: domain updated by an onchange', function (assert) { assert.expect(4); this.data.partner.onchanges = { int_field: function () {}, }; var domain = []; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'onchange') { domain = [['id', 'in', [10]]]; return $.when({ domain: { trululu: domain, } }); } if (args.method === 'name_search') { assert.deepEqual(args.args[1], domain, "sent domain should be correct"); } return this._super(route, args); }, viewOptions: { mode: 'edit', }, }); assert.containsN(form, '.o_field_widget[name=trululu] option', 4, "should be 4 options in the selection"); // trigger an onchange that will update the domain testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 2); assert.containsOnce(form, '.o_field_widget[name=trululu] option', "should be 1 option in the selection"); form.destroy(); }); QUnit.test('required selection widget should not have blank option', function (assert) { assert.expect(3); this.data.partner.fields.feedback_value = { type: "selection", required: true, selection : [['good', 'Good'], ['bad', 'Bad']], default: 'good', string: 'Good' }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '', res_id: 1 }); testUtils.form.clickEdit(form); assert.strictEqual(form.$('.o_field_widget[name=color]')[0].options.length, 3, "non required selection field must have 3 options 1 blank option and 2 value options"); assert.strictEqual(form.$('.o_field_widget[name=feedback_value]')[0].options.length, 2, "should have only 2 options without blank option"); // change value to update widget modifier values testUtils.fields.editSelect(form.$('.o_field_widget[name=feedback_value]'), '"bad"'); assert.strictEqual(form.$('.o_field_widget[name=color]')[0].options.length, 2, "should have only 2 options"); form.destroy(); }); QUnit.module('FieldMany2ManyTags'); QUnit.test('fieldmany2many tags with and without color', function (assert) { assert.expect(5); this.data.partner.fields.partner_ids = {string: "Partner", type: "many2many", relation: 'partner'}; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '', mockRPC: function (route, args) { if (args.method ==='read' && args.model === 'partner_type') { assert.deepEqual(args.args , [[12], ['display_name']], "should not read any color field"); } else if (args.method ==='read' && args.model === 'partner') { assert.deepEqual(args.args , [[1], ['display_name', 'color']], "should read color field"); } return this._super.apply(this, arguments); } }); // add a tag on field partner_ids testUtils.fields.many2one.clickOpenDropdown('partner_ids'); testUtils.fields.many2one.clickHighlightedItem('partner_ids'); // add a tag on field timmy testUtils.fields.many2one.clickOpenDropdown('timmy'); var $input = form.$('.o_field_many2manytags[name="timmy"] input'); assert.strictEqual($input.autocomplete('widget').find('li').length, 3, "autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')"); testUtils.fields.many2one.clickHighlightedItem('timmy'); assert.containsOnce(form, '.o_field_many2manytags[name="timmy"] .badge', "should contain 1 tag"); assert.ok(form.$('.o_field_many2manytags[name="timmy"] .badge:contains("gold")').length, "should contain newly added tag 'gold'"); form.destroy(); }); QUnit.test('fieldmany2many tags with color: rendering and edition', function (assert) { assert.expect(28); this.data.partner.records[0].timmy = [12, 14]; this.data.partner_type.records.push({id: 13, display_name: "red", color: 8}); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '', res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { var commands = args.args[1].timmy; assert.strictEqual(commands.length, 1, "should have generated one command"); assert.strictEqual(commands[0][0], 6, "generated command should be REPLACE WITH"); assert.ok(_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), [12, 13]), "new value should be [12, 13]"); } if (args.method ==='read' && args.model === 'partner_type') { assert.deepEqual(args.args[1], ['display_name', 'color'], "should read the color field"); } return this._super.apply(this, arguments); }, }); assert.containsN(form, '.o_field_many2manytags .badge .dropdown-toggle', 2, "should contain 2 tags"); assert.ok(form.$('.badge .dropdown-toggle:contains(gold)').length, 'should have fetched and rendered gold partner tag'); assert.ok(form.$('.badge .dropdown-toggle:contains(silver)').length, 'should have fetched and rendered silver partner tag'); assert.strictEqual(form.$('.badge:first()').data('color'), 2, 'should have correctly fetched the color'); testUtils.form.clickEdit(form); assert.containsN(form, '.o_field_many2manytags .badge .dropdown-toggle', 2, "should still contain 2 tags in edit mode"); assert.ok(form.$('.o_tag_color_2 .o_badge_text:contains(gold)').length, 'first tag should still contain "gold" and be color 2 in edit mode'); assert.containsN(form, '.o_field_many2manytags .o_delete', 2, "tags should contain a delete button"); // add an other existing tag var $input = form.$('.o_field_many2manytags input'); testUtils.fields.many2one.clickOpenDropdown('timmy'); assert.strictEqual($input.autocomplete('widget').find('li').length, 1, "autocomplete dropdown should have 1 entry"); assert.strictEqual($input.autocomplete('widget').find('li a:contains("red")').length, 1, "autocomplete dropdown should contain 'red'"); testUtils.fields.many2one.clickHighlightedItem('timmy'); assert.containsN(form, '.o_field_many2manytags .badge .dropdown-toggle', 3, "should contain 3 tags"); assert.ok(form.$('.o_field_many2manytags .badge .dropdown-toggle:contains("red")').length, "should contain newly added tag 'red'"); assert.ok(form.$('.o_field_many2manytags .badge[data-color=8] .dropdown-toggle:contains("red")').length, "should have fetched the color of added tag"); // remove tag with id 14 testUtils.dom.click(form.$('.o_field_many2manytags .badge[data-id=14] .o_delete')); assert.containsN(form, '.o_field_many2manytags .badge .dropdown-toggle', 2, "should contain 2 tags"); assert.ok(!form.$('.o_field_many2manytags .badge .dropdown-toggle:contains("silver")').length, "should not contain tag 'silver' anymore"); // save the record (should do the write RPC with the correct commands) testUtils.form.clickSave(form); // checkbox 'Hide in Kanban' $input = form.$('.o_field_many2manytags .badge[data-id=13] .dropdown-toggle'); // selects 'red' tag testUtils.dom.click($input); var $checkBox = form.$('.o_field_many2manytags .badge[data-id=13] .custom-checkbox input'); assert.strictEqual($checkBox.length, 1, "should have a checkbox in the colorpicker dropdown menu"); assert.notOk($checkBox.is(':checked'), "should have unticked checkbox in colorpicker dropdown menu"); testUtils.fields.editAndTrigger($checkBox, null,['mouseenter','mousedown']); $input = form.$('.o_field_many2manytags .badge[data-id=13] .dropdown-toggle'); // refresh testUtils.dom.click($input); $checkBox = form.$('.o_field_many2manytags .badge[data-id=13] .custom-checkbox input'); // refresh assert.equal($input.parent().data('color'), "0", "should become transparent when toggling on checkbox"); assert.ok($checkBox.is(':checked'), "should have a ticked checkbox in colorpicker dropdown menu after mousedown"); testUtils.fields.editAndTrigger($checkBox, null,['mouseenter','mousedown']); $input = form.$('.o_field_many2manytags .badge[data-id=13] .dropdown-toggle'); // refresh testUtils.dom.click($input); $checkBox = form.$('.o_field_many2manytags .badge[data-id=13] .custom-checkbox input'); // refresh assert.equal($input.parent().data('color'), "8", "should revert to old color when toggling off checkbox"); assert.notOk($checkBox.is(':checked'), "should have an unticked checkbox in colorpicker dropdown menu after 2nd click"); // TODO: it would be nice to test the behaviors of the autocomplete dropdown // (like refining the research, creating new tags...), but ui-autocomplete // makes it difficult to test form.destroy(); }); QUnit.test('fieldmany2many tags view a domain', function (assert) { assert.expect(7); this.data.partner.fields.timmy.domain = [['id', '<', 50]]; this.data.partner.records[0].timmy = [12]; this.data.partner_type.records.push({id: 99, display_name: "red", color: 8}); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'name_search') { assert.deepEqual(args.kwargs.args, [['id', '<', 50], ['id', 'not in', [12]]], "domain sent to name_search should be correct"); return $.when([[14, 'silver']]); } return this._super.apply(this, arguments); } }); assert.containsOnce(form, '.o_field_many2manytags .badge', "should contain 1 tag"); assert.ok(form.$('.badge:contains(gold)').length, 'should have fetched and rendered gold partner tag'); testUtils.form.clickEdit(form); // add an other existing tag var $input = form.$('.o_field_many2manytags input'); testUtils.fields.many2one.clickOpenDropdown('timmy'); assert.strictEqual($input.autocomplete('widget').find('li').length, 1, "autocomplete dropdown should have 1 entry"); assert.strictEqual($input.autocomplete('widget').find('li a:contains("silver")').length, 1, "autocomplete dropdown should contain 'silver'"); testUtils.fields.many2one.clickHighlightedItem('timmy'); assert.containsN(form, '.o_field_many2manytags .badge', 2, "should contain 2 tags"); assert.ok(form.$('.o_field_many2manytags .badge:contains("silver")').length, "should contain newly added tag 'silver'"); form.destroy(); }); QUnit.test('fieldmany2many tags in a new record', function (assert) { assert.expect(7); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '', mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/create') { var commands = args.args[0].timmy; assert.strictEqual(commands.length, 1, "should have generated one command"); assert.strictEqual(commands[0][0], 6, "generated command should be REPLACE WITH"); assert.ok(_.isEqual(commands[0][2], [12]), "new value should be [12]"); } return this._super.apply(this, arguments); } }); assert.hasClass(form.$('.o_form_view'),'o_form_editable', "form should be in edit mode"); testUtils.fields.many2one.clickOpenDropdown('timmy'); assert.strictEqual(form.$('.o_field_many2manytags input').autocomplete('widget').find('li').length, 3, "autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')"); testUtils.fields.many2one.clickHighlightedItem('timmy'); assert.containsOnce(form, '.o_field_many2manytags .badge', "should contain 1 tag"); assert.ok(form.$('.o_field_many2manytags .badge:contains("gold")').length, "should contain newly added tag 'gold'"); // save the record (should do the write RPC with the correct commands) testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('fieldmany2many tags: update color', function (assert) { assert.expect(5); this.data.partner.records[0].timmy = [12, 14]; this.data.partner_type.records[0].color = 0; var color; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '', mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[1], {color: color}, "shoud write the new color"); } return this._super.apply(this, arguments); }, res_id: 1, }); // First checks that default color 0 is rendered as 0 color assert.ok(form.$('.badge.dropdown:first()').is('.o_tag_color_0'), 'first tag color should be 0'); // Update the color in readonly color = 1; testUtils.dom.click(form.$('.badge:first() .dropdown-toggle')); $('.o_colorpicker a[data-color="' + color + '"]').trigger('mousedown'); // choose color 1 assert.strictEqual(form.$('.badge:first()').data('color'), color, 'should have correctly updated the color (in readonly)'); // Update the color in edit color = 6; testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.badge:first() .dropdown-toggle')); $('.o_colorpicker a[data-color="' + color + '"]').trigger('mousedown'); // choose color 6 assert.strictEqual(form.$('.badge:first()').data('color'), color, 'should have correctly updated the color (in edit)'); form.destroy(); }); QUnit.test('fieldmany2many tags in editable list', function (assert) { assert.expect(7); this.data.partner.records[0].timmy = [12]; var list = createView({ View: ListView, model: 'partner', data: this.data, context: {take: 'five'}, arch:'' + '' + '' + '', mockRPC: function (route, args) { if (args.method === 'read' && args.model === 'partner_type') { assert.deepEqual(args.kwargs.context, {take: 'five'}, 'The context should be passed to the RPC'); } return this._super.apply(this, arguments); } }); assert.containsOnce(list, '.o_data_row:first .o_field_many2manytags .badge', "m2m field should contain one tag"); // edit first row testUtils.dom.click(list.$('.o_data_row:first td:nth(2)')); var $m2o = list.$('.o_data_row:first .o_field_many2manytags .o_field_many2one'); assert.strictEqual($m2o.length, 1, "a many2one widget should have been instantiated"); // add a tag testUtils.fields.many2one.clickOpenDropdown('timmy'); testUtils.fields.many2one.clickHighlightedItem('timmy'); assert.containsN(list, '.o_data_row:first .o_field_many2manytags .badge', 2, "m2m field should contain 2 tags"); // leave edition testUtils.dom.click(list.$('.o_data_row:nth(1) td:nth(2)')); assert.containsN(list, '.o_data_row:first .o_field_many2manytags .badge', 2, "m2m field should contain 2 tags"); list.destroy(); }); QUnit.test('search more in many2one: group and use the pager', async function (assert) { assert.expect(2); this.data.partner.records.push({ id: 5, display_name: "Partner 4", }, { id: 6, display_name: "Partner 5", }, { id: 7, display_name: "Partner 6", }, { id: 8, display_name: "Partner 7", }, { id: 9, display_name: "Partner 8", }, { id: 10, display_name: "Partner 9", }); this.data.partner.fields.datetime.searchable = true; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner,false,list': '', 'partner,false,search': '' + ' ' + '', }, viewOptions: { mode: 'edit', }, }); testUtils.fields.many2one.clickOpenDropdown('trululu'); await testUtils.fields.many2one.clickItem('trululu', 'Search'); testUtils.dom.click($('.modal .o_search_options .fa-bars')); testUtils.dom.click($('.modal .o_search_options .o_group_by_menu a:contains(Bar)')); testUtils.dom.click($('.modal .o_group_header:first')); assert.strictEqual($('.modal tbody:nth(1) .o_data_row').length, 7, "should display 7 records in the first page"); testUtils.dom.click($('.modal .o_group_header:first .o_pager_next')); assert.strictEqual($('.modal tbody:nth(1) .o_data_row').length, 1, "should display 1 record in the second page"); form.destroy(); }); QUnit.test('many2many_tags can load more than 40 records', function (assert) { assert.expect(1); this.data.partner.fields.partner_ids = {string: "Partner", type: "many2many", relation: 'partner'}; this.data.partner.records[0].partner_ids = []; for (var i = 15; i < 115; i++) { this.data.partner.records.push({id: i, display_name: 'walter' + i}); this.data.partner.records[0].partner_ids.push(i); } var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 1, }); assert.containsN(form, '.o_field_widget[name="partner_ids"] .badge', 100, 'should have rendered 100 tags'); form.destroy(); }); QUnit.test('field many2many_tags keeps focus when being edited', function (assert) { assert.expect(7); this.data.partner.records[0].timmy = [12]; this.data.partner.onchanges.foo = function (obj) { obj.timmy = [[5]]; // DELETE command }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '', res_id: 1, }); testUtils.form.clickEdit(form); assert.containsOnce(form, '.o_field_many2manytags .badge', "should contain one tag"); // update foo, which will trigger an onchange and update timmy // -> m2mtags input should not have taken the focus form.$('input:first').focus(); testUtils.fields.editInput(form.$('input:first'), 'trigger onchange'); assert.containsNone(form, '.o_field_many2manytags .badge', "should contain no tags"); assert.strictEqual(form.$('input:first').get(0), document.activeElement, "foo input should have kept the focus"); // add a tag -> m2mtags input should still have the focus testUtils.fields.many2one.clickOpenDropdown('timmy'); testUtils.fields.many2one.clickHighlightedItem('timmy'); assert.containsOnce(form, '.o_field_many2manytags .badge', "should contain a tag"); assert.strictEqual(form.$('.o_field_many2manytags input').get(0), document.activeElement, "m2m tags input should have kept the focus"); // remove a tag -> m2mtags input should still have the focus testUtils.dom.click(form.$('.o_field_many2manytags .o_delete')); assert.containsNone(form, '.o_field_many2manytags .badge', "should contain no tags"); assert.strictEqual(form.$('.o_field_many2manytags input').get(0), document.activeElement, "m2m tags input should have kept the focus"); form.destroy(); }); QUnit.test('widget many2many_tags: tags title attribute', function (assert) { assert.expect(1); this.data.turtle.records[0].partner_ids = [2]; var form = createView({ View: FormView, model: 'turtle', data: this.data, arch:'
' + '' + '' + '' + '' + '
', res_id: 1, }); assert.deepEqual( form.$('.o_field_many2manytags.o_field_widget .badge .o_badge_text').attr('title'), 'second record', 'the title should be filled in' ); form.destroy(); }); QUnit.test('widget many2many_tags: toggle colorpicker multiple times', function (assert) { assert.expect(11); this.data.partner.records[0].timmy = [12]; this.data.partner_type.records[0].color = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual($('.o_field_many2manytags .badge').length, 1, "should have one tag"); assert.strictEqual($('.o_field_many2manytags .badge').data('color'), 0, "tag should have color 0"); assert.strictEqual($('.o_colorpicker:visible').length, 0, "colorpicker should be closed"); // click on the badge to open colorpicker testUtils.dom.click(form.$('.o_field_many2manytags .badge .dropdown-toggle')); assert.strictEqual($('.o_colorpicker:visible').length, 1, "colorpicker should be open"); // click on the badge again to close colorpicker testUtils.dom.click(form.$('.o_field_many2manytags .badge .dropdown-toggle')); assert.strictEqual($('.o_field_many2manytags .badge').data('color'), 0, "tag should still have color 0"); assert.strictEqual($('.o_colorpicker:visible').length, 0, "colorpicker should be closed"); // click on the badge to open colorpicker testUtils.dom.click(form.$('.o_field_many2manytags .badge .dropdown-toggle')); assert.strictEqual($('.o_colorpicker:visible').length, 1, "colorpicker should be open"); // click on the colorpicker, but not on a color testUtils.dom.click(form.$('.o_colorpicker')); assert.strictEqual($('.o_field_many2manytags .badge').data('color'), 0, "tag should still have color 0"); assert.strictEqual($('.o_colorpicker:visible').length, 0, "colorpicker should be closed"); // click on the badge to open colorpicker testUtils.dom.click(form.$('.o_field_many2manytags .badge .dropdown-toggle')); // click on a color in the colorpicker form.$('.o_colorpicker .o_tag_color_2').trigger('mousedown'); assert.strictEqual($('.o_field_many2manytags .badge').data('color'), 2, "tag should have color 2"); assert.strictEqual($('.o_colorpicker:visible').length, 0, "colorpicker should be closed"); form.destroy(); }); QUnit.module('FieldRadio'); QUnit.test('fieldradio widget on a many2one in a new record', function (assert) { assert.expect(6); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', }); assert.ok(form.$('div.o_radio_item').length, "should have rendered outer div"); assert.containsN(form, 'input.o_radio_input', 2, "should have 2 possible choices"); assert.ok(form.$('label.o_form_label:contains(xphone)').length, "one of them should be xphone"); assert.containsNone(form, 'input:checked', "none of the input should be checked"); testUtils.dom.click(form.$("input.o_radio_input:first")); assert.containsOnce(form, 'input:checked', "one of the input should be checked"); testUtils.form.clickSave(form); var newRecord = _.last(this.data.partner.records); assert.strictEqual(newRecord.product_id, 37, "should have saved record with correct value"); form.destroy(); }); QUnit.test('fieldradio change value by onchange', function (assert) { assert.expect(4); this.data.partner.onchanges = {bar: function (obj) { obj.product_id = obj.bar ? 41 : 37; obj.color = obj.bar ? 'red' : 'black'; }}; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '', }); testUtils.dom.click(form.$("input[type='checkbox']")); assert.containsOnce(form, 'input.o_radio_input[data-value="37"]:checked', "one of the input should be checked"); assert.containsOnce(form, 'input.o_radio_input[data-value="black"]:checked', "the other of the input should be checked"); testUtils.dom.click(form.$("input[type='checkbox']")); assert.containsOnce(form, 'input.o_radio_input[data-value="41"]:checked', "the other of the input should be checked"); assert.containsOnce(form, 'input.o_radio_input[data-value="red"]:checked', "one of the input should be checked"); form.destroy(); }); QUnit.test('fieldradio widget on a selection in a new record', function (assert) { assert.expect(4); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', }); assert.ok(form.$('div.o_radio_item').length, "should have rendered outer div"); assert.containsN(form, 'input.o_radio_input', 2, "should have 2 possible choices"); assert.ok(form.$('label.o_form_label:contains(Red)').length, "one of them should be Red"); // click on 2nd option testUtils.dom.click(form.$("input.o_radio_input").eq(1)); testUtils.form.clickSave(form); var newRecord = _.last(this.data.partner.records); assert.strictEqual(newRecord.color, 'black', "should have saved record with correct value"); form.destroy(); }); QUnit.test('fieldradio widget has o_horizontal or o_vertical class', function (assert) { assert.expect(2); this.data.partner.fields.color2 = this.data.partner.fields.color; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '
', }); var btn1 = form.$('div.o_field_radio.o_vertical'); var btn2 = form.$('div.o_field_radio.o_horizontal'); assert.strictEqual(btn1.length, 1, "should have o_vertical class"); assert.strictEqual(btn2.length, 1, "should have o_horizontal class"); form.destroy(); }); QUnit.test('fieldradio widget with numerical keys encoded as strings', function (assert) { assert.expect(5); this.data.partner.fields.selection = { type: 'selection', selection: [['0', "Red"], ['1', "Black"]], }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { assert.strictEqual(args.args[1].selection, '1', "should write correct value"); } return this._super.apply(this, arguments); }, }); assert.strictEqual(form.$('.o_field_widget').text(), '', "field should be unset"); testUtils.form.clickEdit(form); assert.containsNone(form, '.o_radio_input:checked', "no value should be checked"); testUtils.dom.click(form.$("input.o_radio_input:nth(1)")); testUtils.form.clickSave(form); assert.strictEqual(form.$('.o_field_widget').text(), 'Black', "value should be 'Black'"); testUtils.form.clickEdit(form); assert.containsOnce(form, '.o_radio_input[data-index=1]:checked', "'Black' should be checked"); form.destroy(); }); QUnit.test('widget radio on a many2one: domain updated by an onchange', function (assert) { assert.expect(4); this.data.partner.onchanges = { int_field: function () {}, }; var domain = []; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'onchange') { domain = [['id', 'in', [10]]]; return $.when({ value: { trululu: false, }, domain: { trululu: domain, }, }); } if (args.method === 'search_read') { assert.deepEqual(args.kwargs.domain, domain, "sent domain should be correct"); } return this._super(route, args); }, viewOptions: { mode: 'edit', }, }); assert.containsN(form, '.o_field_widget[name=trululu] .o_radio_item', 3, "should be 3 radio buttons"); // trigger an onchange that will update the domain testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 2); assert.containsNone(form, '.o_field_widget[name=trululu] .o_radio_item', "should be no more radio button"); form.destroy(); }); QUnit.module('FieldSelectionBadge'); QUnit.test('FieldSelectionBadge widget on a many2one in a new record', function (assert) { assert.expect(6); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', }); assert.ok(form.$('span.o_selection_badge').length, "should have rendered outer div"); assert.containsN(form, 'span.o_selection_badge', 2, "should have 2 possible choices"); assert.ok(form.$('span.o_selection_badge:contains(xphone)').length, "one of them should be xphone"); assert.containsNone(form, 'span.active', "none of the input should be checked"); testUtils.dom.click($("span.o_selection_badge:first")); assert.containsOnce(form, 'span.active', "one of the input should be checked"); testUtils.form.clickSave(form); var newRecord = _.last(this.data.partner.records); assert.strictEqual(newRecord.product_id, 37, "should have saved record with correct value"); form.destroy(); }); QUnit.test('FieldSelectionBadge widget on a selection in a new record', function (assert) { assert.expect(4); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', }); assert.ok(form.$('span.o_selection_badge').length, "should have rendered outer div"); assert.containsN(form, 'span.o_selection_badge', 2, "should have 2 possible choices"); assert.ok(form.$('span.o_selection_badge:contains(Red)').length, "one of them should be Red"); // click on 2nd option testUtils.dom.click(form.$("span.o_selection_badge").eq(1)); testUtils.form.clickSave(form); var newRecord = _.last(this.data.partner.records); assert.strictEqual(newRecord.color, 'black', "should have saved record with correct value"); form.destroy(); }); QUnit.test('FieldSelectionBadge widget on a selection in a readonly mode', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', }); assert.containsOnce(form, 'span.o_readonly_modifier', "should have 1 possible value in readonly mode"); form.destroy(); }); QUnit.module('FieldMany2ManyCheckBoxes'); QUnit.test('widget many2many_checkboxes', function (assert) { assert.expect(10); this.data.partner.records[0].timmy = [12]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '
', res_id: 1, }); assert.containsN(form, 'div.o_field_widget div.custom-checkbox', 2, "should have fetched and displayed the 2 values of the many2many"); assert.ok(form.$('div.o_field_widget div.custom-checkbox input').eq(0).prop('checked'), "first checkbox should be checked"); assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(1).prop('checked'), "second checkbox should not be checked"); assert.ok(form.$('div.o_field_widget div.custom-checkbox input').prop('disabled'), "the checkboxes should be disabled"); testUtils.form.clickEdit(form); assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').prop('disabled'), "the checkboxes should not be disabled"); // add a m2m value by clicking on input testUtils.dom.click(form.$('div.o_field_widget div.custom-checkbox input').eq(1)); testUtils.form.clickSave(form); assert.deepEqual(this.data.partner.records[0].timmy, [12, 14], "should have added the second element to the many2many"); assert.containsN(form, 'input:checked', 2, "both checkboxes should be checked"); // remove a m2m value by clinking on label testUtils.form.clickEdit(form); testUtils.dom.click(form.$('div.o_field_widget div.custom-checkbox > label').eq(0)); testUtils.form.clickSave(form); assert.deepEqual(this.data.partner.records[0].timmy, [14], "should have removed the first element to the many2many"); assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(0).prop('checked'), "first checkbox should be checked"); assert.ok(form.$('div.o_field_widget div.custom-checkbox input').eq(1).prop('checked'), "second checkbox should not be checked"); form.destroy(); }); QUnit.test('widget many2many_checkboxes: start non empty, then remove twice', function (assert) { assert.expect(2); this.data.partner.records[0].timmy = [12,14]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '
', res_id: 1, viewOptions: {mode: 'edit'}, }); testUtils.dom.click(form.$('div.o_field_widget div.custom-checkbox input').eq(0)); testUtils.dom.click(form.$('div.o_field_widget div.custom-checkbox input').eq(1)); testUtils.form.clickSave(form); assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(0).prop('checked'), "first checkbox should not be checked"); assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(1).prop('checked'), "second checkbox should not be checked"); form.destroy(); }); QUnit.module('FieldMany2ManyBinaryMultiFiles'); QUnit.test('widget many2many_binary', function (assert) { assert.expect(14); this.data['ir.attachment'] = { fields: { name: {string:"Name", type: "char"}, mimetype: {string: "Mimetype", type: "char"}, }, records: [{ id: 17, name: 'Marley&Me.jpg', mimetype: 'jpg', }], }; this.data.turtle.fields.picture_ids = { string: "Pictures", type: "many2many", relation: 'ir.attachment', }; this.data.turtle.records[0].picture_ids = [17]; var form = createView({ View: FormView, model: 'turtle', data: this.data, arch:'
' + '' + '
', archs: { 'ir.attachment,false,list': '', }, res_id: 1, mockRPC: function (route, args) { assert.step(route); if (route === '/web/dataset/call_kw/ir.attachment/read') { assert.deepEqual(args.args[1], ['name', 'datas_fname', 'mimetype']); } return this._super.apply(this, arguments); }, }); assert.containsOnce(form, 'div.o_field_widget.oe_fileupload', "there should be the attachment widget"); assert.strictEqual(form.$('div.o_field_widget.oe_fileupload .oe_attachments').children().length, 1, "there should be no attachment"); assert.containsNone(form, 'div.o_field_widget.oe_fileupload .o_attach', "there should not be an Add button (readonly)"); assert.containsNone(form, 'div.o_field_widget.oe_fileupload .oe_attachment .oe_delete', "there should not be a Delete button (readonly)"); // to edit mode testUtils.form.clickEdit(form); assert.containsOnce(form, 'div.o_field_widget.oe_fileupload .o_attach', "there should be an Add button"); assert.strictEqual(form.$('div.o_field_widget.oe_fileupload .o_attach').text().trim(), "Pictures", "the button should be correctly named"); assert.containsOnce(form, 'div.o_field_widget.oe_fileupload .o_hidden_input_file form', "there should be a hidden form to upload attachments"); // TODO: add an attachment // no idea how to test this // delete the attachment testUtils.dom.click(form.$('div.o_field_widget.oe_fileupload .oe_attachment .oe_delete')); assert.verifySteps([ '/web/dataset/call_kw/turtle/read', '/web/dataset/call_kw/ir.attachment/read', ]); testUtils.form.clickSave(form); assert.strictEqual(form.$('div.o_field_widget.oe_fileupload .oe_attachments').children().length, 0, "there should be no attachment"); form.destroy(); }); QUnit.test('name_create in form dialog', async function (assert) { assert.expect(2); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '', mockRPC: function (route, args) { if (args.method === 'name_create') { assert.step('name_create'); } return this._super.apply(this, arguments); }, }); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.many2one.searchAndClickItem('product_id', {selector:'.modal', search:'new record'}); assert.verifySteps(['name_create']); form.destroy(); }); QUnit.module('FieldReference'); QUnit.test('Reference field in modal readonly mode', function (assert) { assert.expect(4); this.data.partner.records[0].p = [2]; this.data.partner.records[1].trululu = 1; this.data.partner.records[1].reference = 'product,41'; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '', archs: { 'partner,false,form': '
', 'partner,false,list': '', }, res_id: 1, }); // Cuurent Form assert.equal(form.$('.o_form_uri.o_field_widget[name=reference]').text(), 'xphone', 'the field reference of the form should have the right value'); var $cell_o2m = form.$('.o_data_cell'); assert.equal($cell_o2m.text(), 'second record', 'the list should have one record'); testUtils.dom.click($cell_o2m); // In modal var $modal = $('.modal-lg'); assert.equal($modal.length, 1, 'there should be one modal opened'); assert.equal($modal.find('.o_form_uri.o_field_widget[name=reference]').text(), 'xpad', 'The field reference in the modal should have the right value'); testUtils.dom.click($modal.find('.o_form_button_cancel')); form.destroy(); }); QUnit.test('Reference field in modal write mode', function (assert) { assert.expect(5); this.data.partner.records[0].p = [2]; this.data.partner.records[1].trululu = 1; this.data.partner.records[1].reference = 'product,41'; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '', archs: { 'partner,false,form': '
', 'partner,false,list': '', }, res_id: 1, }); // current form testUtils.form.clickEdit(form); var $fieldRef = form.$('.o_field_widget.o_field_many2one[name=reference]'); assert.equal($fieldRef.find('option:selected').text(), 'Product', 'The reference field\'s model should be Product'); assert.equal($fieldRef.find('.o_input.ui-autocomplete-input').val(), 'xphone', 'The reference field\'s record should be xphone'); testUtils.dom.click(form.$('.o_data_cell')); // In modal var $modal = $('.modal-lg'); assert.equal($modal.length, 1, 'there should be one modal opened'); var $fieldRefModal = $modal.find('.o_field_widget.o_field_many2one[name=reference]'); assert.equal($fieldRefModal.find('option:selected').text(), 'Product', 'The reference field\'s model should be Product'); assert.equal($fieldRefModal.find('.o_input.ui-autocomplete-input').val(), 'xpad', 'The reference field\'s record should be xpad'); form.destroy(); }); QUnit.test('reference in form view', function (assert) { assert.expect(15); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', archs: { 'product,false,form': '
', }, res_id: 1, mockRPC: function (route, args) { if (args.method === 'get_formview_action') { assert.deepEqual(args.args[0], [37], "should call get_formview_action with correct id"); return $.when({ res_id: 17, type: 'ir.actions.act_window', target: 'current', res_model: 'res.partner' }); } if (args.method === 'get_formview_id') { assert.deepEqual(args.args[0], [37], "should call get_formview_id with correct id"); return $.when(false); } if (args.method === 'name_search') { assert.strictEqual(args.model, 'partner_type', "the name_search should be done on the newly set model"); } if (args.method === 'write') { assert.strictEqual(args.model, 'partner', "should write on the current model"); assert.deepEqual(args.args, [[1], {reference: 'partner_type,12'}], "should write the correct value"); } return this._super(route, args); }, }); testUtils.mock.intercept(form, 'do_action', function (event) { assert.strictEqual(event.data.action.res_id, 17, "should do a do_action with correct parameters"); }); assert.strictEqual(form.$('a.o_form_uri:contains(xphone)').length, 1, "should contain a link"); testUtils.dom.click(form.$('a.o_form_uri')); testUtils.form.clickEdit(form); assert.containsN(form, '.o_field_widget', 2, "should contain two field widgets (selection and many2one)"); assert.containsOnce(form, '.o_field_many2one', "should contain one many2one"); assert.strictEqual(form.$('.o_field_widget select').val(), "product", "widget should contain one select with the model"); assert.strictEqual(form.$('.o_field_widget input').val(), "xphone", "widget should contain one input with the record"); var options = _.map(form.$('.o_field_widget select > option'), function (el) { return $(el).val(); }); assert.deepEqual(options, ['', 'product', 'partner_type', 'partner'], "the options should be correctly set"); testUtils.dom.click(form.$('.o_external_button')); assert.strictEqual($('.modal .modal-title').text().trim(), 'Open: custom label', "dialog title should display the custom string label"); testUtils.dom.click($('.modal .o_form_button_cancel')); testUtils.fields.editSelect(form.$('.o_field_widget select'), 'partner_type'); assert.strictEqual(form.$('.o_field_widget input').val(), "", "many2one value should be reset after model change"); testUtils.fields.many2one.clickOpenDropdown('reference'); testUtils.fields.many2one.clickHighlightedItem('reference'); testUtils.form.clickSave(form); assert.strictEqual(form.$('a.o_form_uri:contains(gold)').length, 1, "should contain a link with the new value"); form.destroy(); }); QUnit.test('default_get and onchange with a reference field', function (assert) { assert.expect(8); this.data.partner.fields.reference.default = 'product,37'; this.data.partner.onchanges = { int_field: function (obj) { if (obj.int_field !== 0) { obj.reference = 'partner_type,' + obj.int_field; } }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', viewOptions: { mode: 'edit', }, mockRPC: function (route, args) { if (args.method === 'name_get') { assert.step(args.model); } return this._super(route, args); }, }); assert.verifySteps(['product'], "the first name_get should have been done"); assert.strictEqual(form.$('.o_field_widget[name="reference"] select').val(), "product", "reference field model should be correctly set"); assert.strictEqual(form.$('.o_field_widget[name="reference"] input').val(), "xphone", "reference field value should be correctly set"); // trigger onchange testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 12); assert.verifySteps(['product', 'partner_type'], "the second name_get should have been done"); assert.strictEqual(form.$('.o_field_widget[name="reference"] select').val(), "partner_type", "reference field model should be correctly set"); assert.strictEqual(form.$('.o_field_widget[name="reference"] input').val(), "gold", "reference field value should be correctly set"); form.destroy(); }); QUnit.test('default_get a reference field in a x2m', function (assert) { assert.expect(1); this.data.partner.fields.turtles.default = [ [0, false, {turtle_ref: 'product,37'}] ]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', viewOptions: { mode: 'edit', }, archs: { 'turtle,false,form': '
', }, }); assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_data_row:first').text(), "xphone", "the default value should be correctly handled"); form.destroy(); }); QUnit.test('widget reference on char field, reset by onchange', function (assert) { assert.expect(4); this.data.partner.records[0].foo = 'product,37'; this.data.partner.onchanges = { int_field: function (obj) { obj.foo = 'product,' + obj.int_field; }, }; var nbNameGet = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, viewOptions: { mode: 'edit', }, mockRPC: function (route, args) { if (args.model === 'product' && args.method === 'name_get') { nbNameGet++; } return this._super(route, args); }, }); assert.strictEqual(nbNameGet, 1, "the first name_get should have been done"); assert.strictEqual(form.$('a[name="foo"]').text(), "xphone", "foo field should be correctly set"); // trigger onchange testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 41); assert.strictEqual(nbNameGet, 2, "the second name_get should have been done"); assert.strictEqual(form.$('a[name="foo"]').text(), "xpad", "foo field should have been updated"); form.destroy(); }); QUnit.test('reference and list navigation', function (assert) { assert.expect(2); var list = createView({ View: ListView, model: 'partner', data: this.data, arch: '', }); // edit first row testUtils.dom.click(list.$('.o_data_row .o_data_cell').first()); assert.strictEqual(list.$('.o_data_row:eq(0) .o_field_widget[name="reference"] input')[0], document.activeElement, 'input of first data row should be selected'); // press TAB to go to next line list.$('.o_data_row:eq(0) input').trigger($.Event('keydown', { which: $.ui.keyCode.TAB, keyCode: $.ui.keyCode.TAB, })); assert.strictEqual(list.$('.o_data_row:eq(1) .o_field_widget[name="reference"] select')[0], document.activeElement, 'select of second data row should be selected'); list.destroy(); }); QUnit.test('one2many with extra field from server not in form', function (assert) { assert.expect(6); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner,false,form': '
' + '' + ''}, mockRPC: function(route, args) { if (route === '/web/dataset/call_kw/partner/write') { args.args[1].p[0][2].datetime = '2018-04-05 12:00:00'; } return this._super.apply(this, arguments); } }); testUtils.form.clickEdit(form); var x2mList = form.$('.o_field_x2many_list[name=p]'); // Add a record in the list testUtils.dom.click(x2mList.find('.o_field_x2many_list_row_add a')); var modal = $('.modal-lg'); var nameInput = modal.find('input.o_input[name=display_name]'); testUtils.fields.editInput(nameInput, 'michelangelo'); // Save the record in the modal (though it is still virtual) testUtils.dom.click(modal.find('.btn-primary').first()); assert.equal(x2mList.find('.o_data_row').length, 1, 'There should be 1 records in the x2m list'); var newlyAdded = x2mList.find('.o_data_row').eq(0); assert.equal(newlyAdded.find('.o_data_cell').first().text(), '', 'The create_date field should be empty'); assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo', 'The display name field should have the right value'); // Save the whole thing testUtils.form.clickSave(form); x2mList = form.$('.o_field_x2many_list[name=p]'); // Redo asserts in RO mode after saving assert.equal(x2mList.find('.o_data_row').length, 1, 'There should be 1 records in the x2m list'); newlyAdded = x2mList.find('.o_data_row').eq(0); assert.equal(newlyAdded.find('.o_data_cell').first().text(), '04/05/2018 12:00:00', 'The create_date field should have the right value'); assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo', 'The display name field should have the right value'); form.destroy(); }); QUnit.test('one2many invisible depends on parent field', function (assert) { assert.expect(4); this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.containsN(form, 'th', 2, "should be 2 columns in the one2many"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input')); testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter')); assert.containsOnce(form, 'th', "should be 1 column when the product_id is set"); form.$('.o_field_many2one[name="product_id"] input').val('').trigger('keyup'); assert.containsN(form, 'th', 2, "should be 2 columns in the one2many when product_id is not set"); testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input')); assert.containsOnce(form, 'th', "should be 1 column after the value change"); form.destroy(); }); QUnit.test('one2many column visiblity depends on onchange of parent field', function (assert) { assert.expect(3); this.data.partner.records[0].p = [2]; this.data.partner.records[0].bar = false; this.data.partner.onchanges.p = function (obj) { // set bar to true when line is added if (obj.p.length > 1 && obj.p[1][2].foo === 'New line') { obj.bar = true; } }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, }); // bar is false so there should be 1 column assert.containsOnce(form, 'th', "should be only 1 column ('foo') in the one2many"); assert.containsOnce(form, '.o_list_view .o_data_row', "should contain one row"); testUtils.form.clickEdit(form); // add a new o2m record testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); form.$('.o_field_one2many input:first').focus(); testUtils.fields.editInput(form.$('.o_field_one2many input:first'), 'New line'); testUtils.dom.click(form.$el); assert.containsN(form, 'th', 2, "should be 2 columns('foo' + 'int_field')"); form.destroy(); }); QUnit.test('one2many column_invisible on view not inline', function (assert) { assert.expect(4); this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner,false,list': '' + '' + '' + '', }, }); assert.containsN(form, 'th', 2, "should be 2 columns in the one2many"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input')); testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter')); assert.containsOnce(form, 'th', "should be 1 column when the product_id is set"); form.$('.o_field_many2one[name="product_id"] input').val('').trigger('keyup'); assert.containsN(form, 'th', 2, "should be 2 columns in the one2many when product_id is not set"); testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input')); assert.containsOnce(form, 'th', "should be 1 column after the value change"); form.destroy(); }); QUnit.module('TabNavigation'); QUnit.test('when Navigating to a many2one with tabs, it receives the focus and adds a new line', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', viewOptions: { mode: 'edit', }, data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$el.find('input[name="qux"]')[0], document.activeElement, "initially, the focus should be on the 'qux' field because it is the first input"); form.$el.find('input[name="qux"]').trigger($.Event('keydown', { which: $.ui.keyCode.TAB, keyCode: $.ui.keyCode.TAB, })); assert.strictEqual(assert.strictEqual(form.$el.find('input[name="turtle_foo"]')[0], document.activeElement, "after tab, the focus should be on the many2one on the first input of the newly added line")); form.destroy(); }); QUnit.test('when Navigating to a many to one with tabs, it places the focus on the first visible field', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', viewOptions: { mode: 'edit', }, data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + ''+ '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$el.find('input[name="qux"]')[0], document.activeElement, "initially, the focus should be on the 'qux' field because it is the first input"); form.$el.find('input[name="qux"]').trigger($.Event('keydown', { which: $.ui.keyCode.TAB, keyCode: $.ui.keyCode.TAB, })); testUtils.dom.click(document.activeElement); assert.strictEqual(assert.strictEqual(form.$el.find('input[name="turtle_foo"]')[0], document.activeElement, "after tab, the focus should be on the many2one")); form.destroy(); }); QUnit.test('when Navigating to a many2one with tabs, not filling any field and hitting tab,' + ' we should not add a first line but navigate to the next control', function (assert) { assert.expect(3); this.data.partner.records[0].turtles = []; var form = createView({ View: FormView, model: 'partner', viewOptions: { mode: 'edit', }, data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$el.find('input[name="qux"]')[0], document.activeElement, "initially, the focus should be on the 'qux' field because it is the first input"); form.$el.find('input[name="qux"]').trigger($.Event('keydown', { which: $.ui.keyCode.TAB, keyCode: $.ui.keyCode.TAB, })); // skips the first field of the one2many $(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB})); // skips the second (and last) field of the one2many $(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB})); assert.strictEqual(assert.strictEqual(form.$el.find('input[name="foo"]')[0], document.activeElement, "after tab, the focus should be on the many2one")); form.destroy(); }); QUnit.test('when Navigating to a many to one with tabs, editing in a popup, the popup should receive the focus then give it back', function (assert) { assert.expect(3); this.data.partner.records[0].turtles = []; var form = createView({ View: FormView, model: 'partner', viewOptions: { mode: 'edit', }, data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'turtle,false,form': '
', }, }); assert.strictEqual(form.$el.find('input[name="qux"]')[0], document.activeElement, "initially, the focus should be on the 'qux' field because it is the first input"); form.$el.find('input[name="qux"]').trigger($.Event('keydown', { which: $.ui.keyCode.TAB, keyCode: $.ui.keyCode.TAB, })); assert.strictEqual($.find('input[name="turtle_foo"]')[0], document.activeElement, "when the one2many received the focus, the popup should open because it automatically adds a new line"); $('input[name="turtle_foo"]').trigger($.Event('keydown', { which: $.ui.keyCode.ESCAPE, keyCode: $.ui.keyCode.ESCAPE, })); assert.strictEqual(form.$el.find('.o_field_x2many_list_row_add a')[0], document.activeElement, "after escape, the focus should be back on the add new line link"); form.destroy(); }); QUnit.test('when creating a new many2one on a x2many then discarding it immediately with ESCAPE, it should not crash', function (assert) { var done = assert.async(); assert.expect(1); this.data.partner.records[0].turtles = []; var form = createView({ View: FormView, model: 'partner', viewOptions: { mode: 'edit', }, data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner,false,form': '
' }, }); // add a new line testUtils.dom.click(form.$el.find('.o_field_x2many_list_row_add>a')); // open the field turtle_trululu (one2many) var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; testUtils.dom.click(form.$el.find('.o_input_dropdown>input')); concurrency.delay(0).then(function () { // click create and edit testUtils.dom.click($('.ui-autocomplete .ui-menu-item a:contains(Create and)').trigger('mouseenter')); // hit escape immediately var escapeKey = $.ui.keyCode.ESCAPE; $(document.activeElement).trigger( $.Event('keydown', {which: escapeKey, keyCode: escapeKey})); assert.ok('did not crash'); relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; form.destroy(); done(); }); }); QUnit.test('navigating through an editable list with custom controls [REQUIRE FOCUS]', function (assert) { assert.expect(5); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', viewOptions: { mode: 'edit', }, }); assert.strictEqual(document.activeElement, form.$('.o_field_widget[name="display_name"]')[0], "first input should be focused by default"); // press tab to navigate to the list form.$('.o_field_widget[name="display_name"]').trigger($.Event('keydown', { which: $.ui.keyCode.TAB, keyCode: $.ui.keyCode.TAB, })); // press ESC to cancel 1st control click (create) form.$('.o_data_cell input').trigger($.Event('keydown', { which: $.ui.keyCode.ESCAPE, keyCode: $.ui.keyCode.ESCAPE, })); assert.strictEqual(document.activeElement, form.$('.o_field_x2many_list_row_add a:first')[0], "first editable list control should now have the focus"); // press right to focus the second control form.$('.o_field_x2many_list_row_add a:first').trigger($.Event('keydown', { which: $.ui.keyCode.RIGHT, keyCode: $.ui.keyCode.RIGHT, })); assert.strictEqual(document.activeElement, form.$('.o_field_x2many_list_row_add a:nth(1)')[0], "second editable list control should now have the focus"); // press left to come back to first control form.$('.o_field_x2many_list_row_add a:nth(1)').trigger($.Event('keydown', { which: $.ui.keyCode.LEFT, keyCode: $.ui.keyCode.LEFT, })); assert.strictEqual(document.activeElement, form.$('.o_field_x2many_list_row_add a:first')[0], "first editable list control should now have the focus"); // press tab to leave the list form.$('.o_field_x2many_list_row_add a:first').trigger($.Event('keydown', { which: $.ui.keyCode.TAB, keyCode: $.ui.keyCode.TAB, })); assert.strictEqual(document.activeElement, form.$('.o_field_widget[name="int_field"]')[0], "last input should now be focused"); form.destroy(); }); }); }); });