diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/tests/fields/basic_fields_tests.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/tests/fields/basic_fields_tests.js')
| -rw-r--r-- | addons/web/static/tests/fields/basic_fields_tests.js | 7807 |
1 files changed, 7807 insertions, 0 deletions
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js new file mode 100644 index 00000000..4ccdd292 --- /dev/null +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -0,0 +1,7807 @@ +odoo.define('web.basic_fields_tests', function (require) { +"use strict"; + +var ajax = require('web.ajax'); +var basicFields = require('web.basic_fields'); +var concurrency = require('web.concurrency'); +var config = require('web.config'); +var core = require('web.core'); +var FormView = require('web.FormView'); +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; +var patchDate = testUtils.mock.patchDate; + +var DebouncedField = basicFields.DebouncedField; +var JournalDashboardGraph = basicFields.JournalDashboardGraph; +var _t = core._t; + +// Base64 images for testing purpose +const MY_IMAGE = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='; +const PRODUCT_IMAGE = 'R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7'; +const FR_FLAG_URL = '/base/static/img/country_flags/fr.png'; +const EN_FLAG_URL = '/base/static/img/country_flags/gb.png'; + + +QUnit.module('fields', {}, function () { + +QUnit.module('basic_fields', { + beforeEach: function () { + this.data = { + partner: { + fields: { + date: {string: "A date", type: "date", searchable: true}, + datetime: {string: "A datetime", type: "datetime", searchable: true}, + display_name: {string: "Displayed name", type: "char", searchable: true}, + foo: {string: "Foo", type: "char", default: "My little Foo Value", searchable: true, trim: true}, + bar: {string: "Bar", type: "boolean", default: true, searchable: true}, + empty_string: {string: "Empty string", type: "char", default: false, searchable: true, trim: true}, + txt: {string: "txt", type: "text", default: "My little txt Value\nHo-ho-hoooo Merry Christmas"}, + int_field: {string: "int_field", type: "integer", sortable: true, searchable: true}, + qux: {string: "Qux", type: "float", digits: [16,1], searchable: true}, + p: {string: "one2many field", type: "one2many", relation: 'partner', searchable: true}, + trululu: {string: "Trululu", type: "many2one", relation: 'partner', searchable: true}, + timmy: {string: "pokemon", type: "many2many", relation: 'partner_type', searchable: true}, + product_id: {string: "Product", type: "many2one", relation: 'product', searchable: true}, + sequence: {type: "integer", string: "Sequence", searchable: true}, + currency_id: {string: "Currency", type: "many2one", relation: "currency", searchable: true}, + selection: {string: "Selection", type: "selection", searchable:true, + selection: [['normal', 'Normal'],['blocked', 'Blocked'],['done', 'Done']]}, + document: {string: "Binary", type: "binary"}, + hex_color: {string: "hexadecimal color", type: "char"}, + }, + records: [{ + id: 1, + date: "2017-02-03", + datetime: "2017-02-08 10:00:00", + display_name: "first record", + bar: true, + foo: "yop", + int_field: 10, + qux: 0.44444, + p: [], + timmy: [], + trululu: 4, + selection: 'blocked', + document: 'coucou==\n', + hex_color: '#ff0000', + }, { + id: 2, + display_name: "second record", + bar: true, + foo: "blip", + int_field: 0, + qux: 0, + p: [], + timmy: [], + trululu: 1, + sequence: 4, + currency_id: 2, + selection: 'normal', + }, { + id: 4, + display_name: "aaa", + foo: "abc", + sequence: 9, + int_field: false, + qux: false, + selection: 'done', + }, + {id: 3, bar: true, foo: "gnap", int_field: 80, qux: -3.89859}, + {id: 5, bar: false, foo: "blop", int_field: -4, qux: 9.1, currency_id: 1}], + onchanges: {}, + }, + product: { + fields: { + name: {string: "Product Name", type: "char", searchable: true} + }, + records: [{ + id: 37, + display_name: "xphone", + }, { + id: 41, + display_name: "xpad", + }] + }, + partner_type: { + fields: { + name: {string: "Partner Type", type: "char", searchable: true}, + color: {string: "Color index", type: "integer", searchable: true}, + }, + records: [ + {id: 12, display_name: "gold", color: 2}, + {id: 14, display_name: "silver", color: 5}, + ] + }, + currency: { + fields: { + digits: { string: "Digits" }, + symbol: {string: "Currency Sumbol", type: "char", searchable: true}, + position: {string: "Currency Position", type: "char", searchable: true}, + }, + records: [{ + id: 1, + display_name: "$", + symbol: "$", + position: "before", + }, { + id: 2, + display_name: "€", + symbol: "€", + position: "after", + }] + }, + "ir.translation": { + fields: { + lang_code: {type: "char"}, + value: {type: "char"}, + res_id: {type: "integer"} + }, + records: [{ + id: 99, + res_id: 37, + value: '', + lang_code: 'en_US' + }] + }, + }; + } +}, function () { + + QUnit.module('DebouncedField'); + + QUnit.test('debounced fields do not trigger call _setValue once destroyed', async function (assert) { + assert.expect(4); + + var def = testUtils.makeTestPromise(); + var _doAction = DebouncedField.prototype._doAction; + DebouncedField.prototype._doAction = function () { + _doAction.apply(this, arguments); + def.resolve(); + }; + var _setValue = DebouncedField.prototype._setValue; + DebouncedField.prototype._setValue = function () { + assert.step('_setValue'); + _setValue.apply(this, arguments); + }; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + fieldDebounce: 3, + viewOptions: { + mode: 'edit', + }, + }); + + // change the value + testUtils.fields.editInput(form.$('input[name=foo]'), 'new value'); + assert.verifySteps([], "_setValue shouldn't have been called yet"); + + // save + await testUtils.form.clickSave(form); + assert.verifySteps(['_setValue'], "_setValue should have been called once"); + + // destroy the form view + def = testUtils.makeTestPromise(); + form.destroy(); + await testUtils.nextMicrotaskTick(); + + // wait for the debounced callback to be called + assert.verifySteps([], + "_setValue should not have been called after widget destruction"); + + DebouncedField.prototype._doAction = _doAction; + DebouncedField.prototype._setValue = _setValue; + }); + + QUnit.module('FieldBoolean'); + + QUnit.test('boolean field in form view', async function (assert) { + assert.expect(13); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form><label for="bar" string="Awesome checkbox"/><field name="bar"/></form>', + res_id: 1, + }); + + assert.containsOnce(form, '.o_field_boolean input:checked', + "checkbox should be checked"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, '.o_field_boolean input:checked', + "checkbox should still be checked"); + + // uncheck the checkbox + await testUtils.dom.click(form.$('.o_field_boolean input:checked')); + assert.containsNone(form, '.o_field_boolean input:checked', + "checkbox should no longer be checked"); + + // save + await testUtils.form.clickSave(form); + assert.containsNone(form, '.o_field_boolean input:checked', + "checkbox should still no longer be checked"); + + // switch to edit mode and test the opposite change + await testUtils.form.clickEdit(form); + assert.containsNone(form, '.o_field_boolean input:checked', + "checkbox should still be unchecked"); + + // check the checkbox + await testUtils.dom.click(form.$('.o_field_boolean input')); + assert.containsOnce(form, '.o_field_boolean input:checked', + "checkbox should now be checked"); + + // uncheck it back + await testUtils.dom.click(form.$('.o_field_boolean input')); + assert.containsNone(form, '.o_field_boolean input:checked', + "checkbox should now be unchecked"); + + // check the checkbox by clicking on label + await testUtils.dom.click(form.$('.o_form_view label:first')); + assert.containsOnce(form, '.o_field_boolean input:checked', + "checkbox should now be checked"); + + // uncheck it back + await testUtils.dom.click(form.$('.o_form_view label:first')); + assert.containsNone(form, '.o_field_boolean input:checked', + "checkbox should now be unchecked"); + + // check the checkbox by hitting the "enter" key after focusing it + await testUtils.dom.triggerEvents(form.$('.o_field_boolean input'), [ + "focusin", + {type: "keydown", which: $.ui.keyCode.ENTER}, + {type: "keyup", which: $.ui.keyCode.ENTER}]); + assert.containsOnce(form, '.o_field_boolean input:checked', + "checkbox should now be checked"); + // blindly press enter again, it should uncheck the checkbox + await testUtils.dom.triggerEvent(document.activeElement, "keydown", + {which: $.ui.keyCode.ENTER}); + assert.containsNone(form, '.o_field_boolean input:checked', + "checkbox should not be checked"); + await testUtils.nextTick(); + // blindly press enter again, it should check the checkbox back + await testUtils.dom.triggerEvent(document.activeElement, "keydown", + {which: $.ui.keyCode.ENTER}); + assert.containsOnce(form, '.o_field_boolean input:checked', + "checkbox should still be checked"); + + // save + await testUtils.form.clickSave(form); + assert.containsOnce(form, '.o_field_boolean input:checked', + "checkbox should still be checked"); + form.destroy(); + }); + + QUnit.test('boolean field in editable list view', async function (assert) { + assert.expect(11); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom"><field name="bar"/></tree>', + }); + + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input').length, 5, + "should have 5 checkboxes"); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input:checked').length, 4, + "should have 4 checked input"); + + // Edit a line + var $cell = list.$('tr.o_data_row:has(.custom-checkbox input:checked) td:not(.o_list_record_selector)').first(); + assert.ok($cell.find('.custom-checkbox input:checked').prop('disabled'), + "input should be disabled in readonly mode"); + await testUtils.dom.click($cell); + assert.ok(!$cell.find('.custom-checkbox input:checked').prop('disabled'), + "input should not have the disabled property in edit mode"); + await testUtils.dom.click($cell.find('.custom-checkbox input:checked')); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + $cell = list.$('tr.o_data_row:has(.custom-checkbox input:not(:checked)) td:not(.o_list_record_selector)').first(); + assert.ok($cell.find('.custom-checkbox input:not(:checked)').prop('disabled'), + "input should be disabled again"); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input').length, 5, + "should still have 5 checkboxes"); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input:checked').length, 3, + "should now have only 3 checked input"); + + // Re-Edit the line and fake-check the checkbox + await testUtils.dom.click($cell); + await testUtils.dom.click($cell.find('.custom-checkbox input')); + await testUtils.dom.click($cell.find('.custom-checkbox input')); + + // Save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input').length, 5, + "should still have 5 checkboxes"); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input:checked').length, 3, + "should still have only 3 checked input"); + + // Re-Edit the line to check the checkbox back but this time click on + // the checkbox directly in readonly mode ! + $cell = list.$('tr.o_data_row:has(.custom-checkbox input:not(:checked)) td:not(.o_list_record_selector)').first(); + await testUtils.dom.click($cell.find('.custom-checkbox .custom-control-label')); + await testUtils.nextTick(); + + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input').length, 5, + "should still have 5 checkboxes"); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input:checked').length, 4, + "should now have 4 checked input back"); + list.destroy(); + }); + + QUnit.module('FieldBooleanToggle'); + + QUnit.test('use boolean toggle widget in form view', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form><field name="bar" widget="boolean_toggle"/></form>', + res_id: 2, + }); + + assert.containsOnce(form, ".custom-checkbox.o_boolean_toggle", "Boolean toggle widget applied to boolean field"); + form.destroy(); + }); + + QUnit.module('FieldToggleButton'); + + QUnit.test('use toggle_button in list view', async function (assert) { + assert.expect(6); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree>' + + '<field name="bar" widget="toggle_button" ' + + 'options="{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}"/>' + + '</tree>', + }); + + assert.containsN(list, 'button i.fa.fa-circle.o_toggle_button_success', 4, + "should have 4 green buttons"); + assert.containsOnce(list, 'button i.fa.fa-circle.text-muted', + "should have 1 muted button"); + + assert.hasAttrValue(list.$('.o_list_view button').first(), 'title', + "Reported in last payslips", "active buttons should have proper tooltip"); + assert.hasAttrValue(list.$('.o_list_view button').last(), 'title', + "To Report in Payslip", "inactive buttons should have proper tooltip"); + + // clicking on first button to check the state is properly changed + await testUtils.dom.click(list.$('.o_list_view button').first()); + assert.containsN(list, 'button i.fa.fa-circle.o_toggle_button_success', 3, + "should have 3 green buttons"); + + await testUtils.dom.click(list.$('.o_list_view button').first()); + assert.containsN(list, 'button i.fa.fa-circle.o_toggle_button_success', 4, + "should have 4 green buttons"); + list.destroy(); + }); + + QUnit.test('toggle_button in form view (edit mode)', async function (assert) { + assert.expect(6); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="bar" widget="toggle_button" ' + + 'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' + + '</form>', + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.step('write'); + } + return this._super.apply(this, arguments); + }, + res_id: 2, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length, + 1, "should be green"); + + // click on the button to toggle the value + await testUtils.dom.click(form.$('.o_field_widget[name=bar]')); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length, + 1, "should be gray"); + assert.verifySteps([]); + + // save + await testUtils.form.clickSave(form); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length, + 1, "should still be gray"); + assert.verifySteps(['write']); + + form.destroy(); + }); + + QUnit.test('toggle_button in form view (readonly mode)', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="bar" widget="toggle_button" ' + + 'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' + + '</form>', + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.step('write'); + } + return this._super.apply(this, arguments); + }, + res_id: 2, + }); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length, + 1, "should be green"); + + // click on the button to toggle the value + await testUtils.dom.click(form.$('.o_field_widget[name=bar]')); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length, + 1, "should be gray"); + assert.verifySteps(['write']); + + form.destroy(); + }); + + QUnit.module('FieldFloat'); + + QUnit.test('float field when unset', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" digits="[5,3]"/>' + + '</sheet>' + + '</form>', + res_id: 4, + }); + + assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty', + 'Non-set float field should be considered as 0.'); + assert.strictEqual(form.$('.o_field_widget').text(), "0.000", + 'Non-set float field should be considered as 0.'); + + form.destroy(); + }); + + QUnit.test('float fields use correct digit precision', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="qux"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + assert.strictEqual(form.$('span.o_field_number:contains(0.4)').length, 1, + "should contain a number rounded to 1 decimal"); + form.destroy(); + }); + + QUnit.test('float field in list view no widget', async function (assert) { + assert.expect(5); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" digits="[5,3]"/>' + + '</sheet>' + + '</form>', + res_id: 2, + }); + + assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty', + 'Float field should be considered set for value 0.'); + assert.strictEqual(form.$('.o_field_widget').first().text(), '0.000', + 'The value should be displayed properly.'); + + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('input[name=qux]').val(), '0.000', + 'The value should be rendered with correct precision.'); + + await testUtils.fields.editInput(form.$('input[name=qux]'), '108.2458938598598'); + assert.strictEqual(form.$('input[name=qux]').val(), '108.2458938598598', + 'The value should not be formated yet.'); + + await testUtils.fields.editInput(form.$('input[name=qux]'), '18.8958938598598'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').first().text(), '18.896', + 'The new value should be rounded properly.'); + + form.destroy(); + }); + + QUnit.test('float field in form view', async function (assert) { + assert.expect(5); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="float" digits="[5,3]"/>' + + '</sheet>' + + '</form>', + res_id: 2, + }); + + assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty', + 'Float field should be considered set for value 0.'); + assert.strictEqual(form.$('.o_field_widget').first().text(), '0.000', + 'The value should be displayed properly.'); + + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('input[name=qux]').val(), '0.000', + 'The value should be rendered with correct precision.'); + + await testUtils.fields.editInput(form.$('input[name=qux]'), '108.2458938598598'); + assert.strictEqual(form.$('input[name=qux]').val(), '108.2458938598598', + 'The value should not be formated yet.'); + + await testUtils.fields.editInput(form.$('input[name=qux]'), '18.8958938598598'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').first().text(), '18.896', + 'The new value should be rounded properly.'); + + form.destroy(); + }); + + QUnit.test('float field using formula in form view', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="float" digits="[5,3]"/>' + + '</sheet>' + + '</form>', + res_id: 2, + }); + + // Test computation with priority of operation + await testUtils.form.clickEdit(form); + await testUtils.fields.editInput(form.$('input[name=qux]'), '=20+3*2'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').first().text(), '26.000', + 'The new value should be calculated properly.'); + + // Test computation with ** operand + await testUtils.form.clickEdit(form); + await testUtils.fields.editInput(form.$('input[name=qux]'), '=2**3'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').first().text(), '8.000', + 'The new value should be calculated properly.'); + + // Test computation with ^ operant which should do the same as ** + await testUtils.form.clickEdit(form); + await testUtils.fields.editInput(form.$('input[name=qux]'), '=2^3'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').first().text(), '8.000', + 'The new value should be calculated properly.'); + + // Test computation and rounding + await testUtils.form.clickEdit(form); + await testUtils.fields.editInput(form.$('input[name=qux]'), '=100/3'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').first().text(), '33.333', + 'The new value should be calculated properly.'); + + form.destroy(); + }); + + QUnit.test('float field using incorrect formula in form view', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="float" digits="[5,3]"/>' + + '</sheet>' + + '</form>', + res_id: 2, + }); + + // Test that incorrect value is not computed + await testUtils.form.clickEdit(form); + await testUtils.fields.editInput(form.$('input[name=qux]'), '=abc'); + await testUtils.form.clickSave(form); + assert.hasClass(form.$('.o_form_view'),'o_form_editable', + "form view should still be editable"); + assert.hasClass(form.$('input[name=qux]'),'o_field_invalid', + "fload field should be displayed as invalid"); + + await testUtils.fields.editInput(form.$('input[name=qux]'), '=3:2?+4'); + await testUtils.form.clickSave(form); + assert.hasClass(form.$('.o_form_view'),'o_form_editable', + "form view should still be editable"); + assert.hasClass(form.$('input[name=qux]'),'o_field_invalid', + "float field should be displayed as invalid"); + + form.destroy(); + }); + + QUnit.test('float field in editable list view', async function (assert) { + assert.expect(4); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom">' + + '<field name="qux" widget="float" digits="[5,3]"/>' + + '</tree>', + }); + + var zeroValues = list.$('td.o_data_cell').filter(function () {return $(this).text() === '';}); + assert.strictEqual(zeroValues.length, 1, + 'Unset float values should be rendered as empty strings.'); + + // switch to edit mode + var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first(); + await testUtils.dom.click($cell); + + assert.containsOnce(list, 'input[name="qux"]', + 'The view should have 1 input for editable float.'); + + await testUtils.fields.editInput(list.$('input[name="qux"]'), '108.2458938598598'); + assert.strictEqual(list.$('input[name="qux"]').val(), '108.2458938598598', + 'The value should not be formated yet.'); + + await testUtils.fields.editInput(list.$('input[name="qux"]'), '18.8958938598598'); + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.strictEqual(list.$('.o_field_widget').first().text(), '18.896', + 'The new value should be rounded properly.'); + + list.destroy(); + }); + + QUnit.test('do not trigger a field_changed if they have not changed', async function (assert) { + assert.expect(2); + + this.data.partner.records[1].qux = false; + this.data.partner.records[1].int_field = false; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="float" digits="[5,3]"/>' + + '<field name="int_field"/>' + + '</sheet>' + + '</form>', + res_id: 2, + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + } + }); + + await testUtils.form.clickEdit(form); + await testUtils.form.clickSave(form); + + assert.verifySteps(['read']); // should not have save as nothing changed + + form.destroy(); + }); + + QUnit.test('float widget on monetary field', async function (assert) { + assert.expect(1); + + this.data.partner.fields.monetary = {string: "Monetary", type: 'monetary'}; + this.data.partner.records[0].monetary = 9.99; + this.data.partner.records[0].currency_id = 1; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="monetary" widget="float"/>' + + '<field name="currency_id" invisible="1"/>' + + '</sheet>' + + '</form>', + res_id: 1, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + assert.strictEqual(form.$('.o_field_widget[name=monetary]').text(), '9.99', + 'value should be correctly formatted (with the float formatter)'); + + form.destroy(); + }); + + QUnit.test('float field with monetary widget and decimal precision', async function (assert) { + assert.expect(5); + + this.data.partner.records = [{ + id: 1, + qux: -8.89859, + currency_id: 1, + }]; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="monetary" options="{\'field_digits\': True}"/>' + + '<field name="currency_id" invisible="1"/>' + + '</sheet>' + + '</form>', + res_id: 1, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + // Non-breaking space between the currency and the amount + assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a0-8.9', + 'The value should be displayed properly.'); + + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '-8.9', + 'The input should be rendered without the currency symbol.'); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().first().text(), '$', + 'The input should be preceded by a span containing the currency symbol.'); + + await testUtils.fields.editInput(form.$('.o_field_monetary input'), '109.2458938598598'); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '109.2458938598598', + 'The value should not be formated yet.'); + + await testUtils.form.clickSave(form); + // Non-breaking space between the currency and the amount + assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a0109.2', + 'The new value should be rounded properly.'); + + form.destroy(); + }); + + QUnit.test('float field with type number option', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="qux" options="{\'type\': \'number\'}"/>' + + '</form>', + res_id: 4, + translateParameters: { + thousands_sep: ",", + grouping: [3, 0], + }, + }); + + await testUtils.form.clickEdit(form); + assert.ok(form.$('.o_field_widget')[0].hasAttribute('type'), + 'Float field with option type must have a type attribute.'); + assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'number', + 'Float field with option type must have a type attribute equals to "number".'); + await testUtils.fields.editInput(form.$('input[name=qux]'), '123456.7890'); + await testUtils.form.clickSave(form); + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget').val(), '123456.789', + 'Float value must be not formatted if input type is number.'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').text(), '123,456.8', + 'Float value must be formatted in readonly view even if the input type is number.'); + + form.destroy(); + }); + + QUnit.test('float field with type number option and comma decimal separator', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="qux" options="{\'type\': \'number\'}"/>' + + '</form>', + res_id: 4, + translateParameters: { + thousands_sep: ".", + decimal_point: ",", + grouping: [3, 0], + }, + }); + + await testUtils.form.clickEdit(form); + assert.ok(form.$('.o_field_widget')[0].hasAttribute('type'), + 'Float field with option type must have a type attribute.'); + assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'number', + 'Float field with option type must have a type attribute equals to "number".'); + await testUtils.fields.editInput(form.$('input[name=qux]'), '123456.789'); + await testUtils.form.clickSave(form); + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget').val(), '123456.789', + 'Float value must be not formatted if input type is number.'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').text(), '123.456,8', + 'Float value must be formatted in readonly view even if the input type is number.'); + + form.destroy(); + }); + + + QUnit.test('float field without type number option', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="qux"/>' + + '</form>', + res_id: 4, + translateParameters: { + thousands_sep: ",", + grouping: [3, 0], + }, + }); + + await testUtils.form.clickEdit(form); + assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'text', + 'Float field with option type must have a text type (default type).'); + + await testUtils.fields.editInput(form.$('input[name=qux]'), '123456.7890'); + await testUtils.form.clickSave(form); + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget').val(), '123,456.8', + 'Float value must be formatted if input type isn\'t number.'); + + form.destroy(); + }); + + QUnit.module('Percentage'); + + QUnit.test('percentage widget in form view', async function (assert) { + assert.expect(6); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` <form string="Partners"> + <field name="qux" widget="percentage"/> + </form>`, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].qux, 0.24, 'the correct float value should be saved'); + } + return this._super(...arguments); + }, + res_id: 1, + }); + + assert.strictEqual(form.$('.o_field_widget').first().text(), '44.4%', + 'The value should be displayed properly.'); + + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '44.4', + 'The input should be rendered without the percentage symbol.'); + assert.strictEqual(form.$('.o_field_widget[name=qux] span').text(), '%', + 'The input should be followed by a span containing the percentage symbol.'); + + await testUtils.fields.editInput(form.$('.o_field_float_percentage input'), '24'); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '24', + 'The value should not be formated yet.'); + + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').text(), '24%', + 'The new value should be formatted properly.'); + + form.destroy(); + }); + + QUnit.module('FieldEmail'); + + QUnit.test('email field in form view', async function (assert) { + assert.expect(7); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo" widget="email"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + var $mailtoLink = form.$('a.o_form_uri.o_field_widget.o_text_overflow'); + assert.strictEqual($mailtoLink.length, 1, + "should have a anchor with correct classes"); + assert.strictEqual($mailtoLink.text(), 'yop', + "the value should be displayed properly"); + assert.hasAttrValue($mailtoLink, 'href', 'mailto:yop', + "should have proper mailto prefix"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, 'input[type="text"].o_field_widget', + "should have an input for the email field"); + assert.strictEqual(form.$('input[type="text"].o_field_widget').val(), 'yop', + "input should contain field value in edit mode"); + + // change value in edit mode + await testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), 'new'); + + // save + await testUtils.form.clickSave(form); + $mailtoLink = form.$('a.o_form_uri.o_field_widget.o_text_overflow'); + assert.strictEqual($mailtoLink.text(), 'new', + "new value should be displayed properly"); + assert.hasAttrValue($mailtoLink, 'href', 'mailto:new', + "should still have proper mailto prefix"); + + form.destroy(); + }); + + QUnit.test('email field in editable list view', async function (assert) { + assert.expect(10); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom"><field name="foo" widget="email"/></tree>', + }); + + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5, + "should have 5 cells"); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yop', + "value should be displayed properly as text"); + + var $mailtoLink = list.$('a.o_form_uri.o_field_widget.o_text_overflow'); + assert.strictEqual($mailtoLink.length, 5, + "should have anchors with correct classes"); + assert.hasAttrValue($mailtoLink.first(), 'href', 'mailto:yop', + "should have proper mailto prefix"); + + // Edit a line and check the result + var $cell = list.$('tbody td:not(.o_list_record_selector)').first(); + await testUtils.dom.click($cell); + assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode'); + assert.strictEqual($cell.find('input').val(), 'yop', + 'should have the corect value in internal input'); + await testUtils.fields.editInput($cell.find('input'), 'new'); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + $cell = list.$('tbody td:not(.o_list_record_selector)').first(); + assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore'); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'new', + "value should be properly updated"); + $mailtoLink = list.$('a.o_form_uri.o_field_widget.o_text_overflow'); + assert.strictEqual($mailtoLink.length, 5, + "should still have anchors with correct classes"); + assert.hasAttrValue($mailtoLink.first(), 'href', 'mailto:new', + "should still have proper mailto prefix"); + + list.destroy(); + }); + + QUnit.test('email field with empty value', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="empty_string" widget="email"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + var $mailtoLink = form.$('a.o_form_uri.o_field_widget.o_text_overflow'); + assert.strictEqual($mailtoLink.text(), '', + "the value should be displayed properly"); + + form.destroy(); + }); + + QUnit.test('email field trim user value', async function (assert) { + assert.expect(1); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form><field name="foo" widget="email"/></form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + await testUtils.fields.editInput(form.$('input[name="foo"]'), ' abc@abc.com '); + await testUtils.form.clickSave(form); + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('input[name="foo"]').val(), 'abc@abc.com', + "Foo value should have been trimmed"); + + form.destroy(); + }); + + + QUnit.module('FieldChar'); + + QUnit.test('char widget isValid method works', async function (assert) { + assert.expect(1); + + this.data.partner.fields.foo.required = true; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="foo"/>' + + '</form>', + res_id: 1, + }); + + var charField = _.find(form.renderer.allFieldWidgets)[0]; + assert.strictEqual(charField.isValid(), true); + form.destroy(); + }); + + QUnit.test('char field in form view', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.strictEqual(form.$('.o_field_widget').text(), 'yop', + "the value should be displayed properly"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, 'input[type="text"].o_field_widget', + "should have an input for the char field"); + assert.strictEqual(form.$('input[type="text"].o_field_widget').val(), 'yop', + "input should contain field value in edit mode"); + + // change value in edit mode + await testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), 'limbo'); + + // save + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').text(), 'limbo', + 'the new value should be displayed'); + form.destroy(); + }); + + QUnit.test('setting a char field to empty string is saved as a false value', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + viewOptions: {mode: 'edit'}, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].foo, false, + 'the foo value should be false'); + } + return this._super.apply(this, arguments); + } + }); + + await testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), ''); + + // save + await testUtils.form.clickSave(form); + form.destroy(); + }); + + QUnit.test('char field with size attribute', async function (assert) { + assert.expect(1); + + this.data.partner.fields.foo.size = 5; // max length + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<sheet>' + + '<group><field name="foo"/></group>' + + '</sheet>' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + assert.hasAttrValue(form.$('input.o_field_widget'), 'maxlength', '5', + "maxlength attribute should have been set correctly on the input"); + + form.destroy(); + }); + + QUnit.test('char field in editable list view', async function (assert) { + assert.expect(6); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom"><field name="foo"/></tree>', + }); + + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5, + "should have 5 cells"); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yop', + "value should be displayed properly as text"); + + // Edit a line and check the result + var $cell = list.$('tbody td:not(.o_list_record_selector)').first(); + await testUtils.dom.click($cell); + assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode'); + assert.strictEqual($cell.find('input').val(), 'yop', + 'should have the corect value in internal input'); + await testUtils.fields.editInput($cell.find('input'), 'brolo'); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + $cell = list.$('tbody td:not(.o_list_record_selector)').first(); + assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore'); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'brolo', + "value should be properly updated"); + list.destroy(); + }); + + QUnit.test('char field translatable', async function (assert) { + assert.expect(12); + + this.data.partner.fields.foo.translate = true; + + var multiLang = _t.database.multi_lang; + _t.database.multi_lang = true; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + session: { + user_context: {lang: 'en_US'}, + }, + mockRPC: function (route, args) { + if (route === "/web/dataset/call_button" && args.method === 'translate_fields') { + assert.deepEqual(args.args, ["partner",1,"foo"], 'should call "call_button" route'); + return Promise.resolve({ + domain: [], + context: {search_default_name: 'partnes,foo'}, + }); + } + if (route === "/web/dataset/call_kw/res.lang/get_installed") { + return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]); + } + if (args.method === "search_read" && args.model == "ir.translation") { + return Promise.resolve([ + {lang: 'en_US', src: 'yop', value: 'yop', id: 42}, + {lang: 'fr_BE', src: 'yop', value: 'valeur français', id: 43} + ]); + } + if (args.method === "write" && args.model == "ir.translation") { + assert.deepEqual(args.args[1], {value: "english value"}, + "the new translation value should be written"); + return Promise.resolve(); + } + return this._super.apply(this, arguments); + }, + }); + await testUtils.form.clickEdit(form); + var $button = form.$('input[type="text"].o_field_char + .o_field_translate'); + assert.strictEqual($button.length, 1, "should have a translate button"); + assert.strictEqual($button.text(), 'EN', 'the button should have as test the current language'); + await testUtils.dom.click($button); + await testUtils.nextTick(); + + assert.containsOnce($(document), '.modal', 'a translate modal should be visible'); + assert.containsN($('.modal .o_translation_dialog'), '.translation', 2, + 'two rows should be visible'); + + var $enField = $('.modal .o_translation_dialog .translation:first() input'); + assert.strictEqual($enField.val(), 'yop', + 'English translation should be filled'); + assert.strictEqual($('.modal .o_translation_dialog .translation:last() input').val(), 'valeur français', + 'French translation should be filled'); + + await testUtils.fields.editInput($enField, "english value"); + await testUtils.dom.click($('.modal button.btn-primary')); // save + await testUtils.nextTick(); + + var $foo = form.$('input[type="text"].o_field_char'); + assert.strictEqual($foo.val(), "english value", + "the new translation was not transfered to modified record"); + + await testUtils.fields.editInput($foo, "new english value"); + + await testUtils.dom.click($button); + await testUtils.nextTick(); + + assert.strictEqual($('.modal .o_translation_dialog .translation:first() input').val(), 'new english value', + 'Modified value should be used instead of translation'); + assert.strictEqual($('.modal .o_translation_dialog .translation:last() input').val(), 'valeur français', + 'French translation should be filled'); + + form.destroy(); + + _t.database.multi_lang = multiLang; + }); + + QUnit.test('html field translatable', async function (assert) { + assert.expect(6); + + this.data.partner.fields.foo.translate = true; + + var multiLang = _t.database.multi_lang; + _t.database.multi_lang = true; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + session: { + user_context: {lang: 'en_US'}, + }, + mockRPC: function (route, args) { + if (route === "/web/dataset/call_button" && args.method === 'translate_fields') { + assert.deepEqual(args.args, ["partner",1,"foo"], 'should call "call_button" route'); + return Promise.resolve({ + domain: [], + context: { + search_default_name: 'partner,foo', + translation_type: 'char', + translation_show_src: true, + }, + }); + } + if (route === "/web/dataset/call_kw/res.lang/get_installed") { + return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]); + } + if (args.method === "search_read" && args.model == "ir.translation") { + return Promise.resolve([ + {lang: 'en_US', src: 'first paragraph', value: 'first paragraph', id: 42}, + {lang: 'en_US', src: 'second paragraph', value: 'second paragraph', id: 43}, + {lang: 'fr_BE', src: 'first paragraph', value: 'premier paragraphe', id: 44}, + {lang: 'fr_BE', src: 'second paragraph', value: 'deuxième paragraphe', id: 45}, + ]); + } + if (args.method === "write" && args.model == "ir.translation") { + assert.deepEqual(args.args[1], {value: "first paragraph modified"}, + "Wrong update on translation"); + return Promise.resolve(); + } + return this._super.apply(this, arguments); + }, + }); + await testUtils.form.clickEdit(form); + var $foo = form.$('input[type="text"].o_field_char'); + + // this will not affect the translate_fields effect until the record is + // saved but is set for consistency of the test + await testUtils.fields.editInput($foo, "<p>first paragraph</p><p>second paragraph</p>"); + + var $button = form.$('input[type="text"].o_field_char + .o_field_translate'); + await testUtils.dom.click($button); + await testUtils.nextTick(); + + assert.containsOnce($(document), '.modal', 'a translate modal should be visible'); + assert.containsN($('.modal .o_translation_dialog'), '.translation', 4, + 'four rows should be visible'); + + var $enField = $('.modal .o_translation_dialog .translation:first() input'); + assert.strictEqual($enField.val(), 'first paragraph', + 'first part of english translation should be filled'); + + await testUtils.fields.editInput($enField, "first paragraph modified"); + await testUtils.dom.click($('.modal button.btn-primary')); // save + await testUtils.nextTick(); + + assert.strictEqual($foo.val(), "<p>first paragraph</p><p>second paragraph</p>", + "the new partial translation should not be transfered"); + + form.destroy(); + + _t.database.multi_lang = multiLang; + }); + + QUnit.test('char field translatable in create mode', async function (assert) { + assert.expect(1); + + this.data.partner.fields.foo.translate = true; + + var multiLang = _t.database.multi_lang; + _t.database.multi_lang = true; + + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + }); + var $button = form.$('input[type="text"].o_field_char + .o_field_translate'); + assert.strictEqual($button.length, 1, "should have a translate button in create mode"); + form.destroy(); + + _t.database.multi_lang = multiLang; + }); + + QUnit.test('char field does not allow html injections', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + await testUtils.fields.editInput(form.$('input[name=foo]'), '<script>throw Error();</script>'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').text(), '<script>throw Error();</script>', + 'the value should have been properly escaped'); + + form.destroy(); + }); + + QUnit.test('char field trim (or not) characters', async function (assert) { + assert.expect(2); + + this.data.partner.fields.foo2 = {string: "Foo2", type: "char", trim: false}; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '<field name="foo2"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + await testUtils.fields.editInput(form.$('input[name="foo"]'), ' abc '); + await testUtils.fields.editInput(form.$('input[name="foo2"]'), ' def '); + + await testUtils.form.clickSave(form); + + // edit mode + await testUtils.form.clickEdit(form); + + assert.strictEqual(form.$('input[name="foo"]').val(), 'abc', 'Foo value should have been trimmed'); + assert.strictEqual(form.$('input[name="foo2"]').val(), ' def ', 'Foo2 value should not have been trimmed'); + + form.destroy(); + }); + + QUnit.test('input field: change value before pending onchange returns', async function (assert) { + assert.expect(3); + + this.data.partner.onchanges = { + product_id: function () {}, + }; + + var def; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<sheet>' + + '<field name="p">' + + '<tree editable="bottom">' + + '<field name="product_id"/>' + + '<field name="foo"/>' + + '</tree>' + + '</field>' + + '</sheet>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === "onchange") { + return Promise.resolve(def).then(function () { + return result; + }); + } else { + return result; + } + }, + viewOptions: { + mode: 'edit', + }, + }); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + assert.strictEqual(form.$('input[name="foo"]').val(), 'My little Foo Value', + 'should contain the default value'); + + def = testUtils.makeTestPromise(); + + await testUtils.fields.many2one.clickOpenDropdown('product_id'); + await testUtils.fields.many2one.clickHighlightedItem('product_id'); + + // set foo before onchange + await testUtils.fields.editInput(form.$('input[name="foo"]'), "tralala"); + assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala', + 'input should contain tralala'); + + // complete the onchange + def.resolve(); + await testUtils.nextTick(); + + assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala', + 'input should contain the same value as before onchange'); + + form.destroy(); + }); + + QUnit.test('input field: change value before pending onchange returns (with fieldDebounce)', async function (assert) { + // this test is exactly the same as the previous one, except that we set + // here a fieldDebounce to accurately reproduce what happens in practice: + // the field doesn't notify the changes on 'input', but on 'change' event. + assert.expect(5); + + this.data.partner.onchanges = { + product_id: function (obj) { + obj.int_field = obj.product_id ? 7 : false; + }, + }; + + let def; + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form> + <field name="p"> + <tree editable="bottom"> + <field name="product_id"/> + <field name="foo"/> + <field name="int_field"/> + </tree> + </field> + </form>`, + async mockRPC(route, args) { + const result = this._super(...arguments); + if (args.method === "onchange") { + await Promise.resolve(def); + } + return result; + }, + fieldDebounce: 5000, + }); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + assert.strictEqual(form.$('input[name="foo"]').val(), 'My little Foo Value', + 'should contain the default value'); + + def = testUtils.makeTestPromise(); + + await testUtils.fields.many2one.clickOpenDropdown('product_id'); + await testUtils.fields.many2one.clickHighlightedItem('product_id'); + + // set foo before onchange + await testUtils.fields.editInput(form.$('input[name="foo"]'), "tralala"); + assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala'); + assert.strictEqual(form.$('input[name="int_field"]').val(), ''); + + // complete the onchange + def.resolve(); + await testUtils.nextTick(); + + assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala', + 'foo should contain the same value as before onchange'); + assert.strictEqual(form.$('input[name="int_field"]').val(), '7', + 'int_field should contain the value returned by the onchange'); + + form.destroy(); + }); + + QUnit.test('input field: change value before pending onchange renaming', async function (assert) { + assert.expect(3); + + this.data.partner.onchanges = { + product_id: function (obj) { + obj.foo = 'on change value'; + }, + }; + + var def = testUtils.makeTestPromise(); + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<sheet>' + + '<field name="product_id"/>' + + '<field name="foo"/>' + + '</sheet>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === "onchange") { + return def.then(function () { + return result; + }); + } else { + return result; + } + }, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual(form.$('input[name="foo"]').val(), 'yop', + 'should contain the correct value'); + + await testUtils.fields.many2one.clickOpenDropdown('product_id'); + await testUtils.fields.many2one.clickHighlightedItem('product_id'); + + // set foo before onchange + testUtils.fields.editInput(form.$('input[name="foo"]'), "tralala"); + assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala', + 'input should contain tralala'); + + // complete the onchange + def.resolve(); + assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala', + 'input should contain the same value as before onchange'); + + form.destroy(); + }); + + QUnit.test('input field: change password value', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="foo" password="True"/>' + + '</form>', + res_id: 1, + }); + + assert.notEqual(form.$('.o_field_char').text(), "yop", + "password field value should not be visible in read mode"); + assert.strictEqual(form.$('.o_field_char').text(), "***", + "password field value should be hidden with '*' in read mode"); + + await testUtils.form.clickEdit(form); + + assert.hasAttrValue(form.$('input.o_field_char'), 'type', 'password', + "password field input should be with type 'password' in edit mode"); + assert.strictEqual(form.$('input.o_field_char').val(), 'yop', + "password field input value should be the (non-hidden) password value"); + + form.destroy(); + }); + + QUnit.test('input field: empty password', async function (assert) { + assert.expect(3); + + this.data.partner.records[0].foo = false; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="foo" password="True"/>' + + '</form>', + res_id: 1, + }); + + assert.strictEqual(form.$('.o_field_char').text(), "", + "password field value should be empty in read mode"); + + await testUtils.form.clickEdit(form); + + assert.hasAttrValue(form.$('input.o_field_char'), 'type', 'password', + "password field input should be with type 'password' in edit mode"); + assert.strictEqual(form.$('input.o_field_char').val(), '', + "password field input value should be the (non-hidden, empty) password value"); + + form.destroy(); + }); + + QUnit.test('input field: set and remove value, then wait for onchange', async function (assert) { + assert.expect(2); + + this.data.partner.onchanges = { + product_id(obj) { + obj.foo = obj.product_id ? "onchange value" : false; + }, + }; + + let def; + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form> + <field name="p"> + <tree editable="bottom"> + <field name="product_id"/> + <field name="foo"/> + </tree> + </field> + </form>`, + async mockRPC(route, args) { + const result = this._super(...arguments); + if (args.method === "onchange") { + await Promise.resolve(def); + } + return result; + }, + fieldDebounce: 1000, // needed to accurately mock what really happens + }); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + assert.strictEqual(form.$('input[name="foo"]').val(), ""); + + await testUtils.fields.editInput(form.$('input[name="foo"]'), "test"); // set value for foo + await testUtils.fields.editInput(form.$('input[name="foo"]'), ""); // remove value for foo + + // trigger the onchange by setting a product + await testUtils.fields.many2one.clickOpenDropdown('product_id'); + await testUtils.fields.many2one.clickHighlightedItem('product_id'); + assert.strictEqual(form.$('input[name="foo"]').val(), 'onchange value', + 'input should contain correct value after onchange'); + + form.destroy(); + }); + + QUnit.module('UrlWidget'); + + QUnit.test('url widget in form view', async function (assert) { + assert.expect(9); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo" widget="url"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.containsOnce(form, 'a.o_form_uri.o_field_widget.o_text_overflow', + "should have a anchor with correct classes"); + assert.hasAttrValue(form.$('a.o_form_uri.o_field_widget.o_text_overflow'), 'href', 'http://yop', + "should have proper href link"); + assert.hasAttrValue(form.$('a.o_form_uri.o_field_widget.o_text_overflow'), 'target', '_blank', + "should have target attribute set to _blank"); + assert.strictEqual(form.$('a.o_form_uri.o_field_widget.o_text_overflow').text(), 'yop', + "the value should be displayed properly"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, 'input[type="text"].o_field_widget', + "should have an input for the char field"); + assert.strictEqual(form.$('input[type="text"].o_field_widget').val(), 'yop', + "input should contain field value in edit mode"); + + // change value in edit mode + testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), 'limbo'); + + // save + await testUtils.form.clickSave(form); + assert.containsOnce(form, 'a.o_form_uri.o_field_widget.o_text_overflow', + "should still have a anchor with correct classes"); + assert.hasAttrValue(form.$('a.o_form_uri.o_field_widget.o_text_overflow'), 'href', 'http://limbo', + "should have proper new href link"); + assert.strictEqual(form.$('a.o_form_uri.o_field_widget.o_text_overflow').text(), 'limbo', + 'the new value should be displayed'); + + form.destroy(); + }); + + QUnit.test('url widget takes text from proper attribute', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="foo" widget="url" text="kebeclibre"/>' + + '</form>', + res_id: 1, + }); + + assert.strictEqual(form.$('a[name="foo"]').text(), 'kebeclibre', + "url text should come from the text attribute"); + form.destroy(); + }); + + QUnit.test('url widget: href attribute and website_path option', async function (assert) { + assert.expect(4); + + this.data.partner.fields.url1 = { string: "Url 1", type: "char", default: "www.url1.com" }; + this.data.partner.fields.url2 = { string: "Url 2", type: "char", default: "www.url2.com" }; + this.data.partner.fields.url3 = { string: "Url 3", type: "char", default: "http://www.url3.com" }; + this.data.partner.fields.url4 = { string: "Url 4", type: "char", default: "https://url4.com" }; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form> + <field name="url1" widget="url"/> + <field name="url2" widget="url" options="{'website_path': True}"/> + <field name="url3" widget="url"/> + <field name="url4" widget="url"/> + </form>`, + res_id: 1, + }); + + assert.strictEqual(form.$('a[name="url1"]').attr('href'), 'http://www.url1.com'); + assert.strictEqual(form.$('a[name="url2"]').attr('href'), 'www.url2.com'); + assert.strictEqual(form.$('a[name="url3"]').attr('href'), 'http://www.url3.com'); + assert.strictEqual(form.$('a[name="url4"]').attr('href'), 'https://url4.com'); + + form.destroy(); + }); + + QUnit.test('char field in editable list view', async function (assert) { + assert.expect(10); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom"><field name="foo" widget="url"/></tree>', + }); + + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5, + "should have 5 cells"); + assert.containsN(list, 'a.o_form_uri.o_field_widget.o_text_overflow', 5, + "should have 5 anchors with correct classes"); + assert.hasAttrValue(list.$('a.o_form_uri.o_field_widget.o_text_overflow').first(), 'href', 'http://yop', + "should have proper href link"); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yop', + "value should be displayed properly as text"); + + // Edit a line and check the result + var $cell = list.$('tbody td:not(.o_list_record_selector)').first(); + await testUtils.dom.click($cell); + assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode'); + assert.strictEqual($cell.find('input').val(), 'yop', + 'should have the corect value in internal input'); + await testUtils.fields.editInput($cell.find('input'), 'brolo'); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + $cell = list.$('tbody td:not(.o_list_record_selector)').first(); + assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore'); + assert.containsN(list, 'a.o_form_uri.o_field_widget.o_text_overflow', 5, + "should still have 5 anchors with correct classes"); + assert.hasAttrValue(list.$('a.o_form_uri.o_field_widget.o_text_overflow').first(), 'href', 'http://brolo', + "should have proper new href link"); + assert.strictEqual(list.$('a.o_form_uri.o_field_widget.o_text_overflow').first().text(), 'brolo', + "value should be properly updated"); + + list.destroy(); + }); + + QUnit.module('CopyClipboard'); + + QUnit.test('Char & Text Fields: Copy to clipboard button', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<div>' + + '<field name="txt" widget="CopyClipboardText"/>' + + '<field name="foo" widget="CopyClipboardChar"/>' + + '</div>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.containsOnce(form, '.o_clipboard_button.o_btn_text_copy',"Should have copy button on text type field"); + assert.containsOnce(form, '.o_clipboard_button.o_btn_char_copy',"Should have copy button on char type field"); + + form.destroy(); + }); + + QUnit.test('CopyClipboard widget on unset field', async function (assert) { + assert.expect(1); + + this.data.partner.records[0].foo = false; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<sheet>' + + '<group>' + + '<field name="foo" widget="CopyClipboardChar" />' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.containsNone(form, '.o_field_copy[name="foo"] .o_clipboard_button', + "foo (unset) should not contain a button"); + + form.destroy(); + }); + + QUnit.test('CopyClipboard widget on readonly unset fields in create mode', async function (assert) { + assert.expect(1); + + this.data.partner.fields.display_name.readonly = true; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<sheet>' + + '<group>' + + '<field name="display_name" widget="CopyClipboardChar" />' + + '</group>' + + '</sheet>' + + '</form>', + }); + + assert.containsNone(form, '.o_field_copy[name="display_name"] .o_clipboard_button', + "the readonly unset field should not contain a button"); + + form.destroy(); + }); + + QUnit.module('FieldText'); + + QUnit.test('text fields are correctly rendered', async function (assert) { + assert.expect(7); + + this.data.partner.fields.foo.type = 'text'; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="int_field"/>' + + '<field name="foo"/>' + + '</form>', + res_id: 1, + }); + + assert.ok(form.$('.o_field_text').length, "should have a text area"); + assert.strictEqual(form.$('.o_field_text').text(), 'yop', 'should be "yop" in readonly'); + + await testUtils.form.clickEdit(form); + + var $textarea = form.$('textarea.o_field_text'); + assert.ok($textarea.length, "should have a text area"); + assert.strictEqual($textarea.val(), 'yop', 'should still be "yop" in edit'); + + testUtils.fields.editInput($textarea, 'hello'); + assert.strictEqual($textarea.val(), 'hello', 'should be "hello" after first edition'); + + testUtils.fields.editInput($textarea, 'hello world'); + assert.strictEqual($textarea.val(), 'hello world', 'should be "hello world" after second edition'); + + await testUtils.form.clickSave(form); + + assert.strictEqual(form.$('.o_field_text').text(), 'hello world', + 'should be "hello world" after save'); + form.destroy(); + }); + + QUnit.test('text fields in edit mode have correct height', async function (assert) { + assert.expect(2); + + this.data.partner.fields.foo.type = 'text'; + this.data.partner.records[0].foo = "f\nu\nc\nk\nm\ni\nl\ng\nr\no\nm"; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="foo"/>' + + '</form>', + res_id: 1, + }); + + var $field = form.$('.o_field_text'); + + assert.strictEqual($field[0].offsetHeight, $field[0].scrollHeight, + "text field should not have a scroll bar"); + + await testUtils.form.clickEdit(form); + + var $textarea = form.$('textarea:first'); + + // the difference is to take small calculation errors into account + assert.strictEqual($textarea[0].clientHeight, $textarea[0].scrollHeight, + "textarea should not have a scroll bar"); + form.destroy(); + }); + + QUnit.test('text fields in edit mode, no vertical resize', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="txt"/>' + + '</form>', + res_id: 1, + }); + + await testUtils.form.clickEdit(form); + + var $textarea = form.$('textarea:first'); + + assert.strictEqual($textarea.css('resize'), 'none', + "should not have vertical resize"); + + form.destroy(); + }); + + QUnit.test('text fields should have correct height after onchange', async function (assert) { + assert.expect(2); + + const damnLongText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Donec est massa, gravida eget dapibus ac, eleifend eget libero. + Suspendisse feugiat sed massa eleifend vestibulum. Sed tincidunt + velit sed lacinia lacinia. Nunc in fermentum nunc. Vestibulum ante + ipsum primis in faucibus orci luctus et ultrices posuere cubilia + Curae; Nullam ut nisi a est ornare molestie non vulputate orci. + Nunc pharetra porta semper. Mauris dictum eu nulla a pulvinar. Duis + eleifend odio id ligula congue sollicitudin. Curabitur quis aliquet + nunc, ut aliquet enim. Suspendisse malesuada felis non metus + efficitur aliquet.`; + + this.data.partner.records[0].txt = damnLongText; + this.data.partner.records[0].bar = false; + this.data.partner.onchanges = { + bar: function (obj) { + obj.txt = damnLongText; + }, + }; + const form = await createView({ + arch: ` + <form string="Partners"> + <field name="bar"/> + <field name="txt" attrs="{'invisible': [('bar', '=', True)]}"/> + </form>`, + data: this.data, + model: 'partner', + res_id: 1, + View: FormView, + viewOptions: { mode: 'edit' }, + }); + + const textarea = form.el.querySelector('textarea[name="txt"]'); + const initialHeight = textarea.offsetHeight; + + await testUtils.fields.editInput($(textarea), 'Short value'); + + assert.ok(textarea.offsetHeight < initialHeight, + "Textarea height should have shrank"); + + await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input')); + await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input')); + + assert.strictEqual(textarea.offsetHeight, initialHeight, + "Textarea height should be reset"); + + form.destroy(); + }); + + QUnit.test('text fields in editable list have correct height', async function (assert) { + assert.expect(2); + + this.data.partner.records[0].txt = "a\nb\nc\nd\ne\nf"; + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<list editable="top">' + + '<field name="foo"/>' + + '<field name="txt"/>' + + '</list>', + }); + + // Click to enter edit: in this test we specifically do not set + // the focus on the textarea by clicking on another column. + // The main goal is to test the resize is actually triggered in this + // particular case. + await testUtils.dom.click(list.$('.o_data_cell:first')); + var $textarea = list.$('textarea:first'); + + // make sure the correct data is there + assert.strictEqual($textarea.val(), this.data.partner.records[0].txt); + + // make sure there is no scroll bar + assert.strictEqual($textarea[0].clientHeight, $textarea[0].scrollHeight, + "textarea should not have a scroll bar"); + + list.destroy(); + }); + + QUnit.test('text fields in edit mode should resize on reset', async function (assert) { + assert.expect(1); + + this.data.partner.fields.foo.type = 'text'; + + this.data.partner.onchanges = { + bar: function (obj) { + obj.foo = 'a\nb\nc\nd\ne\nf'; + }, + }; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="bar"/>' + + '<field name="foo"/>' + + '</form>', + res_id: 1, + }); + + // edit the form + // trigger a textarea reset (through onchange) by clicking the box + // then check there is no scroll bar + await testUtils.form.clickEdit(form); + + await testUtils.dom.click(form.$('div[name="bar"] input')); + + var $textarea = form.$('textarea:first'); + assert.strictEqual($textarea.innerHeight(), $textarea[0].scrollHeight, + "textarea should not have a scroll bar"); + + form.destroy(); + }); + + QUnit.test('text field translatable', async function (assert) { + assert.expect(3); + + this.data.partner.fields.txt.translate = true; + + var multiLang = _t.database.multi_lang; + _t.database.multi_lang = true; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="txt"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + if (route === "/web/dataset/call_button" && args.method === 'translate_fields') { + assert.deepEqual(args.args, ["partner",1,"txt"], 'should call "call_button" route'); + return Promise.resolve({ + domain: [], + context: {search_default_name: 'partnes,foo'}, + }); + } + if (route === "/web/dataset/call_kw/res.lang/get_installed") { + return Promise.resolve([["en_US"], ["fr_BE"]]); + } + return this._super.apply(this, arguments); + }, + }); + await testUtils.form.clickEdit(form); + var $button = form.$('textarea + .o_field_translate'); + assert.strictEqual($button.length, 1, "should have a translate button"); + await testUtils.dom.click($button); + assert.containsOnce($(document), '.modal', 'there should be a translation modal'); + form.destroy(); + _t.database.multi_lang = multiLang; + }); + + QUnit.test('text field translatable in create mode', async function (assert) { + assert.expect(1); + + this.data.partner.fields.txt.translate = true; + + var multiLang = _t.database.multi_lang; + _t.database.multi_lang = true; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="txt"/>' + + '</group>' + + '</sheet>' + + '</form>', + }); + var $button = form.$('textarea + .o_field_translate'); + assert.strictEqual($button.length, 1, "should have a translate button in create mode"); + form.destroy(); + + _t.database.multi_lang = multiLang; + }); + + QUnit.test('go to next line (and not the next row) when pressing enter', async function (assert) { + assert.expect(4); + + this.data.partner.fields.foo.type = 'text'; + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<list editable="top">' + + '<field name="int_field"/>' + + '<field name="foo"/>' + + '<field name="qux"/>' + + '</list>', + }); + + await testUtils.dom.click(list.$('tbody tr:first .o_list_text')); + 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(); + }); + + // Firefox-specific + // Copying from <div style="white-space:pre-wrap"> does not keep line breaks + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1390115 + QUnit.test('copying text fields in RO mode should preserve line breaks', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="txt"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + // Copying from a div tag with white-space:pre-wrap doesn't work in Firefox + assert.strictEqual(form.$('[name="txt"]').prop("tagName").toLowerCase(), 'span', + "the field contents should be surrounded by a span tag"); + + form.destroy(); + }); + + QUnit.module('FieldBinary'); + + QUnit.test('binary fields are correctly rendered', async function (assert) { + assert.expect(16); + + // save the session function + var oldGetFile = session.get_file; + session.get_file = function (option) { + assert.strictEqual(option.data.field, 'document', + "we should download the field document"); + assert.strictEqual(option.data.data, 'coucou==\n', + "we should download the correct data"); + option.complete(); + return Promise.resolve(); + }; + + this.data.partner.records[0].foo = 'coucou.txt'; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="document" filename="foo"/>' + + '<field name="foo"/>' + + '</form>', + res_id: 1, + }); + + assert.containsOnce(form, 'a.o_field_widget[name="document"] > .fa-download', + "the binary field should be rendered as a downloadable link in readonly"); + assert.strictEqual(form.$('a.o_field_widget[name="document"]').text().trim(), 'coucou.txt', + "the binary field should display the name of the file in the link"); + assert.strictEqual(form.$('.o_field_char').text(), 'coucou.txt', + "the filename field should have the file name as value"); + + await testUtils.dom.click(form.$('a.o_field_widget[name="document"]')); + + await testUtils.form.clickEdit(form); + + assert.containsNone(form, 'a.o_field_widget[name="document"] > .fa-download', + "the binary field should not be rendered as a downloadable link in edit"); + assert.strictEqual(form.$('div.o_field_binary_file[name="document"] > input').val(), 'coucou.txt', + "the binary field should display the file name in the input edit mode"); + assert.hasAttrValue(form.$('.o_field_binary_file > input'), 'readonly', 'readonly', + "the input should be readonly"); + assert.containsOnce(form, '.o_field_binary_file > .o_clear_file_button', + "there shoud be a button to clear the file"); + assert.strictEqual(form.$('input.o_field_char').val(), 'coucou.txt', + "the filename field should have the file name as value"); + + + await testUtils.dom.click(form.$('.o_field_binary_file > .o_clear_file_button')); + + assert.isNotVisible(form.$('.o_field_binary_file > input'), + "the input should be hidden"); + assert.strictEqual(form.$('.o_field_binary_file > .o_select_file_button:not(.o_hidden)').length, 1, + "there shoud be a button to upload the file"); + assert.strictEqual(form.$('input.o_field_char').val(), '', + "the filename field should be empty since we removed the file"); + + await testUtils.form.clickSave(form); + assert.containsNone(form, 'a.o_field_widget[name="document"] > .fa-download', + "the binary field should not render as a downloadable link since we removed the file"); + assert.strictEqual(form.$('a.o_field_widget[name="document"]').text().trim(), '', + "the binary field should not display a filename in the link since we removed the file"); + assert.strictEqual(form.$('.o_field_char').text().trim(), '', + "the filename field should be empty since we removed the file"); + + form.destroy(); + + // restore the session function + session.get_file = oldGetFile; + }); + + QUnit.test('binary fields: option accepted_file_extensions', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: `<form string="Partners"> + <field name="document" widget="binary" options="{'accepted_file_extensions': '.dat,.bin'}"/> + </form>` + }); + assert.strictEqual(form.$('input.o_input_file').attr('accept'), '.dat,.bin', + "the input should have the correct ``accept`` attribute"); + form.destroy(); + }); + + QUnit.test('binary fields that are readonly in create mode do not download', async function (assert) { + assert.expect(2); + + // save the session function + var oldGetFile = session.get_file; + session.get_file = function (option) { + assert.step('We shouldn\'t be getting the file.'); + return oldGetFile.bind(session)(option); + }; + + this.data.partner.onchanges = { + product_id: function (obj) { + obj.document = "onchange==\n"; + }, + }; + + this.data.partner.fields.document.readonly = true; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="product_id"/>' + + '<field name="document" filename="\'yooo\'"/>' + + '</form>', + res_id: 1, + }); + + await testUtils.form.clickCreate(form); + await testUtils.fields.many2one.clickOpenDropdown('product_id'); + await testUtils.fields.many2one.clickHighlightedItem('product_id'); + + assert.containsOnce(form, 'a.o_field_widget[name="document"] > .fa-download', + 'The link to download the binary should be present'); + + testUtils.dom.click(form.$('a.o_field_widget[name="document"]')); + + assert.verifySteps([]); // We shouldn't have passed through steps + + form.destroy(); + session.get_file = oldGetFile; + }); + + QUnit.module('FieldPdfViewer'); + + QUnit.test("pdf_viewer without data", async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<field name="document" widget="pdf_viewer"/>' + + '</form>', + }); + + assert.hasClass(form.$('.o_field_widget'), 'o_field_pdfviewer'); + assert.strictEqual(form.$('.o_select_file_button:not(.o_hidden)').length, 1, + "there should be a visible 'Upload' button"); + assert.isNotVisible(form.$('.o_field_widget iframe.o_pdfview_iframe'), + "there should be an invisible iframe"); + assert.containsOnce(form, 'input[type="file"]', + "there should be one input"); + + form.destroy(); + }); + + QUnit.test("pdf_viewer: basic rendering", async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + res_id: 1, + arch: + '<form>' + + '<field name="document" widget="pdf_viewer"/>' + + '</form>', + mockRPC: function (route) { + if (route.indexOf('/web/static/lib/pdfjs/web/viewer.html') !== -1) { + return Promise.resolve(); + } + return this._super.apply(this, arguments); + } + }); + + assert.hasClass(form.$('.o_field_widget'), 'o_field_pdfviewer'); + assert.strictEqual(form.$('.o_select_file_button:not(.o_hidden)').length, 0, + "there should not be a any visible 'Upload' button"); + assert.isVisible(form.$('.o_field_widget iframe.o_pdfview_iframe'), + "there should be an visible iframe"); + assert.hasAttrValue(form.$('.o_field_widget iframe.o_pdfview_iframe'), 'data-src', + '/web/static/lib/pdfjs/web/viewer.html?file=%2Fweb%2Fcontent%3Fmodel%3Dpartner%26field%3Ddocument%26id%3D1#page=1', + "the src attribute should be correctly set on the iframe"); + + form.destroy(); + }); + + QUnit.test("pdf_viewer: upload rendering", async function (assert) { + assert.expect(6); + + testUtils.mock.patch(field_registry.map.pdf_viewer, { + on_file_change: function (ev) { + ev.target = {files: [new Blob()]}; + this._super.apply(this, arguments); + }, + _getURI: function (fileURI) { + this._super.apply(this, arguments); + assert.step('_getURI'); + assert.ok(_.str.startsWith(fileURI, 'blob:')); + this.PDFViewerApplication = { + open: function (URI) { + assert.step('open'); + assert.ok(_.str.startsWith(URI, 'blob:')); + }, + }; + return 'about:blank'; + }, + }); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<field name="document" widget="pdf_viewer"/>' + + '</form>', + }); + + // first upload initialize iframe + form.$('input[type="file"]').trigger('change'); + assert.verifySteps(['_getURI']); + // second upload call pdfjs method inside iframe + form.$('input[type="file"]').trigger('change'); + assert.verifySteps(['open']); + + testUtils.mock.unpatch(field_registry.map.pdf_viewer); + form.destroy(); + }); + + QUnit.test('text field rendering in list view', async function (assert) { + assert.expect(1); + + var data = { + foo: { + fields: {foo: {string: "F", type: "text"}}, + records: [{id: 1, foo: "some text"}] + }, + }; + var list = await createView({ + View: ListView, + model: 'foo', + data: data, + arch: '<tree><field name="foo"/></tree>', + }); + + assert.strictEqual(list.$('tbody td.o_list_text:contains(some text)').length, 1, + "should have a td with the .o_list_text class"); + list.destroy(); + }); + + QUnit.test("binary fields input value is empty whean clearing after uploading", async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="document" filename="foo"/>' + + '<field name="foo"/>' + + '</form>', + res_id: 1, + }); + + await testUtils.form.clickEdit(form); + + // // We need to convert the input type since we can't programmatically set the value of a file input + form.$('.o_input_file').attr('type', 'text').val('coucou.txt'); + + assert.strictEqual(form.$('.o_input_file').val(), 'coucou.txt', + "input value should be changed to \"coucou.txt\""); + + await testUtils.dom.click(form.$('.o_field_binary_file > .o_clear_file_button')); + + assert.strictEqual(form.$('.o_input_file').val(), '', + "input value should be empty"); + + form.destroy(); + }); + + QUnit.test('field text in editable list view', async function (assert) { + assert.expect(1); + + this.data.partner.fields.foo.type = 'text'; + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree string="Phonecalls" editable="top">' + + '<field name="foo"/>' + + '</tree>', + }); + + await testUtils.dom.click(list.$buttons.find('.o_list_button_add')); + + assert.strictEqual(list.$('textarea').first().get(0), document.activeElement, + "text area should have the focus"); + list.destroy(); + }); + + QUnit.module('FieldImage'); + + QUnit.test('image fields are correctly rendered', async function (assert) { + assert.expect(7); + + this.data.partner.records[0].__last_update = '2017-02-08 10:00:00'; + this.data.partner.records[0].document = MY_IMAGE; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="document" widget="image" options="{\'size\': [90, 90]}"/> ' + + '</form>', + res_id: 1, + async mockRPC(route, args) { + if (route === '/web/dataset/call_kw/partner/read') { + assert.deepEqual(args.args[1], ['document', '__last_update', 'display_name'], "The fields document, display_name and __last_update should be present when reading an image"); + } + if (route === `data:image/png;base64,${MY_IMAGE}`) { + assert.ok(true, "should called the correct route"); + return 'wow'; + } + return this._super.apply(this, arguments); + }, + }); + + assert.hasClass(form.$('div[name="document"]'),'o_field_image', + "the widget should have the correct class"); + assert.containsOnce(form, 'div[name="document"] > img', + "the widget should contain an image"); + assert.hasClass(form.$('div[name="document"] > img'),'img-fluid', + "the image should have the correct class"); + assert.hasAttrValue(form.$('div[name="document"] > img'), 'width', "90", + "the image should correctly set its attributes"); + assert.strictEqual(form.$('div[name="document"] > img').css('max-width'), "90px", + "the image should correctly set its attributes"); + form.destroy(); + }); + + QUnit.test('image fields are correctly replaced when given an incorrect value', async function (assert) { + assert.expect(7); + + this.data.partner.records[0].__last_update = '2017-02-08 10:00:00'; + this.data.partner.records[0].document = 'incorrect_base64_value'; + + testUtils.mock.patch(basicFields.FieldBinaryImage, { + // Delay the _render function: this will ensure that the error triggered + // by the incorrect base64 value is dispatched before the src is replaced + // (see test_utils_mock.removeSrcAttribute), since that function is called + // when the element is inserted into the DOM. + async _render() { + const result = this._super.apply(this, arguments); + await concurrency.delay(100); + return result; + }, + }); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form string="Partners"> + <field name="document" widget="image" options="{'size': [90, 90]}"/> + </form>`, + res_id: 1, + async mockRPC(route, args) { + const _super = this._super; + if (route === '/web/static/src/img/placeholder.png') { + assert.step('call placeholder route'); + } + return _super.apply(this, arguments); + }, + }); + + assert.hasClass(form.$('div[name="document"]'),'o_field_image', + "the widget should have the correct class"); + assert.containsOnce(form, 'div[name="document"] > img', + "the widget should contain an image"); + assert.hasClass(form.$('div[name="document"] > img'), 'img-fluid', + "the image should have the correct class"); + assert.hasAttrValue(form.$('div[name="document"] > img'), 'width', "90", + "the image should correctly set its attributes"); + assert.strictEqual(form.$('div[name="document"] > img').css('max-width'), "90px", + "the image should correctly set its attributes"); + + assert.verifySteps(['call placeholder route']); + + form.destroy(); + testUtils.mock.unpatch(basicFields.FieldBinaryImage); + }); + + QUnit.test('image: option accepted_file_extensions', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: `<form string="Partners"> + <field name="document" widget="image" options="{'accepted_file_extensions': '.png,.jpeg'}"/> + </form>` + }); + assert.strictEqual(form.$('input.o_input_file').attr('accept'), '.png,.jpeg', + "the input should have the correct ``accept`` attribute"); + form.destroy(); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: `<form string="Partners"> + <field name="document" widget="image"/> + </form>` + }); + assert.strictEqual(form.$('input.o_input_file').attr('accept'), 'image/*', + 'the default value for the attribute "accept" on the "image" widget must be "image/*"'); + form.destroy(); + }); + + QUnit.test('image fields in subviews are loaded correctly', async function (assert) { + assert.expect(6); + + this.data.partner.records[0].__last_update = '2017-02-08 10:00:00'; + this.data.partner.records[0].document = MY_IMAGE; + this.data.partner_type.fields.image = {name: 'image', type: 'binary'}; + this.data.partner_type.records[0].image = PRODUCT_IMAGE; + this.data.partner.records[0].timmy = [12]; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="document" widget="image" options="{\'size\': [90, 90]}"/>' + + '<field name="timmy" widget="many2many">' + + '<tree>' + + '<field name="display_name"/>' + + '</tree>' + + '<form>' + + '<field name="image" widget="image"/>' + + '</form>' + + '</field>' + + '</form>', + res_id: 1, + async mockRPC(route) { + if (route === `data:image/png;base64,${MY_IMAGE}`) { + assert.step("The view's image should have been fetched"); + return 'wow'; + } + if (route === `data:image/gif;base64,${PRODUCT_IMAGE}`) { + assert.step("The dialog's image should have been fetched"); + return; + } + return this._super.apply(this, arguments); + }, + }); + assert.verifySteps(["The view's image should have been fetched"]); + + assert.containsOnce(form, 'tr.o_data_row', + 'There should be one record in the many2many'); + + // Actual flow: click on an element of the m2m to get its form view + await testUtils.dom.click(form.$('tbody td:contains(gold)')); + assert.strictEqual($('.modal').length, 1, + 'The modal should have opened'); + assert.verifySteps(["The dialog's image should have been fetched"]); + + form.destroy(); + }); + + QUnit.test('image fields in x2many list are loaded correctly', async function (assert) { + assert.expect(2); + + this.data.partner_type.fields.image = {name: 'image', type: 'binary'}; + this.data.partner_type.records[0].image = PRODUCT_IMAGE; + this.data.partner.records[0].timmy = [12]; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="timmy" widget="many2many">' + + '<tree>' + + '<field name="image" widget="image"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + async mockRPC(route) { + if (route === `data:image/gif;base64,${PRODUCT_IMAGE}`) { + assert.ok(true, "The list's image should have been fetched"); + return; + } + return this._super.apply(this, arguments); + }, + }); + + assert.containsOnce(form, 'tr.o_data_row', + 'There should be one record in the many2many'); + + form.destroy(); + }); + + QUnit.test('image fields with required attribute', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="document" required="1" widget="image"/>' + + '</form>', + mockRPC: function (route, args) { + if (args.method === 'create') { + throw new Error("Should not do a create RPC with unset required image field"); + } + return this._super.apply(this, arguments); + }, + }); + + await testUtils.form.clickSave(form); + + assert.hasClass(form.$('.o_form_view'),'o_form_editable', + "form view should still be editable"); + assert.hasClass(form.$('.o_field_widget'),'o_field_invalid', + "image field should be displayed as invalid"); + + form.destroy(); + }); + + /** + * Same tests than for Image fields, but for Char fields with image_url widget. + */ + QUnit.module('FieldChar-ImageUrlWidget', { + beforeEach: function () { + // specific sixth partner data for image_url widget tests + this.data.partner.records.push({id: 6, bar: false, foo: FR_FLAG_URL, int_field: 5, qux: 0.0, timmy: []}); + }, + }); + + QUnit.test('image fields are correctly rendered', async function (assert) { + assert.expect(6); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="foo" widget="image_url" options="{\'size\': [90, 90]}"/> ' + + '</form>', + res_id: 6, + async mockRPC(route, args) { + if (route === FR_FLAG_URL) { + assert.ok(true, "the correct route should have been called."); + } + return this._super.apply(this, arguments); + }, + }); + + assert.hasClass(form.$('div[name="foo"]'), 'o_field_image', + "the widget should have the correct class"); + assert.containsOnce(form, 'div[name="foo"] > img', + "the widget should contain an image"); + assert.hasClass(form.$('div[name="foo"] > img'), 'img-fluid', + "the image should have the correct class"); + assert.hasAttrValue(form.$('div[name="foo"] > img'), 'width', "90", + "the image should correctly set its attributes"); + assert.strictEqual(form.$('div[name="foo"] > img').css('max-width'), "90px", + "the image should correctly set its attributes"); + form.destroy(); + }); + + QUnit.test('image_url widget in subviews are loaded correctly', async function (assert) { + assert.expect(6); + + this.data.partner_type.fields.image = {name: 'image', type: 'char'}; + this.data.partner_type.records[0].image = EN_FLAG_URL; + this.data.partner.records[5].timmy = [12]; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="foo" widget="image_url" options="{\'size\': [90, 90]}"/>' + + '<field name="timmy" widget="many2many">' + + '<tree>' + + '<field name="display_name"/>' + + '</tree>' + + '<form>' + + '<field name="image" widget="image_url"/>' + + '</form>' + + '</field>' + + '</form>', + res_id: 6, + async mockRPC(route) { + if (route === FR_FLAG_URL) { + assert.step("The view's image should have been fetched"); + return 'wow'; + } + if (route === EN_FLAG_URL) { + assert.step("The dialog's image should have been fetched"); + return; + } + return this._super.apply(this, arguments); + }, + }); + assert.verifySteps(["The view's image should have been fetched"]); + + assert.containsOnce(form, 'tr.o_data_row', + 'There should be one record in the many2many'); + + // Actual flow: click on an element of the m2m to get its form view + await testUtils.dom.click(form.$('tbody td:contains(gold)')); + assert.strictEqual($('.modal').length, 1, + 'The modal should have opened'); + assert.verifySteps(["The dialog's image should have been fetched"]); + + form.destroy(); + }); + + QUnit.test('image fields in x2many list are loaded correctly', async function (assert) { + assert.expect(2); + + this.data.partner_type.fields.image = {name: 'image', type: 'char'}; + this.data.partner_type.records[0].image = EN_FLAG_URL; + this.data.partner.records[5].timmy = [12]; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="timmy" widget="many2many">' + + '<tree>' + + '<field name="image" widget="image_url"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 6, + async mockRPC(route) { + if (route === EN_FLAG_URL) { + assert.ok(true, "The list's image should have been fetched"); + return; + } + return this._super.apply(this, arguments); + }, + }); + + assert.containsOnce(form, 'tr.o_data_row', + 'There should be one record in the many2many'); + + form.destroy(); + }); + + QUnit.module('JournalDashboardGraph', { + beforeEach: function () { + _.extend(this.data.partner.fields, { + graph_data: { string: "Graph Data", type: "text" }, + graph_type: { + string: "Graph Type", + type: "selection", + selection: [['line', 'Line'], ['bar', 'Bar']] + }, + }); + this.data.partner.records[0].graph_type = "bar"; + this.data.partner.records[1].graph_type = "line"; + var graph_values = [ + {'value': 300, 'label': '5-11 Dec'}, + {'value': 500, 'label': '12-18 Dec'}, + {'value': 100, 'label': '19-25 Dec'}, + ]; + this.data.partner.records[0].graph_data = JSON.stringify([{ + color: 'red', + title: 'Partner 0', + values: graph_values, + key: 'A key', + area: true, + }]); + this.data.partner.records[1].graph_data = JSON.stringify([{ + color: 'blue', + title: 'Partner 1', + values: graph_values, + key: 'A key', + area: true, + }]); + }, + }); + + QUnit.test('graph dashboard widget attach/detach callbacks', async function (assert) { + // This widget is rendered with Chart.js. + var done = assert.async(); + assert.expect(6); + + testUtils.mock.patch(JournalDashboardGraph, { + on_attach_callback: function () { + assert.step('on_attach_callback'); + }, + on_detach_callback: function () { + assert.step('on_detach_callback'); + }, + }); + + createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test">' + + '<field name="graph_type"/>' + + '<templates><t t-name="kanban-box">' + + '<div>' + + '<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' + + '</div>' + + '</t>' + + '</templates></kanban>', + domain: [['id', 'in', [1, 2]]], + }).then(function (kanban) { + assert.verifySteps([ + 'on_attach_callback', + 'on_attach_callback' + ]); + + kanban.on_detach_callback(); + + assert.verifySteps([ + 'on_detach_callback', + 'on_detach_callback' + ]); + + kanban.destroy(); + testUtils.mock.unpatch(JournalDashboardGraph); + done(); + }); + }); + + QUnit.test('graph dashboard widget is rendered correctly', async function (assert) { + var done = assert.async(); + assert.expect(3); + + createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test">' + + '<field name="graph_type"/>' + + '<templates><t t-name="kanban-box">' + + '<div>' + + '<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' + + '</div>' + + '</t>' + + '</templates></kanban>', + domain: [['id', 'in', [1, 2]]], + }).then(function (kanban) { + concurrency.delay(0).then(function () { + assert.strictEqual(kanban.$('.o_kanban_record:first() .o_graph_barchart').length, 1, + "graph of first record should be a barchart"); + assert.strictEqual(kanban.$('.o_kanban_record:nth(1) .o_dashboard_graph').length, 1, + "graph of second record should be a linechart"); + + // force a re-rendering of the first record (to check if the + // previous rendered graph is correctly removed from the DOM) + var firstRecordState = kanban.model.get(kanban.handle).data[0]; + return kanban.renderer.updateRecord(firstRecordState); + }).then(function () { + return concurrency.delay(0); + }).then(function () { + assert.strictEqual(kanban.$('.o_kanban_record:first() canvas').length, 1, + "there should be only one rendered graph by record"); + + kanban.destroy(); + done(); + }); + }); + }); + + QUnit.test('rendering of a field with dashboard_graph widget in an updated kanban view (ungrouped)', async function (assert) { + + var done = assert.async(); + assert.expect(2); + + createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test">' + + '<field name="graph_type"/>' + + '<templates><t t-name="kanban-box">' + + '<div>' + + '<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' + + '</div>' + + '</t>' + + '</templates></kanban>', + domain: [['id', 'in', [1, 2]]], + }).then(function (kanban) { + concurrency.delay(0).then(function () { + assert.containsN(kanban, '.o_dashboard_graph canvas', 2, "there should be two graph rendered"); + return kanban.update({}); + }).then(function () { + return concurrency.delay(0); // one graph is re-rendered + }).then(function () { + assert.containsN(kanban, '.o_dashboard_graph canvas', 2, "there should be one graph rendered"); + kanban.destroy(); + done(); + }); + }); + }); + + QUnit.test('rendering of a field with dashboard_graph widget in an updated kanban view (grouped)', async function (assert) { + + var done = assert.async(); + assert.expect(2); + + createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test">' + + '<field name="graph_type"/>' + + '<templates><t t-name="kanban-box">' + + '<div>' + + '<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' + + '</div>' + + '</t>' + + '</templates></kanban>', + domain: [['id', 'in', [1, 2]]], + }).then(function (kanban) { + concurrency.delay(0).then(function () { + assert.containsN(kanban, '.o_dashboard_graph canvas', 2, "there should be two graph rendered"); + return kanban.update({groupBy: ['selection'], domain: [['int_field', '=', 10]]}); + }).then(function () { + assert.containsOnce(kanban, '.o_dashboard_graph canvas', "there should be one graph rendered"); + kanban.destroy(); + done(); + }); + }); + }); + + QUnit.module('AceEditor'); + + QUnit.test('ace widget on text fields works', async function (assert) { + assert.expect(2); + var done = assert.async(); + + this.data.partner.fields.foo.type = 'text'; + testUtils.createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="foo" widget="ace"/>' + + '</form>', + res_id: 1, + }).then(function (form) { + assert.ok('ace' in window, "the ace library should be loaded"); + assert.ok(form.$('div.ace_content').length, "should have rendered something with ace editor"); + form.destroy(); + done(); + }); + }); + + QUnit.module('HandleWidget'); + + QUnit.test('handle widget in x2m', async function (assert) { + assert.expect(6); + + this.data.partner.records[0].p = [2, 4]; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="p">' + + '<tree editable="bottom">' + + '<field name="sequence" widget="handle"/>' + + '<field name="display_name"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + assert.strictEqual(form.$('td span.o_row_handle').text(), "", + "handle should not have any content"); + + assert.notOk(form.$('td span.o_row_handle').is(':visible'), + "handle should be invisible in readonly mode"); + + assert.containsN(form, 'span.o_row_handle', 2, "should have 2 handles"); + + await testUtils.form.clickEdit(form); + + assert.hasClass(form.$('td:first'),'o_handle_cell', + "column widget should be displayed in css class"); + + assert.ok(form.$('td span.o_row_handle').is(':visible'), + "handle should be visible in readonly mode"); + + testUtils.dom.click(form.$('td').eq(1)); + assert.containsOnce(form, 'td:first span.o_row_handle', + "content of the cell should have been replaced"); + form.destroy(); + }); + + QUnit.test('handle widget with falsy values', async function (assert) { + assert.expect(1); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree>' + + '<field name="sequence" widget="handle"/>' + + '<field name="display_name"/>' + + '</tree>', + }); + + assert.containsN(list, '.o_row_handle:visible', this.data.partner.records.length, + 'there should be a visible handle for each record'); + list.destroy(); + }); + + + QUnit.module('FieldDateRange'); + + QUnit.test('Datetime field [REQUIRE FOCUS]', async function (assert) { + assert.expect(20); + + this.data.partner.fields.datetime_end = {string: 'Datetime End', type: 'datetime'}; + this.data.partner.records[0].datetime_end = '2017-03-13 00:00:00'; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="datetime" widget="daterange" options="{\'related_end_date\': \'datetime_end\'}"/>' + + '<field name="datetime_end" widget="daterange" options="{\'related_start_date\': \'datetime\'}"/>' + + '</form>', + res_id: 1, + session: { + getTZOffset: function () { + return 330; + }, + }, + }); + + // Check date display correctly in readonly + assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/08/2017 15:30:00', + "the start date should be correctly displayed in readonly"); + assert.strictEqual(form.$('.o_field_date_range:last').text(), '03/13/2017 05:30:00', + "the end date should be correctly displayed in readonly"); + + // Edit + await testUtils.form.clickEdit(form); + + // Check date range picker initialization + assert.containsN(document.body, '.daterangepicker', 2, + "should initialize 2 date range picker"); + assert.strictEqual($('.daterangepicker:first').css('display'), 'block', + "date range picker should be opened initially"); + assert.strictEqual($('.daterangepicker:last').css('display'), 'none', + "date range picker should be closed initially"); + assert.strictEqual($('.daterangepicker:first .drp-calendar.left .active.start-date').text(), '8', + "active start date should be '8' in date range picker"); + assert.strictEqual($('.daterangepicker:first .drp-calendar.left .hourselect').val(), '15', + "active start date hour should be '15' in date range picker"); + assert.strictEqual($('.daterangepicker:first .drp-calendar.left .minuteselect').val(), '30', + "active start date minute should be '30' in date range picker"); + assert.strictEqual($('.daterangepicker:first .drp-calendar.right .active.end-date').text(), '13', + "active end date should be '13' in date range picker"); + assert.strictEqual($('.daterangepicker:first .drp-calendar.right .hourselect').val(), '5', + "active end date hour should be '5' in date range picker"); + assert.strictEqual($('.daterangepicker:first .drp-calendar.right .minuteselect').val(), '30', + "active end date minute should be '30' in date range picker"); + assert.containsN($('.daterangepicker:first .drp-calendar.left .minuteselect'), 'option', 12, + "minute selection should contain 12 options (1 for each 5 minutes)"); + + // Close picker + await testUtils.dom.click($('.daterangepicker:first .cancelBtn')); + assert.strictEqual($('.daterangepicker:first').css('display'), 'none', + "date range picker should be closed"); + + // Try to check with end date + await testUtils.dom.click(form.$('.o_field_date_range:last')); + assert.strictEqual($('.daterangepicker:last').css('display'), 'block', + "date range picker should be opened"); + assert.strictEqual($('.daterangepicker:last .drp-calendar.left .active.start-date').text(), '8', + "active start date should be '8' in date range picker"); + assert.strictEqual($('.daterangepicker:last .drp-calendar.left .hourselect').val(), '15', + "active start date hour should be '15' in date range picker"); + assert.strictEqual($('.daterangepicker:last .drp-calendar.left .minuteselect').val(), '30', + "active start date minute should be '30' in date range picker"); + assert.strictEqual($('.daterangepicker:last .drp-calendar.right .active.end-date').text(), '13', + "active end date should be '13' in date range picker"); + assert.strictEqual($('.daterangepicker:last .drp-calendar.right .hourselect').val(), '5', + "active end date hour should be '5' in date range picker"); + assert.strictEqual($('.daterangepicker:last .drp-calendar.right .minuteselect').val(), '30', + "active end date minute should be '30' in date range picker"); + + form.destroy(); + }); + + QUnit.test('Date field [REQUIRE FOCUS]', async function (assert) { + assert.expect(18); + + this.data.partner.fields.date_end = {string: 'Date End', type: 'date'}; + this.data.partner.records[0].date_end = '2017-02-08'; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="date" widget="daterange" options="{\'related_end_date\': \'date_end\'}"/>' + + '<field name="date_end" widget="daterange" options="{\'related_start_date\': \'date\'}"/>' + + '</form>', + res_id: 1, + session: { + getTZOffset: function () { + return 330; + }, + }, + }); + + // Check date display correctly in readonly + assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/03/2017', + "the start date should be correctly displayed in readonly"); + assert.strictEqual(form.$('.o_field_date_range:last').text(), '02/08/2017', + "the end date should be correctly displayed in readonly"); + + // Edit + await testUtils.form.clickEdit(form); + + // Check date range picker initialization + assert.containsN(document.body, '.daterangepicker', 2, + "should initialize 2 date range picker"); + assert.strictEqual($('.daterangepicker:first').css('display'), 'block', + "date range picker should be opened initially"); + assert.strictEqual($('.daterangepicker:last').css('display'), 'none', + "date range picker should be closed initially"); + assert.strictEqual($('.daterangepicker:first .active.start-date').text(), '3', + "active start date should be '3' in date range picker"); + assert.strictEqual($('.daterangepicker:first .active.end-date').text(), '8', + "active end date should be '8' in date range picker"); + + // Change date + await testUtils.dom.triggerMouseEvent($('.daterangepicker:first .drp-calendar.left .available:contains("16")'), 'mousedown'); + await testUtils.dom.triggerMouseEvent($('.daterangepicker:first .drp-calendar.right .available:contains("12")'), 'mousedown'); + await testUtils.dom.click($('.daterangepicker:first .applyBtn')); + + // Check date after change + assert.strictEqual($('.daterangepicker:first').css('display'), 'none', + "date range picker should be closed"); + assert.strictEqual(form.$('.o_field_date_range:first').val(), '02/16/2017', + "the date should be '02/16/2017'"); + assert.strictEqual(form.$('.o_field_date_range:last').val(), '03/12/2017', + "'the date should be '03/12/2017'"); + + // Try to change range with end date + await testUtils.dom.click(form.$('.o_field_date_range:last')); + assert.strictEqual($('.daterangepicker:last').css('display'), 'block', + "date range picker should be opened"); + assert.strictEqual($('.daterangepicker:last .active.start-date').text(), '16', + "start date should be a 16 in date range picker"); + assert.strictEqual($('.daterangepicker:last .active.end-date').text(), '12', + "end date should be a 12 in date range picker"); + + // Change date + await testUtils.dom.triggerMouseEvent($('.daterangepicker:last .drp-calendar.left .available:contains("13")'), 'mousedown'); + await testUtils.dom.triggerMouseEvent($('.daterangepicker:last .drp-calendar.right .available:contains("18")'), 'mousedown'); + await testUtils.dom.click($('.daterangepicker:last .applyBtn')); + + // Check date after change + assert.strictEqual($('.daterangepicker:last').css('display'), 'none', + "date range picker should be closed"); + assert.strictEqual(form.$('.o_field_date_range:first').val(), '02/13/2017', + "the start date should be '02/13/2017'"); + assert.strictEqual(form.$('.o_field_date_range:last').val(), '03/18/2017', + "the end date should be '03/18/2017'"); + + // Save + await testUtils.form.clickSave(form); + + // Check date after save + assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/13/2017', + "the start date should be '02/13/2017' after save"); + assert.strictEqual(form.$('.o_field_date_range:last').text(), '03/18/2017', + "the end date should be '03/18/2017' after save"); + + form.destroy(); + }); + + QUnit.test('daterangepicker should disappear on scrolling outside of it', async function (assert) { + assert.expect(2); + + this.data.partner.fields.datetime_end = {string: 'Datetime End', type: 'datetime'}; + this.data.partner.records[0].datetime_end = '2017-03-13 00:00:00'; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form> + <field name="datetime" widget="daterange" options="{'related_end_date': 'datetime_end'}"/> + <field name="datetime_end" widget="daterange" options="{'related_start_date': 'datetime'}"/> + </form>`, + res_id: 1, + }); + + await testUtils.form.clickEdit(form); + await testUtils.dom.click(form.$('.o_field_date_range:first')); + + assert.isVisible($('.daterangepicker:first'), "date range picker should be opened"); + + form.el.dispatchEvent(new Event('scroll')); + assert.isNotVisible($('.daterangepicker:first'), "date range picker should be closed"); + + form.destroy(); + }); + + QUnit.test('Datetime field manually input value should send utc value to server', async function (assert) { + assert.expect(4); + + this.data.partner.fields.datetime_end = { string: 'Datetime End', type: 'datetime' }; + this.data.partner.records[0].datetime_end = '2017-03-13 00:00:00'; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form> + <field name="datetime" widget="daterange" options="{'related_end_date': 'datetime_end'}"/> + <field name="datetime_end" widget="daterange" options="{'related_start_date': 'datetime'}"/> + </form>`, + res_id: 1, + session: { + getTZOffset: function () { + return 330; + }, + }, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.deepEqual(args.args[1], { datetime: '2017-02-08 06:00:00' }); + } + return this._super(...arguments); + }, + }); + + // check date display correctly in readonly + assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/08/2017 15:30:00', + "the start date should be correctly displayed in readonly"); + assert.strictEqual(form.$('.o_field_date_range:last').text(), '03/13/2017 05:30:00', + "the end date should be correctly displayed in readonly"); + + // edit form + await testUtils.form.clickEdit(form); + // update input for Datetime + await testUtils.fields.editInput(form.$('.o_field_date_range:first'), '02/08/2017 11:30:00'); + // save form + await testUtils.form.clickSave(form); + + assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/08/2017 11:30:00', + "the start date should be correctly displayed in readonly after manual update"); + + form.destroy(); + }); + + QUnit.test('Daterange field manually input wrong value should show toaster', async function (assert) { + assert.expect(5); + + this.data.partner.fields.date_end = { string: 'Date End', type: 'date' }; + this.data.partner.records[0].date_end = '2017-02-08'; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form> + <field name="date" widget="daterange" options="{'related_end_date': 'date_end'}"/> + <field name="date_end" widget="daterange" options="{'related_start_date': 'date'}"/> + </form>`, + interceptsPropagate: { + call_service: function (ev) { + if (ev.data.service === 'notification') { + assert.strictEqual(ev.data.method, 'notify'); + assert.strictEqual(ev.data.args[0].title, 'Invalid fields:'); + assert.strictEqual(ev.data.args[0].message, '<ul><li>A date</li></ul>'); + } + } + }, + }); + + await testUtils.fields.editInput(form.$('.o_field_date_range:first'), 'blabla'); + // click outside daterange field + await testUtils.dom.click(form.$el); + assert.hasClass(form.$('input[name=date]'), 'o_field_invalid', + "date field should be displayed as invalid"); + // update input date with right value + await testUtils.fields.editInput(form.$('.o_field_date_range:first'), '02/08/2017'); + assert.doesNotHaveClass(form.$('input[name=date]'), 'o_field_invalid', + "date field should not be displayed as invalid now"); + + // again enter wrong value and try to save should raise invalid fields value + await testUtils.fields.editInput(form.$('.o_field_date_range:first'), 'blabla'); + await testUtils.form.clickSave(form); + + form.destroy(); + }); + + QUnit.module('FieldDate'); + + QUnit.test('date field: toggle datepicker [REQUIRE FOCUS]', async function (assert) { + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form><field name="foo"/><field name="date"/></form>', + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + }, + }); + + assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 0, + "datepicker should be closed initially"); + + await testUtils.dom.openDatepicker(form.$('.o_datepicker')); + + assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 1, + "datepicker should be opened"); + + // focus another field + await testUtils.dom.click(form.$('.o_field_widget[name=foo]').focus().mouseenter()); + + assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 0, + "datepicker should close itself when the user clicks outside"); + + form.destroy(); + }); + + QUnit.test('date field: toggle datepicker far in the future', async function (assert) { + assert.expect(3); + + this.data.partner.records = [{ + id: 1, + date: "9999-12-30", + foo: "yop", + }] + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form><field name="foo"/><field name="date"/></form>', + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 0, + "datepicker should be closed initially"); + + testUtils.openDatepicker(form.$('.o_datepicker')); + + assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 1, + "datepicker should be opened"); + + // focus another field + form.$('.o_field_widget[name=foo]').click().focus(); + + assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 0, + "datepicker should close itself when the user clicks outside"); + + form.destroy(); + }); + + QUnit.test('date field is empty if no date is set', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="date"/></form>', + res_id: 4, + }); + var $span = form.$('span.o_field_widget'); + assert.strictEqual($span.length, 1, "should have one span in the form view"); + assert.strictEqual($span.text(), "", "and it should be empty"); + form.destroy(); + }); + + QUnit.test('date field: set an invalid date when the field is already set', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners"><field name="date"/></form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + var $input = form.$('.o_field_widget[name=date] input'); + + assert.strictEqual($input.val(), "02/03/2017"); + + $input.val('mmmh').trigger('change'); + assert.strictEqual($input.val(), "02/03/2017", "should have reset the original value"); + + form.destroy(); + }); + + QUnit.test('date field: set an invalid date when the field is not set yet', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners"><field name="date"/></form>', + res_id: 4, + viewOptions: { + mode: 'edit', + }, + }); + + var $input = form.$('.o_field_widget[name=date] input'); + + assert.strictEqual($input.text(), ""); + + $input.val('mmmh').trigger('change'); + assert.strictEqual($input.text(), "", "The date field should be empty"); + + form.destroy(); + }); + + QUnit.test('date field value should not set on first click', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="date"/></form>', + res_id: 4, + }); + + await testUtils.form.clickEdit(form); + + // open datepicker and select a date + testUtils.dom.openDatepicker(form.$('.o_datepicker')); + assert.strictEqual(form.$('.o_datepicker_input').val(), '', "date field's input should be empty on first click"); + testUtils.dom.click($('.day:contains(22)')); + + // re-open datepicker + testUtils.dom.openDatepicker(form.$('.o_datepicker')); + assert.strictEqual($('.day.active').text(), '22', + "datepicker should be highlight with 22nd day of month"); + + form.destroy(); + }); + + QUnit.test('date field in form view (with positive time zone offset)', async function (assert) { + assert.expect(8); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="date"/></form>', + res_id: 1, + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + assert.strictEqual(args.args[1].date, '2017-02-22', 'the correct value should be saved'); + } + return this._super.apply(this, arguments); + }, + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + }, + session: { + getTZOffset: function () { + return 120; // Should be ignored by date fields + }, + }, + }); + + assert.strictEqual(form.$('.o_field_date').text(), '02/03/2017', + 'the date should be correctly displayed in readonly'); + + // switch to edit mode + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017', + 'the date should be correct in edit mode'); + + // open datepicker and select another value + testUtils.dom.openDatepicker(form.$('.o_datepicker')); + assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open'); + assert.strictEqual($('.day.active').data('day'), '02/03/2017', 'datepicker should be highlight February 3'); + testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first()); + testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)').first()); + testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)')); + testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(1)); + testUtils.dom.click($('.day:contains(22)')); + assert.ok(!$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed'); + assert.strictEqual(form.$('.o_datepicker_input').val(), '02/22/2017', + 'the selected date should be displayed in the input'); + + // save + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_date').text(), '02/22/2017', + 'the selected date should be displayed after saving'); + form.destroy(); + }); + + QUnit.test('date field in form view (with negative time zone offset)', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="date"/></form>', + res_id: 1, + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + }, + session: { + getTZOffset: function () { + return -120; // Should be ignored by date fields + }, + }, + }); + + assert.strictEqual(form.$('.o_field_date').text(), '02/03/2017', + 'the date should be correctly displayed in readonly'); + + // switch to edit mode + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017', + 'the date should be correct in edit mode'); + + form.destroy(); + }); + + QUnit.test('date field dropdown disappears on scroll', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<div class="scrollable" style="height: 2000px;">' + + '<field name="date"/>' + + '</div>' + + '</form>', + res_id: 1, + }); + + await testUtils.form.clickEdit(form); + await testUtils.dom.openDatepicker(form.$('.o_datepicker')); + + assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget', "datepicker should be opened"); + + form.el.dispatchEvent(new Event('wheel')); + assert.containsNone($('body'), '.bootstrap-datetimepicker-widget', "datepicker should be closed"); + + form.destroy(); + }); + + QUnit.test('date field with warn_future option', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="date" options="{\'datepicker\': {\'warn_future\': true}}"/>' + + '</form>', + res_id: 4, + }); + + // switch to edit mode + await testUtils.form.clickEdit(form); + // open datepicker and select another value + await testUtils.dom.openDatepicker(form.$('.o_datepicker')); + 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').eq(11)); + await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(11)); + await testUtils.dom.click($('.day:contains(31)')); + + var $warn = form.$('.o_datepicker_warning:visible'); + assert.strictEqual($warn.length, 1, "should have a warning in the form view"); + + await testUtils.fields.editSelect(form.$('.o_field_widget[name=date] input'), ''); // remove the value + + $warn = form.$('.o_datepicker_warning:visible'); + assert.strictEqual($warn.length, 0, "the warning in the form view should be hidden"); + + form.destroy(); + }); + + QUnit.test('date field with warn_future option: do not overwrite datepicker option', async function (assert) { + assert.expect(2); + + // Making sure we don't have a legit default value + // or any onchange that would set the value + this.data.partner.fields.date.default = undefined; + this.data.partner.onchanges = {}; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="foo" />' + // Do not let the date field get the focus in the first place + '<field name="date" options="{\'datepicker\': {\'warn_future\': true}}"/>' + + '</form>', + res_id: 1, + }); + + // switch to edit mode + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('input[name="date"]').val(), '02/03/2017', + 'The existing record should have a value for the date field'); + + // save with no changes + await testUtils.form.clickSave(form); + + //Create a new record + await testUtils.form.clickCreate(form); + + assert.notOk(form.$('input[name="date"]').val(), + 'The new record should not have a value that the framework would have set'); + + form.destroy(); + }); + + QUnit.test('date field in editable list view', async function (assert) { + assert.expect(8); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom">' + + '<field name="date"/>' + + '</tree>', + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + }, + session: { + getTZOffset: function () { + return 0; + }, + }, + }); + + var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first(); + assert.strictEqual($cell.text(), '02/03/2017', + 'the date should be displayed correctly in readonly'); + await testUtils.dom.click($cell); + + assert.containsOnce(list, 'input.o_datepicker_input', + "the view should have a date input for editable mode"); + + assert.strictEqual(list.$('input.o_datepicker_input').get(0), document.activeElement, + "date input should have the focus"); + + assert.strictEqual(list.$('input.o_datepicker_input').val(), '02/03/2017', + 'the date should be correct in edit mode'); + + // open datepicker and select another value + await testUtils.dom.openDatepicker(list.$('.o_datepicker')); + assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open'); + 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(1)); + await testUtils.dom.click($('.day:contains(22)')); + assert.ok(!$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed'); + assert.strictEqual(list.$('.o_datepicker_input').val(), '02/22/2017', + 'the selected date should be displayed in the input'); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.strictEqual(list.$('tr.o_data_row td:not(.o_list_record_selector)').text(), '02/22/2017', + 'the selected date should be displayed after saving'); + + list.destroy(); + }); + + QUnit.test('date field remove value', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="date"/></form>', + res_id: 1, + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + assert.strictEqual(args.args[1].date, false, 'the correct value should be saved'); + } + return this._super.apply(this, arguments); + }, + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + }, + }); + + // switch to edit mode + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017', + 'the date should be correct in edit mode'); + + await testUtils.fields.editAndTrigger(form.$('.o_datepicker_input'), '', ['input', 'change', 'focusout']); + assert.strictEqual(form.$('.o_datepicker_input').val(), '', + 'should have correctly removed the value'); + + // save + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_date').text(), '', + 'the selected date should be displayed after saving'); + + form.destroy(); + }); + + QUnit.test('do not trigger a field_changed for datetime field with date widget', async function (assert) { + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="datetime" widget="date"/></form>', + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + time_format: '%H:%M:%S', + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(form.$('.o_datepicker_input').val(), '02/08/2017', + 'the date should be correct'); + + testUtils.fields.editAndTrigger(form.$('input[name="datetime"]'),'02/08/2017', ['input', 'change', 'focusout']); + await testUtils.form.clickSave(form); + + assert.verifySteps(['read']); // should not have save as nothing changed + + form.destroy(); + }); + + QUnit.test('field date should select its content onclick when there is one', async function (assert) { + assert.expect(2); + var done = assert.async(); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form><field name="date"/></form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + form.$el.on({ + 'show.datetimepicker': function () { + assert.ok($('.bootstrap-datetimepicker-widget').is(':visible'), + 'bootstrap-datetimepicker is visible'); + assert.strictEqual(window.getSelection().toString(), "02/03/2017", + 'The whole input of the date field should have been selected'); + done(); + } + }); + + testUtils.dom.openDatepicker(form.$('.o_datepicker')); + + form.destroy(); + }); + + QUnit.test('date field support internalization', async function (assert) { + assert.expect(2); + + var originalLocale = moment.locale(); + var originalParameters = _.clone(core._t.database.parameters); + + _.extend(core._t.database.parameters, {date_format: '%d. %b %Y', time_format: '%H:%M:%S'}); + moment.defineLocale('norvegianForTest', { + monthsShort: 'jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.'.split('_'), + monthsParseExact: true, + dayOfMonthOrdinalParse: /\d{1,2}\./, + ordinal: '%d.', + }); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="date"/></form>', + res_id: 1, + }); + + var dateViewForm = form.$('.o_field_date').text(); + await testUtils.dom.click(form.$buttons.find('.o_form_button_edit')); + await testUtils.openDatepicker(form.$('.o_datepicker')); + assert.strictEqual(form.$('.o_datepicker_input').val(), dateViewForm, + "input date field should be the same as it was in the view form"); + + await testUtils.dom.click($('.day:contains(30)')); + var dateEditForm = form.$('.o_datepicker_input').val(); + await testUtils.dom.click(form.$buttons.find('.o_form_button_save')); + assert.strictEqual(form.$('.o_field_date').text(), dateEditForm, + "date field should be the same as the one selected in the view form"); + + moment.locale(originalLocale); + moment.updateLocale('norvegianForTest', null); + core._t.database.parameters = originalParameters; + + form.destroy(); + }); + + QUnit.test('date field: hit enter should update value', async function (assert) { + assert.expect(2); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="date"/></form>', + res_id: 1, + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + }, + viewOptions: { + mode: 'edit', + }, + }); + + const year = (new Date()).getFullYear(); + + await testUtils.fields.editInput(form.el.querySelector('input[name="date"]'), '01/08'); + await testUtils.fields.triggerKeydown(form.el.querySelector('input[name="date"]'), 'enter'); + assert.strictEqual(form.el.querySelector('input[name="date"]').value, '01/08/' + year); + + await testUtils.fields.editInput(form.el.querySelector('input[name="date"]'), '08/01'); + await testUtils.fields.triggerKeydown(form.el.querySelector('input[name="date"]'), 'enter'); + assert.strictEqual(form.el.querySelector('input[name="date"]').value, '08/01/' + year); + + form.destroy(); + }); + + QUnit.module('FieldDatetime'); + + QUnit.test('datetime field in form view', async function (assert) { + assert.expect(7); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners"><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; + }, + }, + }); + + var expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone + assert.strictEqual(form.$('.o_field_date').text(), expectedDateString, + 'the datetime should be correctly displayed in readonly'); + + // switch to edit mode + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_datepicker_input').val(), expectedDateString, + 'the datetime should be correct in edit mode'); + + // datepicker should not open on focus + assert.containsNone($('body'), '.bootstrap-datetimepicker-widget'); + + testUtils.dom.openDatepicker(form.$('.o_datepicker')); + assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget'); + + // select 22 February at 8:23:33 + 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')); + await testUtils.dom.click($('.bootstrap-datetimepicker-widget .second:contains(35)')); + assert.ok(!$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed'); + + var newExpectedDateString = "04/22/2017 08:25:35"; + assert.strictEqual(form.$('.o_datepicker_input').val(), newExpectedDateString, + 'the selected date should be displayed in the input'); + + // save + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_date').text(), newExpectedDateString, + 'the selected date should be displayed after saving'); + + 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); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="txt"/>' + + '<field name="datetime" invisible="True"/></form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + form.$el.find('textarea[name=txt]').trigger($.Event('keydown', { + which: $.ui.keyCode.TAB, + keyCode: $.ui.keyCode.TAB, + })); + assert.strictEqual(document.activeElement, form.$buttons.find('.o_form_button_save')[0], + "the save button should be selected, because the datepicker did not capture the focus"); + form.destroy(); + }); + + QUnit.test('datetime field with datetime formatted without second', async function (assert) { + assert.expect(2); + + this.data.partner.fields.datetime.default = "2017-08-02 12:00:05"; + this.data.partner.fields.datetime.required = true; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="datetime"/></form>', + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + time_format: '%H:%M', + }, + }); + + var expectedDateString = "08/02/2017 12:00"; // 10:00:00 without timezone + assert.strictEqual(form.$('.o_field_date input').val(), expectedDateString, + 'the datetime should be correctly displayed in readonly'); + + await testUtils.form.clickDiscard(form); + + assert.strictEqual($('.modal').length, 0, + "there should not be a Warning dialog"); + + form.destroy(); + }); + + QUnit.test('datetime field in editable list view', async function (assert) { + assert.expect(9); + + 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; + }, + }, + }); + + var expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone + var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first(); + assert.strictEqual($cell.text(), expectedDateString, + 'the datetime should be correctly displayed in readonly'); + + // switch to edit mode + await testUtils.dom.click($cell); + assert.containsOnce(list, 'input.o_datepicker_input', + "the view should have a date input for editable mode"); + + assert.strictEqual(list.$('input.o_datepicker_input').get(0), document.activeElement, + "date input should have the focus"); + + assert.strictEqual(list.$('input.o_datepicker_input').val(), expectedDateString, + 'the date should be correct in edit mode'); + + assert.containsNone($('body'), '.bootstrap-datetimepicker-widget'); + testUtils.dom.openDatepicker(list.$('.o_datepicker')); + assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget'); + + // select 22 February at 8:23:33 + 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')); + await testUtils.dom.click($('.bootstrap-datetimepicker-widget .second:contains(35)')); + assert.ok(!$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed'); + + var newExpectedDateString = "04/22/2017 08:25:35"; + assert.strictEqual(list.$('.o_datepicker_input').val(), newExpectedDateString, + 'the selected datetime should be displayed in the input'); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.strictEqual(list.$('tr.o_data_row td:not(.o_list_record_selector)').text(), newExpectedDateString, + 'the selected datetime should be displayed after saving'); + + 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 multi_edit="1">' + + '<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); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="datetime"/></form>', + res_id: 1, + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + assert.strictEqual(args.args[1].datetime, false, 'the correct value should be saved'); + } + return this._super.apply(this, arguments); + }, + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + time_format: '%H:%M:%S', + }, + session: { + getTZOffset: function () { + return 120; + }, + }, + }); + + // switch to edit mode + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_datepicker_input').val(), '02/08/2017 12:00:00', + 'the date time should be correct in edit mode'); + + await testUtils.fields.editAndTrigger($('.o_datepicker_input'), '', ['input', 'change', 'focusout']); + assert.strictEqual(form.$('.o_datepicker_input').val(), '', + "should have an empty input"); + + // save + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_date').text(), '', + 'the selected date should be displayed after saving'); + + form.destroy(); + }); + + QUnit.test('datetime field with date/datetime widget (with day change)', async function (assert) { + assert.expect(2); + + this.data.partner.records[0].p = [2]; + this.data.partner.records[1].datetime = "2017-02-08 02:00:00"; // UTC + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="p">' + + '<tree>' + + '<field name="datetime"/>' + + '</tree>' + + '<form>' + + '<field name="datetime" widget="date"/>' + + '</form>' + + '</field>' + + '</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 -240; + }, + }, + }); + + var expectedDateString = "02/07/2017 22:00:00"; // local time zone + assert.strictEqual(form.$('.o_field_widget[name=p] .o_data_cell').text(), expectedDateString, + 'the datetime (datetime widget) should be correctly displayed in tree view'); + + // switch to form view + await testUtils.dom.click(form.$('.o_field_widget[name=p] .o_data_row')); + assert.strictEqual($('.modal .o_field_date[name=datetime]').text(), '02/07/2017', + 'the datetime (date widget) should be correctly displayed in form view'); + + form.destroy(); + }); + + QUnit.test('datetime field with date/datetime widget (without day change)', async function (assert) { + assert.expect(2); + + this.data.partner.records[0].p = [2]; + this.data.partner.records[1].datetime = "2017-02-08 10:00:00"; // without timezone + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="p">' + + '<tree>' + + '<field name="datetime"/>' + + '</tree>' + + '<form>' + + '<field name="datetime" widget="date"/>' + + '</form>' + + '</field>' + + '</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 -240; + }, + }, + }); + + var expectedDateString = "02/08/2017 06:00:00"; // with timezone + assert.strictEqual(form.$('.o_field_widget[name=p] .o_data_cell').text(), expectedDateString, + 'the datetime (datetime widget) should be correctly displayed in tree view'); + + // switch to form view + await testUtils.dom.click(form.$('.o_field_widget[name=p] .o_data_row')); + assert.strictEqual($('.modal .o_field_date[name=datetime]').text(), '02/08/2017', + 'the datetime (date widget) should be correctly displayed in form view'); + + form.destroy(); + }); + + QUnit.test('datepicker option: daysOfWeekDisabled', async function (assert) { + assert.expect(42); + + this.data.partner.fields.datetime.default = "2017-08-02 12:00:05"; + this.data.partner.fields.datetime.required = true; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="datetime" ' + + 'options=\'{"datepicker": {"daysOfWeekDisabled": [0, 6]}}\'/>' + + '</form>', + res_id: 1, + }); + + await testUtils.form.clickCreate(form); + testUtils.dom.openDatepicker(form.$('.o_datepicker')); + $.each($('.day:last-child,.day:nth-child(2)'), function (index, value) { + assert.hasClass(value, 'disabled', 'first and last days must be disabled'); + }); + // the assertions below could be replaced by a single hasClass classic on the jQuery set using the idea + // All not <=> not Exists. But we want to be sure that the set is non empty. We don't have an helper + // function for that. + $.each($('.day:not(:last-child):not(:nth-child(2))'), function (index, value) { + assert.doesNotHaveClass(value, 'disabled', 'other days must stay clickable'); + }); + form.destroy(); + }); + + QUnit.module('RemainingDays'); + + QUnit.test('remaining_days widget on a date field in list view', async function (assert) { + assert.expect(16); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11 + this.data.partner.records = [ + { id: 1, date: '2017-10-08' }, // today + { id: 2, date: '2017-10-09' }, // tomorrow + { id: 3, date: '2017-10-07' }, // yesterday + { id: 4, date: '2017-10-10' }, // + 2 days + { id: 5, date: '2017-10-05' }, // - 3 days + { id: 6, date: '2018-02-08' }, // + 4 months (diff >= 100 days) + { id: 7, date: '2017-06-08' }, // - 4 months (diff >= 100 days) + { id: 8, date: false }, + ]; + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree><field name="date" widget="remaining_days"/></tree>', + }); + + assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Yesterday'); + assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'In 2 days'); + assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '3 days ago'); + assert.strictEqual(list.$('.o_data_cell:nth(5)').text(), '02/08/2018'); + assert.strictEqual(list.$('.o_data_cell:nth(6)').text(), '06/08/2017'); + assert.strictEqual(list.$('.o_data_cell:nth(7)').text(), ''); + + assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/08/2017'); + + assert.hasClass(list.$('.o_data_cell:nth(0) div'), 'text-bf text-warning'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(1) div'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(2) div'), 'text-bf text-danger'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(3) div'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(4) div'), 'text-bf text-danger'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(5) div'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(6) div'), 'text-bf text-danger'); + + list.destroy(); + unpatchDate(); + }); + + QUnit.test('remaining_days widget on a date field in form view', async function (assert) { + assert.expect(4); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11 + this.data.partner.records = [ + { id: 1, date: '2017-10-08' }, // today + ]; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form><field name="date" widget="remaining_days"/></form>', + res_id: 1, + }); + + assert.strictEqual(form.$('.o_field_widget').text(), 'Today'); + assert.hasClass(form.$('.o_field_widget'), 'text-bf text-warning'); + + // in edit mode, this widget should not be editable. + await testUtils.form.clickEdit(form); + + assert.hasClass(form.$('.o_form_view'), 'o_form_editable'); + assert.containsOnce(form, 'div.o_field_widget[name=date]'); + + form.destroy(); + unpatchDate(); + }); + + QUnit.test('remaining_days widget on a datetime field in list view in UTC', async function (assert) { + assert.expect(16); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11 + this.data.partner.records = [ + { id: 1, datetime: '2017-10-08 20:00:00' }, // today + { id: 2, datetime: '2017-10-09 08:00:00' }, // tomorrow + { id: 3, datetime: '2017-10-07 18:00:00' }, // yesterday + { id: 4, datetime: '2017-10-10 22:00:00' }, // + 2 days + { id: 5, datetime: '2017-10-05 04:00:00' }, // - 3 days + { id: 6, datetime: '2018-02-08 04:00:00' }, // + 4 months (diff >= 100 days) + { id: 7, datetime: '2017-06-08 04:00:00' }, // - 4 months (diff >= 100 days) + { id: 8, datetime: false }, + ]; + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree><field name="datetime" widget="remaining_days"/></tree>', + session: { + getTZOffset: () => 0, + }, + }); + + assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Yesterday'); + assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'In 2 days'); + assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '3 days ago'); + assert.strictEqual(list.$('.o_data_cell:nth(5)').text(), '02/08/2018'); + assert.strictEqual(list.$('.o_data_cell:nth(6)').text(), '06/08/2017'); + assert.strictEqual(list.$('.o_data_cell:nth(7)').text(), ''); + + assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/08/2017'); + + assert.hasClass(list.$('.o_data_cell:nth(0) div'), 'text-bf text-warning'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(1) div'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(2) div'), 'text-bf text-danger'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(3) div'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(4) div'), 'text-bf text-danger'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(5) div'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(6) div'), 'text-bf text-danger'); + + list.destroy(); + unpatchDate(); + }); + + QUnit.test('remaining_days widget on a datetime field in list view in UTC+6', async function (assert) { + assert.expect(6); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11, UTC+6 + this.data.partner.records = [ + { id: 1, datetime: '2017-10-08 20:00:00' }, // tomorrow + { id: 2, datetime: '2017-10-09 08:00:00' }, // tomorrow + { id: 3, datetime: '2017-10-07 18:30:00' }, // today + { id: 4, datetime: '2017-10-07 12:00:00' }, // yesterday + { id: 5, datetime: '2017-10-09 20:00:00' }, // + 2 days + ]; + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree><field name="datetime" widget="remaining_days"/></tree>', + session: { + getTZOffset: () => 360, + }, + }); + + assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'Yesterday'); + assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), 'In 2 days'); + + assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/09/2017'); + + list.destroy(); + unpatchDate(); + }); + + QUnit.test('remaining_days widget on a date field in list view in UTC-6', async function (assert) { + assert.expect(6); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11 + this.data.partner.records = [ + { id: 1, date: '2017-10-08' }, // today + { id: 2, date: '2017-10-09' }, // tomorrow + { id: 3, date: '2017-10-07' }, // yesterday + { id: 4, date: '2017-10-10' }, // + 2 days + { id: 5, date: '2017-10-05' }, // - 3 days + ]; + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree><field name="date" widget="remaining_days"/></tree>', + session: { + getTZOffset: () => -360, + }, + }); + + assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Yesterday'); + assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'In 2 days'); + assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '3 days ago'); + + assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/08/2017'); + + list.destroy(); + unpatchDate(); + }); + + QUnit.test('remaining_days widget on a datetime field in list view in UTC-8', async function (assert) { + assert.expect(5); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11, UTC-8 + this.data.partner.records = [ + { id: 1, datetime: '2017-10-08 20:00:00' }, // today + { id: 2, datetime: '2017-10-09 07:00:00' }, // today + { id: 3, datetime: '2017-10-09 10:00:00' }, // tomorrow + { id: 4, datetime: '2017-10-08 06:00:00' }, // yesterday + { id: 5, datetime: '2017-10-07 02:00:00' }, // - 2 days + ]; + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree><field name="datetime" widget="remaining_days"/></tree>', + session: { + getTZOffset: () => -560, + }, + }); + + assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'Yesterday'); + assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '2 days ago'); + + list.destroy(); + unpatchDate(); + }); + + QUnit.module('FieldMonetary'); + + QUnit.test('monetary field in form view', async function (assert) { + assert.expect(5); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="monetary"/>' + + '<field name="currency_id" invisible="1"/>' + + '</sheet>' + + '</form>', + res_id: 5, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + // Non-breaking space between the currency and the amount + assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a09.10', + 'The value should be displayed properly.'); + + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '9.10', + 'The input should be rendered without the currency symbol.'); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().first().text(), '$', + 'The input should be preceded by a span containing the currency symbol.'); + + await testUtils.fields.editInput(form.$('.o_field_monetary input'), '108.2458938598598'); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '108.2458938598598', + 'The value should not be formated yet.'); + + await testUtils.form.clickSave(form); + // Non-breaking space between the currency and the amount + assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a0108.25', + 'The new value should be rounded properly.'); + + form.destroy(); + }); + + QUnit.test('monetary field rounding using formula in form view', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="monetary"/>' + + '<field name="currency_id" invisible="1"/>' + + '</sheet>' + + '</form>', + res_id: 5, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + // Test computation and rounding + await testUtils.form.clickEdit(form); + await testUtils.fields.editInput(form.$('.o_field_monetary input'), '=100/3'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a033.33', + 'The new value should be calculated and rounded properly.'); + + form.destroy(); + }); + + QUnit.test('monetary field with currency symbol after', async function (assert) { + assert.expect(5); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="monetary"/>' + + '<field name="currency_id" invisible="1"/>' + + '</sheet>' + + '</form>', + res_id: 2, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + // Non-breaking space between the currency and the amount + assert.strictEqual(form.$('.o_field_widget').first().text(), '0.00\u00a0€', + 'The value should be displayed properly.'); + + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '0.00', + 'The input should be rendered without the currency symbol.'); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().eq(1).text(), '€', + 'The input should be followed by a span containing the currency symbol.'); + + await testUtils.fields.editInput(form.$('.o_field_widget[name=qux] input'), '108.2458938598598'); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '108.2458938598598', + 'The value should not be formated yet.'); + + await testUtils.form.clickSave(form); + // Non-breaking space between the currency and the amount + assert.strictEqual(form.$('.o_field_widget').first().text(), '108.25\u00a0€', + 'The new value should be rounded properly.'); + + form.destroy(); + }); + + QUnit.test('monetary field with currency digits != 2', async function (assert) { + assert.expect(5); + + this.data.partner.records = [{ + id: 1, + bar: false, + foo: "pouet", + int_field: 68, + qux: 99.1234, + currency_id: 1, + }]; + this.data.currency.records = [{ + id: 1, + display_name: "VEF", + symbol: "Bs.F", + position: "after", + digits: [16, 4], + }]; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="monetary"/>' + + '<field name="currency_id" invisible="1"/>' + + '</sheet>' + + '</form>', + res_id: 1, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + // Non-breaking space between the currency and the amount + assert.strictEqual(form.$('.o_field_widget').first().text(), '99.1234\u00a0Bs.F', + 'The value should be displayed properly.'); + + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '99.1234', + 'The input should be rendered without the currency symbol.'); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().eq(1).text(), 'Bs.F', + 'The input should be followed by a span containing the currency symbol.'); + + await testUtils.fields.editInput(form.$('.o_field_widget[name=qux] input'), '99.111111111'); + assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '99.111111111', + 'The value should not be formated yet.'); + + await testUtils.form.clickSave(form); + // Non-breaking space between the currency and the amount + assert.strictEqual(form.$('.o_field_widget').first().text(), '99.1111\u00a0Bs.F', + 'The new value should be rounded properly.'); + + form.destroy(); + }); + + QUnit.test('monetary field in editable list view', async function (assert) { + assert.expect(9); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom">' + + '<field name="qux" widget="monetary"/>' + + '<field name="currency_id" invisible="1"/>' + + '</tree>', + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + var dollarValues = list.$('td').filter(function () {return _.str.include($(this).text(), '$');}); + assert.strictEqual(dollarValues.length, 1, + 'Only one line has dollar as a currency.'); + + var euroValues = list.$('td').filter(function () {return _.str.include($(this).text(), '€');}); + assert.strictEqual(euroValues.length, 1, + 'One one line has euro as a currency.'); + + var zeroValues = list.$('td.o_data_cell').filter(function () {return $(this).text() === '';}); + assert.strictEqual(zeroValues.length, 1, + 'Unset float values should be rendered as empty strings.'); + + // switch to edit mode + var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector):contains($)'); + await testUtils.dom.click($cell); + + assert.strictEqual($cell.children().length, 1, + 'The cell td should only contain the special div of monetary widget.'); + assert.containsOnce(list, '[name="qux"] input', + 'The view should have 1 input for editable monetary float.'); + assert.strictEqual(list.$('[name="qux"] input').val(), '9.10', + 'The input should be rendered without the currency symbol.'); + assert.strictEqual(list.$('[name="qux"] input').parent().children().first().text(), '$', + 'The input should be preceded by a span containing the currency symbol.'); + + await testUtils.fields.editInput(list.$('[name="qux"] input'), '108.2458938598598'); + assert.strictEqual(list.$('[name="qux"] input').val(), '108.2458938598598', + 'The typed value should be correctly displayed.'); + + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.strictEqual(list.$('tr.o_data_row td:not(.o_list_record_selector):contains($)').text(), '$\u00a0108.25', + 'The new value should be rounded properly.'); + + list.destroy(); + }); + + QUnit.test('monetary field with real monetary field in model', async function (assert) { + assert.expect(7); + + this.data.partner.fields.qux.type = "monetary"; + this.data.partner.fields.quux = { + string: "Quux", type: "monetary", digits: [16,1], searchable: true, readonly: true, + }; + + (_.find(this.data.partner.records, function (record) { return record.id === 5; })).quux = 4.2; + + this.data.partner.onchanges = { + bar: function (obj) { + obj.qux = obj.bar ? 100 : obj.qux; + }, + }; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux"/>' + + '<field name="quux"/>' + + '<field name="currency_id"/>' + + '<field name="bar"/>' + + '</sheet>' + + '</form>', + res_id: 5, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + assert.strictEqual(form.$('.o_field_monetary').first().html(), "$ 9.10", + "readonly value should contain the currency"); + assert.strictEqual(form.$('.o_field_monetary').first().next().html(), "$ 4.20", + "readonly value should contain the currency"); + + await testUtils.form.clickEdit(form); + + assert.strictEqual(form.$('.o_field_monetary > input').val(), "9.10", + "input value in edition should only contain the value, without the currency"); + + await testUtils.dom.click(form.$('input[type="checkbox"]')); + assert.containsOnce(form, '.o_field_monetary > input', + "After the onchange, the monetary <input/> should not have been duplicated"); + assert.containsOnce(form, '.o_field_monetary[name=quux]', + "After the onchange, the monetary readonly field should not have been duplicated"); + + await testUtils.fields.many2one.clickOpenDropdown('currency_id'); + await testUtils.fields.many2one.clickItem('currency_id','€'); + assert.strictEqual(form.$('.o_field_monetary > span').html(), "€", + "After currency change, the monetary field currency should have been updated"); + assert.strictEqual(form.$('.o_field_monetary').first().next().html(), "4.20 €", + "readonly value should contain the updated currency"); + + form.destroy(); + }); + + QUnit.test('monetary field with monetary field given in options', async function (assert) { + assert.expect(1); + + this.data.partner.fields.qux.type = "monetary"; + this.data.partner.fields.company_currency_id = { + string: "Company Currency", type: "many2one", relation: "currency", + }; + this.data.partner.records[4].company_currency_id = 2; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" options="{\'currency_field\': \'company_currency_id\'}"/>' + + '<field name="company_currency_id"/>' + + '</sheet>' + + '</form>', + res_id: 5, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + assert.strictEqual(form.$('.o_field_monetary').html(), "9.10 €", + "field monetary should be formatted with correct currency"); + + form.destroy(); + }); + + QUnit.test('should keep the focus when being edited in x2many lists', async function (assert) { + assert.expect(6); + + this.data.partner.fields.currency_id.default = 1; + this.data.partner.fields.m2m = { + string: "m2m", type: "many2many", relation: 'partner', default: [[6, false, [2]]], + }; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="p"/>' + + '<field name="m2m"/>' + + '</sheet>' + + '</form>', + archs: { + 'partner,false,list': '<tree editable="bottom">' + + '<field name="qux" widget="monetary"/>' + + '<field name="currency_id" invisible="1"/>' + + '</tree>', + }, + session: { + currencies: _.indexBy(this.data.currency.records, 'id'), + }, + }); + + // test the monetary field inside the one2many + var $o2m = form.$('.o_field_widget[name=p]'); + await testUtils.dom.click($o2m.find('.o_field_x2many_list_row_add a')); + await testUtils.fields.editInput($o2m.find('.o_field_widget input'), "22"); + + assert.strictEqual($o2m.find('.o_field_widget input').get(0), document.activeElement, + "the focus should still be on the input"); + assert.strictEqual($o2m.find('.o_field_widget input').val(), "22", + "the value should not have been formatted yet"); + + await testUtils.dom.click(form.$el); + + assert.strictEqual($o2m.find('.o_field_widget[name=qux]').html(), "$ 22.00", + "the value should have been formatted after losing the focus"); + + // test the monetary field inside the many2many + var $m2m = form.$('.o_field_widget[name=m2m]'); + await testUtils.dom.click($m2m.find('.o_data_row td:first')); + await testUtils.fields.editInput($m2m.find('.o_field_widget input'), "22"); + + assert.strictEqual($m2m.find('.o_field_widget input').get(0), document.activeElement, + "the focus should still be on the input"); + assert.strictEqual($m2m.find('.o_field_widget input').val(), "22", + "the value should not have been formatted yet"); + + await testUtils.dom.click(form.$el); + + assert.strictEqual($m2m.find('.o_field_widget[name=qux]').html(), "22.00 €", + "the value should have been formatted after losing the focus"); + + form.destroy(); + }); + + QUnit.test('monetary field with currency set by an onchange',async 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 = await 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'), + }, + }); + + await testUtils.dom.click(list.$buttons.find('.o_list_button_add')); + assert.containsOnce(list, 'div.o_field_widget[name=qux] input', + "monetary field should have been rendered correctly (without currency)"); + assert.containsNone(list, '.o_field_widget[name=qux] span', + "monetary field should have been rendered correctly (without currency)"); + + // set a value for int_field -> should set the currency and re-render qux + await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'),'7'); + assert.containsOnce(list, 'div.o_field_widget[name=qux] input', + "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'); + await testUtils.dom.click($quxInput); + 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 + await testUtils.dom.click(list.$('.o_field_widget[name=int_field]')); + await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'),'0'); + $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.containsNone(list, '.o_field_widget[name=qux] span', + "monetary field should have been re-rendered correctly (without currency)"); + await testUtils.dom.click($quxInput); + 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', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="int_field"/></form>', + res_id: 4, + }); + + assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty', + 'Non-set integer field should be recognized as 0.'); + assert.strictEqual(form.$('.o_field_widget').text(), "0", + 'Non-set integer field should be recognized as 0.'); + + form.destroy(); + }); + + QUnit.test('integer field in form view', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="int_field"/></form>', + res_id: 2, + }); + + assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty', + 'Integer field should be considered set for value 0.'); + + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('input[name=int_field]').val(), '0', + 'The value should be rendered correctly in edit mode.'); + + await testUtils.fields.editInput(form.$('input[name=int_field]'), '-18'); + assert.strictEqual(form.$('input[name=int_field]').val(), '-18', + 'The value should be correctly displayed in the input.'); + + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').text(), '-18', + 'The new value should be saved and displayed properly.'); + + form.destroy(); + }); + + QUnit.test('integer field rounding using formula in form view', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="int_field"/></form>', + res_id: 2, + }); + + // Test computation and rounding + await testUtils.form.clickEdit(form); + await testUtils.fields.editInput(form.$('input[name=int_field]'), '=100/3'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').first().text(), '33', + 'The new value should be calculated properly.'); + + form.destroy(); + }); + + QUnit.test('integer field in form view with virtual id', async function (assert) { + assert.expect(1); + var params = { + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="id"/></form>', + }; + + params.res_id = this.data.partner.records[1].id = "2-20170808020000"; + var form = await createView(params); + assert.strictEqual(form.$('.o_field_widget').text(), "2-20170808020000", + "Should display virtual id"); + + form.destroy(); + }); + + QUnit.test('integer field in editable list view', async function (assert) { + assert.expect(4); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom">' + + '<field name="int_field"/>' + + '</tree>', + }); + + var zeroValues = list.$('td').filter(function () {return $(this).text() === '0';}); + assert.strictEqual(zeroValues.length, 1, + 'Unset integer values should not be rendered as zeros.'); + + // switch to edit mode + var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first(); + await testUtils.dom.click($cell); + + assert.containsOnce(list, 'input[name="int_field"]', + 'The view should have 1 input for editable integer.'); + + await testUtils.fields.editInput(list.$('input[name="int_field"]'), '-28'); + assert.strictEqual(list.$('input[name="int_field"]').val(), '-28', + 'The value should be displayed properly in the input.'); + + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.strictEqual(list.$('td:not(.o_list_record_selector)').first().text(), '-28', + 'The new value should be saved and displayed properly.'); + + list.destroy(); + }); + + QUnit.test('integer field with type number option', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="int_field" options="{\'type\': \'number\'}"/>' + + '</form>', + res_id: 4, + translateParameters: { + thousands_sep: ",", + grouping: [3, 0], + }, + }); + + await testUtils.form.clickEdit(form); + assert.ok(form.$('.o_field_widget')[0].hasAttribute('type'), + 'Integer field with option type must have a type attribute.'); + assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'number', + 'Integer field with option type must have a type attribute equals to "number".'); + + await testUtils.fields.editInput(form.$('input[name=int_field]'), '1234567890'); + await testUtils.form.clickSave(form); + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget').val(), '1234567890', + 'Integer value must be not formatted if input type is number.'); + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').text(), '1,234,567,890', + 'Integer value must be formatted in readonly view even if the input type is number.'); + + form.destroy(); + }); + + QUnit.test('integer field without type number option', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="int_field"/>' + + '</form>', + res_id: 4, + translateParameters: { + thousands_sep: ",", + grouping: [3, 0], + }, + }); + + await testUtils.form.clickEdit(form); + assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'text', + 'Integer field without option type must have a text type (default type).'); + + await testUtils.fields.editInput(form.$('input[name=int_field]'), '1234567890'); + await testUtils.form.clickSave(form); + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget').val(), '1,234,567,890', + 'Integer value must be formatted if input type isn\'t number.'); + + form.destroy(); + }); + + QUnit.test('integer field without formatting', async function (assert) { + assert.expect(3); + + this.data.partner.records = [{ + 'id': 999, + 'int_field': 8069, + }]; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="int_field" options="{\'format\': \'false\'}"/>' + + '</form>', + res_id: 999, + translateParameters: { + thousands_sep: ",", + grouping: [3, 0], + }, + }); + + assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode'); + assert.strictEqual(form.$('.o_field_widget[name=int_field]').text(), '8069', + 'Integer value must not be formatted'); + await testUtils.form.clickEdit(form); + + assert.strictEqual(form.$('.o_field_widget').val(), '8069', + 'Integer value must not be formatted'); + + form.destroy(); + }); + + QUnit.test('integer field is formatted by default', async function (assert) { + assert.expect(3); + + this.data.partner.records = [{ + 'id': 999, + 'int_field': 8069, + }]; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="int_field" />' + + '</form>', + res_id: 999, + translateParameters: { + thousands_sep: ",", + grouping: [3, 0], + }, + }); + assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode'); + assert.strictEqual(form.$('.o_field_widget[name=int_field]').text(), '8,069', + 'Integer value must be formatted by default'); + await testUtils.form.clickEdit(form); + + assert.strictEqual(form.$('.o_field_widget').val(), '8,069', + 'Integer value must be formatted by default'); + + form.destroy(); + }); + + QUnit.module('FieldFloatTime'); + + QUnit.test('float_time field in form view', async function (assert) { + assert.expect(5); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="float_time"/>' + + '</sheet>' + + '</form>', + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + // 48 / 60 = 0.8 + assert.strictEqual(args.args[1].qux, -11.8, 'the correct float value should be saved'); + } + return this._super.apply(this, arguments); + }, + res_id: 5, + }); + + // 9 + 0.1 * 60 = 9.06 + assert.strictEqual(form.$('.o_field_widget').first().text(), '09:06', + 'The formatted time value should be displayed properly.'); + + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('input[name=qux]').val(), '09:06', + 'The value should be rendered correctly in the input.'); + + await testUtils.fields.editInput(form.$('input[name=qux]'), '-11:48'); + assert.strictEqual(form.$('input[name=qux]').val(), '-11:48', + 'The new value should be displayed properly in the input.'); + + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').first().text(), '-11:48', + 'The new value should be saved and displayed properly.'); + + form.destroy(); + }); + + + QUnit.module('FieldFloatFactor'); + + QUnit.test('float_factor field in form view', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="float_factor" options="{\'factor\': 0.5}" digits="[16,2]"/>' + + '</sheet>' + + '</form>', + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + // 16.4 / 2 = 8.2 + assert.strictEqual(args.args[1].qux, 4.6, 'the correct float value should be saved'); + } + return this._super.apply(this, arguments); + }, + res_id: 5, + }); + assert.strictEqual(form.$('.o_field_widget').first().text(), '4.55', // 9.1 / 0.5 + 'The formatted value should be displayed properly.'); + + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('input[name=qux]').val(), '4.55', + 'The value should be rendered correctly in the input.'); + + await testUtils.fields.editInput(form.$('input[name=qux]'), '2.3'); + + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget').first().text(), '2.30', + 'The new value should be saved and displayed properly.'); + + form.destroy(); + }); + + QUnit.module('FieldFloatToggle'); + + QUnit.test('float_toggle field in form view', async function (assert) { + assert.expect(5); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="qux" widget="float_toggle" options="{\'factor\': 0.125, \'range\': [0, 1, 0.75, 0.5, 0.25]}" digits="[5,3]"/>' + + '</sheet>' + + '</form>', + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + // 1.000 / 0.125 = 8 + assert.strictEqual(args.args[1].qux, 8, 'the correct float value should be saved'); + } + return this._super.apply(this, arguments); + }, + res_id: 1, + }); + assert.strictEqual(form.$('.o_field_widget').first().text(), '0.056', + 'The formatted time value should be displayed properly.'); + + await testUtils.form.clickEdit(form); + + assert.strictEqual(form.$('button.o_field_float_toggle').text(), '0.056', + 'The value should be rendered correctly on the button.'); + + await testUtils.dom.click(form.$('button.o_field_float_toggle')); + + assert.strictEqual(form.$('button.o_field_float_toggle').text(), '1.000', + 'The value should be rendered correctly on the button.'); + + await testUtils.form.clickSave(form); + + assert.strictEqual(form.$('.o_field_widget').first().text(), '1.000', + 'The new value should be saved and displayed properly.'); + + form.destroy(); + }); + + + QUnit.module('PhoneWidget'); + + QUnit.test('phone field in form view on normal screens', async function (assert) { + assert.expect(5); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="foo" widget="phone"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + config: { + device: { + size_class: config.device.SIZES.LG, + }, + }, + }); + + var $phone = form.$('a.o_field_widget.o_form_uri'); + assert.strictEqual($phone.length, 1, + "should have rendered the phone number as a link with correct classes"); + assert.strictEqual($phone.text(), 'yop', + "value should be displayed properly"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, 'input[type="text"].o_field_widget', + "should have an input for the phone field"); + assert.strictEqual(form.$('input[type="text"].o_field_widget').val(), 'yop', + "input should contain field value in edit mode"); + + // change value in edit mode + await testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), 'new'); + + // save + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('a.o_field_widget.o_form_uri').text(), 'new', + "new value should be displayed properly"); + + form.destroy(); + }); + + QUnit.test('phone field in editable list view on normal screens', async function (assert) { + assert.expect(8); + var doActionCount = 0; + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>', + config: { + device: { + size_class: config.device.SIZES.LG, + }, + }, + }); + + assert.containsN(list, 'tbody td:not(.o_list_record_selector)', 5); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) a').first().text(), 'yop', + "value should be displayed properly with a link to send SMS"); + + assert.containsN(list, 'a.o_field_widget.o_form_uri', 5, + "should have the correct classnames"); + + // Edit a line and check the result + var $cell = list.$('tbody td:not(.o_list_record_selector)').first(); + await testUtils.dom.click($cell); + assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode'); + assert.strictEqual($cell.find('input').val(), 'yop', + 'should have the corect value in internal input'); + await testUtils.fields.editInput($cell.find('input'), 'new'); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + $cell = list.$('tbody td:not(.o_list_record_selector)').first(); + assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore'); + assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) a').first().text(), 'new', + "value should be properly updated"); + assert.containsN(list, 'a.o_field_widget.o_form_uri', 5, + "should still have links with correct classes"); + + list.destroy(); + }); + + QUnit.test('use TAB to navigate to a phone field', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="display_name"/>' + + '<field name="foo" widget="phone"/>' + + '</group>' + + '</sheet>' + + '</form>', + }); + + testUtils.dom.click(form.$('input[name=display_name]')); + assert.strictEqual(form.$('input[name="display_name"]')[0], document.activeElement, + "display_name should be focused"); + form.$('input[name="display_name"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB})); + assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement, + "foo should be focused"); + + form.destroy(); + }); + + QUnit.module('PriorityWidget'); + + QUnit.test('priority widget when not set', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="selection" widget="priority"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 2, + }); + + assert.strictEqual(form.$('.o_field_widget.o_priority:not(.o_field_empty)').length, 1, + "widget should be considered set, even though there is no value for this field"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2, + "should have two stars for representing each possible value: no star, one star and two stars"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 0, + "should have no full star since there is no value"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 2, + "should have two empty stars since there is no value"); + + form.destroy(); + }); + + QUnit.test('priority widget in form view', async function (assert) { + assert.expect(22); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="selection" widget="priority"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.strictEqual(form.$('.o_field_widget.o_priority:not(.o_field_empty)').length, 1, + "widget should be considered set"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2, + "should have two stars for representing each possible value: no star, one star and two stars"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 1, + "should have one full star since the value is the second value"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 1, + "should have one empty star since the value is the second value"); + + // hover last star + form.$('.o_field_widget.o_priority a.o_priority_star.fa-star-o').last().trigger('mouseover'); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2, + "should have two stars for representing each possible value: no star, one star and two stars"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 2, + "should temporary have two full stars since we are hovering the third value"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 0, + "should temporary have no empty star since we are hovering the third value"); + + // Here we should test with mouseout, but currently the effect associated with it + // occurs in a setTimeout after 200ms so it's not trivial to test it here. + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2, + "should still have two stars"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 1, + "should still have one full star since the value is the second value"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 1, + "should still have one empty star since the value is the second value"); + + // save + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2, + "should still have two stars"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 1, + "should still have one full star since the value is the second value"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 1, + "should still have one empty star since the value is the second value"); + + // switch to edit mode to check that the new value was properly written + await testUtils.form.clickEdit(form); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2, + "should still have two stars"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 1, + "should still have one full star since the value is the second value"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 1, + "should still have one empty star since the value is the second value"); + + // click on the second star in edit mode + await testUtils.dom.click(form.$('.o_field_widget.o_priority a.o_priority_star.fa-star-o').last()); + + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2, + "should still have two stars"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 2, + "should now have two full stars since the value is the third value"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 0, + "should now have no empty star since the value is the third value"); + + // save + await testUtils.form.clickSave(form); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2, + "should still have two stars"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 2, + "should now have two full stars since the value is the third value"); + assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 0, + "should now have no empty star since the value is the third value"); + + form.destroy(); + }); + + QUnit.test('priority widget in editable list view', async function (assert) { + assert.expect(25); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom"><field name="selection" widget="priority"/></tree>', + }); + + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority:not(.o_field_empty)').length, 1, + "widget should be considered set"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2, + "should have two stars for representing each possible value: no star, one star and two stars"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 1, + "should have one full star since the value is the second value"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 1, + "should have one empty star since the value is the second value"); + + // Here we should test with mouseout, but currently the effect associated with it + // occurs in a setTimeout after 200ms so it's not trivial to test it here. + + // switch to edit mode and check the result + var $cell = list.$('tbody td:not(.o_list_record_selector)').first(); + await testUtils.dom.click($cell); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2, + "should have two stars for representing each possible value: no star, one star and two stars"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 1, + "should have one full star since the value is the second value"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 1, + "should have one empty star since the value is the second value"); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2, + "should have two stars for representing each possible value: no star, one star and two stars"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 1, + "should have one full star since the value is the second value"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 1, + "should have one empty star since the value is the second value"); + + // hover last star + list.$('.o_data_row .o_priority a.o_priority_star.fa-star-o').first().trigger('mouseenter'); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2, + "should have two stars for representing each possible value: no star, one star and two stars"); + assert.strictEqual(list.$('.o_data_row').first().find('a.o_priority_star.fa-star').length, 2, + "should temporary have two full stars since we are hovering the third value"); + assert.strictEqual(list.$('.o_data_row').first().find('a.o_priority_star.fa-star-o').length, 0, + "should temporary have no empty star since we are hovering the third value"); + + // click on the first star in readonly mode + await testUtils.dom.click(list.$('.o_priority a.o_priority_star.fa-star').first()); + + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2, + "should still have two stars"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 0, + "should now have no full star since the value is the first value"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 2, + "should now have two empty stars since the value is the first value"); + + // re-enter edit mode to force re-rendering the widget to check if the value was correctly saved + $cell = list.$('tbody td:not(.o_list_record_selector)').first(); + await testUtils.dom.click($cell); + + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2, + "should still have two stars"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 0, + "should now only have no full star since the value is the first value"); + assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 2, + "should now have two empty stars since the value is the first value"); + + // Click on second star in edit mode + await testUtils.dom.click(list.$('.o_priority a.o_priority_star.fa-star-o').last()); + + assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star').length, 2, + "should still have two stars"); + assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star').length, 2, + "should now have two full stars since the value is the third value"); + assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star-o').length, 0, + "should now have no empty star since the value is the third value"); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star').length, 2, + "should still have two stars"); + assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star').length, 2, + "should now have two full stars since the value is the third value"); + assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star-o').length, 0, + "should now have no empty star since the value is the third value"); + + list.destroy(); + }); + + QUnit.test('priority widget with readonly attribute', async function (assert) { + assert.expect(1); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form> + <field name="selection" widget="priority" readonly="1"/> + </form>`, + res_id: 2, + }); + + assert.containsN(form, '.o_field_widget.o_priority span', 2, + "stars of priority widget should rendered with span tag if readonly"); + + form.destroy(); + }); + + QUnit.module('StateSelection Widget'); + + QUnit.test('state_selection widget in form view', async function (assert) { + assert.expect(21); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="selection" widget="state_selection"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + viewOptions: { + disable_autofocus: true, + }, + }); + + assert.containsOnce(form, '.o_field_widget.o_selection > a span.o_status.o_status_red', + "should have one red status since selection is the second, blocked state"); + assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_green', + "should not have one green status since selection is the second, blocked state"); + assert.containsNone(form, '.dropdown-menu.state:visible', + "there should not be a dropdown"); + + // Click on the status button to make the dropdown appear + await testUtils.dom.click(form.$('.o_field_widget.o_selection .o_status').first()); + assert.containsOnce(form, '.dropdown-menu.state:visible', + "there should be a dropdown"); + assert.containsN(form, '.dropdown-menu.state:visible .dropdown-item', 2, + "there should be two options in the dropdown"); + + // Click on the first option, "Normal" + await testUtils.dom.click(form.$('.dropdown-menu.state:visible .dropdown-item').first()); + assert.containsNone(form, '.dropdown-menu.state:visible', + "there should not be a dropdown anymore"); + assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_red', + "should not have one red status since selection is the first, normal state"); + assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_green', + "should not have one green status since selection is the first, normal state"); + assert.containsOnce(form, '.o_field_widget.o_selection > a span.o_status', + "should have one grey status since selection is the first, normal state"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsNone(form, '.dropdown-menu.state:visible', + "there should still not be a dropdown"); + assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_red', + "should still not have one red status since selection is the first, normal state"); + assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_green', + "should still not have one green status since selection is the first, normal state"); + assert.containsOnce(form, '.o_field_widget.o_selection > a span.o_status', + "should still have one grey status since selection is the first, normal state"); + + // Click on the status button to make the dropdown appear + await testUtils.dom.click(form.$('.o_field_widget.o_selection .o_status').first()); + assert.containsOnce(form, '.dropdown-menu.state:visible', + "there should be a dropdown"); + assert.containsN(form, '.dropdown-menu.state:visible .dropdown-item', 2, + "there should be two options in the dropdown"); + + // Click on the last option, "Done" + await testUtils.dom.click(form.$('.dropdown-menu.state:visible .dropdown-item').last()); + assert.containsNone(form, '.dropdown-menu.state:visible', + "there should not be a dropdown anymore"); + assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_red', + "should not have one red status since selection is the third, done state"); + assert.containsOnce(form, '.o_field_widget.o_selection > a span.o_status.o_status_green', + "should have one green status since selection is the third, done state"); + + // save + await testUtils.form.clickSave(form); + assert.containsNone(form, '.dropdown-menu.state:visible', + "there should still not be a dropdown anymore"); + assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_red', + "should still not have one red status since selection is the third, done state"); + assert.containsOnce(form, '.o_field_widget.o_selection > a span.o_status.o_status_green', + "should still have one green status since selection is the third, done state"); + + form.destroy(); + }); + + QUnit.test('state_selection widget with readonly modifier', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form><field name="selection" widget="state_selection" readonly="1"/></form>', + res_id: 1, + }); + + assert.hasClass(form.$('.o_selection'), 'o_readonly_modifier'); + assert.hasClass(form.$('.o_selection > a'), 'disabled'); + assert.isNotVisible(form.$('.dropdown-menu.state')); + + await testUtils.dom.click(form.$('.o_selection > a')); + assert.isNotVisible(form.$('.dropdown-menu.state')); + + form.destroy(); + }); + + QUnit.test('state_selection widget in editable list view', async function (assert) { + assert.expect(32); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom">' + + '<field name="foo"/>' + + '<field name="selection" widget="state_selection"/>' + + '</tree>', + }); + + assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 5, + "should have five status selection widgets"); + assert.containsOnce(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_red', + "should have one red status"); + assert.containsOnce(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_green', + "should have one green status"); + assert.containsNone(list, '.dropdown-menu.state:visible', + "there should not be a dropdown"); + + // Click on the status button to make the dropdown appear + var $cell = list.$('tbody td.o_state_selection_cell').first(); + await testUtils.dom.click(list.$('.o_state_selection_cell .o_selection > a span.o_status').first()); + assert.doesNotHaveClass($cell.parent(), 'o_selected_row', + 'should not be in edit mode since we clicked on the state selection widget'); + assert.containsOnce(list, '.dropdown-menu.state:visible', + "there should be a dropdown"); + assert.containsN(list, '.dropdown-menu.state:visible .dropdown-item', 2, + "there should be two options in the dropdown"); + + // Click on the first option, "Normal" + await testUtils.dom.click(list.$('.dropdown-menu.state:visible .dropdown-item').first()); + assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 5, + "should still have five status selection widgets"); + assert.containsNone(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_red', + "should now have no red status"); + assert.containsOnce(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_green', + "should still have one green status"); + assert.containsNone(list, '.dropdown-menu.state:visible', + "there should not be a dropdown"); + + // switch to edit mode and check the result + $cell = list.$('tbody td.o_state_selection_cell').first(); + await testUtils.dom.click($cell); + assert.hasClass($cell.parent(),'o_selected_row', + 'should now be in edit mode'); + assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 5, + "should still have five status selection widgets"); + assert.containsNone(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_red', + "should now have no red status"); + assert.containsOnce(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_green', + "should still have one green status"); + assert.containsNone(list, '.dropdown-menu.state:visible', + "there should not be a dropdown"); + + // Click on the status button to make the dropdown appear + await testUtils.dom.click(list.$('.o_state_selection_cell .o_selection > a span.o_status').first()); + assert.containsOnce(list, '.dropdown-menu.state:visible', + "there should be a dropdown"); + assert.containsN(list, '.dropdown-menu.state:visible .dropdown-item', 2, + "there should be two options in the dropdown"); + + // Click on another row + var $lastCell = list.$('tbody td.o_state_selection_cell').last(); + await testUtils.dom.click($lastCell); + assert.containsNone(list, '.dropdown-menu.state:visible', + "there should not be a dropdown anymore"); + var $firstCell = list.$('tbody td.o_state_selection_cell').first(); + assert.doesNotHaveClass($firstCell.parent(), 'o_selected_row', + 'first row should not be in edit mode anymore'); + assert.hasClass($lastCell.parent(),'o_selected_row', + 'last row should be in edit mode'); + + // Click on the last status button to make the dropdown appear + await testUtils.dom.click(list.$('.o_state_selection_cell .o_selection > a span.o_status').last()); + assert.containsOnce(list, '.dropdown-menu.state:visible', + "there should be a dropdown"); + assert.containsN(list, '.dropdown-menu.state:visible .dropdown-item', 2, + "there should be two options in the dropdown"); + + // Click on the last option, "Done" + await testUtils.dom.click(list.$('.dropdown-menu.state:visible .dropdown-item').last()); + assert.containsNone(list, '.dropdown-menu.state:visible', + "there should not be a dropdown anymore"); + assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 5, + "should still have five status selection widgets"); + assert.containsNone(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_red', + "should still have no red status"); + assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_green', 2, + "should now have two green status"); + assert.containsNone(list, '.dropdown-menu.state:visible', + "there should not be a dropdown"); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 5, + "should have five status selection widgets"); + assert.containsNone(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_red', + "should have no red status"); + assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_green', 2, + "should have two green status"); + assert.containsNone(list, '.dropdown-menu.state:visible', + "there should not be a dropdown"); + + list.destroy(); + }); + + + QUnit.module('FavoriteWidget'); + + QUnit.test('favorite widget in kanban view', async function (assert) { + assert.expect(4); + + var kanban = await createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test">' + + '<templates>' + + '<t t-name="kanban-box">' + + '<div>' + + '<field name="bar" widget="boolean_favorite" />' + + '</div>' + + '</t>' + + '</templates>' + + '</kanban>', + domain: [['id', '=', 1]], + }); + + assert.containsOnce(kanban, '.o_kanban_record .o_field_widget.o_favorite > a i.fa.fa-star', + 'should be favorite'); + assert.strictEqual(kanban.$('.o_kanban_record .o_field_widget.o_favorite > a').text(), ' Remove from Favorites', + 'the label should say "Remove from Favorites"'); + + // click on favorite + await testUtils.dom.click(kanban.$('.o_field_widget.o_favorite')); + assert.containsNone(kanban, '.o_kanban_record .o_field_widget.o_favorite > a i.fa.fa-star', + 'should not be favorite'); + assert.strictEqual(kanban.$('.o_kanban_record .o_field_widget.o_favorite > a').text(), ' Add to Favorites', + 'the label should say "Add to Favorites"'); + + kanban.destroy(); + }); + + QUnit.test('favorite widget in form view', async function (assert) { + assert.expect(10); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="bar" widget="boolean_favorite" />' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.containsOnce(form, '.o_field_widget.o_favorite > a i.fa.fa-star', + 'should be favorite'); + assert.strictEqual(form.$('.o_field_widget.o_favorite > a').text(), ' Remove from Favorites', + 'the label should say "Remove from Favorites"'); + + // click on favorite + await testUtils.dom.click(form.$('.o_field_widget.o_favorite')); + assert.containsNone(form, '.o_field_widget.o_favorite > a i.fa.fa-star', + 'should not be favorite'); + assert.strictEqual(form.$('.o_field_widget.o_favorite > a').text(), ' Add to Favorites', + 'the label should say "Add to Favorites"'); + + // switch to edit mode + await testUtils.form.clickEdit(form); + assert.containsOnce(form, '.o_field_widget.o_favorite > a i.fa.fa-star-o', + 'should not be favorite'); + assert.strictEqual(form.$('.o_field_widget.o_favorite > a').text(), ' Add to Favorites', + 'the label should say "Add to Favorites"'); + + // click on favorite + await testUtils.dom.click(form.$('.o_field_widget.o_favorite')); + assert.containsOnce(form, '.o_field_widget.o_favorite > a i.fa.fa-star', + 'should be favorite'); + assert.strictEqual(form.$('.o_field_widget.o_favorite > a').text(), ' Remove from Favorites', + 'the label should say "Remove from Favorites"'); + + // save + await testUtils.form.clickSave(form); + assert.containsOnce(form, '.o_field_widget.o_favorite > a i.fa.fa-star', + 'should be favorite'); + assert.strictEqual(form.$('.o_field_widget.o_favorite > a').text(), ' Remove from Favorites', + 'the label should say "Remove from Favorites"'); + + form.destroy(); + }); + + QUnit.test('favorite widget in editable list view without label', async function (assert) { + assert.expect(4); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom">' + + '<field name="bar" widget="boolean_favorite" nolabel="1" />' + + '</tree>', + }); + + assert.containsOnce(list, '.o_data_row:first .o_field_widget.o_favorite > a i.fa.fa-star', + 'should be favorite'); + + // switch to edit mode + await testUtils.dom.click(list.$('tbody td:not(.o_list_record_selector)').first()); + assert.containsOnce(list, '.o_data_row:first .o_field_widget.o_favorite > a i.fa.fa-star', + 'should be favorite'); + + // click on favorite + await testUtils.dom.click(list.$('.o_data_row:first .o_field_widget.o_favorite')); + assert.containsNone(list, '.o_data_row:first .o_field_widget.o_favorite > a i.fa.fa-star', + 'should not be favorite'); + + // save + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.containsOnce(list, '.o_data_row:first .o_field_widget.o_favorite > a i.fa.fa-star-o', + 'should not be favorite'); + + list.destroy(); + }); + + + QUnit.module('LabelSelectionWidget'); + + QUnit.test('label_selection widget in form view', async function (assert) { + assert.expect(12); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="selection" widget="label_selection" ' + + ' options="{\'classes\': {\'normal\': \'secondary\', \'blocked\': \'warning\',\'done\': \'success\'}}"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.containsOnce(form, '.o_field_widget.badge.badge-warning', + "should have a warning status label since selection is the second, blocked state"); + assert.containsNone(form, '.o_field_widget.badge.badge-secondary', + "should not have a default status since selection is the second, blocked state"); + assert.containsNone(form, '.o_field_widget.badge.badge-success', + "should not have a success status since selection is the second, blocked state"); + assert.strictEqual(form.$('.o_field_widget.badge.badge-warning').text(), 'Blocked', + "the label should say 'Blocked' since this is the label value for that state"); + + // // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, '.o_field_widget.badge.badge-warning', + "should have a warning status label since selection is the second, blocked state"); + assert.containsNone(form, '.o_field_widget.badge.badge-secondary', + "should not have a default status since selection is the second, blocked state"); + assert.containsNone(form, '.o_field_widget.badge.badge-success', + "should not have a success status since selection is the second, blocked state"); + assert.strictEqual(form.$('.o_field_widget.badge.badge-warning').text(), 'Blocked', + "the label should say 'Blocked' since this is the label value for that state"); + + // save + await testUtils.form.clickSave(form); + assert.containsOnce(form, '.o_field_widget.badge.badge-warning', + "should have a warning status label since selection is the second, blocked state"); + assert.containsNone(form, '.o_field_widget.badge.badge-secondary', + "should not have a default status since selection is the second, blocked state"); + assert.containsNone(form, '.o_field_widget.badge.badge-success', + "should not have a success status since selection is the second, blocked state"); + assert.strictEqual(form.$('.o_field_widget.badge.badge-warning').text(), 'Blocked', + "the label should say 'Blocked' since this is the label value for that state"); + + form.destroy(); + }); + + QUnit.test('label_selection widget in editable list view', async function (assert) { + assert.expect(21); + + var list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree editable="bottom">' + + '<field name="foo"/>' + + '<field name="selection" widget="label_selection"' + + ' options="{\'classes\': {\'normal\': \'secondary\', \'blocked\': \'warning\',\'done\': \'success\'}}"/>' + + '</tree>', + }); + + assert.strictEqual(list.$('.o_field_widget.badge:not(:empty)').length, 3, + "should have three visible status labels"); + assert.containsOnce(list, '.o_field_widget.badge.badge-warning', + "should have one warning status label"); + assert.strictEqual(list.$('.o_field_widget.badge.badge-warning').text(), 'Blocked', + "the warning label should read 'Blocked'"); + assert.containsOnce(list, '.o_field_widget.badge.badge-secondary', + "should have one default status label"); + assert.strictEqual(list.$('.o_field_widget.badge.badge-secondary').text(), 'Normal', + "the default label should read 'Normal'"); + assert.containsOnce(list, '.o_field_widget.badge.badge-success', + "should have one success status label"); + assert.strictEqual(list.$('.o_field_widget.badge.badge-success').text(), 'Done', + "the success label should read 'Done'"); + + // switch to edit mode and check the result + await testUtils.dom.clickFirst(list.$('tbody td:not(.o_list_record_selector)')); + assert.strictEqual(list.$('.o_field_widget.badge:not(:empty)').length, 3, + "should have three visible status labels"); + assert.containsOnce(list, '.o_field_widget.badge.badge-warning', + "should have one warning status label"); + assert.strictEqual(list.$('.o_field_widget.badge.badge-warning').text(), 'Blocked', + "the warning label should read 'Blocked'"); + assert.containsOnce(list, '.o_field_widget.badge.badge-secondary', + "should have one default status label"); + assert.strictEqual(list.$('.o_field_widget.badge.badge-secondary').text(), 'Normal', + "the default label should read 'Normal'"); + assert.containsOnce(list, '.o_field_widget.badge.badge-success', + "should have one success status label"); + assert.strictEqual(list.$('.o_field_widget.badge.badge-success').text(), 'Done', + "the success label should read 'Done'"); + + // save and check the result + await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); + assert.strictEqual(list.$('.o_field_widget.badge:not(:empty)').length, 3, + "should have three visible status labels"); + assert.containsOnce(list, '.o_field_widget.badge.badge-warning', + "should have one warning status label"); + assert.strictEqual(list.$('.o_field_widget.badge.badge-warning').text(), 'Blocked', + "the warning label should read 'Blocked'"); + assert.containsOnce(list, '.o_field_widget.badge.badge-secondary', + "should have one default status label"); + assert.strictEqual(list.$('.o_field_widget.badge.badge-secondary').text(), 'Normal', + "the default label should read 'Normal'"); + assert.containsOnce(list, '.o_field_widget.badge.badge-success', + "should have one success status label"); + assert.strictEqual(list.$('.o_field_widget.badge.badge-success').text(), 'Done', + "the success label should read 'Done'"); + + list.destroy(); + }); + + + QUnit.module('StatInfo'); + + QUnit.test('statinfo widget formats decimal precision', async function (assert) { + // sometimes the round method can return numbers such as 14.000001 + // when asked to round a number to 2 decimals, as such is the behaviour of floats. + // we check that even in that eventuality, only two decimals are displayed + assert.expect(2); + + this.data.partner.fields.monetary = {string: "Monetary", type: 'monetary'}; + this.data.partner.records[0].monetary = 9.999999; + this.data.partner.records[0].currency_id = 1; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<button class="oe_stat_button" name="items" icon="fa-gear">' + + '<field name="qux" widget="statinfo"/>' + + '</button>' + + '<button class="oe_stat_button" name="money" icon="fa-money">' + + '<field name="monetary" widget="statinfo"/>' + + '</button>' + + '</form>', + res_id: 1, + }); + + // formatFloat renders according to this.field.digits + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').eq(0).text(), + '0.4', "Default precision should be [16,1]"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').eq(1).text(), + '10.00', "Currency decimal precision should be 2"); + + form.destroy(); + }); + + QUnit.test('statinfo widget in form view', async function (assert) { + assert.expect(9); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<div class="oe_button_box" name="button_box">' + + '<button class="oe_stat_button" name="items" type="object" icon="fa-gear">' + + '<field name="int_field" widget="statinfo"/>' + + '</button>' + + '</div>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info', + "should have one stat button"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(), + '10', "should have 10 as value"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(), + 'int_field', "should have 'int_field' as text"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info', + "should still have one stat button"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(), + '10', "should still have 10 as value"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(), + 'int_field', "should have 'int_field' as text"); + + // save + await testUtils.form.clickSave(form); + assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info', + "should have one stat button"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(), + '10', "should have 10 as value"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(), + 'int_field', "should have 'int_field' as text"); + + form.destroy(); + }); + + QUnit.test('statinfo widget in form view with specific label_field', async function (assert) { + assert.expect(9); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<div class="oe_button_box" name="button_box">' + + '<button class="oe_stat_button" name="items" type="object" icon="fa-gear">' + + '<field string="Useful stat button" name="int_field" widget="statinfo" ' + + 'options="{\'label_field\': \'foo\'}"/>' + + '</button>' + + '</div>' + + '<group>' + + '<field name="foo" invisible="1"/>' + + '<field name="bar"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info', + "should have one stat button"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(), + '10', "should have 10 as value"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(), + 'yop', "should have 'yop' as text, since it is the value of field foo"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info', + "should still have one stat button"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(), + '10', "should still have 10 as value"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(), + 'yop', "should have 'yop' as text, since it is the value of field foo"); + + // save + await testUtils.form.clickSave(form); + assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info', + "should have one stat button"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(), + '10', "should have 10 as value"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(), + 'yop', "should have 'yop' as text, since it is the value of field foo"); + + form.destroy(); + }); + + QUnit.test('statinfo widget in form view with no label', async function (assert) { + assert.expect(9); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<div class="oe_button_box" name="button_box">' + + '<button class="oe_stat_button" name="items" type="object" icon="fa-gear">' + + '<field string="Useful stat button" name="int_field" widget="statinfo" nolabel="1"/>' + + '</button>' + + '</div>' + + '<group>' + + '<field name="foo" invisible="1"/>' + + '<field name="bar"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info', + "should have one stat button"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(), + '10', "should have 10 as value"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(), + '', "should not have any label"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info', + "should still have one stat button"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(), + '10', "should still have 10 as value"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(), + '', "should not have any label"); + + // save + await testUtils.form.clickSave(form); + assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info', + "should have one stat button"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(), + '10', "should have 10 as value"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(), + '', "should not have any label"); + + form.destroy(); + }); + + + QUnit.module('PercentPie'); + + QUnit.test('percentpie widget in form view with value < 50%', async function (assert) { + assert.expect(12); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="int_field" widget="percentpie"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie', + "should have a pie chart"); + assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(), + '10%', "should have 10% as pie value since int_field=10"); + assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'), + 'transform: rotate(180deg);'), "left mask should be covering the whole left side of the pie"); + assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last().attr('style'), + 'transform: rotate(36deg);'), "right mask should be rotated from 360*(10/100) = 36 degrees"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie', + "should have a pie chart"); + assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(), + '10%', "should have 10% as pie value since int_field=10"); + assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'), + 'transform: rotate(180deg);'), "left mask should be covering the whole left side of the pie"); + assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last().attr('style'), + 'transform: rotate(36deg);'), "right mask should be rotated from 360*(10/100) = 36 degrees"); + + // save + await testUtils.form.clickSave(form); + assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie', + "should have a pie chart"); + assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(), + '10%', "should have 10% as pie value since int_field=10"); + assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'), + 'transform: rotate(180deg);'), "left mask should be covering the whole left side of the pie"); + assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last().attr('style'), + 'transform: rotate(36deg);'), "right mask should be rotated from 360*(10/100) = 36 degrees"); + + form.destroy(); + }); + + QUnit.test('percentpie widget in form view with value > 50%', async function (assert) { + assert.expect(12); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group>' + + '<field name="int_field" widget="percentpie"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 3, + }); + + assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie', + "should have a pie chart"); + assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(), + '80%', "should have 80% as pie value since int_field=80"); + assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'), + 'transform: rotate(288deg);'), "left mask should be rotated from 360*(80/100) = 288 degrees"); + assert.hasClass(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last(),'o_full', + "right mask should be hidden since the value > 50%"); + + // switch to edit mode and check the result + await testUtils.form.clickEdit(form); + assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie', + "should have a pie chart"); + assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(), + '80%', "should have 80% as pie value since int_field=80"); + assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'), + 'transform: rotate(288deg);'), "left mask should be rotated from 360*(80/100) = 288 degrees"); + assert.hasClass(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last(),'o_full', + "right mask should be hidden since the value > 50%"); + + // save + await testUtils.form.clickSave(form); + assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie', + "should have a pie chart"); + assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(), + '80%', "should have 80% as pie value since int_field=80"); + assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'), + 'transform: rotate(288deg);'), "left mask should be rotated from 360*(80/100) = 288 degrees"); + assert.hasClass(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last(),'o_full', + "right mask should be hidden since the value > 50%"); + + form.destroy(); + }); + + // TODO: This test would pass without any issue since all the classes and + // custom style attributes are correctly set on the widget in list + // view, but since the scss itself for this widget currently only + // applies inside the form view, the widget is unusable. This test can + // be uncommented when we refactor the scss files so that this widget + // stylesheet applies in both form and list view. + // QUnit.test('percentpie widget in editable list view', async function(assert) { + // assert.expect(10); + // + // var list = await createView({ + // View: ListView, + // model: 'partner', + // data: this.data, + // arch: '<tree editable="bottom">' + + // '<field name="foo"/>' + + // '<field name="int_field" widget="percentpie"/>' + + // '</tree>', + // }); + // + // assert.containsN(list, '.o_field_percent_pie .o_pie', 5, + // "should have five pie charts"); + // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_pie_value').first().text(), + // '10%', "should have 10% as pie value since int_field=10"); + // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').first().attr('style'), + // 'transform: rotate(180deg);', "left mask should be covering the whole left side of the pie"); + // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'), + // 'transform: rotate(36deg);', "right mask should be rotated from 360*(10/100) = 36 degrees"); + // + // // switch to edit mode and check the result +// testUtils.dom.click( list.$('tbody td:not(.o_list_record_selector)').first()); + // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_pie_value').first().text(), + // '10%', "should have 10% as pie value since int_field=10"); + // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').first().attr('style'), + // 'transform: rotate(180deg);', "left mask should be covering the whole right side of the pie"); + // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'), + // 'transform: rotate(36deg);', "right mask should be rotated from 360*(10/100) = 36 degrees"); + // + // // save +// testUtils.dom.click( list.$buttons.find('.o_list_button_save')); + // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_pie_value').first().text(), + // '10%', "should have 10% as pie value since int_field=10"); + // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').first().attr('style'), + // 'transform: rotate(180deg);', "left mask should be covering the whole right side of the pie"); + // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'), + // 'transform: rotate(36deg);', "right mask should be rotated from 360*(10/100) = 36 degrees"); + // + // list.destroy(); + // }); + + + QUnit.module('FieldDomain'); + + QUnit.test('The domain editor should not crash the view when given a dynamic filter', async function (assert) { + //dynamic filters (containing variables, such as uid, parent or today) + //are not handled by the domain editor, but it shouldn't crash the view + assert.expect(1); + + this.data.partner.records[0].foo = '[["int_field", "=", uid]]'; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<field name="foo" widget="domain" options="{\'model\': \'partner\'}"/>' + + '<field name="int_field" invisible="1"/>' + + '</form>', + res_id: 1, + session: { + user_context: {uid: 14}, + }, + }); + + assert.strictEqual(form.$('.o_read_mode').text(), "This domain is not supported.", + "The widget should not crash the view, but gracefully admit its failure."); + form.destroy(); + }); + + QUnit.test('basic domain field usage is ok', async function (assert) { + assert.expect(7); + + this.data.partner.records[0].foo = "[]"; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<sheet>' + + '<group>' + + '<field name="foo" widget="domain" options="{\'model\': \'partner_type\'}"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + await testUtils.form.clickEdit(form); + + // As the domain is empty, there should be a button to add the first + // domain part + var $domain = form.$(".o_field_domain"); + var $domainAddFirstNodeButton = $domain.find(".o_domain_add_first_node_button"); + assert.equal($domainAddFirstNodeButton.length, 1, + "there should be a button to create first domain element"); + + // Clicking on the button should add the [["id", "=", "1"]] domain, so + // there should be a field selector in the DOM + await testUtils.dom.click($domainAddFirstNodeButton); + var $fieldSelector = $domain.find(".o_field_selector"); + assert.equal($fieldSelector.length, 1, + "there should be a field selector"); + + // Focusing the field selector input should open the field selector + // popover + await testUtils.dom.triggerEvents($fieldSelector, 'focus'); + var $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover"); + assert.ok($fieldSelectorPopover.is(":visible"), + "field selector popover should be visible"); + + assert.containsOnce($fieldSelectorPopover, '.o_field_selector_search input', + "field selector popover should contain a search input"); + + // The popover should contain the list of partner_type fields and so + // there should be the "Color index" field + var $lis = $fieldSelectorPopover.find("li"); + var $colorIndex = $(); + $lis.each(function () { + var $li = $(this); + if ($li.html().indexOf("Color index") >= 0) { + $colorIndex = $li; + } + }); + assert.equal($colorIndex.length, 1, + "field selector popover should contain 'Color index' field"); + + // Clicking on this field should close the popover, then changing the + // associated value should reveal one matched record + await testUtils.dom.click($colorIndex); + await testUtils.fields.editAndTrigger($('.o_domain_leaf_value_input'), 2, ['change']); + assert.equal($domain.find(".o_domain_show_selection_button").text().trim().substr(0, 2), "1 ", + "changing color value to 2 should reveal only one record"); + + // Saving the form view should show a readonly domain containing the + // "color" field + await testUtils.form.clickSave(form); + $domain = form.$(".o_field_domain"); + assert.ok($domain.html().indexOf("Color index") >= 0, + "field selector readonly value should now contain 'Color index'"); + form.destroy(); + }); + + QUnit.test('domain field is correctly reset on every view change', async function (assert) { + assert.expect(7); + + this.data.partner.records[0].foo = '[["id","=",1]]'; + this.data.partner.fields.bar.type = "char"; + this.data.partner.records[0].bar = "product"; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<sheet>' + + '<group>' + + '<field name="bar"/>' + + '<field name="foo" widget="domain" options="{\'model\': \'bar\'}"/>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + await testUtils.form.clickEdit(form); + + // As the domain is equal to [["id", "=", 1]] there should be a field + // selector to change this + var $domain = form.$(".o_field_domain"); + var $fieldSelector = $domain.find(".o_field_selector"); + assert.equal($fieldSelector.length, 1, + "there should be a field selector"); + + // Focusing its input should open the field selector popover + await testUtils.dom.triggerEvents($fieldSelector, 'focus'); + var $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover"); + assert.ok($fieldSelectorPopover.is(":visible"), + "field selector popover should be visible"); + + // As the value of the "bar" field is "product", the field selector + // popover should contain the list of "product" fields + var $lis = $fieldSelectorPopover.find("li"); + var $sampleLi = $(); + $lis.each(function () { + var $li = $(this); + if ($li.html().indexOf("Product Name") >= 0) { + $sampleLi = $li; + } + }); + assert.strictEqual($lis.length, 1, + "field selector popover should contain only one field"); + assert.strictEqual($sampleLi.length, 1, + "field selector popover should contain 'Product Name' field"); + + // Now change the value of the "bar" field to "partner_type" + await testUtils.dom.click(form.$("input.o_field_widget")); + await testUtils.fields.editInput(form.$("input.o_field_widget"), "partner_type"); + + // Refocusing the field selector input should open the popover again + $fieldSelector = form.$(".o_field_selector"); + $fieldSelector.trigger('focusin'); + $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover"); + assert.ok($fieldSelectorPopover.is(":visible"), + "field selector popover should be visible"); + + // Now the list of fields should be the ones of the "partner_type" model + $lis = $fieldSelectorPopover.find("li"); + $sampleLi = $(); + $lis.each(function () { + var $li = $(this); + if ($li.html().indexOf("Color index") >= 0) { + $sampleLi = $li; + } + }); + assert.strictEqual($lis.length, 2, + "field selector popover should contain two fields"); + assert.strictEqual($sampleLi.length, 1, + "field selector popover should contain 'Color index' field"); + form.destroy(); + }); + + QUnit.test('domain field can be reset with a new domain (from onchange)', async function (assert) { + assert.expect(2); + + this.data.partner.records[0].foo = '[]'; + this.data.partner.onchanges = { + display_name: function (obj) { + obj.foo = '[["id", "=", 1]]'; + }, + }; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<field name="display_name"/>' + + '<field name="foo" widget="domain" options="{\'model\': \'partner\'}"/>' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + assert.equal(form.$('.o_domain_show_selection_button').text().trim(), '5 record(s)', + "the domain being empty, there should be 5 records"); + + // update display_name to trigger the onchange and reset foo + await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'new value'); + + assert.equal(form.$('.o_domain_show_selection_button').text().trim(), '1 record(s)', + "the domain has changed, there should be only 1 record"); + + form.destroy(); + }); + + QUnit.test('domain field: handle false domain as []', async function (assert) { + assert.expect(3); + + this.data.partner.records[0].foo = false; + this.data.partner.fields.bar.type = "char"; + this.data.partner.records[0].bar = "product"; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<sheet>' + + '<group>' + + '<field name="bar"/>' + + '<field name="foo" widget="domain" options="{\'model\': \'bar\'}"/>' + + '</group>' + + '</sheet>' + + '</form>', + mockRPC: function (route, args) { + if (args.method === 'search_count') { + assert.deepEqual(args.args[0], [], "should send a valid domain"); + } + return this._super.apply(this, arguments); + }, + res_id: 1, + }); + + assert.strictEqual(form.$('.o_field_widget[name=foo]:not(.o_field_empty)').length, 1, + "there should be a domain field, not considered empty"); + + await testUtils.form.clickEdit(form); + + var $warning = form.$('.o_field_widget[name=foo] .text-warning'); + assert.strictEqual($warning.length, 0, "should not display that the domain is invalid"); + + form.destroy(); + }); + + QUnit.test('basic domain field: show the selection', async function (assert) { + assert.expect(2); + + this.data.partner.records[0].foo = "[]"; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<sheet>' + + '<group>' + + '<field name="foo" widget="domain" options="{\'model\': \'partner_type\'}"/>' + + '</group>' + + '</sheet>' + + '</form>', + archs: { + 'partner_type,false,list': '<tree><field name="display_name"/></tree>', + 'partner_type,false,search': '<search><field name="name" string="Name"/></search>', + }, + res_id: 1, + }); + + assert.equal(form.$(".o_domain_show_selection_button").text().trim().substr(0, 2), "2 ", + "selection should contain 2 records"); + + // open the selection + await testUtils.dom.click(form.$(".o_domain_show_selection_button")); + assert.strictEqual($('.modal .o_list_view .o_data_row').length, 2, + "should have open a list view with 2 records in a dialog"); + + // click on a record -> should not open the record + // we don't actually check that it doesn't open the record because even + // if it tries to, it will crash as we don't define an arch in this test + await testUtils.dom.click($('.modal .o_list_view .o_data_row:first .o_data_cell')); + + form.destroy(); + }); + + QUnit.test('field context is propagated when opening selection', async function (assert) { + assert.expect(1); + + this.data.partner.records[0].foo = "[]"; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form> + <field name="foo" widget="domain" options="{'model': 'partner_type'}" context="{'tree_view_ref': 3}"/> + </form> + `, + archs: { + 'partner_type,false,list': '<tree><field name="display_name"/></tree>', + 'partner_type,3,list': '<tree><field name="id"/></tree>', + 'partner_type,false,search': '<search><field name="name" string="Name"/></search>', + }, + res_id: 1, + }); + + await testUtils.dom.click(form.$(".o_domain_show_selection_button")); + + assert.strictEqual($('.modal .o_data_row').text(), '1214', + "should have picked the correct list view"); + + form.destroy(); + }); + + QUnit.module('FieldProgressBar'); + + QUnit.test('Field ProgressBar: max_value should update', async function (assert) { + assert.expect(3); + + this.data.partner.records = this.data.partner.records.slice(0,1); + this.data.partner.records[0].qux = 2; + + this.data.partner.onchanges = { + display_name: function (obj) { + obj.int_field = 999; + obj.qux = 5; + } + }; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="display_name" />' + + '<field name="qux" invisible="1" />' + + '<field name="int_field" widget="progressbar" options="{\'current_value\': \'int_field\', \'max_value\': \'qux\'}" />' + + '</form>', + res_id: 1, + 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', + 'The initial value of the progress bar should be correct'); + + // trigger the onchange + await testUtils.fields.editInput(form.$('.o_input[name=display_name]'), 'new name'); + + 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].message, + "Please enter a numerical value" + ); + } + } + }, + }); + + 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(); + }); + + QUnit.module('FieldColor', { + before: function () { + return ajax.loadXML('/web/static/src/xml/colorpicker.xml', core.qweb); + }, + }); + + QUnit.test('Field Color: default widget state', async function (assert) { + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<field name="hex_color" widget="color" />' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + await testUtils.dom.click(form.$('.o_field_color')); + assert.containsOnce($, '.modal'); + assert.containsNone($('.modal'), '.o_opacity_slider', + "Opacity slider should not be present"); + assert.containsNone($('.modal'), '.o_opacity_input', + "Opacity input should not be present"); + + await testUtils.dom.click($('.modal .btn:contains("Discard")')); + + assert.strictEqual(document.activeElement, form.$('.o_field_color')[0], + "Focus should go back to the color field"); + + form.destroy(); + }); + + QUnit.test('Field Color: behaviour in different views', async function (assert) { + assert.expect(2); + + this.data.partner.records[0].p = [4, 2]; + this.data.partner.records[1].hex_color = '#ff0080'; + + const form = await createView({ + arch: '<form>' + + '<field name="hex_color" widget="color"/>' + + '<field name="p">' + + '<tree editable="top">' + + '<field name="display_name"/>' + + '<field name="hex_color" widget="color"/>' + + '</tree>' + + '</field>' + + '</form>', + data: this.data, + model: 'partner', + res_id: 1, + View: FormView, + }); + + await testUtils.dom.click(form.$('.o_field_color:first()')); + assert.containsNone($(document.body), '.modal', + "Color field in readonly shouldn't be editable"); + + const rowInitialHeight = form.$('.o_data_row:first()').height(); + + await testUtils.form.clickEdit(form); + await testUtils.dom.click(form.$('.o_data_row:first() .o_data_cell:first()')); + + assert.strictEqual(rowInitialHeight, form.$('.o_data_row:first()').height(), + "Color field shouldn't change the color height when edited"); + + form.destroy(); + }); + + QUnit.test('Field Color: pick and reset colors', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<field name="hex_color" widget="color" />' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual($('.o_field_color').css('backgroundColor'), 'rgb(255, 0, 0)', + "Background of the color field should be initially red"); + + await testUtils.dom.click(form.$('.o_field_color')); + await testUtils.fields.editAndTrigger($('.modal .o_hex_input'), '#00ff00', ['change']); + await testUtils.dom.click($('.modal .btn:contains("Choose")')); + + assert.strictEqual($('.o_field_color').css('backgroundColor'), 'rgb(0, 255, 0)', + "Background of the color field should be updated to green"); + + form.destroy(); + }); + + QUnit.module('FieldColorPicker'); + + QUnit.test('FieldColorPicker: can navigate away with TAB', async function (assert) { + assert.expect(1); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form string="Partners"> + <field name="int_field" widget="color_picker"/> + <field name="foo" /> + </form>`, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + form.$el.find('a.oe_kanban_color_1')[0].focus(); + + form.$el.find('a.oe_kanban_color_1').trigger($.Event('keydown', { + which: $.ui.keyCode.TAB, + keyCode: $.ui.keyCode.TAB, + })); + assert.strictEqual(document.activeElement, form.$el.find('input[name="foo"]')[0], + "foo field should be focused"); + form.destroy(); + }); + + + QUnit.module('FieldBadge'); + + QUnit.test('FieldBadge component on a char field in list view', async function (assert) { + assert.expect(3); + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: `<list><field name="display_name" widget="badge"/></list>`, + }); + + assert.containsOnce(list, '.o_field_badge[name="display_name"]:contains(first record)'); + assert.containsOnce(list, '.o_field_badge[name="display_name"]:contains(second record)'); + assert.containsOnce(list, '.o_field_badge[name="display_name"]:contains(aaa)'); + + list.destroy(); + }); + + QUnit.test('FieldBadge component on a selection field in list view', async function (assert) { + assert.expect(3); + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: `<list><field name="selection" widget="badge"/></list>`, + }); + + assert.containsOnce(list, '.o_field_badge[name="selection"]:contains(Blocked)'); + assert.containsOnce(list, '.o_field_badge[name="selection"]:contains(Normal)'); + assert.containsOnce(list, '.o_field_badge[name="selection"]:contains(Done)'); + + list.destroy(); + }); + + QUnit.test('FieldBadge component on a many2one field in list view', async function (assert) { + assert.expect(2); + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: `<list><field name="trululu" widget="badge"/></list>`, + }); + + assert.containsOnce(list, '.o_field_badge[name="trululu"]:contains(first record)'); + assert.containsOnce(list, '.o_field_badge[name="trululu"]:contains(aaa)'); + + list.destroy(); + }); + + QUnit.test('FieldBadge component with decoration-xxx attributes', async function (assert) { + assert.expect(6); + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: ` + <list> + <field name="selection"/> + <field name="foo" widget="badge" decoration-danger="selection == 'done'" decoration-warning="selection == 'blocked'"/> + </list>`, + }); + + assert.containsN(list, '.o_field_badge[name="foo"]', 5); + assert.containsOnce(list, '.o_field_badge[name="foo"].bg-danger-light'); + assert.containsOnce(list, '.o_field_badge[name="foo"].bg-warning-light'); + + await list.reload(); + + assert.containsN(list, '.o_field_badge[name="foo"]', 5); + assert.containsOnce(list, '.o_field_badge[name="foo"].bg-danger-light'); + assert.containsOnce(list, '.o_field_badge[name="foo"].bg-warning-light'); + + list.destroy(); + }); +}); +}); +}); |
