summaryrefslogtreecommitdiff
path: root/addons/web/static/tests
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/tests')
-rw-r--r--addons/web/static/tests/chrome/action_manager_tests.js2
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js537
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_many2many_tests.js41
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_many2one_tests.js75
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js2
-rw-r--r--addons/web/static/tests/helpers/mock_server.js10
-rw-r--r--addons/web/static/tests/views/graph_tests.js2
-rw-r--r--addons/web/static/tests/views/list_tests.js656
-rw-r--r--addons/web/static/tests/views/mobile_control_panel_tests.js68
-rw-r--r--addons/web/static/tests/views/search_panel_tests.js106
-rw-r--r--addons/web/static/tests/widgets/domain_selector_tests.js1
11 files changed, 1437 insertions, 63 deletions
diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js
index bc40fc97011..002a52f572f 100644
--- a/addons/web/static/tests/chrome/action_manager_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_tests.js
@@ -3518,7 +3518,7 @@ QUnit.module('ActionManager', {
active_ids: [1],
},
flags: {
- withSearchPanel: false,
+ searchPanelDefaultNoFilter: true,
},
});
var checkSessionStorage = false;
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index eca412c036a..aa86ff925e4 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -11,6 +11,7 @@ var KanbanView = require('web.KanbanView');
var ListView = require('web.ListView');
var session = require('web.session');
var testUtils = require('web.test_utils');
+var testUtilsDom = require('web.test_utils_dom');
var field_registry = require('web.field_registry');
var createView = testUtils.createView;
@@ -3361,6 +3362,63 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('datetime fields do not trigger fieldChange before datetime completly picked', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.onchanges = {
+ datetime: function () {},
+ };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="datetime"/></form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.step('onchange');
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+
+ testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget');
+
+ // select a date and time
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first());
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(3));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day:contains(22)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .fa-clock-o'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-hour'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .hour:contains(08)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-minute'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .minute:contains(25)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-second'));
+ assert.verifySteps([], "should not have done any onchange yet");
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .second:contains(35)'));
+
+ assert.containsNone($('body'), '.bootstrap-datetimepicker-widget');
+ assert.strictEqual(form.$('.o_datepicker_input').val(), "04/22/2017 08:25:35");
+ assert.verifySteps(['onchange'], "should have done only one onchange");
+
+ form.destroy();
+ });
QUnit.test('datetime field not visible in form view should not capture the focus on keyboard navigation', async function (assert) {
assert.expect(1);
@@ -3483,6 +3541,45 @@ QUnit.module('basic_fields', {
list.destroy();
});
+ QUnit.test('multi edition of datetime field in list view: edit date in input', async function (assert) {
+ assert.expect(4);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="datetime"/>' +
+ '</tree>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ });
+
+ // select two records and edit them
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first .o_data_cell'));
+ assert.containsOnce(list, 'input.o_datepicker_input');
+ list.$('.o_datepicker_input').val("10/02/2019 09:00:00");
+ await testUtils.dom.triggerEvents(list.$('.o_datepicker_input'), ['change']);
+
+ assert.containsOnce(document.body, '.modal');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ assert.strictEqual(list.$('.o_data_row:first .o_data_cell').text(), "10/02/2019 09:00:00");
+ assert.strictEqual(list.$('.o_data_row:nth(1) .o_data_cell').text(), "10/02/2019 09:00:00");
+
+ list.destroy();
+ });
+
QUnit.test('datetime field remove value', async function (assert) {
assert.expect(4);
@@ -5859,7 +5956,7 @@ QUnit.module('basic_fields', {
QUnit.module('FieldProgressBar');
QUnit.test('Field ProgressBar: max_value should update', async function (assert) {
- assert.expect(2);
+ assert.expect(3);
this.data.partner.records = this.data.partner.records.slice(0,1);
this.data.partner.records[0].qux = 2;
@@ -5884,6 +5981,15 @@ QUnit.module('basic_fields', {
viewOptions: {
mode: 'edit',
},
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(
+ args.args[1],
+ {int_field: 999, qux: 5, display_name: 'new name'},
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
});
assert.strictEqual(form.$('.o_progressbar_value').text(), '10 / 2',
@@ -5895,6 +6001,435 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('.o_progressbar_value').text(), '999 / 5',
'The value of the progress bar should be correct after the update');
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: value should not update in readonly mode when sliding the bar', async function (assert) {
+ assert.expect(4);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(route);
+ return this._super.apply(this, arguments);
+ }
+ });
+ var $view = $('#qunit-fixture').contents();
+ $view.prependTo('body'); // => select with click position
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct')
+
+ var $progressBarEl = form.$('.o_progress');
+ var top = $progressBarEl.offset().top + 5;
+ var left = $progressBarEl.offset().left + 5;
+ try {
+ testUtils.triggerPositionalMouseEvent(left, top, "click");
+ } catch (e) {
+ form.destroy();
+ $view.remove();
+ throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.');
+ }
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'New value should be different than initial after click');
+
+ assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
+
+ form.destroy();
+ $view.remove();
+ });
+
+ QUnit.test('Field ProgressBar: value should not update in edit mode when sliding the bar', async function (assert) {
+ assert.expect(6);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ assert.step(route);
+ return this._super.apply(this, arguments);
+ }
+ });
+ var $view = $('#qunit-fixture').contents();
+ $view.prependTo('body'); // => select with click position
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct')
+
+ var $progressBarEl = form.$('.o_progress');
+ var top = $progressBarEl.offset().top + 5;
+ var left = $progressBarEl.offset().left + 5;
+ try {
+ testUtils.triggerPositionalMouseEvent(left, top, "click");
+ } catch (e) {
+ form.destroy();
+ $view.remove();
+ throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.');
+ }
+ assert.strictEqual(form.$('.o_progressbar_value.o_input').val(), "99",
+ 'Value of input is not changed');
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'New value should be different than initial after click');
+
+ assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
+
+ form.destroy();
+ $view.remove();
+ });
+
+ QUnit.test('Field ProgressBar: value should update in edit mode when typing in input', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].int_field, 69,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']);
+
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '69%',
+ 'New value should be different than initial after click');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: value should update in edit mode when typing in input with field max value', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="qux" invisible="1" />' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\'}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].int_field, 69,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']);
+
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '69 / 0',
+ 'New value should be different than initial after click');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: max value should update in edit mode when typing in input with field max value', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="qux" invisible="1" />' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\', \'edit_max_value\': true}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].qux, 69,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), "0.44444", 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']);
+
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 69',
+ 'New value should be different than initial after click');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: Standard readonly mode is readonly', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="qux" invisible="1" />' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\', \'edit_max_value\': true}" />' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(route);
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ assert.containsNone(form, '.o_progressbar_value.o_input', 'no input in readonly mode');
+
+ assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: max value should update in readonly mode with right parameter when typing in input with field max value', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="qux" invisible="1" />' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\', \'edit_max_value\': true, \'editable_readonly\': true}" />' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].qux, 69,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), "0.44444", 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']);
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 69',
+ 'New value should be different than initial after changing it');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: value should update in readonly mode with right parameter when typing in input with field value', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'editable_readonly\': true}" />' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].int_field, 69,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), "99", 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '69.6', ['input', 'blur']);
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '69%',
+ 'New value should be different than initial after changing it');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: write float instead of int works, in locale', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ translateParameters: {
+ thousands_sep: "#",
+ decimal_point: ":",
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].int_field, 1037,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '1#037:9', ['input', 'blur']);
+
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '1k%',
+ 'New value should be different than initial after click');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: write gibbrish instead of int throws warning', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ interceptsPropagate: {
+ call_service: function (ev) {
+ if (ev.data.service === 'notification') {
+ assert.strictEqual(ev.data.method, 'notify');
+ assert.strictEqual(ev.data.args[0].title, 'Wrong value entered!');
+ }
+ }
+ },
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, 'trente sept virgule neuf', ['input']);
+
form.destroy();
});
diff --git a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
index db0fb3632d3..487cd8647b8 100644
--- a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
+++ b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
@@ -1106,6 +1106,47 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+ QUnit.test("many2many tags widget: save&new in edit mode doesn't close edit window", async function (assert) {
+ assert.expect(5);
+ for (var i = 1; i <= 10; i++) {
+ this.data.partner_type.records.push({ id: 100 + i, display_name: "Partner" + i});
+ }
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/></search>',
+ 'partner_type,false,form': '<form><field name="display_name"/></form>'
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.fields.many2one.clickOpenDropdown('timmy');
+ await testUtils.fields.many2one.clickItem('timmy','Create and Edit');
+ assert.containsOnce($(document), '.modal .o_form_view', "should have opened the modal");
+
+ // Create multiple records with save & new
+ await testUtils.fields.editInput($('.modal input:first'), 'Ralts');
+ await testUtils.dom.click($('.modal .btn-primary:nth-child(2)'));
+ assert.containsOnce($(document), '.modal .o_form_view', "modal should still be open");
+ assert.equal($('.modal input:first')[0].value, '', "input should be empty")
+
+ // Create another record and click save & close
+ await testUtils.fields.editInput($('.modal input:first'), 'Pikachu');
+ await testUtils.dom.click($('.modal .btn-primary:first'));
+ assert.containsNone($(document),'.modal .o_list_view', "should have closed the modal");
+ assert.containsN(form, '.o_field_many2manytags[name="timmy"] .badge', 2, "many2many tag should now contain 2 records");
+
+ form.destroy();
+ });
+
QUnit.test('many2many list add *many* records, remove, re-add', async function (assert) {
assert.expect(5);
diff --git a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
index f4cd0e1be0b..4d5b5d8aa89 100644
--- a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
+++ b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
@@ -566,6 +566,81 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+ QUnit.test("many2one doesn't trigger field_change when being emptied", async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ arch: `
+ <tree editable="top">
+ <field name="trululu"/>
+ </tree>`,
+ data: this.data,
+ model: 'partner',
+ View: ListView,
+ });
+
+ // Select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
+
+ const $input = list.$('.o_field_widget[name=trululu] input');
+
+ await testUtils.fields.editInput($input, "");
+ await testUtils.dom.triggerEvents($input, ['keyup']);
+
+ assert.containsNone(document.body, '.modal',
+ "No save should be triggered when removing value");
+
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ await testUtils.fields.many2one.clickHighlightedItem('trululu');
+
+ assert.containsOnce(document.body, '.modal',
+ "Saving should be triggered when selecting a value");
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ list.destroy();
+ });
+
+ QUnit.test("focus tracking on a many2one in a list", async function (assert) {
+ assert.expect(4);
+
+ const list = await createView({
+ arch: '<tree editable="top"><field name="trululu"/></tree>',
+ archs: {
+ 'partner,false,form': '<form string="Partners"><field name="foo"/></form>',
+ },
+ data: this.data,
+ model: 'partner',
+ View: ListView,
+ });
+
+ // Select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
+
+ const input = list.$('.o_data_row:first() .o_data_cell:first() input')[0];
+
+ assert.strictEqual(document.activeElement, input, "Input should be focused when activated");
+
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ await testUtils.fields.many2one.clickItem('trululu', 'Create');
+
+ // At this point, if the focus is correctly registered by the m2o, there
+ // should be only one modal (the "Create" one) and none for saving changes.
+ assert.containsOnce(document.body, '.modal', "There should be only one modal");
+
+ await testUtils.dom.click($('.modal .btn:not(.btn-primary)'));
+
+ assert.strictEqual(document.activeElement, input, "Input should be focused after dialog closes");
+ assert.strictEqual(input.value, "", "Input should be empty after discard");
+
+ list.destroy();
+ });
+
QUnit.test('many2one fields with option "no_open"', async function (assert) {
assert.expect(3);
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 7f3f54e3ad7..24ce3e892c8 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -1886,7 +1886,7 @@ QUnit.module('relational_fields', {
});
assert.containsN(form, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, "should have 2 records");
- assert.strictEqual(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img').data('src'), '/web/image/partner/2/image_64',
+ assert.strictEqual(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img').data('src'), '/web/image/partner/2/image_128',
"should have correct avatar image");
form.destroy();
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index d77e8ceae79..f09456956ef 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -326,10 +326,12 @@ var MockServer = Class.extend({
if (field.type === "one2many" || field.type === "many2many") {
field.views = {};
_.each(node.childNodes, function (children) {
- relModel = field.relation;
- relFields = $.extend(true, {}, self.data[relModel].fields);
- field.views[children.tagName] = self._fieldsViewGet(children, relModel,
- relFields, _.extend({}, context, {base_model_name: model}));
+ if (children.tagName) { // skip text nodes
+ relModel = field.relation;
+ relFields = $.extend(true, {}, self.data[relModel].fields);
+ field.views[children.tagName] = self._fieldsViewGet(children, relModel,
+ relFields, _.extend({}, context, {base_model_name: model}));
+ }
});
}
diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js
index 7cbab784829..d44d2e61a81 100644
--- a/addons/web/static/tests/views/graph_tests.js
+++ b/addons/web/static/tests/views/graph_tests.js
@@ -133,7 +133,7 @@ QUnit.module('Views', {
data: [3,5],
label: "Count",
originIndex: 0,
- stack: undefined,
+ stack: "",
}
);
assert.checkLegend(graph, 'Count');
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index c97b1916265..82607c43689 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -6,6 +6,7 @@ var BasicModel = require('web.BasicModel');
var core = require('web.core');
var basicFields = require('web.basic_fields');
var FormView = require('web.FormView');
+var ListRenderer = require('web.ListRenderer');
var ListView = require('web.ListView');
var mixins = require('web.mixins');
var NotificationService = require('web.NotificationService');
@@ -214,7 +215,7 @@ QUnit.module('Views', {
});
QUnit.test('simple editable rendering', async function (assert) {
- assert.expect(12);
+ assert.expect(15);
var list = await createView({
View: ListView,
@@ -225,6 +226,7 @@ QUnit.module('Views', {
assert.containsN(list, 'th', 3, "should have 2 th");
assert.containsN(list, 'th', 3, "should have 3 th");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
assert.containsOnce(list, 'td:contains(yop)', "should contain yop");
assert.isVisible(list.$buttons.find('.o_list_button_add'),
@@ -242,6 +244,7 @@ QUnit.module('Views', {
"should have a visible save button");
assert.isVisible(list.$buttons.find('.o_list_button_discard'),
"should have a visible discard button");
+ assert.containsNone(list, '.o_list_record_selector input:enabled');
await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
@@ -251,6 +254,8 @@ QUnit.module('Views', {
"should not have a visible save button");
assert.isNotVisible(list.$buttons.find('.o_list_button_discard'),
"should not have a visible discard button");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
list.destroy();
});
@@ -1139,6 +1144,49 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('head selector is toggled by the other selectors', async function (assert) {
+ assert.expect(6);
+
+ const list = await createView({
+ arch: '<tree><field name="foo"/><field name="bar"/></tree>',
+ data: this.data,
+ groupBy: ['bar'],
+ model: 'foo',
+ View: ListView,
+ });
+
+ assert.ok(!list.$('thead .o_list_record_selector input')[0].checked,
+ "Head selector should be unchecked");
+
+ await testUtils.dom.click(list.$('.o_group_header:first()'));
+ await testUtils.dom.click(list.$('thead .o_list_record_selector input'));
+
+ assert.containsN(list, 'tbody .o_list_record_selector input:checked',
+ 3, "All visible checkboxes should be checked");
+
+ await testUtils.dom.click(list.$('.o_group_header:last()'));
+
+ assert.ok(!list.$('thead .o_list_record_selector input')[0].checked,
+ "Head selector should be unchecked");
+
+ await testUtils.dom.click(list.$('tbody .o_list_record_selector input:last()'));
+
+ assert.ok(list.$('thead .o_list_record_selector input')[0].checked,
+ "Head selector should be checked");
+
+ await testUtils.dom.click(list.$('tbody .o_list_record_selector:first() input'));
+
+ assert.ok(!list.$('thead .o_list_record_selector input')[0].checked,
+ "Head selector should be unchecked");
+
+ await testUtils.dom.click(list.$('.o_group_header:first()'));
+
+ assert.ok(list.$('thead .o_list_record_selector input')[0].checked,
+ "Head selector should be checked");
+
+ list.destroy();
+ });
+
QUnit.test('selection is reset on reload', async function (assert) {
assert.expect(5);
@@ -1516,7 +1564,7 @@ QUnit.module('Views', {
'</tree>',
});
- assert.containsNone(list, 'o_resize', "There shouldn't be any resize handle if no data");
+ assert.containsNone(list, '.o_resize', "There shouldn't be any resize handle if no data");
assertions.forEach(a => {
assert.strictEqual(list.$(`th[data-name="${a.field}"]`)[0].offsetWidth, a.expected,
`Field ${a.type} should have a fixed width of ${a.expected} pixels`);
@@ -1527,6 +1575,66 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('width of some fields should be hardcoded if no data, and list initially invisible', async function (assert) {
+ const assertions = [
+ { field: 'bar', expected: 40, type: 'Boolean' },
+ { field: 'int_field', expected: 74, type: 'Integer' },
+ { field: 'qux', expected: 92, type: 'Float' },
+ { field: 'date', expected: 92, type: 'Date' },
+ { field: 'datetime', expected: 146, type: 'Datetime' },
+ { field: 'amount', expected: 104, type: 'Monetary' },
+ { field: 'the_button', expected: 25, type: 'with custom width' },
+ ];
+ assert.expect(11);
+
+ this.data.foo.fields.foo_o2m = {string: "Foo O2M", type: "one2many", relation: "foo"};
+ const form = await createView({
+ View: FormView,
+ model: 'foo',
+ data: this.data,
+ res_id: 1,
+ viewOptions: { mode: 'edit' },
+ arch: `<form>
+ <sheet>
+ <notebook>
+ <page string="Page1"></page>
+ <page string="Page2">
+ <field name="foo_o2m">
+ <tree editable="bottom">
+ <field name="bar"/>
+ <field name="foo"/>
+ <field name="int_field"/>
+ <field name="qux"/>
+ <field name="date"/>
+ <field name="datetime"/>
+ <field name="amount"/>
+ <button name="the_button" width="25px"/>
+ </tree>
+ </field>
+ </page>
+ </notebook>
+ </sheet>
+ </form>`,
+ });
+
+ assert.isNotVisible(form.$('.o_field_one2many'));
+
+ await testUtils.dom.click(form.$('.nav-item:last-child .nav-link'));
+
+ assert.isVisible(form.$('.o_field_one2many'));
+
+ assert.containsNone(form, '.o_field_one2many .o_resize',
+ "There shouldn't be any resize handle if no data");
+ assertions.forEach(a => {
+ assert.strictEqual(form.$(`.o_field_one2many th[data-name="${a.field}"]`)[0].offsetWidth, a.expected,
+ `Field ${a.type} should have a fixed width of ${a.expected} pixels`);
+ });
+ assert.strictEqual(form.$('.o_field_one2many th[data-name="foo"]')[0].style.width, '100%',
+ "Char field should occupy the remaining space");
+
+ form.destroy();
+ });
+
QUnit.test('editable list: list view in an initially unselected notebook page', async function (assert) {
assert.expect(9);
@@ -1826,7 +1934,7 @@ QUnit.module('Views', {
groupBy: ['int_field'],
});
- assert.containsNone(list, 'o_resize', "There shouldn't be any resize handle if no data");
+ assert.containsNone(list, '.o_resize', "There shouldn't be any resize handle if no data");
assertions.forEach(a => {
assert.strictEqual(list.$(`th[data-name="${a.field}"]`)[0].offsetWidth, a.expected,
`Field ${a.type} should have a fixed width of ${a.expected} pixels`);
@@ -1911,6 +2019,43 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('column widths are re-computed on window resize', async function (assert) {
+ assert.expect(2);
+
+ testUtils.mock.patch(ListRenderer, {
+ RESIZE_DELAY: 0,
+ });
+
+ this.data.foo.records[0].text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' +
+ 'Sed blandit, justo nec tincidunt feugiat, mi justo suscipit libero, sit amet tempus ' +
+ 'ipsum purus bibendum est.';
+ const list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: `<tree editable="bottom">
+ <field name="datetime"/>
+ <field name="text"/>
+ </tree>`,
+ });
+
+ const initialTextWidth = list.$('th[data-name="text"]')[0].offsetWidth;
+ const selectorWidth = list.$('th.o_list_record_selector')[0].offsetWidth;
+
+ // simulate a window resize
+ list.$el.width(`${list.$el.width() / 2}px`);
+ core.bus.trigger('resize');
+ await testUtils.nextTick();
+
+ const postResizeTextWidth = list.$('th[data-name="text"]')[0].offsetWidth;
+ const postResizeSelectorWidth = list.$('th.o_list_record_selector')[0].offsetWidth;
+ assert.ok(postResizeTextWidth < initialTextWidth);
+ assert.strictEqual(selectorWidth, postResizeSelectorWidth);
+
+ testUtils.mock.unpatch(ListRenderer);
+ list.destroy();
+ });
+
QUnit.test('columns with an absolute width are never narrower than that width', async function (assert) {
assert.expect(2);
@@ -3399,6 +3544,193 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('export list view to xls with nested groups', async function (assert) {
+ assert.expect(5);
+
+ let currencies = {};
+ _.each(this.data.res_currency.records, function (currency) {
+ currencies[currency.id] = currency;
+ });
+
+ let list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: `
+ <tree download="1">
+ <field name="foo"/>
+ <field name="bar"/>
+ <field name="int_field" sum="Total"/>
+ <field name="datetime"/>
+ <field name="amount" widget="monetary"/>
+ <field name="currency_id" invisible="1"/>
+ <field name="o2m"/>
+ <field name="m2o"/>
+ <field name="m2m"/>
+ </tree>`,
+ groupBy: ['foo', 'bar'],
+ session: {
+ currencies: currencies,
+ get_file(args) {
+ let data = JSON.parse(args.data.data);
+ assert.strictEqual(args.url, '/web/list/export_xls',
+ "should call get_file with the correct url");
+ assert.deepEqual(data.columns, [
+ {field: "foo", string: "Foo"},
+ {field: "bar", string: "Bar"},
+ {field: "int_field", aggregateValue: 32, string: "int_field"},
+ {field: "datetime", string: "Datetime Field"},
+ {field: "amount", string: "Monetary field"},
+ {field: "o2m", string: "O2M field"},
+ {field: "m2o", string: "M2O field"},
+ {field: "m2m", string: "M2M field"},
+ ], "columns should be in the correct order with the string and aggregate value")
+ assert.deepEqual(data.groups[0], {
+ // Group "yop"
+ isGrouped: true,
+ count: 1,
+ aggregateValues: {int_field: 10},
+ hideHeader: false,
+ value: "yop",
+ data: [{
+ // Group "yop > true"
+ isGrouped: false,
+ count: 1,
+ aggregateValues: {int_field: 10},
+ hideHeader: false,
+ value: "true",
+ data: [{
+ // Record "yop > true"
+ foo: "yop",
+ bar: "True",
+ int_field: "10",
+ datetime: "12/12/2016 10:55:05",
+ amount: "1200.00€",
+ m2m: "2 records",
+ m2o: "Value 1",
+ o2m: "No records",
+ }]
+ }]
+ });
+ assert.deepEqual(data.groups[1], {
+ // Group "blip"
+ isGrouped: true,
+ count: 2,
+ aggregateValues: {int_field: 5},
+ hideHeader: false,
+ value: "blip",
+ data: [{
+ // Group "blip > true"
+ isGrouped: false,
+ count: 1,
+ aggregateValues: {int_field: 9},
+ hideHeader: false,
+ value: "true",
+ data: [],
+ }, {
+ // Group "blip > false"
+ isGrouped: false,
+ count: 1,
+ aggregateValues: {int_field: -4},
+ hideHeader: false,
+ value: "false",
+ data: [],
+ }]
+ });
+ assert.deepEqual(data.groups[2], {
+ // Group "gnap"
+ isGrouped: true,
+ count: 1,
+ aggregateValues: {int_field: 17},
+ hideHeader: false,
+ value: "gnap",
+ data: [],
+ });
+ args.complete();
+ },
+ },
+ });
+
+ // open the first group
+ await testUtils.dom.click(list.$('.o_group_header:first'));
+
+ // open subgroup
+ let $openGroup = list.$('tbody:nth(1)');
+ await testUtils.dom.click($openGroup.find('.o_group_header:first'));
+
+ // open the second group
+ await testUtils.dom.click(list.$('.o_group_header:nth(2)'));
+
+ // Download
+ await testUtils.dom.click(list.$buttons.find('.o_list_download'));
+
+ list.destroy();
+ });
+
+ QUnit.test('export list view to xls', async function (assert) {
+ assert.expect(3);
+
+ let currencies = {};
+ _.each(this.data.res_currency.records, function (currency) {
+ currencies[currency.id] = currency;
+ });
+
+ let list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: `
+ <tree>
+ <field name="foo"/>
+ <field name="bar"/>
+ <field name="int_field" sum="Total"/>
+ <field name="datetime"/>
+ <field name="amount" widget="monetary"/>
+ <field name="currency_id" invisible="1"/>
+ <field name="o2m"/>
+ <field name="m2o"/>
+ <field name="m2m"/>
+ </tree>`,
+ session: {
+ currencies: currencies,
+ get_file(args) {
+ let data = JSON.parse(args.data.data);
+ assert.strictEqual(args.url, '/web/list/export_xls',
+ "should call get_file with the correct url");
+ assert.deepEqual(data.columns, [
+ {field: "foo", string: "Foo"},
+ {field: "bar", string: "Bar"},
+ {field: "int_field", aggregateValue: 32, string: "int_field"},
+ {field: "datetime", string: "Datetime Field"},
+ {field: "amount", string: "Monetary field"},
+ {field: "o2m", string: "O2M field"},
+ {field: "m2o", string: "M2O field"},
+ {field: "m2m", string: "M2M field"},
+ ], "columns should be in the correct order with the string and aggregate value")
+ assert.deepEqual(data.groups[0], {
+ isGrouped: false,
+ count: 4,
+ aggregateValues: {},
+ hideHeader: true,
+ value: "Undefined",
+ data: [
+ {foo: "yop", bar: "True", int_field: "10", datetime: "12/12/2016 10:55:05", amount: "1200.00€", m2m: "2 records", m2o: "Value 1", o2m: "No records"},
+ {foo: "blip", bar: "True", int_field: "9", datetime: "", amount: "$500.00", m2m: "3 records", m2o: "Value 2", o2m: "No records"},
+ {foo: "gnap", bar: "True", int_field: "17", datetime: "", amount: "$300.00", m2m: "No records", m2o: "Value 1", o2m: "No records"},
+ {foo: "blip", bar: "False", int_field: "-4", datetime: "", amount: "$0.00", m2m: "1 record", m2o: "Value 1", o2m: "No records"},
+ ],
+ });
+ args.complete();
+ },
+ },
+ });
+
+ // Download
+ await testUtils.dom.click(list.$buttons.find('.o_list_download'));
+
+ list.destroy();
+ });
+
QUnit.test('grouped list on selection field at level 2', async function (assert) {
assert.expect(4);
@@ -3475,7 +3807,7 @@ QUnit.module('Views', {
});
QUnit.test('edition: create new line, then discard', async function (assert) {
- assert.expect(8);
+ assert.expect(11);
var list = await createView({
View: ListView,
@@ -3490,11 +3822,13 @@ QUnit.module('Views', {
"create button should be visible");
assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 0,
"discard button should be hidden");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
await testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
assert.strictEqual(list.$buttons.find('.o_list_button_add:visible').length, 0,
"create button should be hidden");
assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 1,
"discard button should be visible");
+ assert.containsNone(list, '.o_list_record_selector input:enabled');
await testUtils.dom.click(list.$buttons.find('.o_list_button_discard'));
assert.containsN(list, 'tr.o_data_row', 4,
"should still have 4 records");
@@ -3502,6 +3836,8 @@ QUnit.module('Views', {
"create button should be visible again");
assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 0,
"discard button should be hidden again");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
list.destroy();
});
@@ -4122,37 +4458,6 @@ QUnit.module('Views', {
form.destroy();
});
- QUnit.test('navigation with tab in editable list with only readonly fields', async function (assert) {
- assert.expect(6);
-
- var list = await createView({
- View: ListView,
- model: 'foo',
- data: this.data,
- arch: '<tree editable="bottom">' +
- '<field name="m2o" attrs="{\'readonly\': [(\'int_field\', \'>\', 9)]}"/>' +
- '<field name="int_field" readonly="1"/>' +
- '</tree>',
- });
-
- assert.hasClass(list.$('.o_data_row:first .o_data_cell:first'), 'o_readonly_modifier');
- assert.doesNotHaveClass(list.$('.o_data_row:nth(1) .o_data_cell:first'), 'o_readonly_modifier');
-
- // try to enter first row in edition
- await testUtils.dom.click(list.$('.o_data_row .o_data_cell:first'));
-
- assert.hasClass(list.$('.o_data_row:first'), 'o_selected_row');
- assert.strictEqual(document.activeElement, list.$('.o_selected_row .o_field_widget[name=m2o]').get(0));
-
- // press tab to move to next focusable field (next line here)
- $(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
- await testUtils.nextTick();
- assert.hasClass(list.$('.o_data_row:nth(1)'), 'o_selected_row');
- assert.strictEqual(document.activeElement, list.$('.o_selected_row .o_field_many2one input').get(0));
-
- list.destroy();
- });
-
QUnit.test('edition, then navigation with tab (with a readonly field)', async function (assert) {
// This test makes sure that if we have 2 cells in a row, the first in
// edit mode, and the second one readonly, then if we edit and press TAB,
@@ -5117,8 +5422,34 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('editable list view: single edition with selected records', async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ arch: `<tree editable="top"><field name="foo"/></tree>`,
+ data: this.data,
+ model: 'foo',
+ View: ListView,
+ });
+
+ // Select first record
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+
+ // Edit the second
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_data_cell:first()'));
+ await testUtils.fields.editInput(list.$('.o_data_row:eq(1) .o_data_cell:first() input'), "oui");
+ await testUtils.dom.click($('.o_list_button_save'));
+
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell:first()').text(), "yop",
+ "First row should remain unchanged");
+ assert.strictEqual(list.$('.o_data_row:eq(1) .o_data_cell:first()').text(), "oui",
+ "Second row should have been updated");
+
+ list.destroy();
+ });
+
QUnit.test('editable list view: multi edition', async function (assert) {
- assert.expect(22);
+ assert.expect(24);
var list = await createView({
View: ListView,
@@ -5167,8 +5498,6 @@ QUnit.module('Views', {
assert.verifySteps(['create', 'read']);
// edit a field
- // We deliberately unselect the first row to check the multi edit union (recordId + selectedRecordIds)
- await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'), 666);
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
@@ -5176,6 +5505,8 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal .btn:contains(Cancel)'));
assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), 'yop10',
"changes have been discarded and row is back to readonly");
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:eq(0) .o_data_cell:eq(1)')[0],
+ "focus should be given to the most recently edited cell after discard");
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'), 666);
await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_data_cell:eq(0)'));
@@ -5188,12 +5519,70 @@ QUnit.module('Views', {
assert.strictEqual(list.$('.o_data_row:eq(1) .o_data_cell').text(), "blip666",
"the second row should be updated");
assert.containsNone(list, '.o_data_cell input.o_field_widget', "no field should be editable anymore");
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:eq(0) .o_data_cell:eq(1)')[0],
+ "focus should be given to the most recently edited cell after confirm");
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: multi edition and onchanges', async function (assert) {
+ assert.expect(10);
+
+ this.data.foo.onchanges = {
+ foo: function (obj) {
+ obj.int_field = obj.foo.length;
+ },
+ };
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (args.method === 'write') {
+ args.args[1].int_field = args.args[1].foo.length;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // edit a single record to validate that onchanges work
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+ await testUtils.fields.editInput(list.$('.o_field_widget[name=foo]'), 'hi');
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
+ await testUtils.dom.click(list.$('.o_list_button_discard'));
+
+ assert.containsOnce(document.body, '.modal'); // discard dialog
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ assert.verifySteps(['/web/dataset/search_read', 'onchange']);
+
+ // select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ // edit foo, first row
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+ await testUtils.fields.editInput(list.$('.o_field_widget[name=foo]'), 'hello');
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
+
+ assert.containsOnce(document.body, '.modal'); // save dialog
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), "hello5");
+ assert.strictEqual(list.$('.o_data_row:eq(1) .o_data_cell').text(), "hello5");
+
+ assert.verifySteps(['write', 'read'], "should not perform the onchange in multi edition");
list.destroy();
});
QUnit.test('editable list view: multi edition error and cancellation handling', async function (assert) {
- assert.expect(3);
+ assert.expect(12);
var list = await createView({
View: ListView,
@@ -5201,25 +5590,151 @@ QUnit.module('Views', {
data: this.data,
arch: '<tree editable="bottom">' +
'<field name="foo" required="1"/>' +
+ '<field name="int_field"/>' +
'</tree>',
});
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
// select two records
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
// edit a line and cancel
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+ assert.containsNone(list, '.o_list_record_selector input:enabled');
await testUtils.fields.editInput(list.$('.o_selected_row .o_field_widget[name=foo]'), "abc");
await testUtils.dom.click($('.modal .btn:contains("Cancel")'));
- assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), 'yop', "first cell should have discarded any change");
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), 'yop10', "first cell should have discarded any change");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
+ // edit a line with an invalid format type
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
+ assert.containsNone(list, '.o_list_record_selector input:enabled');
+ await testUtils.fields.editInput(list.$('.o_selected_row .o_field_widget[name=int_field]'), "hahaha");
+ assert.containsOnce(document.body, '.modal', "there should be an opened modal");
+ await testUtils.dom.click($('.modal .btn-primary'));
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), 'yop10', "changes should be discarded");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
// edit a line with an invalid value
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+ assert.containsNone(list, '.o_list_record_selector input:enabled');
await testUtils.fields.editInput(list.$('.o_selected_row .o_field_widget[name=foo]'), "");
assert.containsOnce(document.body, '.modal', "there should be an opened modal");
await testUtils.dom.click($('.modal .btn-primary'));
- assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), 'yop', "changes should be discarded");
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), 'yop10', "changes should be discarded");
+ assert.containsN(list, '.o_list_record_selector input:enabled', 5);
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: multi edition of many2one: set same value', async function (assert) {
+ assert.expect(4);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="m2o"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args, [[1, 2, 3, 4], { m2o: 1 }],
+ "should force write value on all selected records");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(list.$('.o_list_many2one').text(), "Value 1Value 2Value 1Value 1");
+
+ // select all records (the first one has value 1 for m2o)
+ await testUtils.dom.click(list.$('thead .o_list_record_selector input'));
+
+ // set m2o to 1 in first record
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
+ await testUtils.fields.many2one.searchAndClickItem('m2o', {search: 'Value 1'});
+
+ assert.containsOnce(document.body, '.modal');
+
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ assert.strictEqual(list.$('.o_list_many2one').text(), "Value 1Value 1Value 1Value 1");
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: clicking on "Discard changes" in multi edition', async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ arch: `
+ <tree editable="top">
+ <field name="foo"/>
+ </tree>`,
+ data: this.data,
+ model: 'foo',
+ View: ListView,
+ });
+
+ // select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
+ list.$('.o_data_row:first() .o_data_cell:first() input').val("oof");
+
+ const $discardButton = list.$buttons.find('.o_list_button_discard');
+
+ // Simulates an actual click (event chain is: mousedown > change > blur > focus > mouseup > click)
+ await testUtils.dom.triggerEvents($discardButton, ['mousedown']);
+ await testUtils.dom.triggerEvents(list.$('.o_data_row:first() .o_data_cell:first() input'),
+ ['change', 'blur', 'focusout']);
+ await testUtils.dom.triggerEvents($discardButton, ['focus']);
+ $discardButton[0].dispatchEvent(new MouseEvent('mouseup'));
+ await testUtils.dom.click($discardButton);
+
+ assert.ok($('.modal').text().includes("Warning"), "Modal should ask to discard changes");
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ assert.strictEqual(list.$('.o_data_row:first() .o_data_cell:first()').text(), "yop");
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view (multi edition): mousedown on "Discard", but mouseup somewhere else', async function (assert) {
+ assert.expect(1);
+
+ const list = await createView({
+ arch: `
+ <tree editable="top">
+ <field name="foo"/>
+ </tree>`,
+ data: this.data,
+ model: 'foo',
+ View: ListView,
+ });
+
+ // select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
+ list.$('.o_data_row:first() .o_data_cell:first() input').val("oof");
+
+ // Simulates a pseudo drag and drop
+ await testUtils.dom.triggerEvents(list.$buttons.find('.o_list_button_discard'), ['mousedown']);
+ await testUtils.dom.triggerEvents(list.$('.o_data_row:first() .o_data_cell:first() input'),
+ ['change', 'blur', 'focusout']);
+ await testUtils.dom.triggerEvents($(document.body), ['focus']);
+ window.dispatchEvent(new MouseEvent('mouseup'));
+ await testUtils.nextTick();
+
+ assert.ok($('.modal').text().includes("Confirmation"), "Modal should ask to save changes");
+ await testUtils.dom.click($('.modal .btn-primary'));
list.destroy();
});
@@ -5265,6 +5780,65 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('editable list view: many2one with readonly modifier', async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ arch:
+ `<tree editable="top">
+ <field name="m2o" readonly="1"/>
+ <field name="foo"/>
+ </tree>`,
+ data: this.data,
+ model: 'foo',
+ View: ListView,
+ });
+
+ // edit a field
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
+
+ assert.containsOnce(list, '.o_data_row:eq(0) .o_data_cell:eq(0) a[name="m2o"]');
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:eq(0) .o_data_cell:eq(1) input')[0],
+ "focus should go to the char input");
+
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: multi edition: edit and validate last row', async function (assert) {
+ assert.expect(3);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '</tree>',
+ // in this test, we want to accurately mock what really happens, that is, input
+ // fields only trigger their changes on 'change' event, not on 'input'
+ fieldDebounce: 100000,
+ });
+
+ assert.containsN(list, '.o_data_row', 4);
+
+ // select all records
+ await testUtils.dom.click(list.$('.o_list_view thead .o_list_record_selector input'));
+
+ // edit last cell of last line
+ await testUtils.dom.click(list.$('.o_data_row:last .o_data_cell:last'));
+ testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'), '666');
+ await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row .o_data_cell:last input'), 'enter');
+
+ assert.containsOnce(document.body, '.modal');
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ assert.containsN(list, '.o_data_row', 4,
+ "should not create a new row as we were in multi edition");
+
+ list.destroy();
+ });
+
QUnit.test('list grouped by date:month', async function (assert) {
assert.expect(1);
diff --git a/addons/web/static/tests/views/mobile_control_panel_tests.js b/addons/web/static/tests/views/mobile_control_panel_tests.js
index 19226741474..be0d197ef44 100644
--- a/addons/web/static/tests/views/mobile_control_panel_tests.js
+++ b/addons/web/static/tests/views/mobile_control_panel_tests.js
@@ -39,8 +39,61 @@ QUnit.module('Mobile Search view Screen', {
};
},
}, function () {
- QUnit.test('can activate a filter with mobile search view', async function (assert) {
- assert.expect(3);
+ QUnit.test('can activate a filter with mobile search view in quick search mode', async function (assert) {
+ assert.expect(7);
+
+ var searchRPCFlag = false;
+
+ var actionManager = await createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (searchRPCFlag) {
+ assert.deepEqual(args.domain, [['foo', 'ilike', 'A']],
+ "domain should have been properly transferred to list view");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await actionManager.doAction(1);
+
+ assert.strictEqual($('button.o_enable_searchview.fa-search').length, 1,
+ "should display a button to open the searchview");
+ assert.deepEqual($('.o_searchview_input_container:visible').length, 0,
+ "Quick search input should be hidden");
+
+ // open the search view
+ await testUtils.dom.click($('button.o_enable_searchview'));
+ assert.deepEqual($('.o_toggle_searchview_full:visible').length, 1,
+ "should display a button to expand the searchview");
+ assert.deepEqual($('.o_searchview_input_container:visible').length, 1,
+ "Quick search input should now be visible");
+
+ searchRPCFlag = true;
+
+ // use quick search input
+ $('.o_searchview_input')
+ .val("A")
+ .trigger($.Event('keypress', { which: 65, keyCode: 65 }));
+ await testUtils.nextTick();
+ actionManager.$('.o_searchview_input')
+ .trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER }));
+ await testUtils.nextTick();
+
+ // close quick search
+ await testUtils.dom.click($('button.o_enable_searchview.fa-close'));
+ assert.deepEqual($('.o_toggle_searchview_full:visible').length, 0,
+ "Expand icon shoud be hidden");
+ assert.deepEqual($('.o_searchview_input_container:visible').length, 0,
+ "Quick search input should be hidden");
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('can activate a filter with mobile search view in full screen mode', async function (assert) {
+ assert.expect(4);
var filterActiveFlag = false;
@@ -61,20 +114,25 @@ QUnit.module('Mobile Search view Screen', {
assert.ok(!$('.o_mobile_search').is(':visible'),
'mobile search view is not visible');
+
// open the search view
await testUtils.dom.click($('button.o_enable_searchview'));
-
+ // open it in full screen
+ await testUtils.dom.click($('.o_toggle_searchview_full'));
assert.ok($('.o_mobile_search').is(':visible'),
'mobile search view is visible');
// open filter sub menu
await testUtils.dom.click($('button.o_dropdown_toggler_btn').first());
-
filterActiveFlag = true;
-
// click on Active filter
await testUtils.dom.click($('.o_filters_menu a:contains(Active)'));
+ // closing search view
+ await testUtils.dom.click($('.o_mobile_search_close'));
+ assert.ok(!$('.o_mobile_search').is(':visible'),
+ 'mobile search view is not visible');
+
actionManager.destroy();
});
diff --git a/addons/web/static/tests/views/search_panel_tests.js b/addons/web/static/tests/views/search_panel_tests.js
index e580819e0ac..632158d9b1e 100644
--- a/addons/web/static/tests/views/search_panel_tests.js
+++ b/addons/web/static/tests/views/search_panel_tests.js
@@ -571,6 +571,64 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('category has been archived', async function (assert) {
+ assert.expect(2);
+
+ this.data.company.fields.active = {type: 'boolean', string: 'Archived'};
+ this.data.company.records = [
+ {
+ name: 'Company 5',
+ id: 5,
+ active: true,
+ }, {
+ name: 'child of 5 archived',
+ parent_id: 5,
+ id: 666,
+ active: false,
+ }, {
+ name: 'child of 666',
+ parent_id: 666,
+ id: 777,
+ active: true,
+ }
+ ];
+
+ var kanban = await createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div>
+ <field name="foo"/>
+ </div>
+ </t>
+ </templates>
+ </kanban>`,
+ archs: {
+ 'partner,false,search': `<search><searchpanel><field name="company_id"/></searchpanel></search>`,
+ },
+ mockRPC: async function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/search_panel_select_range') {
+ var results = await this._super.apply(this, arguments);
+ results.values = results.values.filter(rec => rec.active !== false);
+ return Promise.resolve(results);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsN(kanban, '.o_search_panel_category_value', 2,
+ 'The number of categories should be 2: All and Company 5');
+
+ assert.containsNone(kanban, '.o_toggle_fold',
+ 'None of the categories should have children');
+
+ kanban.destroy();
+ });
+
QUnit.test('use two categories to refine search', async function (assert) {
assert.expect(14);
@@ -2064,27 +2122,57 @@ QUnit.module('Views', {
actionManager.destroy();
});
- QUnit.test('disable search panel onExecuteAction', async function (assert) {
- assert.expect(6);
+ QUnit.test('after onExecuteAction, selects "All" as default category value', async function (assert) {
+ assert.expect(4);
+
+ var Storage = RamStorage.extend({
+ getItem: function (key) {
+ assert.step('getItem ' + key);
+ return 3; // 'asustek'
+ },
+ setItem: function (key, value) {
+ assert.step('setItem ' + key + ' to ' + value);
+ },
+ });
+ var RamStorageService = AbstractStorageService.extend({
+ storage: new Storage(),
+ });
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
- services: this.services,
+ services: {
+ local_storage: RamStorageService,
+ },
});
await actionManager.doAction(2);
-
await testUtils.dom.click(actionManager.$('.o_form_view button:contains("multi view")'));
+
assert.containsOnce(actionManager, '.o_kanban_view');
- assert.containsNone(actionManager, '.o_search_panel');
+ assert.containsOnce(actionManager, '.o_search_panel');
+ assert.containsOnce(actionManager, '.o_search_panel_category_value:first .active');
- await testUtils.dom.click(actionManager.$('.o_cp_switch_list'));
- assert.containsOnce(actionManager, '.o_list_view');
- assert.containsNone(actionManager, '.o_search_panel');
+ assert.verifySteps([]); // should not communicate with localStorage
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('search panel is not instantiated if stated in context', async function (assert) {
+ assert.expect(2);
+
+ this.actions[0].context = {search_panel: false};
+ var actionManager = await createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ services: this.services,
+ });
+
+ await actionManager.doAction(2);
+ await testUtils.dom.click(actionManager.$('.o_form_view button:contains("multi view")'));
- await testUtils.dom.click(actionManager.$('.o_cp_switch_kanban'));
assert.containsOnce(actionManager, '.o_kanban_view');
assert.containsNone(actionManager, '.o_search_panel');
diff --git a/addons/web/static/tests/widgets/domain_selector_tests.js b/addons/web/static/tests/widgets/domain_selector_tests.js
index 305315b72c6..8c246fd8399 100644
--- a/addons/web/static/tests/widgets/domain_selector_tests.js
+++ b/addons/web/static/tests/widgets/domain_selector_tests.js
@@ -174,6 +174,7 @@ QUnit.module('DomainSelector', {
await testUtils.dom.clickFirst($('.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]'));
assert.notEqual(domainSelector.$(".o_datepicker:visible input").val(), val,
"datepicker value should have changed");
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget a[data-action=close]'));
domainSelector.destroy();
});