summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
diff options
context:
space:
mode:
authorRobot Odoo <xmo+roboto@odoo.com>2018-11-19 15:38:25 +0100
committerGitHub <noreply@github.com>2018-11-19 15:38:25 +0100
commit7c3d5dc067c7c331dcc0b52bafbdfd067087a3c4 (patch)
tree6058f8a893815bf0ed2495649c125523f4def762 /addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
parent8fc246da31aa2a1c770732ae97e0cb4e840f07cf (diff)
parentabf32b8b21394491040e5c11c1fea809c1d7c62c (diff)
[MERGE][REF] *: update js test suite to use helpers
This rev. introduces robust helpers to use in the JS tests suite to interact with DOM and components, and starts using them (almost) everywhere. All the helpers are exposed though testUtils.js. There are 2 kinds of helpers: 1. Assertions ------------- * assert.containsNone, containsN, containsOnce check that the DOM (or a specific part of the DOM) contains a `selector`. It generates a correct error message automatically. ex: assert.notOk(form.$('button').hasClass('btn-primary')); -> assert.doesNotHaveClass(form.$('button'), 'btn-primary'); * assert.isVisible, isNotVisible check that the DOM has an element visible or not. They also check that the element is actually in the DOM (before most tests didn't verify this). * assert.hasClass, doesNotHaveClass, hasAttrVAlue, check specific properties of a DOM element, and also validate that it is applied on a single existing DOM element (before most tests didn't verify this). 2. Utilities ------------ The goal of the utilities is to centralize the definition of many standard components and interactions, ensuring that when we refactor the JS framework, we do not need to change all the tests. Existing mock utilities (addMockEnvironment, intercept, path, patchDate, unpatch and fieldsViewGet) are moved to 'testUtils.mock.*'. Existing DOM utilities are moved to 'testUtils.dom.*'. New dom utilities are created for opendDatePicker, click, clickFirst and clickLast. Helper `click` verifies that there is exactly 1 element visible in the DOM you click on, `clickFirst` and `clickLast` verify that there are more than one element on the DOM. ex: form.$('button').click(); -> testUtils.dom.click(form.$('button')); New Form utilities: (testUtils.form.*) clickEdit, clickSave, clickCreate, clickDiscard, all clicks on the control panel buttons of the form. `reload` reloads the form data. New modal, graph, kanban and pivot utils (testUtils.pivot.*, testUtils.kanban.*, etc.). New fields utils: (testUtils.fields.*) * editInput, editSelect: allow to change the value of a field, using a selector to identify it. They validate that the input exists and trigger the change event automatically. * editAndTrigger: allow to modify a field and trigger specific events after the value change * many2one (testUtils.fields.many2one.*) clickOpenDropdown, clickHighlightedItem, clickItem, searchAndClickItem: use a field name instead of a selector and do all the complex mechanism to open, filter and highlight many2one fields. Joint work with aab, dam, ged, mge, svs and vsc. closes odoo/odoo#28784
Diffstat (limited to 'addons/web/static/tests/fields/relational_fields/field_many2many_tests.js')
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_many2many_tests.js1160
1 files changed, 1160 insertions, 0 deletions
diff --git a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
new file mode 100644
index 00000000000..e6f2c06173f
--- /dev/null
+++ b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
@@ -0,0 +1,1160 @@
+odoo.define('web.field_many_to_many_tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+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" },
+ int_field: { string: "int_field", type: "integer", sortable: true },
+ turtles: { string: "one2many turtle field", type: "one2many", relation: 'turtle', relation_field: 'turtle_trululu' },
+ timmy: { string: "pokemon", type: "many2many", relation: 'partner_type' },
+ color: {
+ type: "selection",
+ selection: [['red', "Red"], ['black', "Black"]],
+ default: 'red',
+ string: "Color",
+ },
+ 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",
+ foo: "yop",
+ int_field: 10,
+ turtles: [2],
+ timmy: [],
+ user_id: 17,
+ reference: 'product,37',
+ }, {
+ id: 2,
+ display_name: "second record",
+ foo: "blip",
+ int_field: 9,
+ timmy: [],
+ user_id: 17,
+ }, {
+ id: 4,
+ display_name: "aaa",
+ }],
+ 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 },
+ partner_ids: { string: "Partner", type: "many2many", relation: 'partner' },
+ },
+ records: [{
+ id: 1,
+ display_name: "leonardo",
+ turtle_foo: "yop",
+ partner_ids: [],
+ }, {
+ id: 2,
+ display_name: "donatello",
+ turtle_foo: "blip",
+ partner_ids: [2, 4],
+ }, {
+ id: 3,
+ display_name: "raphael",
+ turtle_foo: "kawa",
+ partner_ids: [],
+ }],
+ onchanges: {},
+ },
+ user: {
+ fields: {
+ name: { string: "Name", type: "char" },
+ },
+ records: [{
+ id: 17,
+ name: "Aline",
+ }, {
+ id: 19,
+ name: "Christine",
+ }]
+ },
+ };
+ }
+ }, function () {
+ QUnit.module('FieldMany2Many');
+
+ QUnit.test('many2many kanban: edition', function (assert) {
+ assert.expect(32);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.records.push({ id: 15, display_name: "red", color: 6 });
+ this.data.partner_type.records.push({ id: 18, display_name: "yellow", color: 4 });
+ this.data.partner_type.records.push({ id: 21, display_name: "blue", color: 1 });
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<kanban>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times float-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,form': '<form string="Types"><field name="display_name"/></form>',
+ 'partner_type,false,list': '<tree string="Types"><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search string="Types">' +
+ '<field name="name" string="Name"/>' +
+ '</search>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner_type/write') {
+ assert.strictEqual(args.args[1].display_name, "new name", "should write 'new_name'");
+ }
+ if (route === '/web/dataset/call_kw/partner_type/create') {
+ assert.strictEqual(args.args[0].display_name, "A new type", "should create 'A new type'");
+ }
+ 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");
+ // get the created type's id
+ var createdType = _.findWhere(this.data.partner_type.records, {
+ display_name: "A new type"
+ });
+ var ids = _.sortBy([12, 15, 18].concat(createdType.id), _.identity.bind(_));
+ assert.ok(_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), ids),
+ "new value should be " + ids);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // the SelectCreateDialog requests the session, so intercept its custom
+ // event to specify a fake session to prevent it from crashing
+ testUtils.mock.intercept(form, 'get_session', function (event) {
+ event.data.callback({ user_context: {} });
+ });
+
+ assert.ok(!form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_field_many2many .o-kanban-button-new').length,
+ '"Add" button should not be visible in readonly');
+
+ testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_kanban_record:first() span').text(), 'gold',
+ 'display_name of subrecord should be the one in DB');
+ assert.ok(form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should be visible in edit');
+ assert.ok(form.$('.o_field_many2many .o-kanban-button-new').length,
+ '"Add" button should be visible in edit');
+ assert.strictEqual(form.$('.o_field_many2many .o-kanban-button-new').text().trim(), "Add",
+ 'Create button should have "Add" label');
+
+ // edit existing subrecord
+ testUtils.dom.click(form.$('.oe_kanban_global_click:first()'));
+
+ testUtils.fields.editInput($('.modal .o_form_view input'), 'new name');
+ testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_kanban_record:first() span').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // add subrecords
+ // -> single select
+ testUtils.dom.click(form.$('.o_field_many2many .o-kanban-button-new'));
+ assert.ok($('.modal .o_list_view').length, "should have opened a list view in a modal");
+ assert.strictEqual($('.modal .o_list_view tbody .o_list_record_selector').length, 3,
+ "list view should contain 3 records");
+ testUtils.dom.click($('.modal .o_list_view tbody tr:contains(red)'));
+ assert.ok(!$('.modal .o_list_view').length, "should have closed the modal");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 3,
+ 'kanban should now contain 3 records');
+ assert.ok(form.$('.o_kanban_record:contains(red)').length,
+ 'record "red" should be in the kanban');
+
+ // -> multiple select
+ testUtils.dom.click(form.$('.o_field_many2many .o-kanban-button-new'));
+ assert.ok($('.modal .o_select_button').prop('disabled'), "select button should be disabled");
+ assert.strictEqual($('.modal .o_list_view tbody .o_list_record_selector').length, 2,
+ "list view should contain 2 records");
+ testUtils.dom.click($('.modal .o_list_view thead .o_list_record_selector input'));
+ testUtils.dom.click($('.modal .o_select_button'));
+ assert.ok(!$('.modal .o_select_button').prop('disabled'), "select button should be enabled");
+ assert.ok(!$('.modal .o_list_view').length, "should have closed the modal");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 5,
+ 'kanban should now contain 5 records');
+ // -> created record
+ testUtils.dom.click(form.$('.o_field_many2many .o-kanban-button-new'));
+ testUtils.dom.click($('.modal .modal-footer .btn-primary:nth(1)'));
+ assert.ok($('.modal .o_form_view.o_form_editable').length,
+ "should have opened a form view in edit mode, in a modal");
+ testUtils.fields.editInput($('.modal .o_form_view input'), 'A new type');
+ testUtils.dom.click($('.modal:nth(1) footer .btn-primary:first()'));
+ assert.ok(!$('.modal').length, "should have closed both modals");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 6,
+ 'kanban should now contain 6 records');
+ assert.ok(form.$('.o_kanban_record:contains(A new type)').length,
+ 'the newly created type should be in the kanban');
+
+ // delete subrecords
+ testUtils.dom.click(form.$('.o_kanban_record:contains(silver)'));
+ assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1,
+ 'There should be a modal having Remove Button');
+ testUtils.dom.click($('.modal .modal-footer .o_btn_remove'));
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 5,
+ 'should contain 5 records');
+ assert.ok(!form.$('.o_kanban_record:contains(silver)').length,
+ 'the removed record should not be in kanban anymore');
+
+ testUtils.dom.click(form.$('.o_kanban_record:contains(blue) .delete_icon'));
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 4,
+ 'should contain 4 records');
+ assert.ok(!form.$('.o_kanban_record:contains(blue)').length,
+ 'the removed record should not be in kanban anymore');
+
+ // save the record
+ testUtils.form.clickSave(form);;
+ form.destroy();
+ });
+
+ QUnit.test('many2many kanban(editable): properly handle create_text node option', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].timmy = [12];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" options="{\'create_text\': \'Add timmy\'}" mode="kanban">' +
+ '<kanban>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_details">' +
+ '<field name="display_name"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_many2many[name="timmy"] .o-kanban-button-new').text().trim(),
+ "Add timmy", "In M2M Kanban, Add button should have 'Add timmy' label");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many kanban: create action disabled', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<kanban create="0">' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times float-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,list': '<tree><field name="name"/></tree>',
+ 'partner_type,false,search': '<search>' +
+ '<field name="display_name" string="Name"/>' +
+ '</search>',
+ },
+ res_id: 1,
+ session: { user_context: {} },
+ });
+
+ assert.ok(!form.$('.o-kanban-button-new').length,
+ '"Add" button should not be available in readonly');
+
+ testUtils.form.clickEdit(form);
+
+ assert.ok(form.$('.o-kanban-button-new').length,
+ '"Add" button should be available in edit');
+ assert.ok(form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should be visible in edit');
+
+ testUtils.dom.click(form.$('.o-kanban-button-new'));
+ assert.strictEqual($('.modal .modal-footer .btn-primary').length, 1, // only button 'Select'
+ '"Create" button should not be available in the modal');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list (non editable): edition', function (assert) {
+ assert.expect(27);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.records.push({ id: 15, display_name: "bronze", color: 6 });
+ this.data.partner_type.fields.float_field = { string: 'Float', type: 'float' };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree>' +
+ '<field name="display_name"/><field name="float_field"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/></search>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method !== 'load_views') {
+ assert.step(_.last(route.split('/')));
+ }
+ if (args.method === 'write' && args.model === 'partner') {
+ assert.deepEqual(args.args[1].timmy, [
+ [6, false, [12, 15]],
+ ]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.ok(!form.$('.o_list_record_remove').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" should not be visible in readonly');
+
+ testUtils.form.clickEdit(form);
+
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'gold',
+ 'display_name of first subrecord should be the one in DB');
+ assert.ok(form.$('.o_list_record_remove').length,
+ 'delete icon should be visible in edit');
+ assert.ok(form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" should be visible in edit');
+
+ // edit existing subrecord
+ testUtils.dom.click(form.$('.o_list_view tbody tr:first()'));
+
+ testUtils.fields.editInput($('.modal .o_form_view input'), 'new name');
+ testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // add new subrecords
+ testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual($('.modal .o_list_view').length, 1,
+ "a modal should be open");
+ assert.strictEqual($('.modal .o_list_view .o_data_row').length, 1,
+ "the list should contain one row");
+ testUtils.dom.click($('.modal .o_list_view .o_data_row'));
+ assert.strictEqual($('.modal .o_list_view').length, 0,
+ "the modal should be closed");
+ assert.containsN(form, '.o_list_view td.o_list_number', 3,
+ 'should contain 3 subrecords');
+
+ // remove subrecords
+ testUtils.dom.click(form.$('.o_list_record_remove:nth(1)'));
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 subrecords');
+ assert.strictEqual(form.$('.o_list_view .o_data_row td:first').text(), 'new name',
+ 'the updated row still has the correct values');
+
+ // save
+ testUtils.form.clickSave(form);;
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 subrecords');
+ assert.strictEqual(form.$('.o_list_view .o_data_row td:first').text(),
+ 'new name', 'the updated row still has the correct values');
+
+ assert.verifySteps([
+ 'read', // main record
+ 'read', // relational field
+ 'read', // relational record in dialog
+ 'write', // save relational record from dialog
+ 'read', // relational field (updated)
+ 'search_read', // list view in dialog
+ 'read', // relational field (updated)
+ 'write', // save main record
+ 'read', // main record
+ 'read', // relational field
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list (editable): edition', function (assert) {
+ assert.expect(31);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.records.push({ id: 15, display_name: "bronze", color: 6 });
+ this.data.partner_type.fields.float_field = { string: 'Float', type: 'float' };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/><field name="float_field"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/></search>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method !== 'load_views') {
+ assert.step(_.last(route.split('/')));
+ }
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].timmy, [
+ [6, false, [12, 15]],
+ [1, 12, { display_name: 'new name' }],
+ ]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_list_record_remove').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" should not be visible in readonly');
+
+ testUtils.form.clickEdit(form);
+
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'gold',
+ 'display_name of first subrecord should be the one in DB');
+ assert.ok(form.$('.o_list_record_remove').length,
+ 'delete icon should be visible in edit');
+ assert.hasClass(form.$('td.o_list_record_remove button').first(),'fa fa-times',
+ "should have X icons to remove (unlink) records");
+ assert.ok(form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" should not visible in edit');
+
+ // edit existing subrecord
+ testUtils.dom.click(form.$('.o_list_view tbody td:first()'));
+ assert.ok(!$('.modal').length,
+ 'in edit, clicking on a subrecord should not open a dialog');
+ assert.hasClass(form.$('.o_list_view tbody tr:first()'),'o_selected_row',
+ 'first row should be in edition');
+ testUtils.fields.editInput(form.$('.o_list_view input:first()'), 'new name');
+ assert.hasClass(form.$('.o_list_view .o_data_row:first'),'o_selected_row',
+ 'first row should still be in edition');
+ assert.strictEqual(form.$('.o_list_view input[name=display_name]').get(0),
+ document.activeElement, 'edited field should still have the focus');
+ testUtils.dom.click(form.$el);
+ assert.doesNotHaveClass(form.$('.o_list_view tbody tr:first'), 'o_selected_row',
+ 'first row should not be in edition anymore');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
+ 'value of subrecord should have been updated');
+ assert.verifySteps(['read', 'read']);
+
+ // add new subrecords
+ testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual($('.modal .o_list_view').length, 1,
+ "a modal should be open");
+ assert.strictEqual($('.modal .o_list_view .o_data_row').length, 1,
+ "the list should contain one row");
+ testUtils.dom.click($('.modal .o_list_view .o_data_row'));
+ assert.strictEqual($('.modal .o_list_view').length, 0,
+ "the modal should be closed");
+ assert.containsN(form, '.o_list_view td.o_list_number', 3,
+ 'should contain 3 subrecords');
+
+ // remove subrecords
+ testUtils.dom.click(form.$('.o_list_record_remove:nth(1)'));
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 subrecord');
+ assert.strictEqual(form.$('.o_list_view tbody .o_data_row td:first').text(),
+ 'new name', 'the updated row still has the correct values');
+
+ // save
+ testUtils.form.clickSave(form);;
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 subrecords');
+ assert.strictEqual(form.$('.o_list_view .o_data_row td:first').text(),
+ 'new name', 'the updated row still has the correct values');
+
+ assert.verifySteps([
+ 'read', // main record
+ 'read', // relational field
+ 'search_read', // list view in dialog
+ 'read', // relational field (updated)
+ 'write', // save main record
+ 'read', // main record
+ 'read', // relational field
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many: create & delete attributes', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree create="true" delete="true">' +
+ '<field name="color"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, '.o_field_x2many_list_row_add', "should have the 'Add an item' link");
+ assert.containsN(form, '.o_list_record_remove', 2, "should have the 'Add an item' link");
+
+ form.destroy();
+
+ form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree create="false" delete="false">' +
+ '<field name="color"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ testUtils.form.clickEdit(form);
+
+ assert.containsNone(form, '.o_field_x2many_list_row_add', "should not have the 'Add an item' link");
+ assert.containsNone(form, '.o_list_record_remove', "should not have the 'Add an item' link");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list: create action disabled', function (assert) {
+ assert.expect(2);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree create="0">' +
+ '<field name="name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" link should not be available in readonly');
+
+ testUtils.form.clickEdit(form);
+
+ assert.ok(!form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" link should not be available in edit either');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list: list of id as default value', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.turtles.default = [2, 3];
+ this.data.partner.fields.turtles.type = "many2many";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.strictEqual(form.$('td.o_data_cell').text(), "blipkawa",
+ "should have loaded default data");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many checkboxes with default values', function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.turtles.default = [3];
+ this.data.partner.fields.turtles.type = "many2many";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles" widget="many2many_checkboxes">' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0].turtles, [[6, false, [1]]],
+ "correct values should have been sent to create");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.notOk(form.$('.custom-checkbox input').eq(0).prop('checked'),
+ "first checkbox should not be checked");
+ assert.notOk(form.$('.custom-checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+ assert.ok(form.$('.custom-checkbox input').eq(2).prop('checked'),
+ "third checkbox should be checked");
+
+ testUtils.dom.click(form.$('.custom-checkbox input:checked'));
+ testUtils.dom.click(form.$('.custom-checkbox input').first());
+ testUtils.dom.click(form.$('.custom-checkbox input').first());
+ testUtils.dom.click(form.$('.custom-checkbox input').first());
+
+ assert.ok(form.$('.custom-checkbox input').eq(0).prop('checked'),
+ "first checkbox should be checked");
+ assert.notOk(form.$('.custom-checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+ assert.notOk(form.$('.custom-checkbox input').eq(2).prop('checked'),
+ "third checkbox should not be checked");
+
+ testUtils.form.clickSave(form);;
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many checkboxes with default values', function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.turtles.default = [3];
+ this.data.partner.fields.turtles.type = "many2many";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles" widget="many2many_checkboxes">' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0].turtles, [[6, false, [1]]],
+ "correct values should have been sent to create");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.notOk(form.$('.custom-checkbox input').eq(0).prop('checked'),
+ "first checkbox should not be checked");
+ assert.notOk(form.$('.custom-checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+ assert.ok(form.$('.custom-checkbox input').eq(2).prop('checked'),
+ "third checkbox should be checked");
+
+ testUtils.dom.click(form.$('.custom-checkbox input:checked'));
+ testUtils.dom.click(form.$('.custom-checkbox input').first());
+ testUtils.dom.click(form.$('.custom-checkbox input').first());
+ testUtils.dom.click(form.$('.custom-checkbox input').first());
+
+ assert.ok(form.$('.custom-checkbox input').eq(0).prop('checked'),
+ "first checkbox should be checked");
+ assert.notOk(form.$('.custom-checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+ assert.notOk(form.$('.custom-checkbox input').eq(2).prop('checked'),
+ "third checkbox should not be checked");
+
+ testUtils.form.clickSave(form);;
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list with x2many: add a record', function (assert) {
+ assert.expect(18);
+
+ this.data.partner_type.fields.m2m = {
+ string: "M2M", type: "many2many", relation: 'turtle',
+ };
+ this.data.partner_type.records[0].m2m = [1, 2];
+ this.data.partner_type.records[1].m2m = [2, 3];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree>' +
+ '<field name="display_name"/>' +
+ '<field name="m2m" widget="many2many_tags"/>' +
+ '</tree>',
+ 'partner_type,false,search': '<search>' +
+ '<field name="display_name" string="Name"/>' +
+ '</search>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method !== 'load_views') {
+ assert.step(_.last(route.split('/')) + ' on ' + args.model);
+ }
+ if (args.model === 'turtle') {
+ assert.step(args.args[0]); // the read ids
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ testUtils.dom.click($('.modal .o_data_row:first'));
+
+ assert.containsOnce(form, '.o_data_row',
+ "the record should have been added to the relation");
+ assert.strictEqual(form.$('.o_data_row:first .o_badge_text').text(), 'leonardodonatello',
+ "inner m2m should have been fetched and correctly displayed");
+
+ testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ testUtils.dom.click($('.modal .o_data_row:first'));
+
+ assert.containsN(form, '.o_data_row', 2,
+ "the second record should have been added to the relation");
+ assert.strictEqual(form.$('.o_data_row:nth(1) .o_badge_text').text(), 'donatelloraphael',
+ "inner m2m should have been fetched and correctly displayed");
+
+ assert.verifySteps([
+ 'read on partner',
+ 'search_read on partner_type',
+ 'read on turtle',
+ [1, 2, 3],
+ 'read on partner_type',
+ 'read on turtle',
+ [1, 2],
+ 'search_read on partner_type',
+ 'read on turtle',
+ [2, 3],
+ 'read on partner_type',
+ 'read on turtle',
+ [2, 3],
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many with a domain', function (assert) {
+ // The domain specified on the field should not be replaced by the potential
+ // domain the user writes in the dialog, they should rather be concatenated
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" domain="[[\'display_name\', \'=\', \'gold\']]"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>',
+ 'partner_type,false,search': '<search>' +
+ '<field name="display_name" string="Name"/>' +
+ '</search>',
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "should contain only one row (gold)");
+
+ $('.modal .o_searchview_input').trigger({ type: 'keypress', which: 115 }); // s
+ $('.modal .o_searchview_input').trigger({ type: 'keydown', which: 13 }); // enter
+
+ assert.strictEqual($('.modal .o_data_row').length, 0, "should contain no row");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list with onchange and edition of a record', function (assert) {
+ assert.expect(8);
+
+ this.data.partner.fields.turtles.type = "many2many";
+ this.data.partner.onchanges.turtles = function () { };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'turtle,false,form': '<form string="Turtle Power"><field name="turtle_bar"/></form>',
+ },
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ testUtils.form.clickEdit(form);
+ testUtils.dom.click(form.$('td.o_data_cell:first'));
+
+ testUtils.dom.click($('.modal-body input[type="checkbox"]'));
+ testUtils.dom.click($('.modal .modal-footer .btn-primary').first());
+
+ // there is nothing left to save -> should not do a 'write' RPC
+ testUtils.form.clickSave(form);;
+
+ assert.verifySteps([
+ 'read', // read initial record (on partner)
+ 'read', // read many2many turtles
+ 'load_views', // load arch of turtles form view
+ 'read', // read missing field when opening record in modal form view
+ 'write', // when saving the modal
+ 'onchange', // onchange should be triggered on partner
+ 'read', // reload many2many
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange with 40+ commands for a many2many', function (assert) {
+ // this test ensures that the basic_model correctly handles more LINK_TO
+ // commands than the limit of the dataPoint (40 for x2many kanban)
+ assert.expect(23);
+
+ // create a lot of partner_types that will be linked by the onchange
+ var commands = [[5]];
+ for (var i = 0; i < 45; i++) {
+ var id = 100 + i;
+ this.data.partner_type.records.push({ id: id, display_name: "type " + id });
+ commands.push([4, id]);
+ }
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.timmy = commands;
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="timmy">' +
+ '<kanban>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div><t t-esc="record.display_name.value"/></div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].timmy[0][0], 6,
+ "should send a command 6");
+ assert.strictEqual(args.args[1].timmy[0][2].length, 45,
+ "should replace with 45 ids");
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.verifySteps(['read']);
+
+ testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange');
+
+ assert.verifySteps(['read', 'onchange', 'read']);
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records displayed on page 1');
+
+ testUtils.dom.click(form.$('.o_pager_next'));
+ assert.verifySteps(['read', 'onchange', 'read', 'read']);
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '41-45 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
+ 'there should be 5 records displayed on page 2');
+
+ testUtils.form.clickSave(form);;
+
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records displayed on page 1');
+
+ testUtils.dom.click(form.$('.o_pager_next'));
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '41-45 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
+ 'there should be 5 records displayed on page 2');
+
+ testUtils.dom.click(form.$('.o_pager_next'));
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records displayed on page 1');
+
+ form.destroy();
+ });
+
+ QUnit.test('default_get, onchange, onchange on m2m', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges.int_field = function (obj) {
+ if (obj.int_field === 2) {
+ assert.deepEqual(obj.timmy, [
+ [6, false, [12]],
+ [1, 12, { display_name: 'gold' }]
+ ]);
+ }
+ obj.timmy = [
+ [5],
+ [1, 12, { display_name: 'gold' }]
+ ];
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="timmy">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '<field name="int_field"/>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 2);
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_tags', function (assert) {
+ assert.expect(1);
+ this.data.turtle.records[0].partner_ids = [2];
+
+ var form = createView({
+ View: FormView,
+ model: 'turtle',
+ data: this.data,
+ arch: '<form string="Turtles">' +
+ '<sheet>' +
+ '<field name="display_name"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</sheet>' +
+ '</form>',
+ 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('many2many list add *many* records, remove, re-add', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.timmy.domain = [['color', '=', 2]];
+ this.data.partner.fields.timmy.onChange = true;
+ this.data.partner_type.fields.product_ids = { string: "Product", type: "many2many", relation: 'product' };
+
+ for (var i = 0; i < 50; i++) {
+ var new_record_partner_type = { id: 100 + i, display_name: "batch" + i, color: 2 };
+ this.data.partner_type.records.push(new_record_partner_type);
+ }
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" widget="many2many">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '<field name="product_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/><field name="color"/></search>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id");
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ // First round: add 51 records in batch
+ testUtils.dom.click(form.$buttons.find('.btn.btn-primary.o_form_button_edit'));
+ testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ var $modal = $('.modal-lg');
+
+ assert.equal($modal.length, 1,
+ 'There should be one modal');
+
+ testUtils.dom.click($modal.find('thead input[type=checkbox]'));
+
+ testUtils.dom.click($modal.find('.btn.btn-primary.o_select_button'));
+
+ var m2m_records = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_data_cell:not(.o_many2many_tags_cell)');
+ assert.equal(m2m_records.length, 51,
+ 'We should have added all the records present in the search view to the m2m field'); // the 50 in batch + 'gold'
+
+ testUtils.dom.click(form.$buttons.find('.btn.btn-primary.o_form_button_save'));
+
+ // Secound round: remove one record
+ testUtils.dom.click(form.$buttons.find('.btn.btn-primary.o_form_button_edit'));
+ var trash_buttons = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_list_record_remove');
+
+ testUtils.dom.click(trash_buttons.first());
+
+ var pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit');
+ assert.equal(pager_limit.text(), '50',
+ 'We should have 50 records in the m2m field');
+
+ // Third round: re-add 1 records
+ testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ $modal = $('.modal-lg');
+
+ assert.equal($modal.length, 1,
+ 'There should be one modal');
+
+ testUtils.dom.click($modal.find('thead input[type=checkbox]'));
+
+ testUtils.dom.click($modal.find('.btn.btn-primary.o_select_button'));
+
+ pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit');
+ assert.equal(pager_limit.text(), '51',
+ 'We should have 51 records in the m2m field');
+
+ form.destroy();
+ });
+ });
+});
+});