diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-07-07 14:30:30 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-07-07 14:30:30 +0200 |
| commit | bf51fe55a149fb642a410f579dc4b6dcbd794eac (patch) | |
| tree | f4f208f06094bcef6541fd1fc034e0265de4ad21 /addons/web/static/tests | |
| parent | bf32940142beb879211b7034bb77a7845c9d0555 (diff) | |
| parent | 0f2b2c4e2b99df8d68456016a4bea86e26c1c80e (diff) | |
[MERGE] forward port branch saas-16 up to 0f2b2c4e2b9
Diffstat (limited to 'addons/web/static/tests')
| -rw-r--r-- | addons/web/static/tests/fields/basic_fields_tests.js | 101 | ||||
| -rw-r--r-- | addons/web/static/tests/fields/field_utils_tests.js | 23 | ||||
| -rw-r--r-- | addons/web/static/tests/fields/relational_fields_tests.js | 830 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/mock_server.js | 5 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils.js | 9 | ||||
| -rw-r--r-- | addons/web/static/tests/views/basic_model_tests.js | 190 | ||||
| -rw-r--r-- | addons/web/static/tests/views/calendar_tests.js | 102 | ||||
| -rw-r--r-- | addons/web/static/tests/views/form_tests.js | 61 | ||||
| -rw-r--r-- | addons/web/static/tests/views/kanban_tests.js | 2 | ||||
| -rw-r--r-- | addons/web/static/tests/views/list_tests.js | 70 |
10 files changed, 1363 insertions, 30 deletions
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index c492537c63f..c732fe35ce6 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1043,6 +1043,40 @@ QUnit.module('basic_fields', { _t.database.multi_lang = multiLang; }); + QUnit.test('go to next line (and not the next row) when pressing enter', function (assert) { + assert.expect(4); + + this.data.partner.fields.foo.type = 'text'; + var list = createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<list editable="top">' + + '<field name="int_field"/>' + + '<field name="foo"/>' + + '<field name="qux"/>' + + '</list>', + }); + + list.$('tbody tr:first .o_list_text').click(); + var $textarea = list.$('textarea.o_field_text'); + assert.strictEqual($textarea.length, 1, "should have a text area"); + assert.strictEqual($textarea.val(), 'yop', 'should still be "yop" in edit'); + + assert.strictEqual(list.$('textarea').get(0), document.activeElement, + "text area should have the focus"); + + // click on enter + list.$('textarea') + .trigger({type: "keydown", which: $.ui.keyCode.ENTER}) + .trigger({type: "keyup", which: $.ui.keyCode.ENTER}); + + assert.strictEqual(list.$('textarea').first().get(0), document.activeElement, + "text area should still have the focus"); + + list.destroy(); + }); + QUnit.module('FieldBinary'); QUnit.test('binary fields are correctly rendered', function (assert) { @@ -1973,6 +2007,63 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test('monetary field with currency set by an onchange', function (assert) { + // this test ensures that the monetary field can be re-rendered with and + // without currency (which can happen as the currency can be set by an + // onchange) + assert.expect(8); + + this.data.partner.onchanges = { + int_field: function (obj) { + obj.currency_id = obj.int_field ? 2 : null; + }, + }; + + var list = createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="top">' + + '<field name="int_field"/>' + + '<field name="qux" widget="monetary"/>' + + '<field name="currency_id" invisible="1"/>' + + '</tree>', + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + list.$buttons.find('.o_list_button_add').click(); + assert.strictEqual(list.$('div.o_field_widget[name=qux] input').length, 1, + "monetary field should have been rendered correctly (without currency)"); + assert.strictEqual(list.$('.o_field_widget[name=qux] span').length, 0, + "monetary field should have been rendered correctly (without currency)"); + + // set a value for int_field -> should set the currency and re-render qux + list.$('.o_field_widget[name=int_field]').click().val('7').trigger('input'); + assert.strictEqual(list.$('div.o_field_widget[name=qux] input').length, 1, + "monetary field should have been re-rendered correctly (with currency)"); + assert.strictEqual(list.$('.o_field_widget[name=qux] span:contains(€)').length, 1, + "monetary field should have been re-rendered correctly (with currency)"); + var $quxInput = list.$('.o_field_widget[name=qux] input'); + $quxInput.click(); // check that the field is focusable + assert.strictEqual(document.activeElement, $quxInput[0], + "focus should be on the qux field's input"); + + // unset the value of int_field -> should unset the currency and re-render qux + list.$('.o_field_widget[name=int_field]').click().val('0').trigger('input'); + $quxInput = list.$('div.o_field_widget[name=qux] input'); + assert.strictEqual($quxInput.length, 1, + "monetary field should have been re-rendered correctly (without currency)"); + assert.strictEqual(list.$('.o_field_widget[name=qux] span').length, 0, + "monetary field should have been re-rendered correctly (without currency)"); + $quxInput.click(); // check that the field is still focusable + assert.strictEqual(document.activeElement, $quxInput[0], + "focus should be on the qux field's input"); + + list.destroy(); + }); + QUnit.module('FieldInteger'); QUnit.test('integer field when unset', function (assert) { @@ -2024,7 +2115,7 @@ QUnit.module('basic_fields', { }); QUnit.test('integer field in form view with virtual id', function (assert) { - assert.expect(2); + assert.expect(1); var params = { View: FormView, model: 'partner', @@ -2036,15 +2127,7 @@ QUnit.module('basic_fields', { var form = createView(params); assert.strictEqual(form.$('.o_field_widget').text(), "2-20170808020000", "Should display virtual id"); - form.destroy(); - params.res_id = this.data.partner.records[1].id = "2.65-20170808020000"; - try { - form = createView(params); - } catch (error) { - assert.strictEqual(error.message, '"2.65-20170808020000" is not an integer or a virtual id', - "Should throw an exception because it's a wrong value"); - } form.destroy(); }); diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js index e00d45ef91f..a49197f6e65 100644 --- a/addons/web/static/tests/fields/field_utils_tests.js +++ b/addons/web/static/tests/fields/field_utils_tests.js @@ -2,6 +2,7 @@ odoo.define('web.field_utils_tests', function (require) { "use strict"; var core = require('web.core'); +var session = require('web.session'); var fieldUtils = require('web.field_utils'); QUnit.module('fields', {}, function () { @@ -122,7 +123,20 @@ QUnit.test('parse float', function(assert) { }); QUnit.test('parse monetary', function(assert) { - assert.expect(6); + assert.expect(11); + var originalCurrencies = session.currencies; + session.currencies = { + 1: { + digits: [69, 2], + position: "after", + symbol: "€" + }, + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + }; assert.strictEqual(fieldUtils.parse.monetary(""), 0); assert.strictEqual(fieldUtils.parse.monetary("0"), 0); @@ -130,6 +144,13 @@ QUnit.test('parse monetary', function(assert) { assert.strictEqual(fieldUtils.parse.monetary("-100.00"), -100); assert.strictEqual(fieldUtils.parse.monetary("1,000.00"), 1000); assert.strictEqual(fieldUtils.parse.monetary("1,000,000.00"), 1000000); + assert.strictEqual(fieldUtils.parse.monetary("$ 125.00", {}, {currency_id: 3}), 125); + assert.strictEqual(fieldUtils.parse.monetary("1,000.00 €", {}, {currency_id: 1}), 1000); + assert.throws(function() {fieldUtils.parse.monetary("$ 12.00", {}, {currency_id: 3})}, /is not a correct/); + assert.throws(function() {fieldUtils.parse.monetary("$ 12.00", {}, {currency_id: 1})}, /is not a correct/); + assert.throws(function() {fieldUtils.parse.monetary("$ 12.00 34", {}, {currency_id: 3})}, /is not a correct/); + + session.currencies = originalCurrencies; }); }); }); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 3bf541b097d..d4d3a6307f9 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -34,6 +34,8 @@ QUnit.module('relational_fields', { 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, @@ -47,6 +49,7 @@ QUnit.module('relational_fields', { timmy: [], trululu: 4, user_id: 17, + reference: 'product,37', }, { id: 2, display_name: "second record", @@ -963,7 +966,9 @@ QUnit.module('relational_fields', { }); QUnit.test('domain and context are correctly used when doing a name_search in a m2o', function (assert) { - assert.expect(2); + assert.expect(4); + + this.data.partner.records[0].timmy = [12]; var form = createView({ View: FormView, @@ -975,11 +980,13 @@ QUnit.module('relational_fields', { 'domain="[[\'foo\', \'=\', \'bar\'], [\'foo\', \'=\', foo]]" ' + 'context="{\'hello\': \'world\', \'test\': foo}"/>' + '<field name="foo"/>' + + '<field name="trululu" context="{\'timmy\': timmy}" domain="[[\'id\', \'in\', timmy]]"/>' + + '<field name="timmy" widget="many2many_tags" invisible="1"/>' + '</form>', res_id: 1, session: {user_context: {hey: "ho"}}, mockRPC: function (route, args) { - if (route === '/web/dataset/call_kw/product/name_search') { + if (args.method === 'name_search' && args.model === 'product') { assert.deepEqual( args.kwargs.args, [['foo', '=', 'bar'], ['foo', '=', 'yop']], @@ -990,12 +997,20 @@ QUnit.module('relational_fields', { 'the field attr context should have been used for the ' + 'RPC (evaluated and merged with the session one)'); } + if (args.method === 'name_search' && args.model === 'partner') { + assert.deepEqual(args.kwargs.args, [['id', 'in', [12]]], + 'the field attr domain should have been used for the RPC (and evaluated)'); + assert.deepEqual(args.kwargs.context, {hey: 'ho', timmy: [[6, false, [12]]]}, + 'the field attr context should have been used for the RPC (and evaluated)'); + } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); - form.$('input').first().click(); + form.$('.o_field_widget[name=product_id] input').click(); + + form.$('.o_field_widget[name=trululu] input').click(); form.destroy(); }); @@ -1235,6 +1250,56 @@ QUnit.module('relational_fields', { }); }); + QUnit.test('many2one: domain updated by an onchange', function (assert) { + assert.expect(2); + + this.data.partner.onchanges = { + int_field: function () {}, + }; + + var domain = []; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="int_field"/>' + + '<field name="trululu"/>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'onchange') { + domain = [['id', 'in', [10]]]; + return $.when({ + domain: { + trululu: domain, + unexisting_field: domain, + } + }); + } + if (args.method === 'name_search') { + assert.deepEqual(args.kwargs.args, domain, + "sent domain should be correct"); + } + return this._super(route, args); + }, + viewOptions: { + mode: 'edit', + }, + }); + + // trigger a name_search (domain should be []) + form.$('.o_field_widget[name=trululu] input').click(); + // close the dropdown + form.$('.o_field_widget[name=trululu] input').click(); + // trigger an onchange that will update the domain + form.$('.o_field_widget[name=int_field]').val(2).trigger('input'); + // trigger a name_search (domain should be [['id', 'in', [10]]]) + form.$('.o_field_widget[name=trululu] input').click(); + + form.destroy(); + }); + QUnit.module('FieldOne2Many'); QUnit.test('one2many basic properties', function (assert) { @@ -1522,6 +1587,178 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('edition of one2many field with pager', function (assert) { + assert.expect(31); + + var ids = []; + for (var i = 0; i < 45; i++) { + var id = 10 + i; + ids.push(id); + this.data.partner.records.push({ + id: id, + display_name: "relational record " + id, + }); + } + this.data.partner.records[0].p = ids; + + var saveCount = 0; + var checkRead = false; + var readIDs; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="p">' + + '<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 pull-right delete_icon"/>' + + '<span><t t-esc="record.display_name.value"/></span>' + + '</div>' + + '</t>' + + '</templates>' + + '</kanban>' + + '</field>' + + '</form>', + archs: { + 'partner,false,form': '<form><field name="display_name"/></form>', + }, + mockRPC: function (route, args) { + if (args.method === 'read' && checkRead) { + readIDs = args.args[0]; + checkRead = false; + } + if (args.method === 'write') { + saveCount++; + var nbCommands = args.args[1].p.length; + var nbLinkCommands = _.filter(args.args[1].p, function (command) { + return command[0] === 4; + }).length; + switch(saveCount) { + case 1: + assert.strictEqual(nbCommands, 46, + "should send 46 commands (one for each record)"); + assert.strictEqual(nbLinkCommands, 45, + "should send a LINK_TO command for each existing record"); + assert.deepEqual(args.args[1].p[45], [0, false, { + display_name: 'new record', + }], "should sent a CREATE command for the new record"); + break; + case 2: + assert.strictEqual(nbCommands, 46, + "should send 46 commands"); + assert.strictEqual(nbLinkCommands, 45, + "should send a LINK_TO command for each existing record"); + assert.deepEqual(args.args[1].p[45], [2, 10, false], + "should sent a DELETE command for the deleted record"); + break; + case 3: + assert.strictEqual(nbCommands, 47, + "should send 47 commands"); + assert.strictEqual(nbLinkCommands, 43, + "should send a LINK_TO command for each existing record"); + assert.deepEqual(args.args[1].p[43], + [0, false, {display_name: 'new record page 1'}], + "should sent correct CREATE command"); + assert.deepEqual(args.args[1].p[44], + [0, false, {display_name: 'new record page 2'}], + "should sent correct CREATE command"); + assert.deepEqual(args.args[1].p[45], + [2, 11, false], + "should sent correct DELETE command"); + assert.deepEqual(args.args[1].p[46], + [2, 52, false], + "should sent correct DELETE command"); + break; + } + } + return this._super.apply(this, arguments); + }, + res_id: 1, + }); + + assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, + 'there should be 40 records on page 1'); + assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), + '1-40 / 45', "pager range should be correct"); + + // add a record on page one + checkRead = true; + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o-kanban-button-new').click(); + $('.modal input').val('new record').trigger('input'); + $('.modal .modal-footer .btn-primary:first').click(); // save and close + // checks + assert.strictEqual(readIDs, undefined, "should not have read any record"); + assert.strictEqual(form.$('span:contains(new record)').length, 0, + "new record should be on page 2"); + assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, + 'there should be 40 records on page 1'); + assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), + '1-40 / 46', "pager range should be correct"); + assert.strictEqual(form.$('.o_kanban_record:first span:contains(new record)').length, + 0, 'new record should not be on page 1'); + // save + form.$buttons.find('.o_form_button_save').click(); + + // delete a record on page one + checkRead = true; + form.$buttons.find('.o_form_button_edit').click(); + assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 10)').length, + 1, 'first record should be the one with id 10 (next checks rely on that)'); + form.$('.delete_icon:first').click(); + // checks + assert.deepEqual(readIDs, [50], + "should have read a record (to display 40 records on page 1)"); + assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, + 'there should be 40 records on page 1'); + assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), + '1-40 / 45', "pager range should be correct"); + // save + form.$buttons.find('.o_form_button_save').click(); + + // add and delete records in both pages + form.$buttons.find('.o_form_button_edit').click(); + checkRead = true; + readIDs = undefined; + // add and delete a record in page 1 + form.$('.o-kanban-button-new').click(); + $('.modal input').val('new record page 1').trigger('input'); + $('.modal .modal-footer .btn-primary:first').click(); // save and close + assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 11)').length, + 1, 'first record should be the one with id 11 (next checks rely on that)'); + form.$('.delete_icon:first').click(); + assert.deepEqual(readIDs, [51], + "should have read a record (to display 40 records on page 1)"); + // add and delete a record in page 2 + form.$('.o_x2m_control_panel .o_pager_next').click(); + assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 52)').length, + 1, 'first record should be the one with id 52 (next checks rely on that)'); + checkRead = true; + readIDs = undefined; + form.$('.delete_icon:first').click(); + form.$('.o-kanban-button-new').click(); + $('.modal input').val('new record page 2').trigger('input'); + $('.modal .modal-footer .btn-primary:first').click(); // save and close + assert.strictEqual(readIDs, undefined, "should not have read any record"); + // checks + assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5, + 'there should be 5 records on page 2'); + assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), + '41-45 / 45', "pager range should be correct"); + assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 1)').length, + 1, 'new records should be on page 2'); + assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 2)').length, + 1, 'new records should be on page 2'); + // save + form.$buttons.find('.o_form_button_save').click(); + + form.destroy(); + }); + QUnit.test('sorting one2many fields', function (assert) { assert.expect(4); @@ -2357,6 +2594,45 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editable o2m with onchange and required field: delete an invalid line', function (assert) { + assert.expect(5); + + this.data.partner.onchanges = { + turtles: function () {}, + }; + this.data.partner.records[0].turtles = [1]; + this.data.turtle.records[0].product_id = 37; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="top">' + + '<field name="product_id"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + viewOptions: { + mode: 'edit', + }, + }); + + form.$('.o_data_cell:first').click(); + form.$('.o_field_widget[name="product_id"] input').val('').trigger('keyup'); + assert.verifySteps(['read', 'read'], 'no onchange should be done as line is invalid'); + form.$('.o_list_record_delete').click(); + assert.verifySteps(['read', 'read', 'onchange'], 'onchange should have been done'); + + form.destroy(); + }); + QUnit.test('onchange in a one2many', function (assert) { assert.expect(1); @@ -2515,6 +2791,75 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('one2many and onchange (with command DELETE_ALL)', function (assert) { + assert.expect(5); + + this.data.partner.onchanges = { + foo: function (obj) { + obj.p = [[5]]; + }, + p: function () {}, // dummy onchange on the o2m to execute _isX2ManyValid() + }; + this.data.partner.records[0].p = [2]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="foo"/>' + + '<field name="p">' + + '<tree editable="bottom">' + + '<field name="display_name"/>' + + '</tree>' + + '</field>' + + '</form>', + mockRPC: function (method, args) { + if (args.method === 'write') { + assert.deepEqual(args.args[1].p, [ + [0, false, {display_name: 'z'}], + [2, 2, false], + ], "correct commands should be sent"); + } + return this._super.apply(this, arguments); + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual(form.$('.o_data_row').length, 1, + "o2m should contain one row"); + + // empty o2m by triggering the onchange + form.$('.o_field_widget[name=foo]').val('trigger onchange').trigger('input'); + + assert.strictEqual(form.$('.o_data_row').length, 0, + "rows of the o2m should have been deleted"); + + // add two new subrecords + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_widget[name=display_name]').val('x').trigger('input'); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_widget[name=display_name]').val('y').trigger('input'); + + assert.strictEqual(form.$('.o_data_row').length, 2, + "o2m should contain two rows"); + + // empty o2m by triggering the onchange + form.$('.o_field_widget[name=foo]').val('trigger onchange again').trigger('input'); + + assert.strictEqual(form.$('.o_data_row').length, 0, + "rows of the o2m should have been deleted"); + + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_widget[name=display_name]').val('z').trigger('input'); + + form.$buttons.find('.o_form_button_save').click(); + form.destroy(); + }); + QUnit.test('one2many list (editable): readonly domain is evaluated', function (assert) { assert.expect(2); @@ -2782,6 +3127,41 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('value of invisible x2many fields is correctly evaluated in context', function (assert) { + assert.expect(1); + + this.data.partner.records[0].timmy = [12]; + this.data.partner.records[0].p = [2, 3]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form string="Partners">' + + '<field name="product_id" context="{\'p\': p, \'timmy\': timmy}"/>' + + '<field name="p" invisible="1"/>' + + '<field name="timmy" invisible="1"/>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'name_search') { + assert.deepEqual( + args.kwargs.context, { + p: [[4, 2, false], [4, 3, false]], + timmy: [[6, false, [12]]], + }, 'values of x2manys should have been correctly evaluated in context'); + } + return this._super.apply(this, arguments); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_widget[name=product_id] input').click(); + + form.destroy(); + }); + QUnit.test('one2many list, editable, with many2one and with context with parent key', function (assert) { assert.expect(1); @@ -4328,6 +4708,93 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editable list: contexts are correctly sent', function (assert) { + assert.expect(5); + + this.data.partner.records[0].timmy = [12]; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="foo"/>' + + '<field name="timmy" context="{\'key\': parent.foo}">' + + '<tree editable="top">' + + '<field name="display_name"/>' + + '</tree>' + + '</field>' + + '</form>', + mockRPC: function (route, args) { + if (args.method === 'read' && args.model === 'partner') { + assert.deepEqual(args.kwargs.context, { + active_field: 2, + bin_size: true, + someKey: 'some value', + }, "sent context should be correct"); + } + if (args.method === 'read' && args.model === 'partner_type') { + assert.deepEqual(args.kwargs.context, { + key: 'yop', + someKey: 'some value', + }, "sent context should be correct"); + } + if (args.method === 'write') { + assert.deepEqual(args.kwargs.context, { + active_field: 2, + someKey: 'some value', + }, "sent context should be correct"); + } + return this._super.apply(this, arguments); + }, + session: { + user_context: {someKey: 'some value'}, + }, + viewOptions: { + mode: 'edit', + context: {active_field: 2}, + }, + res_id: 1, + }); + + form.$('.o_data_cell:first').click(); + form.$('.o_field_widget[name=display_name]').val('abc').trigger('input'); + form.$buttons.find('.o_form_button_save').click(); + + form.destroy(); + }); + + QUnit.test('resetting invisible one2manys', function (assert) { + assert.expect(3); + + this.data.partner.records[0].turtles = []; + this.data.partner.onchanges.foo = function (obj) { + obj.turtles = [[5], [4, 1]]; + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="foo"/>' + + '<field name="turtles" invisible="1"/>' + + '</form>', + viewOptions: { + mode: 'edit', + }, + res_id: 1, + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + }); + + form.$('input[name="foo"]').val('abcd').trigger('input'); + assert.verifySteps(['read', 'onchange']); + + form.destroy(); + }); + QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { @@ -4987,6 +5454,96 @@ QUnit.module('relational_fields', { 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']); + + form.$('.o_field_widget[name=foo]').val('trigger onchange').trigger('input'); + + 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'); + + form.$('.o_pager_next').click(); + 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'); + + form.$buttons.find('.o_form_button_save').click(); + + 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.$('.o_pager_next').click(); + 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'); + + form.$('.o_pager_next').click(); // back to page 1 + 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.module('FieldStatus'); QUnit.test('static statusbar widget on many2one field', function (assert) { @@ -5380,10 +5937,117 @@ QUnit.module('relational_fields', { 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: '<form string="Partners">' + + '<field name="trululu" widget="selection"/>' + + '</form>', + 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', + }, + }); + + form.$('select').val("false").trigger('change'); + form.$buttons.find('.o_form_button_save').click(); + + 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: '<form>' + + '<field name="int_field"/>' + + '<field name="trululu" widget="selection"/>' + + '</form>', + 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.strictEqual(form.$('.o_field_widget[name=trululu] option').length, 4, + "should be 4 options in the selection"); + + // trigger an onchange that will update the domain + form.$('.o_field_widget[name=int_field]').val(2).trigger('input'); + + assert.strictEqual(form.$('.o_field_widget[name=trululu] option').length, 1, + "should be 1 option in the selection"); + + form.destroy(); + }); + QUnit.module('FieldMany2ManyTags'); - QUnit.test('fieldmany2many tags: rendering and edition', function (assert) { - assert.expect(17); + QUnit.test('fieldmany2many tags without color', function (assert) { + assert.expect(4); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="timmy" widget="many2many_tags"/>' + + '</form>', + 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"); + } + return this._super.apply(this, arguments); + } + }); + var $input = form.$('.o_field_many2manytags input'); + $input.click(); // opens the dropdown + assert.strictEqual($input.autocomplete('widget').find('li').length, 3, + "autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')"); + $input.autocomplete('widget').find('li:first()').click(); // adds a tag + assert.strictEqual(form.$('.o_field_many2manytags > span').length, 1, + "should contain 1 tag"); + assert.ok(form.$('.o_field_many2manytags > span:contains("gold")').length, + "should contain newly added tag 'gold'"); + form.destroy(); + }); + + QUnit.test('fieldmany2many tags with color: rendering and edition', function (assert) { + assert.expect(20); this.data.partner.records[0].timmy = [12, 14]; this.data.partner_type.records.push({id: 13, display_name: "red", color: 8}); @@ -5392,7 +6056,7 @@ QUnit.module('relational_fields', { model: 'partner', data: this.data, arch:'<form string="Partners">' + - '<field name="timmy" widget="many2many_tags" options="{\'no_create_edit\': True}"/>' + + '<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\', \'no_create_edit\': True}"/>' + '</form>', res_id: 1, mockRPC: function (route, args) { @@ -5403,6 +6067,9 @@ QUnit.module('relational_fields', { 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); }, }); @@ -5548,7 +6215,7 @@ QUnit.module('relational_fields', { model: 'partner', data: this.data, arch:'<form string="Partners">' + - '<field name="timmy" widget="many2many_tags"/>' + + '<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\'}"/>' + '</form>', res_id: 1, }); @@ -5794,6 +6461,58 @@ QUnit.module('relational_fields', { 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: '<form>' + + '<field name="int_field"/>' + + '<field name="trululu" widget="radio"/>' + + '</form>', + 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.strictEqual(form.$('.o_field_widget[name=trululu] .o_radio_item').length, 3, + "should be 3 radio buttons"); + + // trigger an onchange that will update the domain + form.$('.o_field_widget[name=int_field]').val(2).trigger('input'); + + assert.strictEqual(form.$('.o_field_widget[name=trululu] .o_radio_item').length, 0, + "should be no more radio button"); + + form.destroy(); + }); + QUnit.module('FieldMany2ManyCheckBoxes'); QUnit.test('widget many2many_checkboxes', function (assert) { @@ -5973,6 +6692,103 @@ QUnit.module('relational_fields', { }); }); + QUnit.module('FieldReference'); + + QUnit.test('reference in form view', function (assert) { + assert.expect(15); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="reference" string="custom label"/>' + + '</group>' + + '</sheet>' + + '</form>', + archs: { + 'product,false,form': '<form string="Product"><field name="display_name"/></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.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"); + form.$('a.o_form_uri').click(); // click on the link in readonly mode (should trigger do_action) + + form.$buttons.find('.o_form_button_edit').click(); + + assert.strictEqual(form.$('.o_field_widget').length, 2, + "should contain two field widgets (selection and many2one)"); + assert.strictEqual(form.$('.o_field_many2one').length, 1, + "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"); + + + form.$('.o_external_button').click(); // click on the external button (should do an RPC) + + assert.strictEqual($('.modal .modal-title').text().trim(), 'Open: custom label', + "dialog title should display the custom string label"); + $('.modal .o_form_button_cancel').click(); + + form.$('.o_field_widget select').val('partner_type').trigger('change'); + assert.strictEqual(form.$('.o_field_widget input').val(), "", + "many2one value should be reset after model change"); + + var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); + // change the value of the m2o with a suggestion of the dropdown + form.$('.o_field_many2one input').click(); // will trigger a name_search on partner_type model + $dropdown.find('li:first()').click(); + + form.$buttons.find('.o_form_button_save').click(); + assert.strictEqual(form.$('a.o_form_uri:contains(gold)').length, 1, + "should contain a link with the new value"); + + form.destroy(); + }); + }); }); }); diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index cdd4ff4e523..cab291ec012 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -94,6 +94,7 @@ var MockServer = Class.extend({ args = JSON.parse(JSON.stringify(args)); if (logLevel === 2) { console.log('%c[rpc] request ' + route, 'color: blue; font-weight: bold;', args); + args = JSON.parse(JSON.stringify(args)); } return this._performRpc(route, args).then(function (result) { var resultString = JSON.stringify(result || false); @@ -567,8 +568,10 @@ var MockServer = Class.extend({ } else { result[fields[i]] = false; } + } else if (field.type === 'one2many' || field.type === 'many2many') { + result[fields[i]] = record[fields[i]] || []; } else { - result[fields[i]] = record[fields[i]]; + result[fields[i]] = record[fields[i]] || false; } } return result; diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index fafe2dee1c6..0b47e9d0df8 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -356,6 +356,15 @@ function createModel(params) { addMockEnvironment(widget, params); + // override the model's 'destroy' so that it calls 'destroy' on the widget + // instead, as the widget is the parent of the model and the mockServer. + model.destroy = function () { + // remove the override to properly destroy the model when it will be + // called the second time (by its parent) + delete model.destroy; + widget.destroy(); + }; + return model; } diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js index 9fbc2acdd88..7f7547262da 100644 --- a/addons/web/static/tests/views/basic_model_tests.js +++ b/addons/web/static/tests/views/basic_model_tests.js @@ -20,6 +20,7 @@ QUnit.module('Views', { product_ids: {string: "Favorite products", type: 'one2many', relation: 'product'}, category: {string: "Category M2M", type: 'many2many', relation: 'partner_type'}, date: {string: "Date Field", type: 'date'}, + reference: {string: "Reference Field", type: 'reference', selection: [["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]}, }, records: [ {id: 1, foo: 'blip', bar: 1, product_id: 37, category: [12], display_name: "first partner", date: "2017-01-25"}, @@ -71,9 +72,15 @@ QUnit.module('Views', { Model: BasicModel, data: this.data, mockRPC: function (route, args) { - assert.strictEqual(args.kwargs.context.active_field, 2, - "should have sent the correct context"); + assert.deepEqual(args.kwargs.context, { + active_field: 2, + bin_size: true, + someKey: 'some value', + }, "should have sent the correct context"); return this._super.apply(this, arguments); + }, + session: { + user_context: {someKey: 'some value'}, } }); @@ -302,6 +309,89 @@ QUnit.module('Views', { model.destroy(); }); + QUnit.test('notifyChange on a one2many', function (assert) { + assert.expect(9); + + this.data.partner.records[1].product_ids = [37]; + this.params.fieldNames = ['product_ids']; + + var model = createModel({ + Model: BasicModel, + data: this.data, + mockRPC: function (route, args) { + if (args.method === 'name_get') { + assert.strictEqual(args.model, 'product'); + } + return this._super(route, args); + }, + }); + + var o2mParams = { + modelName: 'product', + fields: this.data.product.fields, + fieldNames: ['display_name'], + }; + model.load(this.params).then(function (resultID) { + model.load(o2mParams).then(function (newRecordID) { + var record = model.get(resultID); + var x2mListID = record.data.product_ids.id; + + assert.strictEqual(record.data.product_ids.count, 1, + "there should be one record in the relation"); + + // trigger a 'ADD' command + model.notifyChanges(resultID, {product_ids: {operation: 'ADD', id: newRecordID}}); + + assert.deepEqual(model.localData[x2mListID]._changes, [{ + operation: 'ADD', id: newRecordID, + }], "_changes should be correct"); + record = model.get(resultID); + assert.strictEqual(record.data.product_ids.count, 2, + "there should be two records in the relation"); + + // trigger a 'UPDATE' command + model.notifyChanges(resultID, {product_ids: {operation: 'UPDATE', id: newRecordID}}); + + assert.deepEqual(model.localData[x2mListID]._changes, [{ + operation: 'ADD', id: newRecordID, + }, { + operation: 'UPDATE', id: newRecordID, + }], "_changes should be correct"); + record = model.get(resultID); + assert.strictEqual(record.data.product_ids.count, 2, + "there should be two records in the relation"); + + // trigger a 'REMOVE' command on the existing record + var existingRecordID = record.data.product_ids.data[0].id; + model.notifyChanges(resultID, {product_ids: {operation: 'REMOVE', ids: [existingRecordID]}}); + + assert.deepEqual(model.localData[x2mListID]._changes, [{ + operation: 'ADD', id: newRecordID, + }, { + operation: 'UPDATE', id: newRecordID, + }, { + operation: 'REMOVE', id: existingRecordID, + }], + "_changes should be correct"); + record = model.get(resultID); + assert.strictEqual(record.data.product_ids.count, 1, + "there should be one record in the relation"); + + // trigger a 'REMOVE' command on the new record + model.notifyChanges(resultID, {product_ids: {operation: 'REMOVE', ids: [newRecordID]}}); + + assert.deepEqual(model.localData[x2mListID]._changes, [{ + operation: 'REMOVE', id: existingRecordID, + }], "_changes should be correct"); + record = model.get(resultID); + assert.strictEqual(record.data.product_ids.count, 0, + "there should be no record in the relation"); + }); + + }); + model.destroy(); + }); + QUnit.test('notifyChange on a many2one, without display_name', function (assert) { assert.expect(3); @@ -1328,7 +1418,14 @@ QUnit.module('Views', { this.data.partner.onchanges.foo = function (obj) { obj.bar = obj.foo.length; }; - this.data.partner.fields.bar.readonly = true; + this.params.fieldsInfo = { + default: { + foo: {}, + bar: { + modifiers:"{\"readonly\": true}", + }, + } + }; var model = createModel({ Model: BasicModel, @@ -1694,10 +1791,97 @@ QUnit.module('Views', { product_id: 37, product_ids: [], qux: false, + reference: false, total: 0 }, "should use the proper eval context"); }); model.destroy(); }); + QUnit.test('x2manys in contexts and domains are correctly evaluated', function (assert) { + assert.expect(4); + + this.data.partner.records[0].product_ids = [37, 41]; + this.params.fieldNames = Object.keys(this.data.partner.fields); + this.params.fieldsInfo = { + form: { + qux: { + context: "{'category': category, 'product_ids': product_ids}", + domain: "[['id', 'in', category], ['id', 'in', product_ids]]", + relatedFields: this.data.partner.fields, + }, + category: { + relatedFields: this.data.partner_type.fields, + }, + product_ids: { + relatedFields: this.data.product.fields, + }, + }, + }; + this.params.viewType = 'form'; + this.params.res_id = 1; + + var model = createModel({ + Model: BasicModel, + data: this.data, + }); + + model.load(this.params).then(function (resultID) { + var record = model.get(resultID); + var context = record.getContext({fieldName: 'qux'}); + var domain = record.getDomain({fieldName: 'qux'}); + + assert.deepEqual(context, { + category: [12], + product_ids: [37, 41], + }, "x2many values in context manipulated client-side should be lists of ids"); + assert.strictEqual(JSON.stringify(context), + "{\"category\":[[6,false,[12]]],\"product_ids\":[[4,37,false],[4,41,false]]}", + "x2many values in context sent to the server should be commands"); + assert.deepEqual(domain, [ + ['id', 'in', [12]], + ['id', 'in', [37, 41]], + ], "x2many values in domains should be lists of ids"); + assert.strictEqual(JSON.stringify(domain), + "[[\"id\",\"in\",[12]],[\"id\",\"in\",[37,41]]]", + "x2many values in domains should be lists of ids"); + }); + model.destroy(); + }); + + QUnit.test('fetch references in list, with not too many rpcs', function (assert) { + assert.expect(5); + + this.data.partner.records[0].reference = 'product,37'; + this.data.partner.records[1].reference = 'product,41'; + + this.params.fieldNames = ['reference']; + this.params.domain = []; + this.params.groupedBy = []; + this.params.res_id = undefined; + + var model = createModel({ + Model: BasicModel, + data: this.data, + mockRPC: function (route, args) { + assert.step(route); + if (route === "/web/dataset/call_kw/product/name_get") { + assert.deepEqual(args.args, [[37, 41]], + "the name_get should contain the product ids"); + } + return this._super(route, args); + }, + }); + + model.load(this.params).then(function (resultID) { + var record = model.get(resultID); + + assert.strictEqual(record.data[0].data.reference.data.display_name, "xphone", + "name_get should have been correctly fetched"); + assert.verifySteps(["/web/dataset/search_read", "/web/dataset/call_kw/product/name_get"], + "should have done 2 rpc (searchread and name_get for product)"); + }); + model.destroy(); + }); + });}); diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 8aa1f692072..011843eefee 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -143,7 +143,7 @@ QUnit.module('Views', { }; QUnit.test('simple calendar rendering', function (assert) { - assert.expect(19); + assert.expect(22); var calendar = createView({ View: CalendarView, @@ -200,6 +200,13 @@ QUnit.module('Views', { assert.strictEqual($attendeesFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)"); assert.ok($attendeesFilter.find('.o_field_many2one').length, "should display one2many search bar for 'attendees' filter"); + assert.strictEqual(calendar.$('.fc-event').length, 6, + "should display 6 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)"); + calendar.$('.o_calendar_filter .o_checkbox input').first().click(); // Disable first filter + assert.strictEqual(calendar.$('.fc-event').length, 4, "should now only display 4 event"); + calendar.$('.o_calendar_filter .o_checkbox input').eq(1).click(); // Disable second filter + assert.strictEqual(calendar.$('.fc-event').length, 0, "should not display any event anymore"); + // test search bar in filter $sidebar.find('input[type="text"]').trigger('click'); @@ -208,7 +215,7 @@ QUnit.module('Views', { assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 4, "should display 4 filter items for 'attendees'"); $sidebar.find('input[type="text"]').trigger('click'); assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').text(), "partner 4", "should display the last choice in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice - $sidebar.find('.o_calendar_filter_item[data-id="1"] .o_remove').trigger('click'); + $sidebar.find('.o_calendar_filter_item .o_remove').first().trigger('click'); assert.ok($('.modal button.btn:contains(Ok)').length, "should display the confirm message"); $('.modal button.btn:contains(Ok)').trigger('click'); assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 3, "click on remove then should display 3 filter items for 'attendees'"); @@ -349,6 +356,63 @@ QUnit.module('Views', { calendar.destroy(); }); + QUnit.test('quickcreate switching to actual create for required fields', function (assert) { + assert.expect(4); + + var event = $.Event(); + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + '<calendar class="o_calendar_test" '+ + 'string="Events" ' + + 'event_open_popup="true" '+ + 'date_start="start" '+ + 'date_stop="stop" '+ + 'all_day="allday" '+ + 'mode="month" '+ + 'readonly_form_view_id="1">'+ + '<field name="name"/>'+ + '</calendar>', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + mockRPC: function (route, args) { + if (args.method === "create") { + return $.Deferred().reject({ + code: 200, + data: {}, + message: "Odoo server error", + }, event); + } + return this._super(route, args); + }, + }); + + // create a new event + var $cell = calendar.$('.fc-day-grid .fc-row:eq(2) .fc-day:eq(2)'); + testUtils.triggerMouseEvent($cell, "mousedown"); + testUtils.triggerMouseEvent($cell, "mouseup"); + + assert.strictEqual($('.modal-dialog.modal-sm .modal-title').text(), 'Create', + "should open the quick create dialog"); + + $('.modal-body input:first').val('new event in quick create').trigger('input'); + $('.modal button.btn:contains(Create)').trigger('click').trigger('click'); + + // If the event is not default-prevented, a traceback will be raised, which we do not want + assert.ok(event.isDefaultPrevented(), "fail deferred event should have been default-prevented"); + + assert.strictEqual($('.modal-dialog.modal-lg .modal-title').text(), 'Create: Events', + "should have switched to a bigger modal for an actual create rather than quickcreate"); + assert.strictEqual($('.modal-dialog.modal-lg .modal-body .o_form_view.o_form_editable').length, 1, + "should open the full event form view in a dialog"); + + calendar.destroy(); + }); + QUnit.test('create event with timezone in week mode', function (assert) { assert.expect(5); @@ -640,7 +704,7 @@ QUnit.module('Views', { }); QUnit.test('use mini calendar', function (assert) { - assert.expect(2); + assert.expect(12); var calendar = createView({ View: CalendarView, @@ -665,17 +729,36 @@ QUnit.module('Views', { }, }); + assert.strictEqual(calendar.$('.fc-agendaWeek-view').length, 1, "should be in week mode"); assert.strictEqual(calendar.$('.fc-event').length, 9, "should display 9 events on the week (4 event + 5 days event)"); - $('.o_calendar_mini a:contains(19)').click(); + calendar.$('.o_calendar_mini a:contains(19)').click(); + // Clicking on a day in another week should switch to the other week view + assert.strictEqual(calendar.$('.fc-agendaWeek-view').length, 1, "should be in week mode"); + assert.strictEqual(calendar.$('.fc-event').length, 4, "should display 4 events on the week (1 event + 3 days event)"); + // Clicking on a day in the same week should switch to that particular day view + calendar.$('.o_calendar_mini a:contains(18)').click(); + assert.strictEqual(calendar.$('.fc-agendaDay-view').length, 1, "should be in day mode"); + assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day"); + // Clicking on the same day should toggle between day, month and week views + calendar.$('.o_calendar_mini a:contains(18)').click(); + assert.strictEqual(calendar.$('.fc-month-view').length, 1, "should be in month mode"); + assert.strictEqual(calendar.$('.fc-event').length, 7, "should display 7 events on the month (event 5 is on multiple weeks and generates to .fc-event)"); + calendar.$('.o_calendar_mini a:contains(18)').click(); + assert.strictEqual(calendar.$('.fc-agendaWeek-view').length, 1, "should be in week mode"); assert.strictEqual(calendar.$('.fc-event').length, 4, "should display 4 events on the week (1 event + 3 days event)"); + calendar.$('.o_calendar_mini a:contains(18)').click(); + assert.strictEqual(calendar.$('.fc-agendaDay-view').length, 1, "should be in day mode"); + assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day"); calendar.destroy(); }); QUnit.test('rendering, with many2many', function (assert) { - assert.expect(1); + assert.expect(5); this.data.event.fields.partner_ids.type = 'many2many'; + this.data.event.records[0].partner_ids = [1,2,3,4,5]; + this.data.partner.records.push({id: 5, display_name: "partner 5", image: 'EEE'}); var calendar = createView({ View: CalendarView, @@ -698,6 +781,15 @@ QUnit.module('Views', { assert.strictEqual(calendar.$('.o_calendar_filter_items .o_cal_avatar').length, 3, "should have 3 avatars in the side bar"); + var $event1Avatars = calendar.$('.fc-event .o_calendar_avatars').first(); + assert.strictEqual($event1Avatars.find('img').length, 1, "should have 1 avatar"); + assert.strictEqual($event1Avatars.find('span').length, 0, + "should not have a span for more attendees since there is only one"); + + var $event2Avatars = calendar.$('.fc-event:contains(All the day) .o_calendar_avatars'); + assert.strictEqual($event2Avatars.find('img').length, 3, "should have 3 avatars"); + assert.strictEqual($event2Avatars.find('span').text(), '+2', + "should indicate there are 2 more attendees that we don't show"); calendar.destroy(); }); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 0e53d6814d4..0857cb50038 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -2968,6 +2968,53 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('discarding before save returns', function (assert) { + assert.expect(4); + + var def = $.Deferred(); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<group><field name="foo"/></group>' + + '</form>', + res_id: 2, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'write') { + return def.then(_.constant(result)); + } + return result; + }, + viewOptions: { + mode: 'edit', + }, + }); + + form.$('input').val("1234").trigger('input'); + + // save the value and discard directly + form.$buttons.find('.o_form_button_save').click(); + form.discardChanges(); // Simulate click on breadcrumb + + assert.strictEqual(form.$('.o_field_widget[name="foo"]').val(), "1234", + "field foo should still contain new value"); + assert.strictEqual($('.modal').length, 0, + "Confirm dialog should not be displayed"); + + // complete the write + def.resolve(); + + assert.strictEqual($('.modal').length, 0, + "Confirm dialog should not be displayed"); + assert.strictEqual(form.$('.o_field_widget[name="foo"]').text(), "1234", + "value should have been saved and rerendered in readonly"); + + form.destroy(); + }); + QUnit.test('unchanged relational data is sent for onchanges', function (assert) { assert.expect(1); @@ -4361,7 +4408,7 @@ QUnit.module('Views', { }); QUnit.test('default_order on x2many embedded view', function (assert) { - assert.expect(8); + assert.expect(11); this.data.partner.fields.display_name.sortable = true; this.data.partner.records[0].p = [1, 4]; @@ -4421,6 +4468,18 @@ QUnit.module('Views', { assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length, "record xop should be third"); + // client-side sort on edit + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_one2many tbody tr:eq(1) td:contains(yop)').click(); + $('.modal input[name="foo"]').val('zzz').trigger("input"); + $('.modal .modal-footer button:first').click(); // Save + assert.ok(form.$('.o_field_one2many tbody tr:eq(0) td:contains(zzz)').length, + "record zzz should be first"); + assert.ok(form.$('.o_field_one2many tbody tr:eq(1) td:contains(zop)').length, + "record zop should be second"); + assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length, + "record xop should be third"); + form.destroy(); }); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 549b724ca68..89ec104fc41 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -457,7 +457,7 @@ QUnit.module('Views', { arch: '<kanban class="o_kanban_test">' + '<templates><t t-name="kanban-box">' + '<div>' + - '<field name="category_ids"/>' + + '<field name="category_ids" widget="many2many_tags" options="{\'color_field\': \'color\'}"/>' + '<field name="foo"/>' + '<field name="state" widget="priority"/>' + '</div>' + diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index c39e6d2b1b0..09109d92b84 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -2,6 +2,7 @@ odoo.define('web.list_tests', function (require) { "use strict"; var config = require('web.config'); +var FormView = require('web.FormView'); var ListView = require('web.ListView'); var testUtils = require('web.test_utils'); @@ -23,6 +24,8 @@ QUnit.module('Views', { currency_id: {string: "Currency", type: "many2one", relation: "res_currency", default: 1}, datetime: {string: "Datetime Field", type: 'datetime'}, + reference: {string: "Reference Field", type: 'reference', selection: [ + ["bar", "Bar"], ["res_currency", "Currency"], ["event", "Event"]]}, }, records: [ { @@ -37,11 +40,12 @@ QUnit.module('Views', { currency_id: 2, date: "2017-01-25", datetime: "2016-12-12 10:55:05", + reference: 'bar,1', }, {id: 2, bar: true, foo: "blip", int_field: 9, qux: 13, - m2o: 2, m2m: [1, 2, 3], amount: 500}, + m2o: 2, m2m: [1, 2, 3], amount: 500, reference: 'res_currency,1'}, {id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3, - m2o: 1, m2m: [], amount: 300}, + m2o: 1, m2m: [], amount: 300, reference: 'res_currency,2'}, {id: 4, bar: false, foo: "blip", int_field: -4, qux: 9, m2o: 1, m2m: [1], amount: 0}, ] @@ -2400,6 +2404,68 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('reference field rendering', function (assert) { + assert.expect(4); + + this.data.foo.records.push({ + id: 5, + reference: 'res_currency,2', + }); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree><field name="reference"/></tree>', + mockRPC: function (route, args) { + if (args.method === 'name_get') { + assert.step(args.model); + } + return this._super.apply(this, arguments); + }, + }); + + assert.verifySteps(['bar', 'res_currency'], "should have done 1 name_get by model in reference values"); + assert.strictEqual(list.$('tbody td').text(), "Value 1USDEUREUR", + "should have the display_name of the reference"); + list.destroy(); + }); + + QUnit.test('editable list view: contexts are correctly sent', function (assert) { + assert.expect(6); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree editable="top">' + + '<field name="foo"/>' + + '</tree>', + mockRPC: function (route, args) { + var context; + if (route === '/web/dataset/search_read') { + context = args.context; + } else { + context = args.kwargs.context; + } + assert.strictEqual(context.active_field, 2, "context should be correct"); + assert.strictEqual(context.someKey, 'some value', "context should be correct"); + return this._super.apply(this, arguments); + }, + session: { + user_context: {someKey: 'some value'}, + }, + viewOptions: { + context: {active_field: 2}, + }, + }); + + list.$('.o_data_cell:first').click(); + list.$('.o_field_widget[name=foo]').val('abc').trigger('input'); + list.$buttons.find('.o_list_button_save').click(); + + list.destroy(); + }); }); }); |
