Odoo18-Base/addons/web/static/tests/legacy/fields/basic_fields_tests.js
2025-03-10 11:12:23 +07:00

9423 lines
389 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

odoo.define('web.basic_fields_tests', function (require) {
"use strict";
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');
const legacyViewRegistry = require('web.view_registry');
const makeTestEnvironment = require("web.test_env");
const { makeLegacyCommandService } = require("@web/legacy/utils");
const { registry } = require("@web/core/registry");
const { getFixture, legacyExtraNextTick, triggerHotkey, nextTick, click, patchWithCleanup } = require("@web/../tests/helpers/utils");
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
const { registerCleanup } = require("@web/../tests/helpers/cleanup");
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';
let target;
QUnit.module('Legacy fields', {}, function () {
QUnit.module('Legacy basic_fields', {
beforeEach: function () {
registry.category("views").remove("list"); // remove new list from registry
registry.category("views").remove("kanban"); // remove new kanban from registry
registry.category("views").remove("form"); // remove new form from registry
legacyViewRegistry.add("list", ListView); // add legacy list -> will be wrapped and added to new registry
legacyViewRegistry.add("kanban", KanbanView); // add legacy kanban -> will be wrapped and added to new registry
legacyViewRegistry.add("form", FormView); // add legacy form -> will be wrapped and added to new registry
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},
monetary: {string: "MMM", type: "monetary", 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, monetary: 99.9}],
onchanges: {},
},
team: {
fields: {
partner_ids: { string: "Partner", type: "one2many", relation: 'partner' },
},
records: [{
id: 1,
partner_ids: [5],
}],
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",
}]
},
};
target = getFixture();
},
}, 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(15);
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");
assert.containsNone(form, '.o_field_boolean input:disabled',
'checkbox should not be disabled');
// 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");
assert.containsNone(form, '.o_field_boolean input:disabled',
'checkbox should not be disabled');
// 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_legacy_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_legacy_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) .form-check input').length, 5,
"should have 5 checkboxes");
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .form-check input:checked').length, 4,
"should have 4 checked input");
// Edit a line
var $cell = list.$('tr.o_data_row:has(.form-check input:checked) td:not(.o_list_record_selector)').first();
assert.ok($cell.find('.form-check input:checked').prop('disabled'),
"input should be disabled in readonly mode");
await testUtils.dom.click($cell);
assert.ok(!$cell.find('.form-check input:checked').prop('disabled'),
"input should not have the disabled property in edit mode");
await testUtils.dom.click($cell.find('.form-check input:checked'));
// save
await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
$cell = list.$('tr.o_data_row:has(.form-check input:not(:checked)) td:not(.o_list_record_selector)').first();
assert.ok($cell.find('.form-check input:not(:checked)').prop('disabled'),
"input should be disabled again");
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .form-check input').length, 5,
"should still have 5 checkboxes");
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .form-check 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('.form-check input'));
await testUtils.dom.click($cell.find('.form-check input'));
// Save
await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .form-check input').length, 5,
"should still have 5 checkboxes");
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .form-check 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(.form-check input:not(:checked)) td:not(.o_list_record_selector)').first();
await testUtils.dom.click($cell.find('.form-check .form-check-label'));
await testUtils.nextTick();
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .form-check input').length, 5,
"should still have 5 checkboxes");
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .form-check input:checked').length, 4,
"should now have 4 checked input back");
list.destroy();
});
QUnit.test('readonly boolean field', async function (assert) {
assert.expect(6);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="bar" attrs="{'readonly': True}"/>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_field_boolean input:checked',
"checkbox should be checked");
assert.containsOnce(form, '.o_field_boolean input:disabled',
'checkbox should be disabled');
await testUtils.form.clickEdit(form);
assert.containsOnce(form, '.o_field_boolean input:checked',
"checkbox should still be checked");
assert.containsOnce(form, '.o_field_boolean input:disabled',
'checkbox should still be disabled');
await testUtils.dom.click(form.$('.o_field_boolean label'));
assert.containsOnce(form, '.o_field_boolean input:checked',
"checkbox should still be checked");
assert.containsOnce(form, '.o_field_boolean input:disabled',
'checkbox should still be disabled');
form.destroy();
});
QUnit.test('boolean field is editable in an editable form', async function (assert) {
assert.expect(2);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form edit="1">' +
'<field name="bar" widget="boolean"/>' +
'</form>',
});
assert.containsOnce(form, '.o_field_boolean input:enabled',
"the field should be editable");
await testUtils.form.clickSave(form);
assert.containsOnce(form, '.o_field_boolean input:enabled',
"the field should be editable");
form.destroy();
});
QUnit.test('boolean field is not editable in a readonly form', async function (assert) {
assert.expect(1);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form edit="0">' +
'<field name="bar" widget="boolean"/>' +
'</form>',
viewOptions: {
mode: 'readonly',
},
});
assert.containsOnce(form, '.o_field_boolean input:disabled',
"the field should not be editable");
form.destroy();
});
QUnit.test('boolean field is not editable with a readonly modifier', async function (assert) {
assert.expect(1);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="bar" widget="boolean" readonly="1"/>' +
'</form>',
});
assert.containsOnce(form, '.o_field_boolean input:disabled',
"the field should not be editable");
form.destroy();
});
QUnit.module('FieldBooleanToggle');
QUnit.test('use boolean toggle widget in form view', async function (assert) {
assert.expect(7);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="bar" widget="boolean_toggle"/></form>',
res_id: 2,
});
// We don't check the exact value of the background as it change in community/enterprise,
// and it's difficult to check the value of an SVG
assert.containsOnce(form, ".form-check.o_boolean_toggle", "Boolean toggle widget applied to boolean field");
let checkElement = document.querySelector('.form-check.o_boolean_toggle .form-check-input');
assert.ok(checkElement.matches(':checked'), "Boolean toggle should be checked");
let style = window.getComputedStyle(checkElement);
const backgroundImage = style.backgroundImage;
const backgroundColor = style.backgroundColor;
assert.ok(backgroundImage && backgroundColor, "Boolean toggle should have a background image and a background color");
await testUtils.dom.click(form.$('.o_field_widget[name=bar]'));
checkElement = document.querySelector('.form-check.o_boolean_toggle .form-check-input');
assert.notOk(checkElement.matches(':checked'), "Boolean toggle shouldn't be checked");
style = window.getComputedStyle(checkElement);
const newBackgroundImage = style.backgroundImage;
const newBackgroundColor = style.backgroundColor;
assert.ok(backgroundImage && backgroundColor, "Boolean toggle should have a background image and a background color");
assert.notEqual(backgroundImage, newBackgroundImage, "Background image should be different");
assert.notEqual(backgroundColor, newBackgroundColor, "Background color should be different");
form.destroy();
});
QUnit.test('boolean toggle widget is not disabled in readonly mode', async function (assert) {
assert.expect(3);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="bar" widget="boolean_toggle"/></form>',
res_id: 5,
});
assert.containsOnce(form, ".form-check.o_boolean_toggle", "Boolean toggle widget applied to boolean field");
assert.notOk(form.$('.o_boolean_toggle input')[0].checked);
await testUtils.dom.click(form.$('.o_boolean_toggle'));
assert.ok(form.$('.o_boolean_toggle input')[0].checked);
form.destroy();
});
QUnit.test('boolean toggle widget is disabled with a readonly attribute', async function (assert) {
assert.expect(3);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="bar" widget="boolean_toggle" readonly="1"/></form>',
res_id: 5,
});
assert.containsOnce(form, ".form-check.o_boolean_toggle", "Boolean toggle widget applied to boolean field");
await testUtils.dom.click(form.$buttons.find('.o_form_button_edit'));
assert.notOk(form.$('.o_boolean_toggle input')[0].checked);
await testUtils.dom.click(form.$('.o_boolean_toggle'));
assert.notOk(form.$('.o_boolean_toggle input')[0].checked);
form.destroy();
});
QUnit.test('boolean toggle widget is enabled in edit mode', async function (assert) {
assert.expect(3);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="bar" widget="boolean_toggle"/></form>',
res_id: 5,
});
assert.containsOnce(form, ".form-check.o_boolean_toggle", "Boolean toggle widget applied to boolean field");
await testUtils.dom.click(form.$buttons.find('.o_form_button_edit'));
assert.notOk(form.$('.o_boolean_toggle input')[0].checked);
await testUtils.dom.click(form.$('.o_boolean_toggle'));
assert.ok(form.$('.o_boolean_toggle input')[0].checked);
form.destroy();
});
QUnit.module('FieldToggleButton');
QUnit.test('use toggle_button in list view', async function (assert) {
assert.expect(6);
field_registry.add("toggle_button", basicFields.FieldToggleBoolean);
registerCleanup(() => delete field_registry.map.toggle_button);
var list = await createView({
View: ListView,
model: 'partner',
data: this.data,
arch: '<tree>' +
'<field name="bar" widget="toggle_button" ' +
'options="{&quot;active&quot;: &quot;Reported in last payslips&quot;, &quot;inactive&quot;: &quot;To Report in Payslip&quot;}"/>' +
'</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_legacy_list_view button').first(), 'title',
"Reported in last payslips", "active buttons should have proper tooltip");
assert.hasAttrValue(list.$('.o_legacy_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_legacy_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_legacy_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);
field_registry.add("toggle_button", basicFields.FieldToggleBoolean);
registerCleanup(() => delete field_registry.map.toggle_button);
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);
field_registry.add("toggle_button", basicFields.FieldToggleBoolean);
registerCleanup(() => delete field_registry.map.toggle_button);
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.test('toggle_button in form view with readonly modifiers', async function (assert) {
assert.expect(4);
field_registry.add("toggle_button", basicFields.FieldToggleBoolean);
registerCleanup(() => delete field_registry.map.toggle_button);
const 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'}" readonly="True"/>
</form>`,
res_id: 2,
});
assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length,
1, "should be green");
assert.ok(form.$('.o_field_widget[name=bar]').prop('disabled'),
"button should be disabled when readonly attribute is given");
// assert that the button has properly been disabled
assert.ok(form.$('.o_field_widget[name=bar]').get(0).disabled);
assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length,
1, "should be green even after click");
form.destroy();
});
QUnit.module('FieldNumeric');
QUnit.test('numeric field: fields with keydown on numpad decimal key', async function (assert) {
assert.expect(5);
this.data.partner.fields.float_factor_field = { string: "Float Factor", type: 'float_factor' };
this.data.partner.records[0].float_factor_field = 9.99;
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;
this.data.partner.fields.percentage = { string: "Percentage", type: 'percentage' };
this.data.partner.records[0].percentage = .99;
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form string="Partners">
<field name="float_factor_field" options="{'factor': 0.5}"/>
<field name="qux"/>
<field name="int_field"/>
<field name="monetary"/>
<field name="currency_id" invisible="1"/>
<field name="percentage"/>
</form>
`,
res_id: 1,
translateParameters: {
decimal_point: "🇧🇪",
},
});
// Record edit mode
await testUtilsDom.click(form.el.querySelector('.o_form_button_edit'));
// Get all inputs
const floatFactorField = form.el.querySelector('.o_input[name="float_factor_field"]');
const floatInput = form.el.querySelector('.o_input[name="qux"]');
const integerInput = form.el.querySelector('.o_input[name="int_field"]');
const monetaryInput = form.el.querySelector('.o_input[name="monetary"]');
const percentageInput = form.el.querySelector('.o_input[name="percentage"]');
// Dispatch numpad "dot" and numpad "comma" keydown events to all inputs and check
// Numpad "comma" is specific to some countries (Brazil...)
floatFactorField.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' }));
floatFactorField.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' }));
await testUtils.nextTick();
assert.ok(floatFactorField.value.endsWith('🇧🇪🇧🇪'));
floatInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' }));
floatInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' }));
await testUtils.nextTick();
assert.ok(floatInput.value.endsWith('🇧🇪🇧🇪'));
integerInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' }));
integerInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' }));
await testUtils.nextTick();
assert.ok(integerInput.value.endsWith('🇧🇪🇧🇪'));
monetaryInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' }));
monetaryInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' }));
await testUtils.nextTick();
assert.ok(monetaryInput.querySelector('input.o_input') .value.endsWith('🇧🇪🇧🇪'));
percentageInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' }));
percentageInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' }));
await testUtils.nextTick();
assert.ok(percentageInput.querySelector('input.o_input').value.endsWith('🇧🇪🇧🇪'));
form.destroy();
});
QUnit.test('numeric field: field with type `number` and with keydown on numpad decimal key', async function (assert) {
assert.expect(2);
patchWithCleanup(basicFields.NumericField.constructor.prototype, {
_onKeydown(ev) {
const res = this._super(...arguments);
// This _onKeydown handler must not prevent default
// a keydown event for NumericField with type=number
assert.ok(!ev.defaultPrevented);
return res;
},
});
this.data.partner.fields.float_field = { string: "Float", type: 'float' };
this.data.partner.records[0].float_field = 123;
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form string="Partners">
<field name="float_field" options="{'type': 'number'}"/>
</form>
`,
res_id: 1,
});
await testUtilsDom.click(form.el.querySelector('.o_form_button_edit'));
const floatField = form.el.querySelector('.o_input[name="float_field"]');
assert.strictEqual(floatField.type, 'number')
floatField.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' }));
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_legacy_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_legacy_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.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_email > a');
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.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_email > a');
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.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_email > a');
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.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_email > a');
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.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_email > a');
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.test('readonly email field field is properly rerendered after been changed by onchange', async function (assert) {
assert.expect(2);
this.data.partner.records[0].foo = 'dolores.abernathy@delos';
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="int_field" on_change="1"/>' + // onchange to update mobile in readonly mode directly
'<field name="foo" widget="email" readonly="1"/>' + // readonly only, we don't want to go through write mode
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
viewOptions: {mode: 'edit'},
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return Promise.resolve({
value: {
foo: 'lara.espin@unknown', // onchange to update foo in readonly mode directly
},
});
}
return this._super.apply(this, arguments);
},
});
// check initial rendering
assert.strictEqual(form.$('.o_field_email').text(), 'dolores.abernathy@delos',
'Initial email text should be set');
// trigger the onchange to update phone field, but still in readonly mode
await testUtils.fields.editInput($('input[name="int_field"]'), '3');
// check rendering after changes
assert.strictEqual(form.$('.o_field_email').text(), 'lara.espin@unknown',
'email text should be updated');
form.destroy();
});
QUnit.test("click on email field link don't switch to edit mode", async function (assert) {
testUtils.mock.patch(field_registry.map.email, {
_onClickLink: function (ev) {
assert.step(ev.target.nodeName + " clicked");
this._super.apply(this, arguments);
},
});
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,
});
await testUtils.dom.click(form.el.querySelector('.o_field_email a'));
assert.containsOnce(form.el, ".o_form_readonly", "form view is not in edit mode");
await testUtils.dom.click(form.el.querySelector('.o_field_email'));
assert.verifySteps(["A clicked", "DIV clicked"]);
assert.containsOnce(form.el, ".o_form_editable", "form view is in edit mode");
form.destroy();
testUtils.mock.unpatch(field_registry.map.email);
});
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_kw/partner/get_field_translations") {
assert.deepEqual(args.args, [[1],"foo"], "should translate the foo field of the record");
return Promise.resolve([
[{lang: "en_US", source: "yop", value: "yop"}, {lang: "fr_BE", source: "yop", value: "valeur français"}],
{translation_type: "char", translation_show_source: false},
]);
}
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]);
}
if (route === "/web/dataset/call_kw/partner/update_field_translations") {
assert.deepEqual(args.args, [[1], "foo", {"en_US": "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(8);
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_kw/partner/get_field_translations") {
assert.deepEqual(args.args, [[1],"foo"], "should translate the foo field of the record");
return Promise.resolve([
[{lang: "en_US", source: "first paragraph", value: "first paragraph"},
{lang: "en_US", source: "second paragraph", value: "second paragraph"},
{lang: "fr_BE", source: "first paragraph", value: ""},
{lang: "fr_BE", source: "second paragraph", value: "deuxième paragraphe"}],
{translation_type: "char", translation_show_source: true},
]);
}
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]);
}
if (route === "/web/dataset/call_kw/partner/update_field_translations") {
assert.deepEqual(args.args, [[1], "foo", {
"en_US": {"first paragraph": "first paragraph modified"},
"fr_BE": {
"first paragraph": "premier paragraphe modifié",
"deuxième paragraphe": "deuxième paragraphe modifié",
},
}], "the new translation value should be written");
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');
const $translations = $('.modal .o_translation_dialog .translation input');
const enField1 = $translations[0];
assert.strictEqual(enField1.value, 'first paragraph',
'first part of english translation should be filled');
await testUtils.fields.editInput(enField1, "first paragraph modified");
const frField1 = $translations[2];
assert.strictEqual(frField1.value, '',
'first part of french translation should not be filled');
await testUtils.fields.editInput(frField1, "premier paragraphe modifié");
const frField2 = $translations[3];
assert.strictEqual(frField2.value, 'deuxième paragraphe',
'second part of french translation should be filled');
await testUtils.fields.editInput(frField2, "deuxième paragraphe modifié");
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(10);
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, 'div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a',
"should have a anchor with correct classes");
assert.hasAttrValue(form.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a'), 'href', 'http://yop',
"should have proper href link");
assert.hasAttrValue(form.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a'), 'target', '_blank',
"should have target attribute set to _blank");
assert.strictEqual(form.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a').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, 'div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a',
"should still have a anchor with correct classes");
assert.hasAttrValue(form.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a'), 'href', 'http://limbo',
"should have proper new href link");
assert.strictEqual(form.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a').text(), 'limbo',
'the new value should be displayed');
await testUtils.form.clickEdit(form);
testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), '/web/limbo');
await testUtils.form.clickSave(form);
assert.hasAttrValue(form.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a'), 'href', '/web/limbo',
"should'nt have change link");
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.$('div[name="foo"].o_field_url > a').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.$('div[name="url1"].o_field_url > a').attr('href'), 'http://www.url1.com');
assert.strictEqual(form.$('div[name="url2"].o_field_url > a').attr('href'), 'www.url2.com');
assert.strictEqual(form.$('div[name="url3"].o_field_url > a').attr('href'), 'http://www.url3.com');
assert.strictEqual(form.$('div[name="url4"].o_field_url > a').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, 'div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a', 5,
"should have 5 anchors with correct classes");
assert.hasAttrValue(list.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a').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, 'div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a', 5,
"should still have 5 anchors with correct classes");
assert.hasAttrValue(list.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a').first(), 'href', 'http://brolo',
"should have proper new href link");
assert.strictEqual(list.$('div.o_form_uri.o_field_widget.o_text_overflow.o_field_url > a').first().text(), 'brolo',
"value should be properly updated");
list.destroy();
});
QUnit.test('url widget with falsy value', async function (assert) {
assert.expect(4);
this.data.partner.records[0].foo = false;
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="foo" widget="url"/></form>',
res_id: 1,
});
assert.containsOnce(form, 'div.o_field_widget[name=foo]');
assert.strictEqual(form.$('.o_field_widget[name=foo]').text(), "");
await testUtils.form.clickEdit(form);
assert.containsOnce(form, 'input.o_field_widget[name=foo]');
assert.strictEqual(form.$('.o_field_widget[name=foo]').val(), "");
form.destroy();
});
QUnit.test('url old content is cleaned on render edit', async function (assert) {
assert.expect(3);
this.data.partner.fields.foo2 = { string: "Foo2", type: "char", default: "foo2" };
this.data.partner.onchanges.foo2 = function (record) {
record.foo = record.foo2;
}
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:`
<form string="Partners">
<sheet>
<group>
<field name="foo" widget="url" attrs="{'readonly': True}" />
<field name="foo2" />
</group>
</sheet>
</form>
`,
res_id: 1,
});
assert.strictEqual(form.$('.o_field_widget[name=foo]').text(), 'yop',
"the starting value should be displayed properly");
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('.o_field_widget[name=foo2]').val(), 'foo2',
"input should contain field value in edit mode");
await testUtils.fields.editInput(form.$('.o_field_widget[name=foo2]'), 'bonjour');
assert.strictEqual(form.$('.o_field_widget[name=foo]').text(), 'bonjour',
"Url widget should show the new value and not " + form.$('.o_field_widget[name=foo]').text());
form.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_kw/partner/get_field_translations") {
assert.deepEqual(args.args, [[1],"txt"], "should translate the txt field of the record");
return Promise.resolve([
[{lang: "en_US", source: "yop", value: "yop"}, {lang: "fr_BE", source: "yop", value: "valeur français"}],
{translation_type: "text", translation_show_source: false},
]);
}
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]);
}
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(4);
// 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"]',
'The link to download the binary should be present');
assert.containsNone(form, 'a.o_field_widget[name="document"] > .fa-download',
'The download icon should not be present');
var link = form.$('a.o_field_widget[name="document"]');
assert.ok(link.is(':hidden'), "the link element should not be visible");
// force visibility to test that the clicking has also been disabled
link.removeClass('o_hidden');
testUtils.dom.click(link);
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");
}
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.strictEqual(form.$('div[name="document"] > img')[0].dataset.src,
`data:image/png;base64,${MY_IMAGE}`, "the image should have the correct src");
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 rendered with one dimension set', async function (assert) {
assert.expect(6);
this.data.partner.fields.picture = { string: 'Picture', type: 'binary' };
this.data.partner.records[0].__last_update = '2017-02-08 10:00:00';
this.data.partner.records[0].document = 'myimage1';
this.data.partner.records[0].picture = 'myimage2';
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="document" widget="image" options="{\'size\': [180, 0]}"/> ' +
'<field name="picture" widget="image" options="{\'size\': [0, 270]}"/> ' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route.startsWith('data:image/png;base64,myimage')) {
return Promise.resolve('wow');
}
return this._super.apply(this, arguments);
},
});
assert.containsOnce(form, 'div[name="document"] > img', "the widget should contain an image");
assert.hasAttrValue(form.$('div[name="document"] > img'), 'width', "180",
"the image should correctly set its attributes");
const image1Style = form.$('div[name="document"] > img').attr('style');
assert.ok(['max-width: 180px', 'height: auto', 'max-height: 100%'].every(e => image1Style.includes(e)),
"the image should correctly set its style");
assert.containsOnce(form, 'div[name="picture"] > img', "the widget should contain an image");
assert.hasAttrValue(form.$('div[name="picture"] > img'), 'height', "270",
"the image should correctly set its attributes");
const image2Style = form.$('div[name="picture"] > img').attr('style');
assert.ok(['max-height: 270px', 'width: auto', 'max-width: 100%'].every(e => image2Style.includes(e)),
"the image should correctly set its style");
form.destroy();
});
QUnit.test('image fields are correctly replaced when given an incorrect value', 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 = '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,
});
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.strictEqual(form.$('div[name="document"] > img')[0].dataset.src,
`/web/static/img/placeholder.png`, "the image should have the correct src");
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();
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(4);
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" mode="kanban">' +
// use kanban view as the tree will trigger edit mode
// and thus won't display the field
'<kanban>' +
'<field name="display_name"/>' +
'<templates>' +
'<t t-name="kanban-box">' +
'<div class="oe_kanban_global_click">' +
'<span><t t-esc="record.display_name.value"/></span>' +
'</div>' +
'</t>' +
'</templates>' +
'</kanban>' +
'<form>' +
'<field name="image" widget="image"/>' +
'</form>' +
'</field>' +
'</form>',
res_id: 1,
});
assert.ok(
document.querySelector(`img[data-src="data:image/png;base64,${MY_IMAGE}"]`),
"The view's image is in the DOM"
);
assert.containsOnce(form, '.o_kanban_record.oe_kanban_global_click',
'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.$('.oe_kanban_global_click'));
assert.strictEqual($('.modal').length, 1,
'The modal should have opened');
assert.ok(
document.querySelector(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`),
"The dialog's image is in the DOM"
);
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,
});
assert.containsOnce(form, 'tr.o_data_row',
'There should be one record in the many2many');
assert.ok(
document.querySelector(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`),
"The list's image is in the DOM"
);
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_legacy_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,
});
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.strictEqual(form.$('div[name="foo"] > img')[0].dataset.src,
FR_FLAG_URL, "the image should have the correct src");
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 fields are correctly rendered with one dimension set', async function (assert) {
assert.expect(6);
this.data.partner.fields.tortue = {string: "Tortue", type: "char", default: "a"};
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo" widget="image_url" options="{\'size\': [180, 0]}"/> ' +
'<field name="tortue" widget="image_url" options="{\'size\': [0, 270]}"/> ' +
'</form>',
res_id: 6,
});
assert.containsOnce(form, 'div[name="foo"] > img', "the widget should contain an image");
assert.hasAttrValue(form.$('div[name="foo"] > img'), 'width', "180",
"the image should correctly set its attributes");
const image1Style = form.$('div[name="foo"] > img').attr('style');
assert.ok(['max-width: 180px', 'height: auto', 'max-height: 100%'].every(e => image1Style.includes(e)),
"the image should correctly set its style");
assert.containsOnce(form, 'div[name="tortue"] > img', "the widget should contain an image");
assert.hasAttrValue(form.$('div[name="tortue"] > img'), 'height', "270",
"the image should correctly set its attributes");
const image2Style = form.$('div[name="tortue"] > img').attr('style');
assert.ok(['max-height: 270px', 'width: auto', 'max-width: 100%'].every(e => image2Style.includes(e)),
"the image should correctly set its style");
form.destroy();
});
QUnit.test('image_url widget in subviews are loaded correctly', async function (assert) {
assert.expect(4);
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" mode="kanban">' +
// use kanban view as the tree will trigger edit mode
// and thus won't display the field
'<kanban>' +
'<field name="display_name"/>' +
'<templates>' +
'<t t-name="kanban-box">' +
'<div class="oe_kanban_global_click">' +
'<span><t t-esc="record.display_name.value"/></span>' +
'</div>' +
'</t>' +
'</templates>' +
'</kanban>' +
'<form>' +
'<field name="image" widget="image_url"/>' +
'</form>' +
'</field>' +
'</form>',
res_id: 6,
});
assert.ok(
document.querySelector(`img[data-src="${FR_FLAG_URL}"]`),
"The view's image is in the DOM"
);
assert.containsOnce(form, '.o_kanban_record.oe_kanban_global_click',
'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.$('.oe_kanban_global_click'));
assert.strictEqual($('.modal').length, 1,
'The modal should have opened');
assert.ok(
document.querySelector(`img[data-src="${EN_FLAG_URL}"]`),
"The dialog's image is in the DOM"
);
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,
});
assert.containsOnce(form, 'tr.o_data_row',
'There should be one record in the many2many');
assert.ok(
document.querySelector(`img[data-src="${EN_FLAG_URL}"]`),
"The list's image is in the DOM"
);
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 without quickedit [REQUIRE FOCUS]', async function (assert) {
assert.expect(21);
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'), 'none',
"first date range picker should be closed initially");
assert.strictEqual($('.daterangepicker:last').css('display'), 'none',
"second date range picker should be closed initially");
// open the first one
await testUtils.dom.click(form.$('.o_field_date_range:first'));
assert.strictEqual($('.daterangepicker:first').css('display'), 'block',
"first date range picker should be opened");
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("Datetime field with option format type is 'date'", 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', 'format_type': 'date'}"/>'
<field name="datetime_end" widget="daterange" options="{'related_start_date': 'datetime', 'format_type': 'date'}"/>'
</form>`,
res_id: 1,
session: {
getTZOffset() {
return 330;
},
},
});
assert.strictEqual(form.el.querySelector('.o_field_date_range[name="datetime"]').innerText, '02/08/2017',
"the start date should only show date when option formatType is Date");
assert.strictEqual(form.el.querySelector('.o_field_date_range[name="datetime_end"]').innerText, '03/13/2017',
"the end date should only show date when option formatType is Date");
form.destroy();
});
QUnit.test("Date field with option format type is 'datetime'", async function (assert) {
assert.expect(2);
this.data.partner.fields.date_end = {string: 'Date End', type: 'date'};
this.data.partner.records[0].date_end = '2017-03-13';
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `<form>
<field name="date" widget="daterange" options="{'related_end_date': 'date_end', 'format_type': 'datetime'}"/>
<field name="date_end" widget="daterange" options="{'related_start_date': 'date', 'format_type': 'datetime'}"/>
</form>`,
res_id: 1,
session: {
getTZOffset() {
return 330;
},
},
});
assert.strictEqual(form.el.querySelector('.o_field_date_range[name="date"]').innerText, '02/03/2017 05:30:00',
"the start date should show date with time when option format_type is datatime");
assert.strictEqual(form.el.querySelector('.o_field_date_range[name="date_end"]').innerText, '03/13/2017 05:30:00',
"the end date should show date with time when option format_type is datatime");
form.destroy();
});
QUnit.test('Date field without quickedit [REQUIRE FOCUS]', async function (assert) {
assert.expect(19);
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'), 'none',
"first date range picker should be closed initially");
assert.strictEqual($('.daterangepicker:last').css('display'), 'none',
"second date range picker should be closed initially");
// open the first one
await testUtils.dom.click(form.$('.o_field_date_range:first'));
assert.strictEqual($('.daterangepicker:first').css('display'), 'block',
"first date range picker should be opened");
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('Date field with quickedit [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: {
// #tzoffset_daterange
// Date field should not have an offset as they are ignored.
// However, in the test environement, a UTC timezone is set to run all tests. And if any code does not use the safe timezone method
// provided by the framework (which happens in this case inside the date range picker lib), unexpected behavior kicks in as the timezone
// of the dev machine collides with the timezone set by the test env.
// To avoid failing test on dev's local machines, a hack is to apply an timezone offset greater than the difference between UTC and the dev's
// machine timezone. For belgium, > 60 is enough. For India, > 5h30 is required, hence 330.
// Note that prod and runbot will never have a problem with this, it only happens as you mock the getTZOffset method (like in tests).
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");
// open the first one with quick edit
await testUtils.dom.click(form.$('.o_field_date_range:first'));
// Check date range picker initialization
assert.containsN(document.body, '.daterangepicker', 2,
"should initialize 2 date range picker");
assert.strictEqual($('.daterangepicker:first').css('display'), 'block',
"first date range picker should be opened initially");
assert.strictEqual($('.daterangepicker:last').css('display'), 'none',
"second 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.toString(), '<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.dom.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(3);
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');
const active = document.activeElement;
assert.equal(active.tagName, 'INPUT', "The datepicker input should be focused");
const sel = active.value.slice(active.selectionStart, active.selectionEnd);
assert.strictEqual(sel, "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.dom.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',
},
session: {
getTZOffset: function () {
return 120;
},
},
});
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>' +
// display datetime in readonly as modal will open in edit
'<field name="datetime" widget="date" attrs="{\'readonly\': 1}"/>' +
'</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>' +
// display datetime in readonly as modal will open in edit
'<field name="datetime" widget="date" attrs="{\'readonly\': 1}"/>' +
'</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.test('datetime field: hit enter should update value', async function (assert) {
/*
This test verifies that the field datetime is correctly computed when:
- we press enter to validate our entry
- we click outside the field to validate our entry
- we save
*/
assert.expect(3);
const 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',
},
viewOptions: {
mode: 'edit',
},
session: {
getTZOffset: function () {
return 120;
},
},
});
const datetime = form.el.querySelector('input[name="datetime"]');
// Enter a beginning of date and press enter to validate
await testUtils.fields.editInput(datetime, '01/08/22 14:30:40');
await testUtils.fields.triggerKeydown(datetime, 'enter');
const datetimeValue = `01/08/2022 14:30:40`;
assert.strictEqual(datetime.value, datetimeValue);
// Click outside the field to check that the field is not changed
await testUtils.dom.click(form.$el);
assert.strictEqual(datetime.value, datetimeValue);
// Save and check that it's still ok
await testUtils.form.clickSave(form);
const { textContent } = form.el.querySelector('span[name="datetime"]')
assert.strictEqual(textContent, datetimeValue);
form.destroy();
});
QUnit.test('datetime field with date widget: 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="datetime" widget="date"/></form>',
res_id: 1,
translateParameters: { // Avoid issues due to localization formats
date_format: '%m/%d/%Y',
},
viewOptions: {
mode: 'edit',
},
session: {
getTZOffset: function () {
return 120;
},
},
});
const datetime = form.el.querySelector('input[name="datetime"]');
await testUtils.fields.editInput(datetime, '01/08/22');
await testUtils.fields.triggerKeydown(datetime, 'enter');
assert.strictEqual(datetime.value, '01/08/2022');
// Click outside the field to check that the field is not changed
await testUtils.dom.click(form.$el);
assert.strictEqual(datetime.value, '01/08/2022');
form.destroy();
});
QUnit.test("datetime field: use picker with arabic numbering system", async function (assert) {
assert.expect(2);
const symbols = [
["1", "١"],
["2", "٢"],
["3", "٣"],
["4", "٤"],
["5", "٥"],
["6", "٦"],
["7", "٧"],
["8", "٨"],
["9", "٩"],
["0", "٠"],
];
const symbolMap = Object.fromEntries(symbols);
const numberMap = Object.fromEntries(symbols.map(([latn, arab]) => [arab, latn]));
const originalLocale = moment.locale();
moment.defineLocale("TEST_ar", {
preparse:
(string) => string
.replace(/\u200f/g, "")
.replace(/[١٢٣٤٥٦٧٨٩٠]/g, (match) => numberMap[match])
.replace(/،/g, ","),
postformat:
(string) => string
.replace(/\d/g, (match) => symbolMap[match])
.replace(/,/g, "،"),
});
const form = await createView({
View: FormView,
model: "partner",
data: this.data,
arch: /* xml */ `
<form string="Partners">
<field name="datetime" />
</form>
`,
res_id: 1,
viewOptions: {
mode: "edit",
},
});
const getInput = () => form.el.querySelector("[name=datetime] input")
const click = (el) => testUtils.dom.click($(el));
assert.strictEqual(getInput().value, "٠٢/٠٨/٢٠١٧ ١٠:٠٠:٠٠");
await click(getInput());
await click(document.querySelector("[data-action=togglePicker]"));
await click(document.querySelector("[data-action=showMinutes]"));
await click(document.querySelectorAll("[data-action=selectMinute]")[9]);
await click(document.querySelector("[data-action=showSeconds]"));
await click(document.querySelectorAll("[data-action=selectSecond]")[3]);
assert.strictEqual(getInput().value, "٠٢/٠٨/٢٠١٧ ١٠:٤٥:١٥");
moment.locale(originalLocale);
moment.updateLocale("TEST_ar", null);
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'), 'fw-bold text-warning');
assert.doesNotHaveClass(list.$('.o_data_cell:nth(1) div'), 'fw-bold text-warning text-danger');
assert.hasClass(list.$('.o_data_cell:nth(2) div'), 'fw-bold text-danger');
assert.doesNotHaveClass(list.$('.o_data_cell:nth(3) div'), 'fw-bold text-warning text-danger');
assert.hasClass(list.$('.o_data_cell:nth(4) div'), 'fw-bold text-danger');
assert.doesNotHaveClass(list.$('.o_data_cell:nth(5) div'), 'fw-bold text-warning text-danger');
assert.hasClass(list.$('.o_data_cell:nth(6) div'), 'fw-bold text-danger');
list.destroy();
unpatchDate();
});
QUnit.test('remaining_days widget on a date field in multi edit list view', async function (assert) {
assert.expect(7);
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: 8, date: false },
];
const list = await createView({
View: ListView,
model: 'partner',
data: this.data,
arch: '<tree multi_edit="1"><field name="date" widget="remaining_days"/></tree>',
translateParameters: { // Avoid issues due to localization formats
date_format: '%m/%d/%Y',
},
});
assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow');
// 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', 'should have date picker input');
await testUtils.fields.editAndTrigger(list.$('.o_datepicker_input'), '10/10/2017', ['input', 'change', 'focusout']);
assert.containsOnce(document.body, '.modal');
assert.strictEqual($('.modal .o_field_widget').text(), "In 2 days",
"should have 'In 2 days' value to change");
await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
assert.strictEqual(list.$('.o_data_row:first .o_data_cell').text(), "In 2 days",
"should have 'In 2 days' as date field value");
assert.strictEqual(list.$('.o_data_row:nth(1) .o_data_cell').text(), "In 2 days",
"should have 'In 2 days' as date field value");
list.destroy();
unpatchDate();
});
QUnit.test('remaining_days widget, enter empty value manually in edit list 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 list = await createView({
View: ListView,
model: 'partner',
data: this.data,
arch: '<tree multi_edit="1"><field name="date" widget="remaining_days"/></tree>',
translateParameters: { // Avoid issues due to localization formats
date_format: '%m/%d/%Y',
},
});
assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
// 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:first .o_data_cell'));
assert.containsOnce(list, 'input.o_datepicker_input', 'should have date picker input');
await testUtils.fields.editAndTrigger(list.$('.o_datepicker_input'), '', ['input', 'change']);
await testUtils.dom.click(list.$el);
assert.containsNone(document.body, '.modal');
assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), '');
list.destroy();
unpatchDate();
});
QUnit.test('remaining_days widget, enter wrong value manually in multi edit list view', 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: 8, date: false },
];
const list = await createView({
View: ListView,
model: 'partner',
data: this.data,
arch: '<tree multi_edit="1"><field name="date" widget="remaining_days"/></tree>',
translateParameters: { // Avoid issues due to localization formats
date_format: '%m/%d/%Y',
},
});
assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow');
// 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', 'should have date picker input');
await testUtils.fields.editAndTrigger(list.$('.o_datepicker_input'), 'blabla', ['input', 'change']);
await testUtils.dom.click(list.$el);
assert.containsNone(document.body, '.modal');
assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow');
list.destroy();
unpatchDate();
});
QUnit.test('remaining_days widget on a date field in form view', async function (assert) {
assert.expect(8);
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'), 'fw-bold text-warning');
// in edit mode, this widget should be editable.
await testUtils.form.clickEdit(form);
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable');
assert.containsOnce(form, 'div.o_field_widget[name=date] .o_datepicker');
await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 1,
"datepicker should be opened");
await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day[data-day="10/09/2017"]'));
await testUtils.form.clickSave(form);
assert.strictEqual(form.$('.o_field_widget').text(), 'Tomorrow');
await testUtils.form.clickEdit(form);
await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day[data-day="10/07/2017"]'));
await testUtils.form.clickSave(form);
assert.strictEqual(form.$('.o_field_widget').text(), 'Yesterday');
assert.hasClass(form.$('.o_field_widget'), 'text-danger');
form.destroy();
unpatchDate();
});
QUnit.test('remaining_days widget on a datetime field in form view', 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, datetime: '2017-10-08 10:00:00' }, // today
];
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="datetime" widget="remaining_days"/></form>',
res_id: 1,
});
assert.strictEqual(form.$('.o_field_widget').text(), 'Today');
assert.hasClass(form.$('.o_field_widget'), 'text-warning');
// in edit mode, this widget should be editable.
await testUtils.form.clickEdit(form);
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable');
assert.containsOnce(form, 'div.o_field_widget[name=datetime] .o_datepicker');
await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 1,
"datepicker should be opened");
await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day[data-day="10/09/2017"]'));
await testUtils.dom.click($('a[data-action="close"]'));
await testUtils.form.clickSave(form);
assert.strictEqual(form.$('.o_field_widget').text(), 'Tomorrow');
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'), 'fw-bold text-warning');
assert.doesNotHaveClass(list.$('.o_data_cell:nth(1) div'), 'fw-bold text-warning text-danger');
assert.hasClass(list.$('.o_data_cell:nth(2) div'), 'fw-bold text-danger');
assert.doesNotHaveClass(list.$('.o_data_cell:nth(3) div'), 'fw-bold text-warning text-danger');
assert.hasClass(list.$('.o_data_cell:nth(4) div'), 'fw-bold text-danger');
assert.doesNotHaveClass(list.$('.o_data_cell:nth(5) div'), 'fw-bold text-warning text-danger');
assert.hasClass(list.$('.o_data_cell:nth(6) div'), 'fw-bold 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 in the lines of a form view', async function (assert) {
assert.expect(4);
var form = await createView({
View: FormView,
model: 'team',
data: this.data,
arch:'<form>' +
'<sheet>' +
'<field name="partner_ids">' +
'<tree editable="bottom">' +
'<field name="monetary"/>' +
'<field name="currency_id" invisible="1"/>' +
'</tree>' +
'</field>' +
'</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_list_table .o_list_number').first().text(), '$\u00a099.9',
'The value should be displayed properly.');
assert.hasAttrValue(form.$('.o_list_table .o_list_number').first(), 'title', '$\u00a099.9',
'The title value should be displayed properly.');
await testUtils.form.clickEdit(form);
// Non-breaking space between the currency and the amount
assert.strictEqual(form.$('.o_list_table .o_list_number').first().text(), '$\u00a099.9',
'The value should be displayed properly.');
assert.hasAttrValue(form.$('.o_list_table .o_list_number').first(), 'title', '$\u00a099.9',
'The title value should be displayed 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 without currency symbol', 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" options="{\'no_symbol\': True}"/>' +
'<field name="currency_id" invisible="1"/>' +
'</sheet>' +
'</form>',
res_id: 5,
session: {
currencies: _.indexBy(this.data.currency.records, 'id'),
},
});
await testUtils.form.clickEdit(form);
// Non-breaking space between the currency and the amount
assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), "9.10", "The currency symbol is not displayed");
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(), "$&nbsp;9.10",
"readonly value should contain the currency");
assert.strictEqual(form.$('.o_field_monetary').first().next().html(), "$&nbsp;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&nbsp;€",
"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&nbsp;€",
"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(), "$&nbsp;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&nbsp;€",
"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_legacy_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_legacy_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.test('float_time field value formatted on blur', async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
`<form string="Partners">
<field name="qux" widget="float_time"/>
</form>`,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/partner/write') {
assert.strictEqual(args.args[1].qux, 9.5, 'the correct float value should be saved');
}
return this._super.apply(this, arguments);
},
res_id: 5,
});
assert.strictEqual(form.$('.o_field_widget').first().text(), '09:06',
'The formatted time value should be displayed properly.');
await testUtils.form.clickEdit(form);
await testUtils.fields.editAndTrigger(form.$('input[name=qux]'), '9.5', ['change']);
assert.strictEqual(form.$('input[name=qux]').val(), '09:30',
'The new value should be displayed properly in the input.');
await testUtils.form.clickSave(form);
assert.strictEqual(form.$('.o_field_widget').first().text(), '09:30',
'The new value should be saved and displayed properly.');
form.destroy();
});
QUnit.test('float_time field with invalid value', async function (assert) {
assert.expect(5);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
`<form>
<field name="qux" widget="float_time"/>
</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.toString(), '<ul><li>Qux</li></ul>');
}
}
},
});
await testUtils.fields.editAndTrigger(form.$('input[name=qux]'), 'blabla', ['change']);
await testUtils.form.clickSave(form);
assert.hasClass(form.$('input[name=qux]'), 'o_field_invalid');
await testUtils.fields.editAndTrigger(form.$('input[name=qux]'), '6.5', ['change']);
assert.doesNotHaveClass(form.$('input[name=qux]'), 'o_field_invalid',
"date field should not be displayed as invalid now");
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.test('float_toggle widget in kanban view(readonly) with option force_button', async function (assert) {
assert.expect(2);
var kanban = await createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban>' +
'<templates><t t-name="kanban-box">' +
'<div>' +
'<field name="qux" widget="float_toggle" options="{\'force_button\': true}"/>' +
'</div>' +
'</t>' +
'</templates></kanban>',
domain: [['id', 'in', [1]]],
});
assert.containsOnce(kanban, 'button.o_field_float_toggle', "should have rendered toggle button");
const value = kanban.$('button.o_field_float_toggle').text();
await testUtils.dom.click(kanban.$('button.o_field_float_toggle'));
assert.notEqual(kanban.$('button.o_field_float_toggle').text(), value, "qux field value should be changed");
kanban.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.$('div.o_field_widget.o_form_uri.o_field_phone > a');
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.$('div.o_field_widget.o_form_uri.o_field_phone > a').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 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, 'div.o_field_widget.o_form_uri.o_field_phone > a', 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, 'div.o_field_widget.o_form_uri.o_field_phone > a', 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 tooltip', async function (assert) {
assert.expect(2);
const 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,
});
// check title attribute (for basic html tooltip on all the stars)
const $stars = form.$('.o_field_widget.o_priority').find('a.o_priority_star');
assert.strictEqual($stars[0].title, 'Selection: Blocked',
"Should set field label and correct selection label as title attribute (tooltip)");
assert.strictEqual($stars[1].title, 'Selection: Done',
"Should set field label and correct selection label as title attribute (tooltip)");
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(2);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="selection" widget="priority" readonly="1"/></form>',
res_id: 2,
mockRPC(route, args) {
if (args.method === "write") {
throw new Error("should not save");
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('span.o_priority_star.fa.fa-star-o').length, 2,
"stars of priority widget should rendered with span tag if readonly");
await testUtils.dom.click(form.$('.o_priority_star.fa-star-o').last());
assert.strictEqual(form.$('.o_priority_star.fa.fa-star-o').length, 2,
"should still have two stars");
form.destroy();
});
QUnit.test('priority widget edited by the smart action "Set priority..."', async function (assert) {
assert.expect(4);
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'partner,false,form': '<form>' +
'<field name="selection" widget="priority"/>' +
'</form>',
'partner,false,search': '<search></search>',
};
const serverData = { models: this.data, views}
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
res_id: 1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'partner',
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.containsOnce(target, ".fa-star")
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Set priority...ALT + R")
assert.ok(idx >= 0);
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [
"Normal",
"Blocked",
"Done"
])
await click(target, "#o_command_2")
await legacyExtraNextTick();
assert.containsN(target, ".fa-star", 2)
});
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 for list view with hide_label option', async function (assert) {
assert.expect(6);
Object.assign(this.data.partner.fields, {
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";
const list = await createView({
View: ListView,
model: 'partner',
data: this.data,
arch:`
<tree>
<field name="graph_type" widget="state_selection" options="{'hide_label': True}"/>
<field name="selection" widget="state_selection"/>
</tree>`,
});
assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 10,
"should have ten status selection widgets");
assert.containsN(list, '.o_state_selection_cell .o_selection[name=selection] > span.align-middle', 5,
"should have five label on selection widgets");
assert.containsOnce(list, '.o_state_selection_cell .o_selection[name=selection] > span.align-middle:contains(Done)',
"should have one Done status label");
assert.containsN(list, '.o_state_selection_cell .o_selection[name=selection] > span.align-middle:contains(Normal)', 3,
"should have three Normal status label");
assert.containsN(list, '.o_state_selection_cell .o_selection[name=graph_type] > a span.o_status', 5,
"should have five status selection widgets");
assert.containsNone(list, '.o_state_selection_cell .o_selection[name=graph_type] > span.align-middle',
"should not have status label in selection widgets");
list.destroy();
});
QUnit.test('state_selection widget in editable list view', async function (assert) {
assert.expect(33);
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");
assert.containsNone(list, 'tr.o_selected_row', 'should not be in edit mode');
// 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.test('state_selection edited by the smart action "Set kanban state..."', async function (assert) {
assert.expect(4);
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'partner,false,form': '<form>' +
'<field name="selection" widget="state_selection"/>' +
'</form>',
'partner,false,search': '<search></search>',
};
const serverData = { models: this.data, views}
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
res_id: 1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'partner',
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.containsOnce(target, ".o_status_red")
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Set kanban state...ALT + SHIFT + R")
assert.ok(idx >= 0);
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [
"Normal",
"Blocked",
"Done"
])
await click(target, "#o_command_2")
await legacyExtraNextTick();
assert.containsOnce(target, ".o_status_green")
});
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.text-bg-warning',
"should have a warning status label since selection is the second, blocked state");
assert.containsNone(form, '.o_field_widget.badge.text-bg-secondary',
"should not have a default status since selection is the second, blocked state");
assert.containsNone(form, '.o_field_widget.badge.text-bg-success',
"should not have a success status since selection is the second, blocked state");
assert.strictEqual(form.$('.o_field_widget.badge.text-bg-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.text-bg-warning',
"should have a warning status label since selection is the second, blocked state");
assert.containsNone(form, '.o_field_widget.badge.text-bg-secondary',
"should not have a default status since selection is the second, blocked state");
assert.containsNone(form, '.o_field_widget.badge.text-bg-success',
"should not have a success status since selection is the second, blocked state");
assert.strictEqual(form.$('.o_field_widget.badge.text-bg-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.text-bg-warning',
"should have a warning status label since selection is the second, blocked state");
assert.containsNone(form, '.o_field_widget.badge.text-bg-secondary',
"should not have a default status since selection is the second, blocked state");
assert.containsNone(form, '.o_field_widget.badge.text-bg-success',
"should not have a success status since selection is the second, blocked state");
assert.strictEqual(form.$('.o_field_widget.badge.text-bg-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.text-bg-warning',
"should have one warning status label");
assert.strictEqual(list.$('.o_field_widget.badge.text-bg-warning').text(), 'Blocked',
"the warning label should read 'Blocked'");
assert.containsOnce(list, '.o_field_widget.badge.text-bg-secondary',
"should have one default status label");
assert.strictEqual(list.$('.o_field_widget.badge.text-bg-secondary').text(), 'Normal',
"the default label should read 'Normal'");
assert.containsOnce(list, '.o_field_widget.badge.text-bg-success',
"should have one success status label");
assert.strictEqual(list.$('.o_field_widget.badge.text-bg-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.text-bg-warning',
"should have one warning status label");
assert.strictEqual(list.$('.o_field_widget.badge.text-bg-warning').text(), 'Blocked',
"the warning label should read 'Blocked'");
assert.containsOnce(list, '.o_field_widget.badge.text-bg-secondary',
"should have one default status label");
assert.strictEqual(list.$('.o_field_widget.badge.text-bg-secondary').text(), 'Normal',
"the default label should read 'Normal'");
assert.containsOnce(list, '.o_field_widget.badge.text-bg-success',
"should have one success status label");
assert.strictEqual(list.$('.o_field_widget.badge.text-bg-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.text-bg-warning',
"should have one warning status label");
assert.strictEqual(list.$('.o_field_widget.badge.text-bg-warning').text(), 'Blocked',
"the warning label should read 'Blocked'");
assert.containsOnce(list, '.o_field_widget.badge.text-bg-secondary',
"should have one default status label");
assert.strictEqual(list.$('.o_field_widget.badge.text-bg-secondary').text(), 'Normal',
"the default label should read 'Normal'");
assert.containsOnce(list, '.o_field_widget.badge.text-bg-success',
"should have one success status label");
assert.strictEqual(list.$('.o_field_widget.badge.text-bg-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(4);
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_legacy_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_legacy_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.test('domain field: manually edit domain with textarea', async function (assert) {
assert.expect(9);
const originalDebug = odoo.debug;
odoo.debug = true;
this.data.partner.records[0].foo = false;
this.data.partner.fields.bar.type = "char";
this.data.partner.records[0].bar = "product";
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="bar"/>
<field name="foo" widget="domain" options="{'model': 'bar'}"/>
</form>`,
mockRPC(route, args) {
if (args.method === 'search_count') {
assert.step(JSON.stringify(args.args[0]));
}
return this._super.apply(this, arguments);
},
viewOptions: {
mode: 'edit',
},
res_id: 1,
});
assert.strictEqual(form.$(".o_domain_show_selection_button").text().trim(), "2 record(s)");
assert.verifySteps(["[]"]);
await testUtils.fields.editAndTrigger(form.$('.o_domain_debug_input'), "[['id', '<', 40]]", ["change"]);
// the count should not be re-computed when editing with the textarea
assert.strictEqual(form.$(".o_domain_show_selection_button").text().trim(), "2 record(s)");
assert.verifySteps([]);
await testUtils.form.clickSave(form);
assert.strictEqual(form.$(".o_domain_show_selection_button").text().trim(), "1 record(s)");
assert.verifySteps([
"[[\"id\",\"<\",40]]", // to validate the domain, before saving
"[[\"id\",\"<\",40]]", // to render in readonly once it has been saved
]);
form.destroy();
odoo.debug = originalDebug;
});
QUnit.test('domain field: manually set an invalid domain with textarea', async function (assert) {
assert.expect(9);
const originalDebug = odoo.debug;
odoo.debug = true;
this.data.partner.records[0].foo = false;
this.data.partner.fields.bar.type = "char";
this.data.partner.records[0].bar = "product";
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="bar"/>
<field name="foo" widget="domain" options="{'model': 'bar'}"/>
</form>`,
mockRPC(route, args) {
if (args.method === 'search_count') {
assert.step(JSON.stringify(args.args[0]));
}
if (args.method === "write") {
throw new Error("should not save");
}
return this._super.apply(this, arguments);
},
viewOptions: {
mode: 'edit',
},
res_id: 1,
});
assert.strictEqual(form.$(".o_domain_show_selection_button").text().trim(), "2 record(s)");
assert.verifySteps(["[]"]);
await testUtils.fields.editAndTrigger(form.$('.o_domain_debug_input'), "[['abc']]", ["change"]);
// the count should not be re-computed when editing with the textarea
assert.strictEqual(form.$(".o_domain_show_selection_button").text().trim(), "2 record(s)");
assert.verifySteps([]);
await testUtils.form.clickSave(form);
assert.hasClass(form.$(".o_field_domain"), "o_field_invalid", "the field is marked as invalid");
assert.hasClass(form.$(".o_legacy_form_view"), "o_form_editable", "the view is still in edit mode");
assert.verifySteps(["[[\"abc\"]]"]);
form.destroy();
odoo.debug = originalDebug;
});
QUnit.test('domain field: reload count by clicking on the refresh button', async function (assert) {
assert.expect(7);
const originalDebug = odoo.debug;
odoo.debug = true;
this.data.partner.records[0].foo = "[]";
this.data.partner.fields.bar.type = "char";
this.data.partner.records[0].bar = "product";
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="bar"/>
<field name="foo" widget="domain" options="{'model': 'bar'}"/>
</form>`,
async mockRPC(route, args) {
if (args.method === 'search_count') {
assert.step(JSON.stringify(args.args[0]));
}
return this._super.apply(this, arguments);
},
viewOptions: {
mode: 'edit',
},
res_id: 1,
});
assert.strictEqual(form.$(".o_domain_show_selection_button").text().trim(), "2 record(s)");
await testUtils.fields.editAndTrigger(form.$('.o_domain_debug_input'), "[['id', '<', 40]]", ["change"]);
// the count should not be re-computed when editing with the textarea
assert.strictEqual(form.$(".o_domain_show_selection_button").text().trim(), "2 record(s)");
assert.verifySteps(["[]"]);
// click on the refresh button
await testUtils.dom.click(form.$(".o_refresh_count"));
assert.strictEqual(form.$(".o_domain_show_selection_button").text().trim(), "1 record(s)");
assert.verifySteps(["[[\"id\",\"<\",40]]"]);
form.destroy();
odoo.debug = originalDebug;
});
QUnit.test('domain field: does not wait for the count to render', async function (assert) {
assert.expect(5);
this.data.partner.records[0].foo = "[]";
this.data.partner.fields.bar.type = "char";
this.data.partner.records[0].bar = "product";
const def = testUtils.makeTestPromise();
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="bar"/>
<field name="foo" widget="domain" options="{'model': 'bar'}"/>
</form>`,
async mockRPC(route, args) {
const result = this._super.apply(this, arguments);
if (args.method === 'search_count') {
await def;
}
return result;
},
res_id: 1,
});
assert.containsOnce(form, ".o_field_domain_panel .fa-circle-o-notch.fa-spin");
assert.containsNone(form, ".o_field_domain_panel .o_domain_show_selection_button");
def.resolve();
await testUtils.nextTick();
assert.containsNone(form, ".o_field_domain_panel .fa-circle-o-notch .fa-spin");
assert.containsOnce(form, ".o_field_domain_panel .o_domain_show_selection_button");
assert.strictEqual(form.$(".o_domain_show_selection_button").text().trim(), "2 record(s)");
form.destroy();
});
QUnit.test('domain field: edit domain with dynamic content', async function (assert) {
assert.expect(2);
const originalDebug = odoo.debug;
odoo.debug = true;
let rawDomain = `
[
["date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -365), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S")]
]
`;
this.data.partner.records[0].foo = rawDomain;
this.data.partner.fields.bar.type = "char";
this.data.partner.records[0].bar = "partner";
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="bar"/>
<field name="foo" widget="domain" options="{'model': 'bar'}"/>
</form>`,
async mockRPC(route, args) {
if (args.method === 'write') {
assert.strictEqual(args.args[1].foo, rawDomain);
}
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$(".o_domain_debug_input").val(), rawDomain);
rawDomain = `
[
["date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -1), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S")]
]
`;
await testUtils.fields.editAndTrigger(form.$('.o_domain_debug_input'), rawDomain, ["change"]);
await testUtils.form.clickSave(form);
form.destroy();
odoo.debug = originalDebug;
});
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.dom.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_legacy_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.dom.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_legacy_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_legacy_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_legacy_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_legacy_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_legacy_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_legacy_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_legacy_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_legacy_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.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"].text-bg-danger.bg-opacity-50');
assert.containsOnce(list, '.o_field_badge[name="foo"].text-bg-warning.bg-opacity-50');
await list.reload();
assert.containsN(list, '.o_field_badge[name="foo"]', 5);
assert.containsOnce(list, '.o_field_badge[name="foo"].text-bg-danger.bg-opacity-50');
assert.containsOnce(list, '.o_field_badge[name="foo"].text-bg-warning.bg-opacity-50');
list.destroy();
});
});
});
});