summaryrefslogtreecommitdiff
path: root/addons/web/static/tests
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-07-07 14:30:30 +0200
committerChristophe Simonis <chs@odoo.com>2017-07-07 14:30:30 +0200
commitbf51fe55a149fb642a410f579dc4b6dcbd794eac (patch)
treef4f208f06094bcef6541fd1fc034e0265de4ad21 /addons/web/static/tests
parentbf32940142beb879211b7034bb77a7845c9d0555 (diff)
parent0f2b2c4e2b99df8d68456016a4bea86e26c1c80e (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.js101
-rw-r--r--addons/web/static/tests/fields/field_utils_tests.js23
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js830
-rw-r--r--addons/web/static/tests/helpers/mock_server.js5
-rw-r--r--addons/web/static/tests/helpers/test_utils.js9
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js190
-rw-r--r--addons/web/static/tests/views/calendar_tests.js102
-rw-r--r--addons/web/static/tests/views/form_tests.js61
-rw-r--r--addons/web/static/tests/views/kanban_tests.js2
-rw-r--r--addons/web/static/tests/views/list_tests.js70
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("$&nbsp;125.00", {}, {currency_id: 3}), 125);
+ assert.strictEqual(fieldUtils.parse.monetary("1,000.00&nbsp;€", {}, {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("$&nbsp;12.00", {}, {currency_id: 1})}, /is not a correct/);
+ assert.throws(function() {fieldUtils.parse.monetary("$&nbsp;12.00&nbsp;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();
+ });
});
});