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

12066 lines
461 KiB
JavaScript

odoo.define('web.form_tests', function (require) {
"use strict";
const AbstractField = require("web.AbstractField");
var AbstractStorageService = require('web.AbstractStorageService');
var basicFields = require('web.basic_fields');
var BasicModel = require('web.BasicModel');
var concurrency = require('web.concurrency');
var core = require('web.core');
var fieldRegistry = require('web.field_registry');
const fieldRegistryOwl = require('web.field_registry_owl');
const { FieldBoolean } = require("web.basic_fields_owl");
const FormRenderer = require('web.FormRenderer');
var FormView = require('web.FormView');
var ListView = require('web.ListView');
var KanbanView = require('web.KanbanView');
var mixins = require('web.mixins');
var pyUtils = require('web.py_utils');
var RamStorage = require('web.RamStorage');
var testUtils = require('web.test_utils');
var ViewDialogs = require('web.view_dialogs');
var widgetRegistry = require('web.widget_registry');
const widgetRegistryOwl = require('web.widgetRegistry');
var Widget = require('web.Widget');
const { registry } = require('@web/core/registry');
const legacyViewRegistry = require('web.view_registry');
const { registerCleanup } = require("@web/../tests/helpers/cleanup");
var _t = core._t;
var createView = testUtils.createView;
const { getFixture, legacyExtraNextTick, patchWithCleanup } = require("@web/../tests/helpers/utils");
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
const { makeTestEnv } = require("@web/../tests/helpers/mock_env");
const makeTestEnvironment = require("web.test_env");
const { mapLegacyEnvToWowlEnv } = require("@web/legacy/utils");
const { scrollerService } = require("@web/core/scroller_service");
const { LegacyComponent } = require("@web/legacy/legacy_component");
const { onMounted, onWillUnmount, xml } = require("@odoo/owl");
let serverData;
let target;
QUnit.module('LegacyViews', {
beforeEach: function () {
// Avoid animation to not have to wait until the tooltip is removed
this.initialTooltipDefaultAnimation = Tooltip.Default.animation;
Tooltip.Default.animation = false;
target = getFixture();
registry.category("services").add("scroller", scrollerService);
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: {
display_name: { string: "Displayed name", type: "char" },
foo: {string: "Foo", type: "char", default: "My little Foo Value"},
bar: {string: "Bar", type: "boolean"},
int_field: {string: "int_field", type: "integer", sortable: true},
qux: {string: "Qux", type: "float", digits: [16,1] },
p: {string: "one2many field", type: "one2many", relation: 'partner'},
trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'},
product_id: {string: "Product", type: "many2one", relation: 'product'},
priority: {
string: "Priority",
type: "selection",
selection: [[1, "Low"], [2, "Medium"], [3, "High"]],
default: 1,
},
state: {string: "State", type: "selection", selection: [["ab", "AB"], ["cd", "CD"], ["ef", "EF"]]},
date: {string: "Some Date", type: "date"},
datetime: {string: "Datetime Field", type: 'datetime'},
product_ids: {string: "one2many product", type: "one2many", relation: "product"},
reference: {string: "Reference Field", type: 'reference', selection: [["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]},
},
records: [{
id: 1,
display_name: "first record",
bar: true,
foo: "yop",
int_field: 10,
qux: 0.44,
p: [],
timmy: [],
trululu: 4,
state: "ab",
date: "2017-01-25",
datetime: "2016-12-12 10:55:05",
}, {
id: 2,
display_name: "second record",
bar: true,
foo: "blip",
int_field: 9,
qux: 13,
p: [],
timmy: [],
trululu: 1,
state: "cd",
}, {
id: 4,
display_name: "aaa",
state: "ef",
}, {
id: 5,
display_name: "aaa",
foo:'',
bar:false,
state: "ef",
}],
onchanges: {},
},
product: {
fields: {
display_name: {string: "Product Name", type: "char"},
name: {string: "Product Name", type: "char"},
partner_type_id: {string: "Partner type", type: "many2one", relation: "partner_type"},
},
records: [{
id: 37,
display_name: "xphone",
}, {
id: 41,
display_name: "xpad",
}]
},
partner_type: {
fields: {
name: {string: "Partner Type", type: "char"},
color: {string: "Color index", type: "integer"},
},
records: [
{id: 12, display_name: "gold", color: 2},
{id: 14, display_name: "silver", color: 5},
]
},
user: {
fields: {
name: {string: "Name", type: "char"},
partner_ids: {string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id'},
},
records: [{
id: 17,
name: "Aline",
partner_ids: [1],
}, {
id: 19,
name: "Christine",
}]
},
"res.company": {
fields: {
name: { string: "Name", type: "char" },
},
},
};
this.actions = [{
id: 1,
name: 'Partners Action 1',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'kanban'], [false, 'form']],
}];
// map legacy test data
const actions = {};
this.actions.forEach((act) => {
actions[act.xmlId || act.id] = act;
});
serverData = {
actions,
models: this.data,
};
},
afterEach: async function () {
Tooltip.Default.animation = this.initialTooltipDefaultAnimation;
},
}, function () {
QUnit.module('FormView (legacy)');
QUnit.test('simple form rendering', async function (assert) {
assert.expect(12);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<div class="test" style="opacity: 0.5;">some html<span>aa</span></div>' +
'<sheet>' +
'<group>' +
'<group style="background-color: red">' +
'<field name="foo" style="color: blue"/>' +
'<field name="bar"/>' +
'<field name="int_field" string="f3_description"/>' +
'<field name="qux"/>' +
'</group>' +
'<group>' +
'<div class="hello"></div>' +
'</group>' +
'</group>' +
'<notebook>' +
'<page string="Partner Yo">' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'</tree>' +
'</field>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 2,
});
assert.containsOnce(form, 'div.test');
assert.strictEqual(form.$('div.test').css('opacity'), '0.5',
"should keep the inline style on html elements");
assert.containsOnce(form, 'label:contains(Foo)');
assert.containsOnce(form, 'span:contains(blip)');
assert.hasAttrValue(form.$('.o_group .o_group:first'), 'style', 'background-color: red',
"should apply style attribute on groups");
assert.hasAttrValue(form.$('.o_field_widget[name=foo]'), 'style', 'color: blue',
"should apply style attribute on fields");
assert.containsNone(form, 'label:contains(something_id)');
assert.containsOnce(form, 'label:contains(f3_description)');
assert.containsOnce(form, 'div.o_field_one2many table');
assert.containsOnce(form, 'tbody td:not(.o_list_record_selector) .form-check input:checked');
assert.containsOnce(form, '.o_control_panel .breadcrumb:contains(second record)');
assert.containsNone(form, 'label.o_form_label_empty:contains(timmy)');
form.destroy();
});
QUnit.test('duplicate fields rendered properly', async function (assert) {
assert.expect(6);
this.data.partner.records.push({
id: 6,
bar: true,
foo: "blip",
int_field: 9,
});
var form = await createView({
View: FormView,
viewOptions: { mode: 'edit' },
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<group>' +
'<field name="foo" attrs="{\'invisible\': [(\'bar\',\'=\',True)]}"/>' +
'<field name="foo" attrs="{\'invisible\': [(\'bar\',\'=\',False)]}"/>' +
'<field name="foo"/>' +
'<field name="int_field" attrs="{\'readonly\': [(\'bar\',\'=\',False)]}"/>' +
'<field name="int_field" attrs="{\'readonly\': [(\'bar\',\'=\',True)]}"/>' +
'<field name="bar"/>' +
'</group>' +
'</group>' +
'</form>',
res_id: 6,
});
assert.hasClass(form.$('div.o_group input[name="foo"]:eq(0)'), 'o_invisible_modifier', 'first foo widget should be invisible');
assert.containsOnce(form, 'div.o_group input[name="foo"]:eq(1):not(.o_invisible_modifier)', "second foo widget should be visible");
assert.containsOnce(form, 'div.o_group input[name="foo"]:eq(2):not(.o_invisible_modifier)', "third foo widget should be visible");
await testUtils.fields.editInput(form.$('div.o_group input[name="foo"]:eq(2)'), "hello");
assert.strictEqual(form.$('div.o_group input[name="foo"]:eq(1)').val(), "hello", "second foo widget should be 'hello'");
assert.containsOnce(form, 'div.o_group input[name="int_field"]:eq(0):not(.o_readonly_modifier)', "first int_field widget should not be readonly");
assert.hasClass(form.$('div.o_group span[name="int_field"]:eq(0)'),'o_readonly_modifier', "second int_field widget should be readonly");
form.destroy();
});
QUnit.test('duplicate fields rendered properly (one2many)', async function (assert) {
assert.expect(7);
this.data.partner.records.push({
id: 6,
p: [1],
});
var form = await createView({
View: FormView,
viewOptions: { mode: 'edit' },
model: 'partner',
data: this.data,
arch: '<form>' +
'<notebook>' +
'<page>' +
'<field name="p">' +
'<tree editable="True">' +
'<field name="foo"/>' +
'</tree>' +
'<form/>' +
'</field>' +
'</page>' +
'<page>' +
'<field name="p" readonly="True">' +
'<tree editable="True">' +
'<field name="foo"/>' +
'</tree>' +
'<form/>' +
'</field>' +
'</page>' +
'</notebook>' +
'</form>',
res_id: 6,
});
assert.containsOnce(form, 'div.o_field_one2many:eq(0):not(.o_readonly_modifier)', "first one2many widget should not be readonly");
assert.hasClass(form.$('div.o_field_one2many:eq(1)'),'o_readonly_modifier', "second one2many widget should be readonly");
await testUtils.dom.click(form.$('div.tab-content table.o_list_table:eq(0) tr.o_data_row td.o_data_cell:eq(0)'));
assert.strictEqual(form.$('div.tab-content table.o_list_table tr.o_selected_row input[name="foo"]').val(), "yop",
"first line in one2many of first tab contains yop");
assert.strictEqual(form.$('div.tab-content table.o_list_table:eq(1) tr.o_data_row td.o_data_cell:eq(0)').text(),
"yop", "first line in one2many of second tab contains yop");
await testUtils.fields.editInput(form.$('div.tab-content table.o_list_table tr.o_selected_row input[name="foo"]'), "hello");
assert.strictEqual(form.$('div.tab-content table.o_list_table:eq(1) tr.o_data_row td.o_data_cell:eq(0)').text(), "hello",
"first line in one2many of second tab contains hello");
await testUtils.dom.click(form.$('div.tab-content table.o_list_table:eq(0) a:contains(Add a line)'));
assert.strictEqual(form.$('div.tab-content table.o_list_table tr.o_selected_row input[name="foo"]').val(), "My little Foo Value",
"second line in one2many of first tab contains 'My little Foo Value'");
assert.strictEqual(form.$('div.tab-content table.o_list_table:eq(1) tr.o_data_row:eq(1) td.o_data_cell:eq(0)').text(),
"My little Foo Value", "first line in one2many of second tab contains hello");
form.destroy();
});
QUnit.test('attributes are transferred on async widgets', async function (assert) {
assert.expect(1);
var done = assert.async();
var def = testUtils.makeTestPromise();
var FieldChar = fieldRegistry.get('char');
fieldRegistry.add('asyncwidget', FieldChar.extend({
willStart: function () {
return def;
},
}));
createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="foo" style="color: blue" widget="asyncwidget"/>' +
'</group>' +
'</form>',
res_id: 2,
}).then(function (form) {
assert.hasAttrValue(form.$('.o_field_widget[name=foo]'), 'style', 'color: blue',
"should apply style attribute on fields");
form.destroy();
delete fieldRegistry.map.asyncwidget;
done();
});
def.resolve();
await testUtils.nextTick();
});
QUnit.test('placeholder attribute on input', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<input placeholder="chimay"/>' +
'</form>',
res_id: 2,
});
assert.containsOnce(form, 'input[placeholder="chimay"]');
form.destroy();
});
QUnit.test('decoration works on widgets', 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"/>' +
'<field name="display_name" decoration-danger="int_field &lt; 5"/>' +
'<field name="foo" decoration-danger="int_field &gt; 5"/>' +
'</form>',
res_id: 2,
});
assert.doesNotHaveClass(form.$('span[name="display_name"]'), 'text-danger');
assert.hasClass(form.$('span[name="foo"]'), 'text-danger');
form.destroy();
});
QUnit.test('decoration on widgets are reevaluated if necessary', 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"/>' +
'<field name="display_name" decoration-danger="int_field &lt; 5"/>' +
'</form>',
res_id: 2,
viewOptions: {mode: 'edit'},
});
assert.doesNotHaveClass(form.$('input[name="display_name"]'), 'text-danger');
await testUtils.fields.editInput(form.$('input[name=int_field]'), 3);
assert.hasClass(form.$('input[name="display_name"]'), 'text-danger');
form.destroy();
});
QUnit.test('decoration on widgets works on same widget', 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" decoration-danger="int_field &lt; 5"/>' +
'</form>',
res_id: 2,
viewOptions: {mode: 'edit'},
});
assert.doesNotHaveClass(form.$('input[name="int_field"]'), 'text-danger');
await testUtils.fields.editInput(form.$('input[name=int_field]'), 3);
assert.hasClass(form.$('input[name="int_field"]'), 'text-danger');
form.destroy();
});
QUnit.test('only necessary fields are fetched with correct context', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
// NOTE: actually, the current web client always request the __last_update
// field, not sure why. Maybe this test should be modified.
assert.deepEqual(args.args[1], ["foo", "display_name"],
"should only fetch requested fields");
assert.deepEqual(args.kwargs.context, {bin_size: true},
"bin_size should always be in the context");
return this._super(route, args);
}
});
form.destroy();
});
QUnit.test('group rendering', 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,
});
assert.containsOnce(form, 'table.o_inner_group');
form.destroy();
});
QUnit.test('group containing both a field and a group', async function (assert) {
// The purpose of this test is to check that classnames defined in a
// field widget and those added by the form renderer are correctly
// combined. For instance, the renderer adds className 'o_group_col_x'
// on outer group's children (an outer group being a group that contains
// at least a group).
assert.expect(4);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<field name="foo"/>' +
'<group>' +
'<field name="int_field"/>' +
'</group>' +
'</group>' +
'</form>',
res_id: 1,
});
assert.containsOnce(form, '.o_group .o_field_widget[name=foo]');
assert.containsOnce(form, '.o_group .o_inner_group .o_field_widget[name=int_field]');
assert.hasClass(form.$('.o_field_widget[name=foo]'), 'o_field_char');
assert.hasClass(form.$('.o_field_widget[name=foo]'), 'o_group_col_6');
form.destroy();
});
QUnit.test('Form and subview with _view_ref contexts', async function (assert) {
assert.expect(3);
serverData.models.product.fields.partner_type_ids = {string: "one2many field", type: "one2many", relation: "partner_type"},
serverData.models.product.records = [{id: 1, name: 'Tromblon', partner_type_ids: [12,14]}];
serverData.models.partner.records[0].product_id = 1;
// This is an old test, written before "get_views" (formerly "load_views") automatically
// inlines x2many subviews. As the purpose of this test is to assert that the js fetches
// the correct sub view when it is not inline (which can still happen in nested form views),
// we bypass the inline mecanism of "get_views" by setting widget="one2many" on the field.
serverData.views = {
'product,false,form': '<form>'+
'<field name="name"/>'+
'<field name="partner_type_ids" widget="one2many" context="{\'tree_view_ref\': \'some_other_tree_view\'}"/>' +
'</form>',
'partner_type,false,list': '<tree>'+
'<field name="color"/>'+
'</tree>',
'product,false,search': '<search></search>',
'partner,false,form': '<form>' +
'<field name="name"/>' +
'<field name="product_id" context="{\'tree_view_ref\': \'some_tree_view\'}"/>' +
'</form>',
'partner,false,search': '<search></search>',
};
const mockRPC = (route, args) => {
if (args.method === 'get_views') {
var context = args.kwargs.context;
if (args.model === 'product') {
assert.strictEqual(context.tree_view_ref, 'some_tree_view',
'The correct _view_ref should have been sent to the server, first time');
}
if (args.model === 'partner_type') {
assert.strictEqual(context.base_model_name, 'product',
'The correct base_model_name should have been sent to the server for the subview');
assert.strictEqual(context.tree_view_ref, 'some_other_tree_view',
'The correct _view_ref should have been sent to the server for the subview');
}
}
if (args.method === 'get_formview_action') {
return Promise.resolve({
res_id: 1,
type: 'ir.actions.act_window',
target: 'current',
res_model: args.model,
context: args.kwargs.context,
'view_mode': 'form',
'views': [[false, 'form']],
});
}
};
const webClient = await createWebClient({ serverData, mockRPC});
await doAction(webClient, {
res_id: 1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'partner',
'view_mode': 'form',
'views': [[false, 'form']],
});
await testUtils.dom.click(target.querySelector('.o_field_widget[name="product_id"]'));
});
QUnit.test('invisible fields are properly hidden', 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" invisible="1"/>' +
'<field name="bar"/>' +
'</group>' +
'<field name="qux" invisible="1"/>' +
// x2many field without inline view: as it is always invisible, the view
// should not be fetched. we don't specify any view in this test, so if it
// ever tries to fetch it, it will crash, indicating that this is wrong.
'<field name="p" invisible="True"/>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.containsNone(form, 'label:contains(Foo)');
assert.containsNone(form, '.o_field_widget[name=foo]');
assert.containsNone(form, '.o_field_widget[name=qux]');
assert.containsNone(form, '.o_field_widget[name=p]');
form.destroy();
});
QUnit.test('invisible elements are properly hidden', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<header invisible="1">' +
'<button name="myaction" string="coucou"/>' +
'</header>' +
'<sheet>' +
'<group>' +
'<group string="invgroup" invisible="1">' +
'<field name="foo"/>' +
'</group>' +
'</group>' +
'<notebook>' +
'<page string="visible"/>' +
'<page string="invisible" invisible="1"/>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.containsOnce(form, '.o_form_statusbar.o_invisible_modifier button:contains(coucou)');
assert.containsOnce(form, '.o_notebook li.o_invisible_modifier a:contains(invisible)');
assert.containsOnce(form, 'table.o_inner_group.o_invisible_modifier td:contains(invgroup)');
form.destroy();
});
QUnit.test('invisible attrs on fields are re-evaluated on field change', async function (assert) {
assert.expect(3);
// we set the value bar to simulate a falsy boolean value.
this.data.partner.records[0].bar = false;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet><group>' +
'<field name="product_id"/>' +
'<field name="timmy" invisible="1"/>' +
'<field name="foo" class="foo_field" attrs=\'{"invisible": [["product_id", "=", false]]}\'/>' +
'<field name="bar" class="bar_field" attrs=\'{"invisible":[("bar","=",False),("timmy","=",[])]}\'/>' +
'</group></sheet>' +
'</form>',
res_id: 1,
viewOptions: {
mode:'edit'
},
});
assert.hasClass(form.$('.foo_field'), 'o_invisible_modifier');
assert.hasClass(form.$('.bar_field'), 'o_invisible_modifier');
// set a value on the m2o
await testUtils.fields.many2one.searchAndClickItem('product_id');
assert.doesNotHaveClass(form.$('.foo_field'), 'o_invisible_modifier');
form.destroy();
});
QUnit.test('asynchronous fields can be set invisible', async function (assert) {
assert.expect(1);
var done = assert.async();
var def = testUtils.makeTestPromise();
// we choose this widget because it is a quite simple widget with a non
// empty qweb template
var PercentPieWidget = fieldRegistry.get('percentpie');
fieldRegistry.add('asyncwidget', PercentPieWidget.extend({
willStart: function () {
return def;
},
}));
createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet><group>' +
'<field name="foo"/>' +
'<field name="int_field" invisible="1" widget="asyncwidget"/>' +
'</group></sheet>' +
'</form>',
res_id: 1,
}).then(function (form) {
assert.containsNone(form, '.o_field_widget[name="int_field"]');
form.destroy();
delete fieldRegistry.map.asyncwidget;
done();
});
def.resolve();
});
QUnit.test('properly handle modifiers and attributes on notebook tags', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="product_id"/>' +
'<notebook class="new_class" attrs=\'{"invisible": [["product_id", "=", false]]}\'>' +
'<page string="Foo">' +
'<field name="foo"/>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.hasClass(form.$('.o_notebook'), 'o_invisible_modifier');
assert.hasClass(form.$('.o_notebook'), 'new_class');
form.destroy();
});
QUnit.test('empty notebook', async function (assert) {
assert.expect(2);
const form = await createView({
arch: `
<form string="Partners">
<sheet>
<notebook/>
</sheet>
</form>`,
data: this.data,
model: 'partner',
res_id: 1,
View: FormView,
});
// Does not change when switching state
await testUtils.form.clickEdit(form);
assert.containsNone(form, ':scope .o_notebook .nav');
// Does not change when coming back to initial state
await testUtils.form.clickSave(form);
assert.containsNone(form, ':scope .o_notebook .nav');
form.destroy();
});
QUnit.test('no visible page', async function (assert) {
assert.expect(4);
const form = await createView({
arch: `
<form string="Partners">
<sheet>
<notebook>
<page string="Foo" invisible="1">
<field name="foo"/>
</page>
<page string="Bar" invisible="1">
<field name="bar"/>
</page>
</notebook>
</sheet>
</form>`,
data: this.data,
model: 'partner',
res_id: 1,
View: FormView,
});
// Does not change when switching state
await testUtils.form.clickEdit(form);
for (const nav of form.el.querySelectorAll(':scope .o_notebook .nav')) {
assert.containsNone(nav, '.nav-link.active');
assert.containsN(nav, '.nav-item.o_invisible_modifier', 2);
}
// Does not change when coming back to initial state
await testUtils.form.clickSave(form);
for (const nav of form.el.querySelectorAll(':scope .o_notebook .nav')) {
assert.containsNone(nav, '.nav-link.active');
assert.containsN(nav, '.nav-item.o_invisible_modifier', 2);
}
form.destroy();
});
QUnit.test('notebook: pages with invisible modifiers', async function (assert) {
assert.expect(10);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `<form string="Partners">
<sheet>
<field name="bar"/>
<notebook>
<page string="First" attrs='{"invisible": [["bar", "=", false]]}'>
<field name="foo"/>
</page>
<page string="Second" attrs='{"invisible": [["bar", "=", true]]}'>
<field name="int_field"/>
</page>
<page string="Third">
<field name="qux"/>
</page>
</notebook>
</sheet>
</form>`,
res_id: 1,
});
await testUtils.form.clickEdit(form);
assert.containsOnce(form, ".o_notebook .nav .nav-link.active",
"There should be only one active tab"
);
assert.isVisible(form.$(".o_notebook .nav .nav-item:first"));
assert.hasClass(form.$(".o_notebook .nav .nav-link:first"), "active");
assert.isNotVisible(form.$(".o_notebook .nav .nav-item:eq(1)"));
assert.doesNotHaveClass(form.$(".o_notebook .nav .nav-link:eq(1)"), "active");
await testUtils.dom.click(form.$(".o_field_widget[name=bar] input"));
assert.containsOnce(form, ".o_notebook .nav .nav-link.active",
"There should be only one active tab"
);
assert.isNotVisible(form.$(".o_notebook .nav .nav-item:first"));
assert.doesNotHaveClass(form.$(".o_notebook .nav .nav-link:first"), "active");
assert.isVisible(form.$(".o_notebook .nav .nav-item:eq(1)"));
assert.hasClass(form.$(".o_notebook .nav .nav-link:eq(1)"), "active");
form.destroy();
});
QUnit.test('invisible attrs on first notebook page', async function (assert) {
assert.expect(6);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="product_id"/>' +
'<notebook>' +
'<page string="Foo" attrs=\'{"invisible": [["product_id", "!=", false]]}\'>' +
'<field name="foo"/>' +
'</page>' +
'<page string="Bar">' +
'<field name="bar"/>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
assert.hasClass(form.$('.o_notebook .nav .nav-link:first()'), 'active');
assert.doesNotHaveClass(form.$('.o_notebook .nav .nav-item:first()'), 'o_invisible_modifier');
// set a value on the m2o
await testUtils.fields.many2one.searchAndClickItem('product_id');
assert.doesNotHaveClass(form.$('.o_notebook .nav .nav-link:first()'), 'active');
assert.hasClass(form.$('.o_notebook .nav .nav-item:first()'), 'o_invisible_modifier');
assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(1)'), 'active');
assert.hasClass(form.$('.o_notebook .tab-content .tab-pane:nth(1)'), 'active');
form.destroy();
});
QUnit.test('invisible attrs on notebook page which has only one page', async function (assert) {
assert.expect(4);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="bar"/>' +
'<notebook>' +
'<page string="Foo" attrs=\'{"invisible": [["bar", "!=", false]]}\'>' +
'<field name="foo"/>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
assert.notOk(form.$('.o_notebook .nav .nav-link:first()').hasClass('active'),
'first tab should not be active');
assert.ok(form.$('.o_notebook .nav .nav-item:first()').hasClass('o_invisible_modifier'),
'first tab should be invisible');
// enable checkbox
await testUtils.dom.click(form.$('.o_field_boolean input'));
assert.ok(form.$('.o_notebook .nav .nav-link:first()').hasClass('active'),
'first tab should be active');
assert.notOk(form.$('.o_notebook .nav .nav-item:first()').hasClass('o_invisible_modifier'),
'first tab should be visible');
form.destroy();
});
QUnit.test('first notebook page invisible', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="product_id"/>' +
'<notebook>' +
'<page string="Foo" invisible="1">' +
'<field name="foo"/>' +
'</page>' +
'<page string="Bar">' +
'<field name="bar"/>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.notOk(form.$('.o_notebook .nav .nav-item:first()').is(':visible'),
'first tab should be invisible');
assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(1)'), 'active');
form.destroy();
});
QUnit.test('hide notebook element if all pages hidden', async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `<form string="Partners">
<sheet>
<field name="bar"/>
<notebook class="new_class">
<page string="Foo" attrs="{'invisible': [['bar', '=', true]]}">
<field name="foo"/>
</page>
</notebook>
</sheet>
</form>`,
});
assert.ok(form.$('.o_notebook .nav li:not(.o_invisible_modifier)').length,
"there should be visible page");
assert.notOk(form.$('.o_notebook .nav').hasClass('o_invisible_modifier'),
'the notebook headers should not be hidden if one of the page is visible');
await testUtils.dom.click(form.$('.o_field_boolean input'));
assert.notOk(form.$('.o_notebook .nav li:not(.o_invisible_modifier)').length,
"there should not be any visible page");
assert.ok(form.$('.o_notebook .nav').hasClass('o_invisible_modifier'),
'the notebook headers should be hidden if none of the page is visible');
form.destroy();
});
QUnit.test('autofocus on second notebook page', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="product_id"/>' +
'<notebook>' +
'<page string="Choucroute">' +
'<field name="foo"/>' +
'</page>' +
'<page string="Cassoulet" autofocus="autofocus">' +
'<field name="bar"/>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.doesNotHaveClass(form.$('.o_notebook .nav .nav-link:first()'), 'active');
assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(1)'), 'active');
form.destroy();
});
QUnit.test("notebook page is changing when an anchor is clicked from another page", async (assert) => {
assert.expect(6);
// This should be removed as soon as the view is moved to owl
const wowlEnv = await makeTestEnv();
const legacyEnv = makeTestEnvironment({ bus: core.bus });
mapLegacyEnvToWowlEnv(legacyEnv, wowlEnv);
const scrollableParent = document.createElement("div");
scrollableParent.style.overflow = "auto";
const target = getFixture();
target.append(scrollableParent);
var form = await createView({
View: FormView,
model: "partner",
data: {
partner: {
fields: {},
records: [
{
id: 1,
},
],
},
},
arch: `<form string="Partners">
<sheet>
<notebook>
<page string="Non scrollable page">
<div id="anchor1">No scrollbar!</div>
<a href="#anchor2" class="link2">TO ANCHOR 2</a>
</page>
<page string="Other scrollable page">
<p style="font-size: large">
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
augue.
</p>
<p style="font-size: large">
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
augue.
</p>
<h2 id="anchor2">There is a scroll bar</h2>
<a href="#anchor1" class="link1">TO ANCHOR 1</a>
<p style="font-size: large">
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
augue.
</p>
</page>
</notebook>
</sheet>
</form>`,
res_id: 1,
});
scrollableParent.append(form.el);
// We set the height of the parent to the height of the second pane
// We are then sure there will be no scrollable on this pane but a
// only for the first pane
scrollableParent.style.maxHeight =
scrollableParent.querySelector(".o_action").getBoundingClientRect().height + "px";
// The element must be contained in the scrollable parent (top and bottom)
const isVisible = (el) => {
return (
el.getBoundingClientRect().bottom <= scrollableParent.getBoundingClientRect().bottom &&
el.getBoundingClientRect().top >= scrollableParent.getBoundingClientRect().top
);
};
assert.ok(
scrollableParent
.querySelector(".tab-pane.active")
.contains(scrollableParent.querySelector("#anchor1")),
"the first pane is visible"
);
assert.ok(
!isVisible(scrollableParent.querySelector("#anchor2")),
"the second anchor is not visible"
);
scrollableParent.querySelector(".link2").click();
assert.ok(
scrollableParent
.querySelector(".tab-pane.active")
.contains(scrollableParent.querySelector("#anchor2")),
"the second pane is visible"
);
assert.ok(
isVisible(scrollableParent.querySelector("#anchor2")),
"the second anchor is visible"
);
scrollableParent.querySelector(".link1").click();
assert.ok(
scrollableParent
.querySelector(".tab-pane.active")
.contains(scrollableParent.querySelector("#anchor1")),
"the first pane is visible"
);
assert.ok(isVisible(scrollableParent.querySelector("#anchor1")), "the first anchor is visible");
form.destroy();
});
QUnit.test('notebook name transferred to DOM', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<notebook>' +
'<page name="choucroute" string="Choucroute">' +
'<field name="foo"/>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.hasClass(form.$(".o_notebook .nav .nav-link[name='choucroute']"), 'active');
form.destroy();
});
QUnit.test('invisible attrs on group are re-evaluated on field change', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="bar"/>' +
'<group attrs=\'{"invisible": [["bar", "!=", true]]}\'>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
viewOptions: {
mode: 'edit'
},
});
assert.containsOnce(form, 'div.o_group:visible');
await testUtils.dom.click('.o_field_boolean input', form);
assert.containsOnce(form, 'div.o_group:hidden');
form.destroy();
});
QUnit.test('invisible attrs with zero value in domain and unset value in data', async function (assert) {
assert.expect(1);
this.data.partner.fields.int_field.type = 'monetary';
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'<group attrs=\'{"invisible": [["int_field", "=", 0.0]]}\'>' +
'<div class="hello">this should be invisible</div>' +
'<field name="int_field"/>' +
'</group>' +
'</sheet>' +
'</form>',
});
assert.isNotVisible(form.$('div.hello'));
form.destroy();
});
QUnit.test('reset local state when switching to another view', async function (assert) {
assert.expect(3);
serverData.views = {
'partner,false,form': `<form>
<sheet>
<field name="product_id"/>
<notebook>
<page string="Foo">
<field name="foo"/>
</page>
<page string="Bar">
<field name="bar"/>
</page>
</notebook>
</sheet>
</form>`,
'partner,false,list': '<tree><field name="foo"/></tree>',
'partner,false,search': '<search></search>',
};
serverData.actions = {
1: {
id: 1,
name: 'Partner',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [false, 'form']],
}
};
const webClient = await createWebClient({ serverData });
await doAction(webClient, 1);
await testUtils.dom.click(target.querySelector('.o_list_button_add'));
await legacyExtraNextTick();
assert.containsOnce(target, '.o_legacy_form_view');
// click on second page tab
await testUtils.dom.click($(target).find('.o_notebook .nav-link:eq(1)'));
await testUtils.dom.click('.o_control_panel .o_form_button_cancel');
await legacyExtraNextTick();
assert.containsNone(target, '.o_legacy_form_view');
await testUtils.dom.click(target.querySelector('.o_list_button_add'));
await legacyExtraNextTick();
// check notebook active page is 0th page
assert.hasClass($(target).find('.o_notebook .nav-link:eq(0)'), 'active');
});
QUnit.test('rendering stat buttons with action', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<div name="button_box" class="oe_button_box">' +
'<button class="oe_stat_button" >' +
'<field name="int_field"/>' +
'</button>' +
'<button class="oe_stat_button" name="some_action" type="action" attrs=\'{"invisible": [["bar", "=", true]]}\'>' +
'<field name="bar"/>' +
'</button>' +
'</div>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
});
assert.containsN(form, 'button.oe_stat_button', 2);
assert.containsOnce(form, 'button.oe_stat_button.o_invisible_modifier');
assert.containsOnce(form, 'button.oe_stat_button:disabled');
form.destroy();
});
QUnit.test('rendering stat buttons without action', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<div name="button_box" class="oe_button_box">' +
'<button class="oe_stat_button">' +
'<field name="int_field"/>' +
'</button>' +
'<button class="oe_stat_button" attrs=\'{"invisible": [["bar", "=", true]]}\'>' +
'<field name="bar"/>' +
'</button>' +
'</div>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
});
assert.containsN(form, 'button.oe_stat_button', 2);
assert.containsOnce(form, 'button.oe_stat_button.o_invisible_modifier');
assert.containsN(form, 'button.oe_stat_button:disabled', 2);
form.destroy();
});
QUnit.test('readonly stat buttons stays disabled', async function (assert) {
assert.expect(4);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<div name="button_box" class="oe_button_box">' +
'<button class="oe_stat_button">' +
'<field name="int_field"/>' +
'</button>' +
'<button class="oe_stat_button" type="action" name="some_action">' +
'<field name="bar"/>' +
'</button>' +
'</div>' +
'<group>' +
'<button type="action" name="action_to_perform">Run an action</button>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
});
await testUtils.mock.intercept(form, "execute_action", function (event) {
if (event.data.action_data.name == "action_to_perform") {
assert.containsN(form, 'button.oe_stat_button[disabled]', 2, "While performing the action, both buttons should be disabled.");
event.data.on_success();
}
});
assert.containsN(form, 'button.oe_stat_button', 2);
assert.containsN(form, 'button.oe_stat_button[disabled]', 1);
await testUtils.dom.click('button[name=action_to_perform]');
assert.containsN(form, 'button.oe_stat_button[disabled]', 1, "After performing the action, only one button should be disabled.");
form.destroy();
});
QUnit.test('label uses the string attribute', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<group>' +
'<label for="bar" string="customstring"/>' +
'<div><field name="bar"/></div>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
});
assert.containsOnce(form, 'label.o_form_label:contains(customstring)');
form.destroy();
});
QUnit.test("label ignores the content of the label when present", async function (assert) {
await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<sheet>
<group>
<label for="bar">customstring</label>
<div>
<field name="bar"/>
</div>
</group>
</sheet>
</form>`,
res_id: 2,
});
assert.containsOnce(target, "label.o_form_label");
assert.strictEqual(target.querySelector("label.o_form_label").textContent, "Bar");
});
QUnit.test('input ids for multiple occurrences of fields in form view', async function (assert) {
// A same field can occur several times in the view, but its id must be
// unique by occurrence, otherwise there is a warning in the console (in
// edit mode) as we get several inputs with the same "id" attribute, and
// several labels the same "for" attribute.
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="foo"/>
<label for="qux"/>
<div><field name="qux"/></div>
</group>
<group>
<field name="foo"/>
<label for="qux2"/>
<div><field name="qux" id="qux2"/></div>
</group>
</form>`,
});
const fieldIdAttrs = [...form.$('.o_field_widget')].map(n => n.getAttribute('id'));
const labelForAttrs = [...form.$('.o_form_label')].map(n => n.getAttribute('for'));
assert.strictEqual([...new Set(fieldIdAttrs)].length, 4,
"should have generated a unique id for each field occurrence");
assert.deepEqual(fieldIdAttrs, labelForAttrs,
"the for attribute of labels must coincide with field ids");
form.destroy();
});
QUnit.test('input ids for multiple occurrences of fields in sub form view (inline)', async function (assert) {
// A same field can occur several times in the view, but its id must be
// unique by occurrence, otherwise there is a warning in the console (in
// edit mode) as we get several inputs with the same "id" attribute, and
// several labels the same "for" attribute.
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="p">
<tree><field name="foo"/></tree>
<form>
<group>
<field name="foo"/>
<label for="qux"/>
<div><field name="qux"/></div>
</group>
<group>
<field name="foo"/>
<label for="qux2"/>
<div><field name="qux" id="qux2"/></div>
</group>
</form>
</field>
</form>`,
});
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
assert.containsOnce(document.body, '.modal .o_legacy_form_view');
const fieldIdAttrs = [...$('.modal .o_legacy_form_view .o_field_widget')].map(n => n.getAttribute('id'));
const labelForAttrs = [...$('.modal .o_legacy_form_view .o_form_label')].map(n => n.getAttribute('for'));
assert.strictEqual([...new Set(fieldIdAttrs)].length, 4,
"should have generated a unique id for each field occurrence");
assert.deepEqual(fieldIdAttrs, labelForAttrs,
"the for attribute of labels must coincide with field ids");
form.destroy();
});
QUnit.test('input ids for multiple occurrences of fields in sub form view (not inline)', async function (assert) {
// A same field can occur several times in the view, but its id must be
// unique by occurrence, otherwise there is a warning in the console (in
// edit mode) as we get several inputs with the same "id" attribute, and
// several labels the same "for" attribute.
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="p"/></form>',
archs: {
'partner,false,list': '<tree><field name="foo"/></tree>',
'partner,false,form': `
<form>
<group>
<field name="foo"/>
<label for="qux"/>
<div><field name="qux"/></div>
</group>
<group>
<field name="foo"/>
<label for="qux2"/>
<div><field name="qux" id="qux2"/></div>
</group>
</form>`
},
});
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
assert.containsOnce(document.body, '.modal .o_legacy_form_view');
const fieldIdAttrs = [...$('.modal .o_legacy_form_view .o_field_widget')].map(n => n.getAttribute('id'));
const labelForAttrs = [...$('.modal .o_legacy_form_view .o_form_label')].map(n => n.getAttribute('for'));
assert.strictEqual([...new Set(fieldIdAttrs)].length, 4,
"should have generated a unique id for each field occurrence");
assert.deepEqual(fieldIdAttrs, labelForAttrs,
"the for attribute of labels must coincide with field ids");
form.destroy();
});
QUnit.test('two occurrences of invalid field in form view', async function (assert) {
assert.expect(2);
this.data.partner.fields.trululu.required = true;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="trululu"/>
<field name="trululu"/>
</group>
</form>`,
});
await testUtils.form.clickSave(form);
assert.containsN(form, '.o_form_label.o_field_invalid', 2);
assert.containsN(form, '.o_field_many2one.o_field_invalid', 2);
form.destroy();
});
QUnit.test('tooltips on multiple occurrences of fields and labels', async function (assert) {
assert.expect(4);
const initialDebugMode = odoo.debug;
odoo.debug = false;
this.data.partner.fields.foo.help = 'foo tooltip';
this.data.partner.fields.bar.help = 'bar tooltip';
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="foo"/>
<label for="bar"/>
<div><field name="bar"/></div>
</group>
<group>
<field name="foo"/>
<label for="bar2"/>
<div><field name="bar" id="bar2"/></div>
</group>
</form>`,
});
const $fooLabel1 = form.$('.o_form_label:nth(0)');
$fooLabel1.tooltip('show', false);
$fooLabel1[0].dispatchEvent(new Event('mouseover'));
assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "foo tooltip");
$fooLabel1[0].dispatchEvent(new Event('mouseout'));
const $fooLabel2 = form.$('.o_form_label:nth(2)');
$fooLabel2.tooltip('show', false);
$fooLabel2[0].dispatchEvent(new Event('mouseover'));
assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "foo tooltip");
$fooLabel2[0].dispatchEvent(new Event('mouseout'));
const $barLabel1 = form.$('.o_form_label:nth(1)');
$barLabel1.tooltip('show', false);
$barLabel1[0].dispatchEvent(new Event('mouseover'));
assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "bar tooltip");
$barLabel1[0].dispatchEvent(new Event('mouseout'));
const $barLabel2 = form.$('.o_form_label:nth(3)');
$barLabel2.tooltip('show', false);
$barLabel2[0].dispatchEvent(new Event('mouseover'));
assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "bar tooltip");
$barLabel2[0].dispatchEvent(new Event('mouseout'));
odoo.debug = initialDebugMode;
form.destroy();
});
QUnit.test('readonly attrs on fields are re-evaluated on field change', 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" attrs="{\'readonly\': [[\'bar\', \'=\', True]]}"/>' +
'<field name="bar"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
assert.containsOnce(form, 'span[name="foo"]',
"the foo field widget should be readonly");
await testUtils.dom.click(form.$('.o_field_boolean input'));
assert.containsOnce(form, 'input[name="foo"]',
"the foo field widget should have been rerendered to now be editable");
await testUtils.dom.click(form.$('.o_field_boolean input'));
assert.containsOnce(form, 'span[name="foo"]',
"the foo field widget should have been rerendered to now be readonly again");
await testUtils.dom.click(form.$('.o_field_boolean input'));
assert.containsOnce(form, 'input[name="foo"]',
"the foo field widget should have been rerendered to now be editable again");
form.destroy();
});
QUnit.test('readonly attrs on lines are re-evaluated on field change 2', async function (assert) {
assert.expect(4);
this.data.partner.records[0].product_ids = [37];
this.data.partner.records[0].trululu = false;
this.data.partner.onchanges = {
trululu(record) {
// when trululu changes, push another record in product_ids.
// only push a second record once.
if (record.product_ids.map(command => command[1]).includes(41)) {
return;
}
// copy the list to force it as different from the original
record.product_ids = record.product_ids.slice();
record.product_ids.push([4,41,false]);
}
};
this.data.product.records[0].name = 'test';
// This one is necessary to have a valid, rendered widget
this.data.product.fields.int_field = { type:"integer", string: "intField" };
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="trululu"/>
<field name="product_ids" attrs="{'readonly': [['trululu', '=', False]]}">
<tree editable="top"><field name="int_field" widget="handle" /><field name="name"/></tree>
</field>
</form>
`,
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
for (let value of [true, false, true, false]) {
if (value) {
await testUtils.fields.many2one.clickOpenDropdown('trululu')
await testUtils.fields.many2one.clickHighlightedItem('trululu')
assert.notOk($('.o_field_one2many[name="product_ids"]').hasClass("o_readonly_modifier"), 'lines should not be readonly')
} else {
await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="trululu"] input'), '', ['keyup'])
assert.ok($('.o_field_one2many[name="product_ids"]').hasClass("o_readonly_modifier"), 'lines should be readonly')
}
}
form.destroy();
});
QUnit.test('empty fields have o_form_empty class in readonly mode', async function (assert) {
assert.expect(8);
this.data.partner.fields.foo.default = false; // no default value for this test
this.data.partner.records[1].foo = false; // 1 is record with id=2
this.data.partner.records[1].trululu = false; // 1 is record with id=2
this.data.partner.fields.int_field.readonly = true;
this.data.partner.onchanges.foo = function (obj) {
if (obj.foo === "hello") {
obj.int_field = false;
}
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
'<field name="trululu" attrs="{\'readonly\': [[\'foo\', \'=\', False]]}"/>' +
'<field name="int_field"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
});
assert.containsN(form, '.o_field_widget.o_field_empty', 2,
"should have 2 empty fields with correct class");
assert.containsN(form, '.o_form_label_empty', 2,
"should have 2 muted labels (for the empty fieds) in readonly");
await testUtils.form.clickEdit(form);
assert.containsOnce(form, '.o_field_empty',
"in edit mode, only empty readonly fields should have the o_field_empty class");
assert.containsOnce(form, '.o_form_label_empty',
"in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'test');
assert.containsNone(form, '.o_field_empty',
"after readonly modifier change, the o_field_empty class should have been removed");
assert.containsNone(form, '.o_form_label_empty',
"after readonly modifier change, the o_form_label_empty class should have been removed");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'hello');
assert.containsOnce(form, '.o_field_empty',
"after value changed to false for a readonly field, the o_field_empty class should have been added");
assert.containsOnce(form, '.o_form_label_empty',
"after value changed to false for a readonly field, the o_form_label_empty class should have been added");
form.destroy();
});
QUnit.test('empty fields\' labels still get the empty class after widget rerender', async function (assert) {
assert.expect(6);
this.data.partner.fields.foo.default = false; // no default value for this test
this.data.partner.records[1].foo = false; // 1 is record with id=2
this.data.partner.records[1].display_name = false; // 1 is record with id=2
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<field name="foo"/>' +
'<field name="display_name" attrs="{\'readonly\': [[\'foo\', \'=\', \'readonly\']]}"/>' +
'</group>' +
'</form>',
res_id: 2,
});
assert.containsN(form, '.o_field_widget.o_field_empty', 2);
assert.containsN(form, '.o_form_label_empty', 2,
"should have 1 muted label (for the empty fied) in readonly");
await testUtils.form.clickEdit(form);
assert.containsNone(form, '.o_field_empty',
"in edit mode, only empty readonly fields should have the o_field_empty class");
assert.containsNone(form, '.o_form_label_empty',
"in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'readonly');
await testUtils.fields.editInput(form.$('input[name=foo]'), 'edit');
await testUtils.fields.editInput(form.$('input[name=display_name]'), 'some name');
await testUtils.fields.editInput(form.$('input[name=foo]'), 'readonly');
assert.containsNone(form, '.o_field_empty',
"there still should not be any empty class on fields as the readonly one is now set");
assert.containsNone(form, '.o_form_label_empty',
"there still should not be any empty class on labels as the associated readonly field is now set");
form.destroy();
});
QUnit.test('empty inner readonly fields don\'t have o_form_empty class in "create" mode', async function (assert) {
assert.expect(2);
this.data.partner.fields.product_id.readonly = true;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<group>' +
'<field name="product_id"/>' +
'</group>' +
'</group>' +
'</sheet>' +
'</form>',
});
assert.containsNone(form, '.o_form_label_empty',
"no empty class on label");
assert.containsNone(form, '.o_field_empty',
"no empty class on field");
form.destroy();
});
QUnit.test('label tag added for fields have o_form_empty class in readonly mode if field is empty', async function (assert) {
assert.expect(8);
this.data.partner.fields.foo.default = false; // no default value for this test
this.data.partner.records[1].foo = false; // 1 is record with id=2
this.data.partner.records[1].trululu = false; // 1 is record with id=2
this.data.partner.fields.int_field.readonly = true;
this.data.partner.onchanges.foo = function (obj) {
if (obj.foo === "hello") {
obj.int_field = false;
}
};
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `<form string="Partners">
<sheet>
<label for="foo" string="Foo"/>
<field name="foo"/>
<label for="trululu" string="Trululu" attrs="{'readonly': [['foo', '=', False]]}"/>
<field name="trululu" attrs="{'readonly': [['foo', '=', False]]}"/>
<label for="int_field" string="IntField" attrs="{'readonly': [['int_field', '=', False]]}"/>
<field name="int_field"/>
</sheet>
</form>`,
res_id: 2,
});
assert.containsN(form, '.o_field_widget.o_field_empty', 2,
"should have 2 empty fields with correct class");
assert.containsN(form, '.o_form_label_empty', 2,
"should have 2 muted labels (for the empty fieds) in readonly");
await testUtils.form.clickEdit(form);
assert.containsOnce(form, '.o_field_empty',
"in edit mode, only empty readonly fields should have the o_field_empty class");
assert.containsOnce(form, '.o_form_label_empty',
"in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'test');
assert.containsNone(form, '.o_field_empty',
"after readonly modifier change, the o_field_empty class should have been removed");
assert.containsNone(form, '.o_form_label_empty',
"after readonly modifier change, the o_form_label_empty class should have been removed");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'hello');
assert.containsOnce(form, '.o_field_empty',
"after value changed to false for a readonly field, the o_field_empty class should have been added");
assert.containsOnce(form, '.o_form_label_empty',
"after value changed to false for a readonly field, the o_form_label_empty class should have been added");
form.destroy();
});
QUnit.test('form view can switch to edit mode', async function (assert) {
assert.expect(9);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_readonly');
assert.isVisible(form.$buttons.find('.o_form_buttons_view'));
assert.isNotVisible(form.$buttons.find('.o_form_buttons_edit'));
await testUtils.form.clickEdit(form);
assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode');
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable');
assert.doesNotHaveClass(form.$('.o_legacy_form_view'), 'o_form_readonly');
assert.isNotVisible(form.$buttons.find('.o_form_buttons_view'));
assert.isVisible(form.$buttons.find('.o_form_buttons_edit'));
form.destroy();
});
QUnit.test('required attrs on fields are re-evaluated on field change', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="foo" attrs="{\'required\': [[\'bar\', \'=\', True]]}"/>' +
'<field name="bar"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
assert.containsOnce(form, 'input[name="foo"].o_required_modifier',
"the foo field widget should be required");
await testUtils.dom.click('.o_field_boolean input');
assert.containsOnce(form, 'input[name="foo"]:not(.o_required_modifier)',
"the foo field widget should now have been marked as non-required");
await testUtils.dom.click('.o_field_boolean input');
assert.containsOnce(form, 'input[name="foo"].o_required_modifier',
"the foo field widget should now have been marked as required again");
form.destroy();
});
QUnit.test('required fields should have o_required_modifier in readonly mode', async function (assert) {
assert.expect(2);
this.data.partner.fields.foo.required = 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,
});
assert.containsOnce(form, 'span.o_required_modifier');
await testUtils.form.clickEdit(form);
assert.containsOnce(form, 'input.o_required_modifier',
"in edit mode, should have 1 input with o_required_modifier");
form.destroy();
});
QUnit.test('required float fields works as expected', async function (assert) {
assert.expect(10);
this.data.partner.fields.qux.required = true;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="qux"/>' +
'</group>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
});
assert.hasClass(form.$('input[name="qux"]'), 'o_required_modifier');
assert.strictEqual(form.$('input[name="qux"]').val(), "0.0",
"qux input is 0 by default (float field)");
await testUtils.form.clickSave(form);
assert.containsNone(form.$('input[name="qux"]'), "should have switched to readonly");
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=qux]'), '1');
await testUtils.form.clickSave(form);
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input[name="qux"]').val(), "1.0",
"qux input is properly formatted");
assert.verifySteps(['onchange', 'create', 'read', 'write', 'read']);
form.destroy();
});
QUnit.test('separators', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<separator string="Geolocation"/>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.containsOnce(form, 'div.o_horizontal_separator');
form.destroy();
});
QUnit.test('invisible attrs on separators', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<separator string="Geolocation" attrs=\'{"invisible": [["bar", "=", True]]}\'/>'+
'<field name="bar"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.hasClass(form.$('div.o_horizontal_separator'), 'o_invisible_modifier');
form.destroy();
});
QUnit.test('buttons in form view', async function (assert) {
assert.expect(8);
var rpcCount = 0;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="state" invisible="1"/>' +
'<header>' +
'<button name="post" class="p" string="Confirm" type="object"/>' +
'<button name="some_method" class="s" string="Do it" type="object"/>' +
'<button name="some_other_method" states="ab,ef" string="Do not" type="object"/>' +
'</header>' +
'<sheet>' +
'<group>' +
'<button string="Geolocate" name="geo_localize" icon="fa-check" type="object"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
mockRPC: function () {
rpcCount++;
return this._super.apply(this, arguments);
},
});
assert.containsOnce(form, 'button.btn i.fa.fa-check');
assert.containsN(form, '.o_form_statusbar button', 3);
assert.containsOnce(form, 'button.p[name="post"]:contains(Confirm)');
assert.containsN(form, '.o_form_statusbar button:visible', 2);
await testUtils.mock.intercept(form, 'execute_action', function (ev) {
assert.strictEqual(ev.data.action_data.name, 'post',
"should trigger execute_action with correct method name");
assert.deepEqual(ev.data.env.currentID, 2, "should have correct id in ev data");
ev.data.on_success();
ev.data.on_closed();
});
rpcCount = 0;
await testUtils.dom.click('.o_form_statusbar button.p', form);
assert.strictEqual(rpcCount, 1, "should have done 1 rpcs to reload");
await testUtils.mock.intercept(form, 'execute_action', function (ev) {
ev.data.on_fail();
});
await testUtils.dom.click('.o_form_statusbar button.s', form);
assert.strictEqual(rpcCount, 1,
"should have done 1 rpc, because we do not reload anymore if the server action fails");
form.destroy();
});
QUnit.test('buttons classes in form view', async function (assert) {
assert.expect(16);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form string="Partners">' +
'<header>' +
'<button name="0"/>' +
'<button name="1" class="btn-primary"/>' +
'<button name="2" class="oe_highlight"/>' +
'<button name="3" class="btn-secondary"/>' +
'<button name="4" class="btn-link"/>' +
'<button name="5" class="oe_link"/>' +
'<button name="6" class="btn-success"/>' +
'<button name="7" class="o_this_is_a_button"/>' +
'</header>' +
'<sheet>' +
'<button name="8"/>' +
'<button name="9" class="btn-primary"/>' +
'<button name="10" class="oe_highlight"/>' +
'<button name="11" class="btn-secondary"/>' +
'<button name="12" class="btn-link"/>' +
'<button name="13" class="oe_link"/>' +
'<button name="14" class="btn-success"/>' +
'<button name="15" class="o_this_is_a_button"/>' +
'</sheet>' +
'</form>',
res_id: 2,
});
assert.hasAttrValue(form.$('button[name="0"]'), 'class', 'btn btn-secondary');
assert.hasAttrValue(form.$('button[name="1"]'), 'class', 'btn btn-primary');
assert.hasAttrValue(form.$('button[name="2"]'), 'class', 'btn btn-primary');
assert.hasAttrValue(form.$('button[name="3"]'), 'class', 'btn btn-secondary');
assert.hasAttrValue(form.$('button[name="4"]'), 'class', 'btn btn-link');
assert.hasAttrValue(form.$('button[name="5"]'), 'class', 'btn btn-link');
assert.hasAttrValue(form.$('button[name="6"]'), 'class', 'btn btn-success');
assert.hasAttrValue(form.$('button[name="7"]'), 'class', 'btn o_this_is_a_button btn-secondary');
assert.hasAttrValue(form.$('button[name="8"]'), 'class', 'btn btn-secondary');
assert.hasAttrValue(form.$('button[name="9"]'), 'class', 'btn btn-primary');
assert.hasAttrValue(form.$('button[name="10"]'), 'class', 'btn btn-primary');
assert.hasAttrValue(form.$('button[name="11"]'), 'class', 'btn btn-secondary');
assert.hasAttrValue(form.$('button[name="12"]'), 'class', 'btn btn-link');
assert.hasAttrValue(form.$('button[name="13"]'), 'class', 'btn btn-link');
assert.hasAttrValue(form.$('button[name="14"]'), 'class', 'btn btn-success');
assert.hasAttrValue(form.$('button[name="15"]'), 'class', 'btn o_this_is_a_button');
form.destroy();
});
QUnit.test("nested buttons in form view header", async function (assert) {
assert.expect(4);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form string="Partners">' +
'<header>' +
'<button name="0"/>' +
'<button name="1"/>' +
'<div>' +
'<button name="2"/>' +
'<button name="3"/>' +
'</div>' +
'</header>' +
'</form>',
res_id: 2,
});
var buttons = form.$('.o_form_statusbar button');
assert.hasAttrValue(buttons[0], 'name', '0');
assert.hasAttrValue(buttons[1], 'name', '1');
assert.hasAttrValue(buttons[2], 'name', '2');
assert.hasAttrValue(buttons[3], 'name', '3');
form.destroy();
});
QUnit.test('button in form view and long willStart', async function (assert) {
assert.expect(6);
var rpcCount = 0;
var FieldChar = fieldRegistry.get('char');
fieldRegistry.add('asyncwidget', FieldChar.extend({
willStart: function () {
assert.step('load '+rpcCount);
if (rpcCount === 2) {
return new Promise(() => {});
}
return Promise.resolve();
},
}));
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="state" invisible="1"/>' +
'<header>' +
'<button name="post" class="p" string="Confirm" type="object"/>' +
'</header>' +
'<sheet>' +
'<group>' +
'<field name="foo" widget="asyncwidget"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
mockRPC: function () {
rpcCount++;
return this._super.apply(this, arguments);
},
});
assert.verifySteps(['load 1']);
testUtils.mock.intercept(form, 'execute_action', function (ev) {
ev.data.on_success();
ev.data.on_closed();
});
await testUtils.dom.click('.o_form_statusbar button.p', form);
assert.verifySteps(['load 2']);
await testUtils.dom.click('.o_form_statusbar button.p', form);
assert.verifySteps(['load 3']);
form.destroy();
});
QUnit.test('buttons in form view, new record', async function (assert) {
// this simulates a situation similar to the settings forms.
assert.expect(7);
var resID;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<header>' +
'<button name="post" class="p" string="Confirm" type="object"/>' +
'<button name="some_method" class="s" string="Do it" type="object"/>' +
'</header>' +
'<sheet>' +
'<group>' +
'<button string="Geolocate" name="geo_localize" icon="fa-check" type="object"/>' +
'</group>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'create') {
return this._super.apply(this, arguments).then(function (result) {
resID = result;
return resID;
});
}
return this._super.apply(this, arguments);
},
});
await testUtils.mock.intercept(form, 'execute_action', function (event) {
assert.step('execute_action');
assert.deepEqual(event.data.env.currentID, resID,
"execute action should be done on correct record id");
event.data.on_success();
event.data.on_closed();
});
await testUtils.dom.click('.o_form_statusbar button.p', form);
assert.verifySteps(['onchange', 'create', 'read', 'execute_action', 'read']);
form.destroy();
});
QUnit.test('buttons in form view, new record, with field id in view', async function (assert) {
assert.expect(7);
// buttons in form view are one of the rare example of situation when we
// save a record without reloading it immediately, because we only care
// about its id for the next step. But at some point, if the field id
// is in the view, it was registered in the changes, and caused invalid
// values in the record (data.id was set to null)
var resID;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<header>' +
'<button name="post" class="p" string="Confirm" type="object"/>' +
'</header>' +
'<sheet>' +
'<group>' +
'<field name="id" invisible="1"/>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'create') {
return this._super.apply(this, arguments).then(function (result) {
resID = result;
return resID;
});
}
return this._super.apply(this, arguments);
},
});
await testUtils.mock.intercept(form, 'execute_action', function (event) {
assert.step('execute_action');
assert.deepEqual(event.data.env.currentID, resID,
"execute action should be done on correct record id");
event.data.on_success();
event.data.on_closed();
});
await testUtils.dom.click('.o_form_statusbar button.p', form);
assert.verifySteps(['onchange', 'create', 'read', 'execute_action', 'read']);
form.destroy();
});
QUnit.test('change and save char', async function (assert) {
assert.expect(6);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/></group>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.ok(true, "should call the /write route");
}
return this._super(route, args);
},
res_id: 2,
});
assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
assert.containsOnce(form, 'span:contains(blip)',
"should contain span with field value");
await testUtils.form.clickEdit(form);
assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode');
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
await testUtils.form.clickSave(form);
assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
assert.containsOnce(form, 'span:contains(tralala)',
"should contain span with field value");
form.destroy();
});
QUnit.test('properly reload data from server', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/></group>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'write') {
args.args[1].foo = "apple";
}
return this._super(route, args);
},
res_id: 2,
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
await testUtils.form.clickSave(form);
assert.containsOnce(form, 'span:contains(apple)',
"should contain span with field value");
form.destroy();
});
QUnit.test('disable buttons until reload data from server', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/></group>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'write') {
args.args[1].foo = "apple";
} else if (args.method === 'read') {
// Block the 'read' call
var result = this._super.apply(this, arguments);
return Promise.resolve(def).then(result);
}
return this._super(route, args);
},
res_id: 2,
});
var def = testUtils.makeTestPromise();
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
await testUtils.form.clickSave(form);
// Save button should be disabled
assert.hasAttrValue(form.$buttons.find('.o_form_button_save'), 'disabled', 'disabled');
// Release the 'read' call
await def.resolve();
await testUtils.nextTick();
// Edit button should be enabled after the reload
assert.hasAttrValue(form.$buttons.find('.o_form_button_edit'), 'disabled', undefined);
form.destroy();
});
QUnit.test('properly apply onchange in simple case', async function (assert) {
assert.expect(2);
this.data.partner.onchanges = {
foo: function (obj) {
obj.int_field = obj.foo.length + 1000;
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/><field name="int_field"/></group>' +
'</form>',
res_id: 2,
});
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input[name=int_field]').val(), "9",
"should contain input with initial value");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
assert.strictEqual(form.$('input[name=int_field]').val(), "1007",
"should contain input with onchange applied");
form.destroy();
});
QUnit.test('properly apply onchange when changed field is active field', async function (assert) {
assert.expect(3);
this.data.partner.onchanges = {
int_field: function (obj) {
obj.int_field = 14;
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="int_field"/></group>' +
'</form>',
res_id: 2,
viewOptions: {mode: 'edit'},
});
assert.strictEqual(form.$('input[name=int_field]').val(), "9",
"should contain input with initial value");
await testUtils.fields.editInput(form.$('input[name=int_field]'), '666');
assert.strictEqual(form.$('input[name=int_field]').val(), "14",
"value should have been set to 14 by onchange");
await testUtils.form.clickSave(form);
assert.strictEqual(form.$('.o_field_widget[name=int_field]').text(), "14",
"value should still be 14");
form.destroy();
});
QUnit.test('onchange send only the present fields to the server', async function (assert) {
assert.expect(1);
this.data.partner.records[0].product_id = false;
this.data.partner.onchanges.foo = function (obj) {
obj.foo = obj.foo + " alligator";
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'<field name="p">' +
'<tree>' +
'<field name="bar"/>' +
'<field name="product_id"/>' +
'</tree>' +
'</field>' +
'<field name="timmy"/>' +
'</form>',
archs: {
"partner_type,false,list": '<tree><field name="name"/></tree>'
},
res_id: 1,
mockRPC: function (route, args) {
if (args.method === "onchange") {
assert.deepEqual(args.args[3],
{"foo": "1", "p": "", "p.bar": "", "p.product_id": "", "timmy": "", "timmy.name": ""},
"should send only the fields used in the views");
}
return this._super(route, args);
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
form.destroy();
});
QUnit.test('onchange only send present fields value', async function (assert) {
assert.expect(1);
this.data.partner.onchanges.foo = function (obj) {};
let checkOnchange = false;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="display_name"/>' +
'<field name="qux"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === "onchange" && checkOnchange) {
assert.deepEqual(args.args[1], {
display_name: "first record",
foo: "tralala",
id: 1,
p: [[0, args.args[1].p[0][1], {"display_name": "valid line", "qux": 12.4}]]
}, "should send the values for the present fields");
}
return this._super(route, args);
},
});
await testUtils.form.clickEdit(form);
// add a o2m row
await testUtils.dom.click('.o_field_x2many_list_row_add a');
form.$('.o_field_one2many input:first').focus();
await testUtils.nextTick();
await testUtils.fields.editInput(form.$('.o_field_one2many input[name=display_name]'), 'valid line');
form.$('.o_field_one2many input:last').focus();
await testUtils.nextTick();
await testUtils.fields.editInput(form.$('.o_field_one2many input[name=qux]'), '12.4');
// trigger an onchange by modifying foo
checkOnchange = true;
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
form.destroy();
});
QUnit.test('evaluate in python field options', async function (assert) {
assert.expect(1);
var isOk = false;
var tmp = py.eval;
py.eval = function (expr) {
if (expr === "{'horizontal': true}") {
isOk = true;
}
return tmp.apply(tmp, arguments);
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo" options="{\'horizontal\': true}"/>' +
'</form>',
res_id: 2,
});
py.eval = tmp;
assert.ok(isOk, "should have evaluated the field options");
form.destroy();
});
QUnit.test('can create a record with default values', 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"/>' +
'<field name="bar"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
viewOptions: {
context: {active_field: 2},
},
mockRPC: function (route, args) {
if (args.method === 'create') {
assert.strictEqual(args.kwargs.context.active_field, 2,
"should have send the correct context");
}
return this._super.apply(this, arguments);
},
});
var n = this.data.partner.records.length;
await testUtils.form.clickCreate(form);
assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode');
assert.strictEqual(form.$('input:first').val(), "My little Foo Value",
"should have correct default_get value");
await testUtils.form.clickSave(form);
assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
assert.strictEqual(this.data.partner.records.length, n + 1, "should have created a record");
form.destroy();
});
QUnit.test('default record with a one2many and an onchange on sub field', async function (assert) {
assert.expect(3);
this.data.partner.onchanges.foo = function () {};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'onchange') {
assert.deepEqual(args.args[3], {
p: '',
'p.foo': '1'
}, "onchangeSpec should be correct (with sub fields)");
}
return this._super.apply(this, arguments);
},
});
assert.verifySteps(['onchange']);
form.destroy();
});
QUnit.test('remove default value in subviews', async function (assert) {
assert.expect(2);
this.data.product.onchanges = {}
this.data.product.onchanges.name = function () {};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
viewOptions: {
context: {default_state: "ab"}
},
arch: '<form string="Partners">' +
'<field name="product_ids" context="{\'default_product_uom_qty\': 68}">' +
'<tree editable="top">' +
'<field name="name"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
if (route === "/web/dataset/call_kw/partner/onchange") {
assert.deepEqual(args.kwargs.context, {
default_state: 'ab',
})
}
else if (route === "/web/dataset/call_kw/product/onchange") {
assert.deepEqual(args.kwargs.context, {
default_product_uom_qty: 68,
})
}
return this._super.apply(this, arguments);
},
});
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
form.destroy();
});
QUnit.test('reference field in one2many list', async function (assert) {
assert.expect(1);
this.data.partner.records[0].reference = 'partner,2';
var form = await createView({
View: FormView,
model: 'user',
data: this.data,
arch: `<form>
<field name="name"/>
<field name="partner_ids">
<tree editable="bottom">
<field name="display_name"/>
<field name="reference"/>
</tree>
</field>
</form>`,
archs: {
'partner,false,form': '<form><field name="display_name"/></form>',
},
mockRPC: function (route, args) {
if (args.method === 'get_formview_id') {
return Promise.resolve(false);
}
return this._super(route, args);
},
res_id: 17,
});
// current form
await testUtils.form.clickEdit(form);
// open the modal form view of the record pointed by the reference field
await testUtils.dom.click(form.$('table td[title="first record"]'));
await testUtils.dom.click(form.$('table td button.o_external_button'));
// edit the record in the modal
await testUtils.fields.editInput($('.modal-body input[name="display_name"]'), 'New name');
await testUtils.dom.click($('.modal-dialog footer button:first-child'));
assert.containsOnce(form, '.o_field_cell[title="New name"]', 'should not crash and value must be edited');
form.destroy();
});
QUnit.test('toolbar is hidden when switching to edit mode', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'</form>',
viewOptions: {hasActionMenus: true},
res_id: 1,
});
assert.containsOnce(form, '.o_cp_action_menus');
await testUtils.form.clickEdit(form);
assert.containsNone(form, '.o_cp_action_menus');
await testUtils.form.clickDiscard(form);
assert.containsOnce(form, '.o_cp_action_menus');
form.destroy();
});
QUnit.test('basic default record', async function (assert) {
assert.expect(2);
this.data.partner.fields.foo.default = "default foo value";
var count = 0;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
mockRPC: function (route, args) {
count++;
return this._super(route, args);
},
});
assert.strictEqual(form.$('input[name=foo]').val(), "default foo value", "should have correct default");
assert.strictEqual(count, 1, "should do only one rpc");
form.destroy();
});
QUnit.test('make default record with non empty one2many', async function (assert) {
assert.expect(4);
this.data.partner.fields.p.default = [
[6, 0, []], // replace with zero ids
[0, 0, {foo: "new foo1", product_id: 41, p: [] }], // create a new value
[0, 0, {foo: "new foo2", product_id: 37, p: [] }], // create a new value
];
var nameGetCount = 0;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'<field name="product_id"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'name_get') {
nameGetCount++;
}
return this._super(route, args);
},
});
assert.containsOnce(form, 'td:contains(new foo1)',
"should have new foo1 value in one2many");
assert.containsOnce(form, 'td:contains(new foo2)',
"should have new foo2 value in one2many");
assert.containsOnce(form, 'td:contains(xphone)',
"should have a cell with the name field 'product_id', set to xphone");
assert.strictEqual(nameGetCount, 0, "should have done no nameget");
form.destroy();
});
QUnit.test('make default record with non empty many2one', async function (assert) {
assert.expect(2);
this.data.partner.fields.trululu.default = 4;
var nameGetCount = 0;
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners"><field name="trululu"/></form>',
mockRPC: function (route, args) {
if (args.method === 'name_get') {
nameGetCount++;
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'aaa',
"default value should be correctly displayed");
assert.strictEqual(nameGetCount, 0, "should have done no name_get");
form.destroy();
});
QUnit.test('form view properly change its title', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'first record',
"should have the display name of the record as title");
await testUtils.form.clickCreate(form);
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), _t("New"),
"should have the display name of the record as title");
form.destroy();
});
QUnit.test('archive/unarchive a record', async function (assert) {
assert.expect(10);
// add active field on partner model to have archive option
this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
res_id: 1,
viewOptions: { hasActionMenus: true },
arch: '<form><field name="active"/><field name="foo"/></form>',
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'action_archive') {
this.data.partner.records[0].active = false;
return Promise.resolve();
}
if (args.method === 'action_unarchive') {
this.data.partner.records[0].active = true;
return Promise.resolve();
}
return this._super(...arguments);
},
});
await testUtils.controlPanel.toggleActionMenu(form);
assert.containsOnce(form, '.o_cp_action_menus a:contains(Archive)');
await testUtils.controlPanel.toggleMenuItem(form, "Archive");
assert.containsOnce(document.body, '.modal');
await testUtils.dom.click($('.modal-footer .btn-primary'));
await testUtils.controlPanel.toggleActionMenu(form);
assert.containsOnce(form, '.o_cp_action_menus a:contains(Unarchive)');
await testUtils.controlPanel.toggleMenuItem(form, "Unarchive");
await testUtils.controlPanel.toggleActionMenu(form);
assert.containsOnce(form, '.o_cp_action_menus a:contains(Archive)');
assert.verifySteps([
'read',
'action_archive',
'read',
'action_unarchive',
'read',
]);
form.destroy();
});
QUnit.test('archive action with active field not in view', async function (assert) {
assert.expect(2);
// add active field on partner model, but do not put it in the view
this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
res_id: 1,
viewOptions: { hasActionMenus: true },
arch: '<form><field name="foo"/></form>',
});
await testUtils.controlPanel.toggleActionMenu(form);
assert.containsNone(form, '.o_cp_action_menus a:contains(Archive)');
assert.containsNone(form, '.o_cp_action_menus a:contains(Unarchive)');
form.destroy();
});
QUnit.test('can duplicate a record', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
viewOptions: {hasActionMenus: true},
});
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'first record',
"should have the display name of the record as title");
await testUtils.controlPanel.toggleActionMenu(form);
await testUtils.controlPanel.toggleMenuItem(form, "Duplicate");
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'first record (copy)',
"should have duplicated the record");
assert.strictEqual(form.mode, "edit", 'should be in edit mode');
form.destroy();
});
QUnit.test('duplicating a record preserve the context', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
viewOptions: {hasActionMenus: true, context: {hey: 'hoy'}},
mockRPC: function (route, args) {
if (args.method === 'read') {
// should have 2 read, one for initial load, second for
// read after duplicating
assert.strictEqual(args.kwargs.context.hey, 'hoy',
"should have send the correct context");
}
return this._super.apply(this, arguments);
},
});
await testUtils.controlPanel.toggleActionMenu(form);
await testUtils.controlPanel.toggleMenuItem(form, "Duplicate");
form.destroy();
});
QUnit.test('cannot duplicate a record', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners" duplicate="false">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
viewOptions: {hasActionMenus: true},
});
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'first record',
"should have the display name of the record as title");
assert.containsNone(form, '.o_cp_action_menus a:contains(Duplicate)',
"should not contains a 'Duplicate' action");
form.destroy();
});
QUnit.test('clicking on stat buttons in edit mode', async function (assert) {
assert.expect(9);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<div name="button_box">' +
'<button class="oe_stat_button" name="some_action" type="action">' +
'<field name="bar"/>' +
'</button>' +
'</div>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.strictEqual(args.args[1].foo, "tralala", "should have saved the changes");
}
assert.step(args.method);
return this._super(route, args);
},
});
await testUtils.form.clickEdit(form);
var count = 0;
await testUtils.mock.intercept(form, "execute_action", function (event) {
event.stopPropagation();
count++;
});
await testUtils.dom.click('.oe_stat_button');
assert.strictEqual(count, 1, "should have triggered a execute action");
assert.strictEqual(form.mode, "edit", "form view should be in edit mode");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
await testUtils.dom.click('.oe_stat_button:first');
assert.strictEqual(form.mode, "edit", "form view should be in edit mode");
assert.strictEqual(count, 2, "should have triggered a execute action");
assert.verifySteps(['read', 'write', 'read']);
form.destroy();
});
QUnit.test('clicking on stat buttons save and reload in edit mode', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<div name="button_box">' +
'<button class="oe_stat_button" type="action">' +
'<field name="int_field" widget="statinfo" string="Some number"/>' +
'</button>' +
'</div>' +
'<group>' +
'<field name="name"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'write') {
// simulate an override of the model...
args.args[1].display_name = "GOLDORAK";
args.args[1].name = "GOLDORAK";
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'second record',
"should have correct display_name");
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=name]'), 'some other name');
await testUtils.dom.click('.oe_stat_button');
assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'GOLDORAK',
"should have correct display_name");
form.destroy();
});
QUnit.test('buttons with attr "special" do not trigger a save', async function (assert) {
assert.expect(4);
var executeActionCount = 0;
var writeCount = 0;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'<button string="Do something" class="btn-primary" name="abc" type="object"/>' +
'<button string="Or discard" class="btn-secondary" special="cancel"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'write') {
writeCount++;
}
return this._super(route, args);
},
});
await testUtils.mock.intercept(form, "execute_action", function () {
executeActionCount++;
});
await testUtils.form.clickEdit(form);
// make the record dirty
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
await testUtils.dom.click(form.$('button:contains(Do something)'));
//TODO: VSC: add a next tick ?
assert.strictEqual(writeCount, 1, "should have triggered a write");
assert.strictEqual(executeActionCount, 1, "should have triggered a execute action");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'abcdef');
await testUtils.dom.click(form.$('button:contains(Or discard)'));
assert.strictEqual(writeCount, 1, "should not have triggered a write");
assert.strictEqual(executeActionCount, 2, "should have triggered a execute action");
form.destroy();
});
QUnit.test('buttons with attr "special=save" save', async function (assert) {
assert.expect(5);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'<button string="Save" class="btn-primary" special="save"/>' +
'</form>',
res_id: 1,
intercepts: {
execute_action: function () {
assert.step('execute_action');
},
},
mockRPC: function (route, args) {
assert.step(args.method);
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
await testUtils.dom.click(form.$('button[special="save"]'));
assert.verifySteps(['read', 'write', 'read', 'execute_action']);
form.destroy();
});
QUnit.test('missing widgets do not crash', async function (assert) {
assert.expect(1);
this.data.partner.fields.foo.type = 'new field type without widget';
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
});
assert.containsOnce(form, '.o_field_widget');
form.destroy();
});
QUnit.test('nolabel', async function (assert) {
assert.expect(6);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<group class="firstgroup"><field name="foo" nolabel="1"/></group>' +
'<group class="secondgroup">'+
'<field name="product_id"/>' +
'<field name="int_field" nolabel="1"/><field name="qux" nolabel="1"/>' +
'</group>' +
'<group><field name="bar"/></group>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.containsN(form, "label.o_form_label", 2);
assert.strictEqual(form.$("label.o_form_label").first().text(), "Product",
"one should be the one for the product field");
assert.strictEqual(form.$("label.o_form_label").eq(1).text(), "Bar",
"one should be the one for the bar field");
assert.hasAttrValue(form.$('.firstgroup td').first(), 'colspan', undefined,
"foo td should have a default colspan (1)");
assert.containsN(form, '.secondgroup tr', 2,
"int_field and qux should have same tr");
assert.containsN(form, '.secondgroup tr:first td', 2,
"product_id field should be on its own tr");
form.destroy();
});
QUnit.test('many2one in a one2many', async function (assert) {
assert.expect(1);
this.data.partner.records[0].p = [2];
this.data.partner.records[1].product_id = 37;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="p">' +
'<tree>' +
'<field name="product_id"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
});
assert.containsOnce(form, 'td:contains(xphone)', "should display the name of the many2one");
form.destroy();
});
QUnit.test('circular many2many\'s', async function (assert) {
assert.expect(4);
this.data.partner_type.fields.partner_ids = {string: "partners", type: "many2many", relation: 'partner'};
this.data.partner.records[0].timmy = [12];
this.data.partner_type.records[0].partner_ids = [1];
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="timmy">' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'<form>' +
'<field name="partner_ids">' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'<form>' +
'<field name="display_name"/>' +
'</form>' +
'</field>' +
'</form>' +
'</field>' +
'</form>',
res_id: 1,
});
assert.containsOnce(form, 'td:contains(gold)',
"should display the name of the many2many on the original form");
await testUtils.dom.click(form.$('td:contains(gold)'));
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(document.body, '.modal');
assert.containsOnce($('.modal'), 'td:contains(first record)',
"should display the name of the many2many on the modal form");
await testUtils.dom.click('.modal td:contains(first record)');
await testUtils.nextTick(); // wait for quick edit
assert.containsN(document.body, '.modal', 2,
"there should be 2 modals (partner on top of partner_type) opened");
form.destroy();
});
QUnit.test('discard changes on a non dirty form view', async function (assert) {
assert.expect(4);
var nbWrite = 0;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
res_id: 1,
mockRPC: function (route) {
if (route === '/web/dataset/call_kw/partner/write') {
nbWrite++;
}
return this._super.apply(this, arguments);
},
});
// switch to edit mode
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input[name=foo]').val(), 'yop',
"input should contain yop");
// click on discard
await testUtils.form.clickDiscard(form);
assert.containsNone(document.body, '.modal', 'no confirm modal should be displayed');
assert.strictEqual(form.$('.o_field_widget').text(), 'yop', 'field in readonly should display yop');
assert.strictEqual(nbWrite, 0, "no write RPC should have been done");
form.destroy();
});
QUnit.test('discard changes on a dirty form view', async function (assert) {
assert.expect(5);
var nbWrite = 0;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
res_id: 1,
mockRPC: function (route) {
if (route === '/web/dataset/call_kw/partner/write') {
nbWrite++;
}
return this._super.apply(this, arguments);
},
});
// switch to edit mode and edit the foo field
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input[name=foo]').val(), 'yop', "input should contain yop");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'new value');
assert.strictEqual(form.$('input[name=foo]').val(), 'new value',
"input should contain new value");
// click on discard
await testUtils.form.clickDiscard(form);
assert.containsNone(document.body, '.modal', "no confirm modal should be displayed");
assert.strictEqual(form.$('.o_field_widget').text(), 'yop', 'field in readonly should display yop');
assert.strictEqual(nbWrite, 0, "no write RPC should have been done");
form.destroy();
});
QUnit.test('discard changes on a dirty form view (for date field)', async function (assert) {
assert.expect(1);
// this test checks that the basic model properly handles date object
// when they are discarded and saved. This may be an issue because
// dates are saved as moment object, and were at one point stringified,
// then parsed into string, which is wrong.
this.data.partner.fields.date.default = "2017-01-25";
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="date"></field></form>',
intercepts: {
history_back: function () {
form.update({}, {reload: false});
}
},
});
// focus the buttons before clicking on them to precisely reproduce what
// really happens (mostly because the datepicker lib need that focus
// event to properly focusout the input, otherwise it crashes later on
// when the 'blur' event is triggered by the re-rendering)
form.$buttons.find('.o_form_button_cancel').focus();
await testUtils.dom.click('.o_form_button_cancel');
form.$buttons.find('.o_form_button_save').focus();
await testUtils.dom.click('.o_form_button_save');
assert.containsOnce(form, 'span:contains(2017)');
form.destroy();
});
QUnit.test('discard changes on relational data on new record', 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="p">' +
'<tree editable="top">' +
'<field name="product_id"/>' +
'</tree>' +
'</field>' +
'</group></sheet></form>',
intercepts: {
history_back: function () {
assert.ok(true, "should have sent correct event");
// simulate the response from the action manager, in the case
// where we have only one active view (the form). If there
// was another view, we would have switched to that view
// instead
form.update({}, {reload: false});
}
},
});
// edit the p field
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.fields.many2one.clickOpenDropdown('product_id');
await testUtils.fields.many2one.clickHighlightedItem('product_id');
assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), 'xphone',
"input should contain xphone");
// click on discard
await testUtils.form.clickDiscard(form);
assert.containsNone(form, '.modal', 'modal should not be displayed');
assert.notOk(form.$el.prop('outerHTML').match('xphone'),
"the string xphone should not be present after discarding");
form.destroy();
});
QUnit.test('discard changes on a new (non dirty, except for defaults) form view', async function (assert) {
assert.expect(3);
this.data.partner.fields.foo.default = "ABC";
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
intercepts: {
history_back: function () {
assert.ok(true, "should have sent correct event");
}
}
});
// edit the foo field
assert.strictEqual(form.$('input[name=foo]').val(), 'ABC',
"input should contain ABC");
await testUtils.form.clickDiscard(form);
assert.containsNone(document.body, '.modal',
"there should not be a confirm modal");
form.destroy();
});
QUnit.test('discard changes on a new (dirty) form view', async function (assert) {
assert.expect(7);
this.data.partner.fields.foo.default = "ABC";
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
intercepts: {
history_back: function () {
assert.ok(true, "should have sent correct event");
// simulate the response from the action manager, in the case
// where we have only one active view (the form). If there
// was another view, we would have switched to that view
// instead
form.update({}, {reload: false});
}
},
});
// edit the foo field
assert.strictEqual(form.$('input').val(), 'ABC', 'input should contain ABC');
await testUtils.fields.editInput(form.$('input[name=foo]'), 'DEF');
// discard the changes and check it has properly been discarded
assert.strictEqual(form.$('input').val(), 'DEF', 'input should be DEF');
await testUtils.form.clickDiscard(form);
assert.strictEqual(form.$('input').val(), 'ABC', 'input should now be ABC');
// redirty and discard the field foo (to make sure initial changes haven't been lost)
await testUtils.fields.editInput(form.$('input[name=foo]'), 'GHI');
assert.strictEqual(form.$('input').val(), 'GHI', 'input should be GHI');
await testUtils.form.clickDiscard(form);
assert.strictEqual(form.$('input').val(), 'ABC', 'input should now be ABC');
form.destroy();
});
QUnit.test('discard changes on a duplicated record', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
res_id: 1,
viewOptions: {hasActionMenus: true},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
await testUtils.form.clickSave(form);
await testUtils.controlPanel.toggleActionMenu(form);
await testUtils.controlPanel.toggleMenuItem(form, "Duplicate");
assert.strictEqual(form.$('input[name=foo]').val(), 'tralala', 'input should contain ABC');
await testUtils.form.clickDiscard(form);
assert.containsNone(document.body, '.modal', "there should not be a confirm modal");
form.destroy();
});
QUnit.test("switching to another record from a dirty one", async function (assert) {
assert.expect(11);
var nbWrite = 0;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
viewOptions: {
ids: [1, 2],
index: 0,
},
res_id: 1,
mockRPC: function (route) {
if (route === '/web/dataset/call_kw/partner/write') {
nbWrite++;
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), '1', "pager value should be 1");
assert.strictEqual(testUtils.controlPanel.getPagerSize(form), '2', "pager limit should be 2");
// switch to edit mode
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input[name=foo]').val(), 'yop', "input should contain yop");
// edit the foo field
await testUtils.fields.editInput(form.$('input[name=foo]'), 'new value');
assert.strictEqual(form.$('input').val(), 'new value', 'input should contain new value');
// click on the pager to switch to the next record (will save record)
await testUtils.controlPanel.pagerNext(form);
assert.containsNone(document.body, '.modal', "no confirm modal should be displayed");
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), '2', "pager value should be 2");
assert.strictEqual(form.$('input[name=foo]').val(), 'blip', "input should contain blip");
await testUtils.controlPanel.pagerPrevious(form);
assert.containsNone(document.body, '.modal', "no confirm modal should be displayed");
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), '1', "pager value should be 1");
assert.strictEqual(form.$('input[name=foo]').val(), 'new value', "input should contain new value");
assert.strictEqual(nbWrite, 1, 'one write RPC should have been done');
form.destroy();
});
QUnit.test('handling dirty state: switching to another record', async function (assert) {
assert.expect(12);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"></field>' +
'<field name="priority" widget="priority"></field>' +
'</form>',
viewOptions: {
ids: [1, 2],
index: 0,
},
res_id: 1,
});
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), '1', "pager value should be 1");
// switch to edit mode
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input[name=foo]').val(), 'yop', "input should contain yop");
// edit the foo field
await testUtils.fields.editInput(form.$('input[name=foo]'), 'new value');
assert.strictEqual(form.$('input[name=foo]').val(), 'new value',
"input should contain new value");
await testUtils.form.clickSave(form);
// click on the pager to switch to the next record and cancel the confirm request
await testUtils.controlPanel.pagerNext(form);
assert.containsNone(document.body, '.modal:visible',
"no confirm modal should be displayed");
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), '2', "pager value should be 2");
assert.containsN(form, '.o_priority .fa-star-o', 2,
'priority widget should have been rendered with correct value');
// edit the value in readonly
await testUtils.dom.click(form.$('.o_priority .fa-star-o:first')); // click on the first star
assert.containsOnce(form, '.o_priority .fa-star',
'priority widget should have been updated');
await testUtils.controlPanel.pagerNext(form);
assert.containsNone(document.body, '.modal:visible',
"no confirm modal should be displayed");
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), '1', "pager value should be 1");
// switch to edit mode
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input[name=foo]').val(), 'new value',
"input should contain yop");
// edit the foo field
await testUtils.fields.editInput(form.$('input[name=foo]'), 'wrong value');
await testUtils.form.clickDiscard(form);
assert.containsNone(document.body, '.modal', "no confirm modal should be displayed");
await testUtils.controlPanel.pagerNext(form);
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), '2', "pager value should be 2");
form.destroy();
});
QUnit.test('restore local state when switching to another record', async function (assert) {
assert.expect(4);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<notebook>' +
'<page string="First Page" name="first">' +
'<field name="foo"/>' +
'</page>' +
'<page string="Second page" name="second">' +
'<field name="bar"/>' +
'</page>' +
'</notebook>' +
'</form>',
viewOptions: {
ids: [1, 2],
index: 0,
},
res_id: 1,
});
// click on second page tab
await testUtils.dom.click(form.$('.o_notebook .nav-link:eq(1)'));
assert.doesNotHaveClass(form.$('.o_notebook .nav-link:eq(0)'), 'active');
assert.hasClass(form.$('.o_notebook .nav-link:eq(1)'), 'active');
// click on the pager to switch to the next record
await testUtils.controlPanel.pagerNext(form);
assert.doesNotHaveClass(form.$('.o_notebook .nav-link:eq(0)'), 'active');
assert.hasClass(form.$('.o_notebook .nav-link:eq(1)'), 'active');
form.destroy();
});
QUnit.test('pager is hidden in create mode', async function (assert) {
assert.expect(7);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
viewOptions: {
ids: [1, 2],
index: 0,
},
});
assert.containsOnce(form, '.o_pager');
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), "1",
"current pager value should be 1");
assert.strictEqual(testUtils.controlPanel.getPagerSize(form), "2",
"current pager limit should be 1");
await testUtils.form.clickCreate(form);
assert.containsNone(form, '.o_pager');
await testUtils.form.clickSave(form);
assert.containsOnce(form, '.o_pager');
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), "3",
"current pager value should be 3");
assert.strictEqual(testUtils.controlPanel.getPagerSize(form), "3",
"current pager limit should be 3");
form.destroy();
});
QUnit.test('switching to another record, in readonly mode', async function (assert) {
assert.expect(5);
var pushStateCount = 0;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
viewOptions: {
ids: [1, 2],
index: 0,
},
res_id: 1,
intercepts: {
push_state: function (event) {
pushStateCount++;
}
}
});
assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), "1", 'pager value should be 1');
await testUtils.controlPanel.pagerNext(form);
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), "2", 'pager value should be 2');
assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
assert.strictEqual(pushStateCount, 2, "should have triggered 2 push_state");
form.destroy();
});
QUnit.test('modifiers are reevaluated when creating new record', 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" class="foo_field" attrs=\'{"invisible": [["bar", "=", True]]}\'/>' +
'<field name="bar"/>' +
'</group></sheet>' +
'</form>',
res_id: 1,
});
assert.containsOnce(form, 'span.foo_field');
assert.isNotVisible(form.$('span.foo_field'));
await testUtils.form.clickCreate(form);
assert.containsOnce(form, 'input.foo_field');
assert.isVisible(form.$('input.foo_field'));
form.destroy();
});
QUnit.test('empty readonly fields are visible on new records', async function (assert) {
assert.expect(2);
this.data.partner.fields.foo.readonly = true;
this.data.partner.fields.foo.default = undefined;
this.data.partner.records[0].foo = undefined;
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.containsOnce(form, '.o_field_empty');
await testUtils.form.clickCreate(form);
assert.containsNone(form, '.o_field_empty');
form.destroy();
});
QUnit.test('all group children have correct layout classname', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet><group>' +
'<group class="inner_group">' +
'<field name="name"/>' +
'</group>' +
'<div class="inner_div">' +
'<field name="foo"/>' +
'</div>' +
'</group></sheet>' +
'</form>',
res_id: 1,
});
assert.hasClass(form.$('.inner_group'), 'o_group_col_6'),
assert.hasClass(form.$('.inner_div'), 'o_group_col_6'),
form.destroy();
});
QUnit.test('deleting a record', async function (assert) {
assert.expect(8);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
viewOptions: {
ids: [1, 2, 4],
index: 0,
hasActionMenus: true,
},
res_id: 1,
});
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), "1", 'pager value should be 1');
assert.strictEqual(testUtils.controlPanel.getPagerSize(form), "3", 'pager limit should be 3');
assert.strictEqual(form.$('span:contains(yop)').length, 1,
'should have a field with foo value for record 1');
assert.ok(!$('.modal:visible').length, 'no confirm modal should be displayed');
// open action menu and delete
await testUtils.controlPanel.toggleActionMenu(form);
await testUtils.controlPanel.toggleMenuItem(form, "Delete");
assert.ok($('.modal').length, 'a confirm modal should be displayed');
// confirm the delete
await testUtils.dom.click($('.modal-footer button.btn-primary'));
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), "1", 'pager value should be 1');
assert.strictEqual(testUtils.controlPanel.getPagerSize(form), "2", 'pager limit should be 2');
assert.strictEqual(form.$('span:contains(blip)').length, 1,
'should have a field with foo value for record 2');
form.destroy();
});
QUnit.test('deleting the last record', async function (assert) {
assert.expect(6);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
viewOptions: {
ids: [1],
index: 0,
hasActionMenus: true,
},
res_id: 1,
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
}
});
await testUtils.controlPanel.toggleActionMenu(form);
await testUtils.controlPanel.toggleMenuItem(form, "Delete");
await testUtils.mock.intercept(form, 'history_back', function () {
assert.step('history_back');
});
assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed');
await testUtils.dom.click($('.modal-footer button.btn-primary'));
assert.strictEqual($('.modal').length, 0, 'no confirm modal should be displayed');
assert.verifySteps(['read', 'unlink', 'history_back']);
form.destroy();
});
QUnit.test('empty required fields cannot be saved', async function (assert) {
assert.expect(5);
this.data.partner.fields.foo.required = true;
delete this.data.partner.fields.foo.default;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/></group>' +
'</form>',
services: {
notification: {
notify: function (params) {
if (params.type !== 'danger') {
return;
}
assert.strictEqual(params.title, 'Invalid fields:',
"should have a warning with correct title");
assert.strictEqual(params.message.toString(), '<ul><li>Foo</li></ul>',
"should have a warning with correct message");
}
},
},
});
await testUtils.form.clickSave(form);
assert.hasClass(form.$('label.o_form_label'),'o_field_invalid');
assert.hasClass(form.$('input[name=foo]'),'o_field_invalid');
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
assert.containsNone(form, '.o_field_invalid');
form.destroy();
});
QUnit.test('changes in a readonly form view are saved directly', async function (assert) {
assert.expect(10);
var nbWrite = 0;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="foo"/>' +
'<field name="priority" widget="priority"/>' +
'</group>' +
'</form>',
mockRPC: function (route) {
if (route === '/web/dataset/call_kw/partner/write') {
nbWrite++;
}
return this._super.apply(this, arguments);
},
res_id: 1,
});
assert.containsN(form, '.o_priority .o_priority_star', 2,
'priority widget should have been rendered');
assert.containsN(form, '.o_priority .fa-star-o', 2,
'priority widget should have been rendered with correct value');
// edit the value in readonly
await testUtils.dom.click(form.$('.o_priority .fa-star-o:first'));
assert.strictEqual(nbWrite, 1, 'should have saved directly');
assert.containsOnce(form, '.o_priority .fa-star',
'priority widget should have been updated');
// switch to edit mode and edit the value again
await testUtils.form.clickEdit(form);
assert.containsN(form, '.o_priority .o_priority_star', 2,
'priority widget should have been correctly rendered');
assert.containsOnce(form, '.o_priority .fa-star',
'priority widget should have correct value');
await testUtils.dom.click(form.$('.o_priority .fa-star-o:first'));
assert.strictEqual(nbWrite, 1, 'should not have saved directly');
assert.containsN(form, '.o_priority .fa-star', 2,
'priority widget should have been updated');
// save
await testUtils.form.clickSave(form);
assert.strictEqual(nbWrite, 2, 'should not have saved directly');
assert.containsN(form, '.o_priority .fa-star', 2,
'priority widget should have correct value');
form.destroy();
});
QUnit.test('display a dialog if onchange result is a warning', async function (assert) {
assert.expect(5);
this.data.partner.onchanges = { foo: true };
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/><field name="int_field"/></group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return Promise.resolve({
value: { int_field: 10 },
warning: {
title: "Warning",
message: "You must first select a partner",
type: 'dialog',
}
});
}
return this._super.apply(this, arguments);
},
intercepts: {
warning: function (event) {
assert.strictEqual(event.data.type, 'dialog',
"should have triggered an event with the correct data");
assert.strictEqual(event.data.title, "Warning",
"should have triggered an event with the correct data");
assert.strictEqual(event.data.message, "You must first select a partner",
"should have triggered an event with the correct data");
},
},
});
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input[name=int_field]').val(), '9');
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
assert.strictEqual(form.$('input[name=int_field]').val(), '10');
form.destroy();
});
QUnit.test('display a notificaton if onchange result is a warning with type notification', async function (assert) {
assert.expect(5);
this.data.partner.onchanges = { foo: true };
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/><field name="int_field"/></group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return Promise.resolve({
value: { int_field: 10 },
warning: {
title: "Warning",
message: "You must first select a partner",
type: 'notification',
}
});
}
return this._super.apply(this, arguments);
},
intercepts: {
warning: function (event) {
assert.strictEqual(event.data.type, 'notification',
"should have triggered an event with the correct data");
assert.strictEqual(event.data.title, "Warning",
"should have triggered an event with the correct data");
assert.strictEqual(event.data.message, "You must first select a partner",
"should have triggered an event with the correct data");
},
},
});
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input[name=int_field]').val(), '9');
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
assert.strictEqual(form.$('input[name=int_field]').val(), '10');
form.destroy();
});
QUnit.test('can create record even if onchange returns a warning', async function (assert) {
assert.expect(2);
this.data.partner.onchanges = { foo: true };
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/><field name="int_field"/></group>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return Promise.resolve({
value: { int_field: 10 },
warning: {
title: "Warning",
message: "You must first select a partner"
}
});
}
return this._super.apply(this, arguments);
},
intercepts: {
warning: function (event) {
assert.ok(true, 'should trigger a warning');
},
},
});
assert.strictEqual(form.$('input[name="int_field"]').val(), "10",
"record should have been created and rendered");
form.destroy();
});
QUnit.test('do nothing if add a line in one2many result in a onchange with a warning', async function (assert) {
assert.expect(3);
this.data.partner.onchanges = { foo: true };
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="foo"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return Promise.resolve({
value: {},
warning: {
title: "Warning",
message: "You must first select a partner",
}
});
}
return this._super.apply(this, arguments);
},
intercepts: {
warning: function () {
assert.step("should have triggered a warning");
},
},
});
// go to edit mode, click to add a record in the o2m
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
assert.containsNone(form, 'tr.o_data_row',
"should not have added a line");
assert.verifySteps(["should have triggered a warning"]);
form.destroy();
});
QUnit.test('button box is rendered in create mode', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<div name="button_box" class="oe_button_box">' +
'<button type="object" class="oe_stat_button" icon="fa-check-square">' +
'<field name="bar"/>' +
'</button>' +
'</div>' +
'</form>',
res_id: 2,
});
// readonly mode
assert.containsOnce(form, '.oe_stat_button',
"button box should be displayed in readonly");
// edit mode
await testUtils.form.clickEdit(form);
assert.containsOnce(form, '.oe_stat_button',
"button box should be displayed in edit on an existing record");
// create mode (leave edition first!)
await testUtils.form.clickDiscard(form);
await testUtils.form.clickCreate(form);
assert.containsOnce(form, '.oe_stat_button',
"button box should be displayed when creating a new record as well");
form.destroy();
});
QUnit.test('properly apply onchange on one2many fields', async function (assert) {
assert.expect(5);
this.data.partner.records[0].p = [4];
this.data.partner.onchanges = {
foo: function (obj) {
obj.p = [
[5],
[1, 4, {display_name: "updated record"}],
[0, null, {display_name: "created record"}],
];
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/></group>' +
'<field name="p">' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
});
assert.containsOnce(form, '.o_field_one2many .o_data_row',
"there should be one one2many record linked at first");
assert.strictEqual(form.$('.o_field_one2many .o_data_row td:first').text(), 'aaa',
"the 'display_name' of the one2many record should be correct");
// switch to edit mode
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'let us trigger an onchange');
var $o2m = form.$('.o_field_one2many');
assert.strictEqual($o2m.find('.o_data_row').length, 2,
"there should be two linked record");
assert.strictEqual($o2m.find('.o_data_row:first td:first').text(), 'updated record',
"the 'display_name' of the first one2many record should have been updated");
assert.strictEqual($o2m.find('.o_data_row:nth(1) td:first').text(), 'created record',
"the 'display_name' of the second one2many record should be correct");
form.destroy();
});
QUnit.test('properly apply onchange on one2many fields direct click', async function (assert) {
assert.expect(3);
var def = testUtils.makeTestPromise();
this.data.partner.records[0].p = [2, 4];
this.data.partner.onchanges = {
int_field: function (obj) {
obj.p = [
[5],
[1, 2, {display_name: "updated record 1", int_field: obj.int_field}],
[1, 4, {display_name: "updated record 2", int_field: obj.int_field * 2}],
];
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'</group>' +
'<field name="p">' +
'<tree>' +
'<field name="display_name"/>' +
'<field name="int_field"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
var self = this;
var my_args = arguments;
var my_super = this._super;
return def.then(() => {
return my_super.apply(self, my_args)
});
}
return this._super.apply(this, arguments);
},
archs: {
'partner,false,form': '<form><group><field name="display_name"/><field name="int_field"/></group></form>'
},
viewOptions: {
mode: 'edit',
},
});
// Trigger the onchange
await testUtils.fields.editInput(form.$('input[name=int_field]'), '2');
// Open first record in one2many
await testUtils.dom.click(form.$('.o_data_row:first'));
assert.containsNone(document.body, '.modal');
def.resolve();
await testUtils.nextTick();
assert.containsOnce(document.body, '.modal');
assert.strictEqual($('.modal').find('input[name=int_field]').val(), '2');
form.destroy();
});
QUnit.test('update many2many value in one2many after onchange', async function (assert) {
assert.expect(2);
this.data.partner.records[1].p = [4];
this.data.partner.onchanges = {
foo: function (obj) {
obj.p = [
[5],
[1, 4, {
display_name: "gold",
timmy: [[5]],
}],
];
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="foo"/>' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="display_name" attrs="{\'readonly\': [(\'timmy\', \'=\', false)]}"/>' +
'<field name="timmy"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 2,
});
assert.strictEqual($('div[name="p"] .o_data_row td').text().trim(), "aaaNo records",
"should have proper initial content");
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala');
assert.strictEqual($('div[name="p"] .o_data_row td').text().trim(), "goldNo records",
"should have proper initial content");
form.destroy();
});
QUnit.test('delete a line in a one2many while editing another line', async function (assert) {
assert.expect(2);
this.data.partner.records[0].p = [1, 2];
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="p">' +
'<tree editable="bottom">' +
'<field name="display_name" required="True"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_data_cell').first());
await testUtils.fields.editInput(form.$('input[name=display_name]'), '');
await testUtils.dom.click(form.$('.fa-trash-o').eq(1));
// use of owlCompatibilityExtraNextTick because there are two sequential updates of the
// control panel (which is written in owl): each of them waits for the next animation frame
// to complete
await testUtils.owlCompatibilityExtraNextTick();
assert.hasClass(form.$('.o_data_cell').first(), "o_invalid_cell",
"Cell should be invalidated.");
assert.containsN(form, '.o_data_row', 2,
"The other line should not have been deleted.");
form.destroy();
});
QUnit.test('properly apply onchange on many2many fields', async function (assert) {
assert.expect(14);
this.data.partner.onchanges = {
foo: function (obj) {
obj.timmy = [
[5],
[4, 12],
[4, 14],
];
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/></group>' +
'<field name="timmy">' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'read' && args.model === 'partner_type') {
assert.deepEqual(args.args[0], [12, 14],
"should read both m2m with one RPC");
}
if (args.method === 'write') {
assert.deepEqual(args.args[1].timmy, [[6, false, [12, 14]]],
"should correctly save the changed m2m values");
}
return this._super.apply(this, arguments);
},
res_id: 2,
});
assert.containsNone(form, '.o_field_many2many .o_data_row',
"there should be no many2many record linked at first");
// switch to edit mode
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'let us trigger an onchange');
var $m2m = form.$('.o_field_many2many');
assert.strictEqual($m2m.find('.o_data_row').length, 2,
"there should be two linked records");
assert.strictEqual($m2m.find('.o_data_row:first td:first').text(), 'gold',
"the 'display_name' of the first m2m record should be correctly displayed");
assert.strictEqual($m2m.find('.o_data_row:nth(1) td:first').text(), 'silver',
"the 'display_name' of the second m2m record should be correctly displayed");
await testUtils.form.clickSave(form);
assert.verifySteps(['read', 'onchange', 'read', 'write', 'read', 'read']);
form.destroy();
});
QUnit.test('form with domain widget: opening a many2many form and save should not crash', async function (assert) {
assert.expect(0);
// We just test that there is no crash in this situation
this.data.partner.records[0].timmy = [12];
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
`<form string="Partners">
<group>
<field name="foo" widget="domain"/>
</group>
<field name="timmy">
<tree>
<field name="display_name"/>
</tree>
<form>
<field name="name"/>
<field name="color"/>
</form>
</field>
</form>`,
res_id: 1,
});
// switch to edit mode
await testUtils.form.clickEdit(form);
// open a form view and save many2many record
await testUtils.dom.click(form.$('.o_data_row .o_data_cell:first'));
await testUtils.dom.click($('.modal-dialog footer button:first-child'));
form.destroy();
});
QUnit.test('display_name not sent for onchanges if not in view', async function (assert) {
assert.expect(7);
this.data.partner.records[0].timmy = [12];
this.data.partner.onchanges = {
foo: function () {},
};
this.data.partner_type.onchanges = {
name: function () {},
};
var readInModal = false;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="foo"/>' +
'<field name="timmy">' +
'<tree>' +
'<field name="name"/>' +
'</tree>' +
'<form>' +
'<field name="name"/>' +
'<field name="color"/>' +
'</form>' +
'</field>' +
'</group>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'read' && args.model === 'partner') {
assert.deepEqual(args.args[1], ['foo', 'timmy', 'display_name'],
"should read display_name even if not in the view");
}
if (args.method === 'read' && args.model === 'partner_type') {
if (!readInModal) {
assert.deepEqual(args.args[1], ['name'],
"should not read display_name for records in the list");
} else {
assert.deepEqual(args.args[1], ['name', 'color', 'display_name'],
"should read display_name when opening the subrecord");
}
}
if (args.method === 'onchange' && args.model === 'partner') {
assert.deepEqual(args.args[1], {
id: 1,
foo: 'coucou',
timmy: [[6, false, [12]]],
}, "should only send the value of fields in the view (+ id)");
assert.deepEqual(args.args[3], {
foo: '1',
timmy: '',
'timmy.name': '1',
'timmy.color': '',
}, "only the fields in the view should be in the onchange spec");
}
if (args.method === 'onchange' && args.model === 'partner_type') {
assert.deepEqual(args.args[1], {
id: 12,
name: 'new name',
color: 2,
}, "should only send the value of fields in the view (+ id)");
assert.deepEqual(args.args[3], {
name: '1',
color: '',
}, "only the fields in the view should be in the onchange spec");
}
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
// trigger the onchange
await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), "coucou");
// open a subrecord and trigger an onchange
readInModal = true;
await testUtils.dom.click(form.$('.o_data_row .o_data_cell:first'));
await testUtils.fields.editInput($('.modal .o_field_widget[name=name]'), "new name");
form.destroy();
});
QUnit.test('onchanges on date(time) fields', async function (assert) {
assert.expect(6);
this.data.partner.onchanges = {
foo: function (obj) {
obj.date = '2021-12-12';
obj.datetime = '2021-12-12 10:55:05';
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="foo"/>' +
'<field name="date"/>' +
'<field name="datetime"/>' +
'</group>' +
'</form>',
res_id: 1,
session: {
getTZOffset: function () {
return 120;
},
},
});
assert.strictEqual(form.$('.o_field_widget[name=date]').text(),
'01/25/2017', "the initial date should be correct");
assert.strictEqual(form.$('.o_field_widget[name=datetime]').text(),
'12/12/2016 12:55:05', "the initial datetime should be correct");
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('.o_field_widget[name=date] input').val(),
'01/25/2017', "the initial date should be correct in edit");
assert.strictEqual(form.$('.o_field_widget[name=datetime] input').val(),
'12/12/2016 12:55:05', "the initial datetime should be correct in edit");
// trigger the onchange
await testUtils.fields.editInput(form.$('.o_field_widget[name="foo"]'), "coucou");
assert.strictEqual(form.$('.o_field_widget[name=date] input').val(),
'12/12/2021', "the initial date should be correct in edit");
assert.strictEqual(form.$('.o_field_widget[name=datetime] input').val(),
'12/12/2021 12:55:05', "the initial datetime should be correct in edit");
form.destroy();
});
QUnit.test('onchanges are not sent for each keystrokes', async function (assert) {
var done = assert.async();
assert.expect(5);
var onchangeNbr = 0;
this.data.partner.onchanges = {
foo: function (obj) {
obj.int_field = obj.foo.length + 1000;
},
};
var def = testUtils.makeTestPromise();
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group><field name="foo"/><field name="int_field"/></group>' +
'</form>',
res_id: 2,
fieldDebounce: 3,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'onchange') {
onchangeNbr++;
return concurrency.delay(3).then(function () {
def.resolve();
return result;
});
}
return result;
},
});
await testUtils.form.clickEdit(form);
testUtils.fields.editInput(form.$('input[name=foo]'), '1');
assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet");
testUtils.fields.editInput(form.$('input[name=foo]'), '12');
assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet");
return waitForFinishedOnChange().then(async function () {
assert.strictEqual(onchangeNbr, 1, "one onchange has been called");
// add something in the input, then focus another input
await testUtils.fields.editAndTrigger(form.$('input[name=foo]'), '123', ['change']);
assert.strictEqual(onchangeNbr, 2, "one onchange has been called immediately");
return waitForFinishedOnChange();
}).then(function () {
assert.strictEqual(onchangeNbr, 2, "no extra onchange should have been called");
form.destroy();
done();
});
function waitForFinishedOnChange() {
return def.then(function () {
def = testUtils.makeTestPromise();
return concurrency.delay(0);
});
}
});
QUnit.test('onchanges are not sent for invalid values', async function (assert) {
assert.expect(6);
this.data.partner.onchanges = {
int_field: function (obj) {
obj.foo = String(obj.int_field);
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group><field name="foo"/><field name="int_field"/></group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
// edit int_field, and check that an onchange has been applied
await testUtils.fields.editInput(form.$('input[name="int_field"]'), "123");
assert.strictEqual(form.$('input[name="foo"]').val(), "123",
"the onchange has been applied");
// enter an invalid value in a float, and check that no onchange has
// been applied
await testUtils.fields.editInput(form.$('input[name="int_field"]'), "123a");
assert.strictEqual(form.$('input[name="foo"]').val(), "123",
"the onchange has not been applied");
// save, and check that the int_field input is marked as invalid
await testUtils.form.clickSave(form);
assert.hasClass(form.$('input[name="int_field"]'),'o_field_invalid',
"input int_field is marked as invalid");
assert.verifySteps(['read', 'onchange']);
form.destroy();
});
QUnit.test('rpc complete after destroying parent', async function (assert) {
// We just test that there is no crash in this situation
assert.expect(0);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<button name="update_module" type="object" class="o_form_button_update"/>' +
'</form>',
res_id: 2,
intercepts: {
execute_action: function (event) {
form.destroy();
event.data.on_success();
}
}
});
await testUtils.dom.click(form.$('.o_form_button_update'));
});
QUnit.test('onchanges that complete after discarding', async function (assert) {
assert.expect(6);
var def1 = testUtils.makeTestPromise();
this.data.partner.onchanges = {
foo: function (obj) {
obj.int_field = obj.foo.length + 1000;
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group><field name="foo"/><field name="int_field"/></group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'onchange') {
assert.step('onchange is done');
return def1.then(function () {
return result;
});
}
return result;
},
});
// go into edit mode
assert.strictEqual(form.$('span[name="foo"]').text(), "blip",
"field foo should be displayed to initial value");
await testUtils.form.clickEdit(form);
// edit a value
await testUtils.fields.editInput(form.$('input[name=foo]'), '1234');
// discard changes
await testUtils.form.clickDiscard(form);
assert.containsNone(form, '.modal');
assert.strictEqual(form.$('span[name="foo"]').text(), "blip",
"field foo should still be displayed to initial value");
// complete the onchange
def1.resolve();
await testUtils.nextTick();
assert.strictEqual(form.$('span[name="foo"]').text(), "blip",
"field foo should still be displayed to initial value");
assert.verifySteps(['onchange is done']);
form.destroy();
});
QUnit.test('discarding before save returns', async function (assert) {
assert.expect(4);
var def = testUtils.makeTestPromise();
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group><field name="foo"/></group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'write') {
return def.then(_.constant(result));
}
return result;
},
viewOptions: {
mode: 'edit',
},
});
await testUtils.fields.editInput(form.$('input[name=foo]'), '1234');
// save the value and discard directly
await testUtils.form.clickSave(form);
form.discardChanges();
assert.strictEqual(form.$('.o_field_widget[name="foo"]').val(), "1234",
"field foo should still contain new value");
assert.strictEqual($('.modal').length, 0,
"Confirm dialog should not be displayed");
// complete the write
def.resolve();
await testUtils.nextTick();
assert.strictEqual($('.modal').length, 0,
"Confirm dialog should not be displayed");
assert.strictEqual(form.$('.o_field_widget[name="foo"]').text(), "1234",
"value should have been saved and rerendered in readonly");
form.destroy();
});
QUnit.test('unchanged relational data is sent for onchanges', async function (assert) {
assert.expect(1);
this.data.partner.records[1].p = [4];
this.data.partner.onchanges = {
foo: function (obj) {
obj.int_field = obj.foo.length + 1000;
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
assert.deepEqual(args.args[1].p, [[4, 4, false]],
"should send a command for field p even if it hasn't changed");
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'trigger an onchange');
form.destroy();
});
QUnit.test('onchanges on unknown fields of o2m are ignored', async function (assert) {
// many2one fields need to be postprocessed (the onchange returns [id,
// display_name]), but if we don't know the field, we can't know it's a
// many2one, so it isn't ignored, its value is an array instead of a
// dataPoint id, which may cause errors later (e.g. when saving).
assert.expect(2);
this.data.partner.records[1].p = [4];
this.data.partner.onchanges = {
foo: function () {},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'</tree>' +
'<form>' +
'<field name="foo"/>' +
'<field name="product_id"/>' +
'</form>' +
'</field>' +
'</group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return Promise.resolve({
value: {
p: [
[5],
[1, 4, {
foo: 'foo changed',
product_id: [37, "xphone"],
}]
],
},
});
}
if (args.method === 'write') {
assert.deepEqual(args.args[1].p, [[1, 4, {
foo: 'foo changed',
}]], "should only write value of known fields");
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'trigger an onchange');
await testUtils.owlCompatibilityExtraNextTick();
assert.strictEqual(form.$('.o_data_row td:first').text(), 'foo changed',
"onchange should have been correctly applied on field in o2m list");
await testUtils.form.clickSave(form);
form.destroy();
});
QUnit.test('onchange value are not discarded on o2m edition', async function (assert) {
assert.expect(4);
this.data.partner.records[1].p = [4];
this.data.partner.onchanges = {
foo: function () {},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'</tree>' +
'<form>' +
'<field name="foo"/>' +
'<field name="product_id"/>' +
'</form>' +
'</field>' +
'</group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return Promise.resolve({
value: {
p: [[5], [1, 4, {foo: 'foo changed'}]],
},
});
}
if (args.method === 'write') {
assert.deepEqual(args.args[1].p, [[1, 4, {
foo: 'foo changed',
}]], "should only write value of known fields");
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('.o_data_row td:first').text(), 'My little Foo Value',
"the initial value should be the default one");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'trigger an onchange');
await testUtils.owlCompatibilityExtraNextTick();
assert.strictEqual(form.$('.o_data_row td:first').text(), 'foo changed',
"onchange should have been correctly applied on field in o2m list");
await testUtils.dom.click(form.$('.o_data_row'));
assert.strictEqual($('.modal .modal-title').text().trim(), 'Open: one2many field',
"the field string is displayed in the modal title");
assert.strictEqual($('.modal .o_field_widget').val(), 'foo changed',
"the onchange value hasn't been discarded when opening the o2m");
form.destroy();
});
QUnit.test('args of onchanges in o2m fields are correct (inline edition)', async function (assert) {
assert.expect(3);
this.data.partner.records[1].p = [4];
this.data.partner.fields.p.relation_field = 'rel_field';
this.data.partner.fields.int_field.default = 14;
this.data.partner.onchanges = {
int_field: function (obj) {
obj.foo = '[' + obj.rel_field.foo + '] ' + obj.int_field;
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<field name="foo"/>' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</form>',
res_id: 2,
});
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('.o_data_row td:first').text(), 'My little Foo Value',
"the initial value should be the default one");
await testUtils.dom.click(form.$('.o_data_row td:nth(1)'));
await testUtils.fields.editInput(form.$('.o_data_row input:nth(1)'), 77);
assert.strictEqual(form.$('.o_data_row input:first').val(), '[blip] 77',
"onchange should have been correctly applied");
// create a new o2m record
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
assert.strictEqual(form.$('.o_data_row input:first').val(), '[blip] 14',
"onchange should have been correctly applied after default get");
form.destroy();
});
QUnit.test('args of onchanges in o2m fields are correct (dialog edition)', async function (assert) {
assert.expect(6);
this.data.partner.records[1].p = [4];
this.data.partner.fields.p.relation_field = 'rel_field';
this.data.partner.fields.int_field.default = 14;
this.data.partner.onchanges = {
int_field: function (obj) {
obj.foo = '[' + obj.rel_field.foo + '] ' + obj.int_field;
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<field name="foo"/>' +
'<field name="p" string="custom label">' +
'<tree>' +
'<field name="foo"/>' +
'</tree>' +
'<form>' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'</form>' +
'</field>' +
'</group>' +
'</form>',
res_id: 2,
});
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('.o_data_row td:first').text(), 'My little Foo Value',
"the initial value should be the default one");
await testUtils.dom.click(form.$('.o_data_row td:first'));
await testUtils.nextTick();
await testUtils.fields.editInput($('.modal input:nth(1)'), 77);
assert.strictEqual($('.modal input:first').val(), '[blip] 77',
"onchange should have been correctly applied");
await testUtils.dom.click($('.modal-footer .btn-primary'));
assert.strictEqual(form.$('.o_data_row td:first').text(), '[blip] 77',
"onchange should have been correctly applied");
// create a new o2m record
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
assert.strictEqual($('.modal .modal-title').text().trim(), 'Create custom label',
"the custom field label is applied in the modal title");
assert.strictEqual($('.modal input:first').val(), '[blip] 14',
"onchange should have been correctly applied after default get");
await testUtils.dom.clickFirst($('.modal-footer .btn-primary'));
await testUtils.nextTick();
assert.strictEqual(form.$('.o_data_row:nth(1) td:first').text(), '[blip] 14',
"onchange should have been correctly applied after default get");
form.destroy();
});
QUnit.test('context of onchanges contains the context of changed fields', async function (assert) {
assert.expect(2);
this.data.partner.onchanges = {
foo: function () {},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<field name="foo" context="{\'test\': 1}"/>' +
'<field name="int_field" context="{\'int_ctx\': 1}"/>' +
'</group>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'onchange') {
assert.strictEqual(args.kwargs.context.test, 1,
"the context of the field triggering the onchange should be given");
assert.strictEqual(args.kwargs.context.int_ctx, undefined,
"the context of other fields should not be given");
}
return this._super.apply(this, arguments);
},
res_id: 2,
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'coucou');
form.destroy();
});
QUnit.test('navigation with tab key in form view', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="foo" widget="email"/>' +
'<field name="bar"/>' +
'<field name="display_name" widget="url"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
});
// go to edit mode
await testUtils.form.clickEdit(form);
// focus first input, trigger tab
form.$('input[name="foo"]').focus();
const tabKey = { keyCode: $.ui.keyCode.TAB, which: $.ui.keyCode.TAB };
await testUtils.dom.triggerEvent(form.$('input[name="foo"]'), 'keydown', tabKey);
assert.ok($.contains(form.$('div[name="bar"]')[0], document.activeElement),
"bar checkbox should be focused");
await testUtils.dom.triggerEvent(document.activeElement, 'keydown', tabKey);
assert.strictEqual(form.$('input[name="display_name"]')[0], document.activeElement,
"display_name should be focused");
// simulate shift+tab on active element
const shiftTabKey = Object.assign({}, tabKey, { shiftKey: true });
await testUtils.dom.triggerEvent(document.activeElement, 'keydown', shiftTabKey);
await testUtils.dom.triggerEvent(document.activeElement, 'keydown', shiftTabKey);
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"first input should be focused");
form.destroy();
});
QUnit.test('navigation with tab key in readonly form view', async function (assert) {
assert.expect(3);
this.data.partner.records[1].product_id = 37;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="trululu"/>' +
'<field name="foo"/>' +
'<field name="product_id"/>' +
'<field name="foo" widget="phone"/>' +
'<field name="display_name" widget="url"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
});
// focus first field, trigger tab
form.$('[name="trululu"]').focus();
form.$('[name="trululu"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
form.$('[name="foo"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(form.$('[name="product_id"]')[0], document.activeElement,
"product_id should be focused");
form.$('[name="product_id"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
form.$('[name="foo"]:eq(1)').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(form.$('div[name="display_name"].o_field_url > a')[0], document.activeElement,
"display_name should be focused");
// simulate shift+tab on active element
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
assert.strictEqual(document.activeElement, form.$('[name="trululu"]')[0],
"first many2one should be focused");
form.destroy();
});
QUnit.test('skip invisible fields when navigating with TAB', async function (assert) {
assert.expect(1);
this.data.partner.records[0].bar = true;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet><group>' +
'<field name="foo"/>' +
'<field name="bar" invisible="1"/>' +
'<field name="product_id" attrs=\'{"invisible": [["bar", "=", true]]}\'/>' +
'<field name="int_field"/>' +
'</group></sheet>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
form.$('input[name="foo"]').focus();
form.$('input[name="foo"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(form.$('input[name="int_field"]')[0], document.activeElement,
"int_field should be focused");
form.destroy();
});
QUnit.test('navigation with tab key selects a value in form view', async function (assert) {
assert.expect(5);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="display_name"/>
<field name="int_field"/>
<field name="qux"/>
<field name="trululu"/>
<field name="date"/>
<field name="datetime"/>
</form>`,
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
await testUtils.dom.click(form.el.querySelector('input[name="display_name"]'));
await testUtils.fields.triggerKeydown(document.activeElement, 'tab');
assert.strictEqual(document.getSelection().toString(), "10",
"int_field value should be selected");
await testUtils.fields.triggerKeydown(document.activeElement, 'tab');
assert.strictEqual(document.getSelection().toString(), "0.4",
"qux field value should be selected");
await testUtils.fields.triggerKeydown(document.activeElement, 'tab');
assert.strictEqual(document.getSelection().toString(), "aaa",
"trululu field value should be selected");
await testUtils.fields.triggerKeydown(document.activeElement, 'tab');
assert.strictEqual(document.getSelection().toString(), "01/25/2017",
"date field value should be selected");
await testUtils.fields.triggerKeydown(document.activeElement, 'tab');
assert.strictEqual(document.getSelection().toString(), "12/12/2016 10:55:05",
"datetime field value should be selected");
form.destroy();
});
QUnit.test('clicking on a stat button with a context', async function (assert) {
assert.expect(1);
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" type="action" name="1" context="{\'test\': active_id}">' +
'<field name="qux" widget="statinfo"/>' +
'</button>' +
'</div>' +
'</sheet>' +
'</form>',
res_id: 2,
viewOptions: {
context: {some_context: true},
},
intercepts: {
execute_action: function (e) {
assert.deepEqual(e.data.action_data.context, {
'test': 2
}, "button context should have been evaluated and given to the action, with magicc without previous context");
},
},
});
await testUtils.dom.click(form.$('.oe_stat_button'));
form.destroy();
});
QUnit.test('clicking on a stat button with no context', async function (assert) {
assert.expect(1);
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" type="action" name="1">' +
'<field name="qux" widget="statinfo"/>' +
'</button>' +
'</div>' +
'</sheet>' +
'</form>',
res_id: 2,
viewOptions: {
context: {some_context: true},
},
intercepts: {
execute_action: function (e) {
assert.deepEqual(e.data.action_data.context, {
}, "button context should have been evaluated and given to the action, with magic keys but without previous context");
},
},
});
await testUtils.dom.click(form.$('.oe_stat_button'));
form.destroy();
});
QUnit.test('diplay a stat button outside a buttonbox', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form string="Partners">' +
'<sheet>' +
'<button class="oe_stat_button" type="action" name="1">' +
'<field name="int_field" widget="statinfo"/>' +
'</button>' +
'</sheet>' +
'</form>',
res_id: 2,
});
assert.containsOnce(form, 'button .o_field_widget',
"a field widget should be display inside the button");
assert.strictEqual(form.$('button .o_field_widget').children().length, 2,
"the field widget should have 2 children, the text and the value");
assert.strictEqual(parseInt(form.$('button .o_field_widget .o_stat_value').text()), 9,
"the value rendered should be the same as the field value");
form.destroy();
});
QUnit.test('diplay something else than a button in a buttonbox', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<div name="button_box" class="oe_button_box">' +
'<button type="object" class="oe_stat_button" icon="fa-check-square">' +
'<field name="bar"/>' +
'</button>' +
'<label/>' +
'</div>' +
'</form>',
res_id: 2,
});
assert.strictEqual(form.$('.oe_button_box').children().length, 2,
"button box should contain two children");
assert.containsOnce(form, '.oe_button_box > .oe_stat_button',
"button box should only contain one button");
assert.containsOnce(form, '.oe_button_box > label',
"button box should only contain one label");
form.destroy();
});
QUnit.test('invisible fields are not considered as visible in a buttonbox', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<div name="button_box" class="oe_button_box">' +
'<field name="foo" invisible="1"/>' +
'<field name="bar" invisible="1"/>' +
'<field name="int_field" invisible="1"/>' +
'<field name="qux" invisible="1"/>' +
'<field name="display_name" invisible="1"/>' +
'<field name="state" invisible="1"/>' +
'<field name="date" invisible="1"/>' +
'<field name="datetime" invisible="1"/>' +
'<button type="object" class="oe_stat_button" icon="fa-check-square"/>' +
'</div>' +
'</form>',
res_id: 2,
});
assert.strictEqual(form.$('.oe_button_box').children().length, 1,
"button box should contain only one child");
assert.hasClass(form.$('.oe_button_box'), 'o_not_full',
"the buttonbox should not be full");
form.destroy();
});
QUnit.test('display correctly buttonbox, in large size class', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<div name="button_box" class="oe_button_box">' +
'<button type="object" class="oe_stat_button" icon="fa-check-square">' +
'<field name="bar"/>' +
'</button>' +
'<button type="object" class="oe_stat_button" icon="fa-check-square">' +
'<field name="foo"/>' +
'</button>' +
'</div>' +
'</form>',
res_id: 2,
config: {
device: {size_class: 5},
},
});
assert.strictEqual(form.$('.oe_button_box').children().length, 2,
"button box should contain two children");
form.destroy();
});
QUnit.test('one2many default value creation', async function (assert) {
assert.expect(1);
this.data.partner.records[0].product_ids = [37];
this.data.partner.fields.product_ids.default = [
[0, 0, { name: 'xdroid', partner_type_id: 12 }]
];
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="product_ids" nolabel="1">' +
'<tree editable="top" create="0">' +
'<field name="name" readonly="1"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'create') {
var command = args.args[0].product_ids[0];
assert.strictEqual(command[2].partner_type_id, 12,
"the default partner_type_id should be equal to 12");
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickSave(form);
form.destroy();
});
QUnit.test('many2manys inside one2manys are saved correctly', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="timmy" widget="many2many_tags"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'create') {
var command = args.args[0].p;
assert.deepEqual(command, [[0, command[0][1], {
timmy: [[6, false, [12]]],
}]], "the default partner_type_id should be equal to 12");
}
return this._super.apply(this, arguments);
},
});
// add a o2m subrecord with a m2m tag
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.fields.many2one.clickOpenDropdown('timmy');
await testUtils.fields.many2one.clickHighlightedItem('timmy');
await testUtils.form.clickSave(form);
form.destroy();
});
QUnit.test('one2manys (list editable) inside one2manys are saved correctly', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="p">' +
'<tree>' +
'<field name="p"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
archs: {
"partner,false,form": '<form>' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</form>'
},
mockRPC: function (route, args) {
if (args.method === 'create') {
assert.deepEqual(args.args[0].p,
[[0, args.args[0].p[0][1], {
p: [[0, args.args[0].p[0][2].p[0][1], {display_name: "xtv"}]],
}]],
"create should be called with the correct arguments");
}
return this._super.apply(this, arguments);
},
});
// add a o2m subrecord
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.dom.click($('.modal-body .o_field_one2many .o_field_x2many_list_row_add a'));
await testUtils.fields.editInput($('.modal-body input'), 'xtv');
await testUtils.dom.click($('.modal-footer button:first'));
assert.strictEqual($('.modal').length, 0,
"dialog should be closed");
var row = form.$('.o_field_one2many .o_legacy_list_view .o_data_row');
assert.strictEqual(row.children()[0].textContent, '1 record',
"the cell should contains the number of record: 1");
await testUtils.form.clickSave(form);
form.destroy();
});
QUnit.test('oe_read_only and oe_edit_only classNames on fields inside groups', async function (assert) {
assert.expect(10);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="foo" class="oe_read_only"/>
<field name="bar" class="oe_edit_only"/>
</group>
</form>`,
res_id: 1,
});
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_readonly',
'form should be in readonly mode');
assert.isVisible(form.$('.o_field_widget[name=foo]'));
assert.isVisible(form.$('label:contains(Foo)'));
assert.isNotVisible(form.$('.o_field_widget[name=bar]'));
assert.isNotVisible(form.$('label:contains(Bar)'));
await testUtils.form.clickEdit(form);
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable',
'form should be in readonly mode');
assert.isNotVisible(form.$('.o_field_widget[name=foo]'));
assert.isNotVisible(form.$('label:contains(Foo)'));
assert.isVisible(form.$('.o_field_widget[name=bar]'));
assert.isVisible(form.$('label:contains(Bar)'));
form.destroy();
});
QUnit.test('oe_read_only className is handled in list views', async function (assert) {
assert.expect(7);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="foo"/>' +
'<field name="display_name" class="oe_read_only"/>' +
'<field name="bar"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_readonly',
'form should be in readonly mode');
assert.isVisible(form.$('.o_field_one2many .o_legacy_list_view thead th[data-name="display_name"]'),
'display_name cell should be visible in readonly mode');
await testUtils.form.clickEdit(form);
assert.strictEqual(form.el.querySelector('th[data-name="foo"]').style.width, '100%',
'As the only visible char field, "foo" should take 100% of the remaining space');
assert.strictEqual(form.el.querySelector('th.oe_read_only').style.width, '0px',
'"oe_read_only" in edit mode should have a 0px width');
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable',
'form should be in edit mode');
assert.isNotVisible(form.$('.o_field_one2many .o_legacy_list_view thead th[data-name="display_name"]'),
'display_name cell should not be visible in edit mode');
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.owlCompatibilityExtraNextTick();
assert.hasClass(form.$('.o_legacy_form_view .o_legacy_list_view tbody tr:first input[name="display_name"]'),
'oe_read_only', 'display_name input should have oe_read_only class');
form.destroy();
});
QUnit.test('oe_edit_only className is handled in list views', async function (assert) {
assert.expect(5);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="foo"/>' +
'<field name="display_name" class="oe_edit_only"/>' +
'<field name="bar"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_readonly',
'form should be in readonly mode');
assert.isNotVisible(form.$('.o_field_one2many .o_legacy_list_view thead th[data-name="display_name"]'),
'display_name cell should not be visible in readonly mode');
await testUtils.form.clickEdit(form);
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable',
'form should be in edit mode');
assert.isVisible(form.$('.o_field_one2many .o_legacy_list_view thead th[data-name="display_name"]'),
'display_name cell should be visible in edit mode');
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.owlCompatibilityExtraNextTick();
assert.hasClass(form.$('.o_legacy_form_view .o_legacy_list_view tbody tr:first input[name="display_name"]'),
'oe_edit_only', 'display_name input should have oe_edit_only class');
form.destroy();
});
QUnit.test('*_view_ref in context are passed correctly', async function (assert) {
var done = assert.async();
assert.expect(3);
createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="p" context="{\'tree_view_ref\':\'module.tree_view_ref\'}"/>' +
'</sheet>' +
'</form>',
res_id: 1,
intercepts: {
load_views: function (event) {
var context = event.data.context;
assert.strictEqual(context.tree_view_ref, 'module.tree_view_ref',
"context should contain tree_view_ref");
event.data.on_success();
}
},
viewOptions: {
context: {some_context: false},
},
mockRPC: function (route, args) {
if (args.method === 'read') {
assert.strictEqual('some_context' in args.kwargs.context && !args.kwargs.context.some_context, true,
"the context should have been set");
}
return this._super.apply(this, arguments);
},
}).then(async function (form) {
// reload to check that the record's context hasn't been modified
await form.reload();
form.destroy();
done();
});
});
QUnit.test('non inline subview and create=0 in action context', async function (assert) {
// the create=0 should apply on the main view (form), but not on subviews
assert.expect(2);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="product_ids" mode="kanban"/></form>',
archs: {
"product,false,kanban": `<kanban>
<templates><t t-name="kanban-box">
<div><field name="name"/></div>
</t></templates>
</kanban>`,
},
res_id: 1,
viewOptions: {
context: {create: false},
mode: 'edit',
},
});
assert.containsNone(form, '.o_form_button_create');
assert.containsOnce(form, '.o-kanban-button-new');
form.destroy();
});
QUnit.test('readonly fields with modifiers may be saved', async function (assert) {
// the readonly property on the field description only applies on view,
// this is not a DB constraint. It should be seen as a default value,
// that may be overridden in views, for example with modifiers. So
// basically, a field defined as readonly may be edited.
assert.expect(3);
this.data.partner.fields.foo.readonly = true;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo" attrs="{\'readonly\': [(\'bar\',\'=\',False)]}"/>' +
'<field name="bar"/>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.deepEqual(args.args[1], {foo: 'New foo value'},
"the new value should be saved");
}
return this._super.apply(this, arguments);
},
});
// bar being set to true, foo shouldn't be readonly and thus its value
// could be saved, even if in its field description it is readonly
await testUtils.form.clickEdit(form);
assert.containsOnce(form, 'input[name="foo"]',
"foo field should be editable");
await testUtils.fields.editInput(form.$('input[name="foo"]'), 'New foo value');
await testUtils.form.clickSave(form);
assert.strictEqual(form.$('.o_field_widget[name=foo]').text(), 'New foo value',
"new value for foo field should have been saved");
form.destroy();
});
QUnit.test('readonly set by modifier do not break many2many_tags', async function (assert) {
assert.expect(0);
this.data.partner.onchanges = {
bar: function (obj) {
obj.timmy = [[6, false, [12]]];
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="bar"/>' +
'<field name="timmy" widget="many2many_tags" attrs="{\'readonly\': [(\'bar\',\'=\',True)]}"/>' +
'</sheet>' +
'</form>',
res_id: 5,
});
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_widget[name=bar] input'));
form.destroy();
});
QUnit.test('check if id and active_id are defined', async function (assert) {
assert.expect(2);
let checkOnchange = false;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="p" context="{\'default_trululu\':active_id, \'current_id\':id}">' +
'<tree>' +
'<field name="trululu"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
archs: {
"partner,false,form": '<form><field name="trululu"/></form>'
},
mockRPC: function (route, args) {
if (args.method === 'onchange' && checkOnchange) {
assert.strictEqual(args.kwargs.context.current_id, false,
"current_id should be false");
assert.strictEqual(args.kwargs.context.default_trululu, false,
"default_trululu should be false");
}
return this._super.apply(this, arguments);
},
});
checkOnchange = true;
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
form.destroy();
});
QUnit.test('modifiers are considered on multiple <footer/> tags', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form>' +
'<field name="bar"/>' +
'<footer attrs="{\'invisible\': [(\'bar\',\'=\',False)]}">' +
'<button>Hello</button>' +
'<button>World</button>' +
'</footer>' +
'<footer attrs="{\'invisible\': [(\'bar\',\'!=\',False)]}">' +
'<button>Foo</button>' +
'</footer>' +
'</form>',
res_id: 1,
viewOptions: {
footerToButtons: true,
mode: 'edit',
},
});
assert.deepEqual(getVisibleButtonTexts(), ["Hello", "World"],
"only the first button section should be visible");
await testUtils.dom.click(form.$(".o_field_boolean input"));
assert.deepEqual(getVisibleButtonTexts(), ["Foo"],
"only the second button section should be visible");
form.destroy();
function getVisibleButtonTexts() {
var $visibleButtons = form.$buttons.find('button:visible');
return _.map($visibleButtons, function (el) {
return el.innerHTML.trim();
});
}
});
QUnit.test('buttons in footer are moved to $buttons if necessary', async function (assert) {
assert.expect(4);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'<footer>' +
'<button string="Create" type="object" class="infooter"/>' +
'</footer>' +
'</form>',
res_id: 1,
viewOptions: {footerToButtons: true},
});
assert.containsOnce(form.$('.o_control_panel'), 'button.infooter');
assert.containsNone(form.$('.o_legacy_form_view'), 'button.infooter');
// check that this still works after a reload
await testUtils.form.reload(form);
assert.containsOnce(form.$('.o_control_panel'), 'button.infooter');
assert.containsNone(form.$('.o_legacy_form_view'), 'button.infooter');
form.destroy();
});
QUnit.test('open new record even with warning message', async function (assert) {
assert.expect(3);
this.data.partner.onchanges = { foo: true };
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/></group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return Promise.resolve({
warning: {
title: "Warning",
message: "Any warning."
}
});
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input').val(), 'blip', 'input should contain record value');
await testUtils.fields.editInput(form.$('input[name="foo"]'), "tralala");
assert.strictEqual(form.$('input').val(), 'tralala', 'input should contain new value');
await form.reload({ currentId: false });
assert.strictEqual(form.$('input').val(), '',
'input should have no value after reload');
form.destroy();
});
QUnit.test('render stat button with string inline', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
res_id: 1,
data: this.data,
arch: '<form string="Manufacturing Orders">' +
'<sheet>' +
'<div class="oe_button_box" name="button_box">' +
'<button string="Inventory Moves" class="oe_stat_button" icon="fa-arrows-v"/>' +
'</div>' +
'</sheet>' +
'</form>',
});
var $button = form.$('.o_legacy_form_view .o_form_sheet .oe_button_box .oe_stat_button span');
assert.strictEqual($button.text(), "Inventory Moves",
"the stat button should contain a span with the string attribute value");
form.destroy();
});
QUnit.test('renderer waits for asynchronous fields rendering', async function (assert) {
assert.expect(1);
var done = assert.async();
testUtils.createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="bar"/>' +
'<field name="foo" widget="ace"/>' +
'<field name="int_field"/>' +
'</form>',
res_id: 1,
}).then(function (form) {
assert.containsOnce(form, '.ace_editor',
"should have waited for ace to load its dependencies");
form.destroy();
done();
});
});
QUnit.test('open one2many form containing one2many', async function (assert) {
assert.expect(9);
this.data.partner.records[0].product_ids = [37];
this.data.product.fields.partner_type_ids = {
string: "one2many partner", type: "one2many", relation: "partner_type",
};
this.data.product.records[0].partner_type_ids = [12];
var form = await createView({
View: FormView,
model: 'partner',
res_id: 1,
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="product_ids">' +
'<tree create="0">' +
'<field name="display_name"/>' +
'<field name="partner_type_ids"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</sheet>' +
'</form>',
archs: {
'product,false,form':
'<form string="Products">' +
'<sheet>' +
'<group>' +
'<field name="partner_type_ids">' +
'<tree create="0">' +
'<field name="display_name"/>' +
'<field name="color"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</sheet>' +
'</form>',
},
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
});
var row = form.$('.o_field_one2many .o_legacy_list_view .o_data_row');
assert.strictEqual(row.children()[1].textContent, '1 record',
"the cell should contains the number of record: 1");
await testUtils.dom.click(row);
await testUtils.nextTick(); // wait for quick edit
var modal_row = $('.modal-body .o_form_sheet .o_field_one2many .o_legacy_list_view .o_data_row');
assert.strictEqual(modal_row.children('.o_data_cell').length, 2,
"the row should contains the 2 fields defined in the form view");
assert.strictEqual($(modal_row).text(), "gold2",
"the value of the fields should be fetched and displayed");
assert.verifySteps(['read', 'read', 'get_views', 'read', 'read'],
"there should be 4 read rpcs");
form.destroy();
});
QUnit.test('in edit mode, first field is focused', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"foo field should have focus");
assert.strictEqual(form.$('input[name="foo"]')[0].selectionStart, 3,
"cursor should be at the end");
form.destroy();
});
QUnit.test('autofocus fields are focused', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="bar"/>' +
'<field name="foo" default_focus="1"/>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"foo field should have focus");
form.destroy();
});
QUnit.test('correct amount of buttons', async function (assert) {
assert.expect(7);
var self = this;
var buttons = Array(8).join(
'<button type="object" class="oe_stat_button" icon="fa-check-square">' +
'<field name="bar"/>' +
'</button>'
);
var statButtonSelector = '.oe_stat_button:not(.dropdown-item, .dropdown-toggle)';
var createFormWithDeviceSizeClass = async function (size_class) {
return await createView({
View: FormView,
model: 'partner',
data: self.data,
arch: '<form>' +
'<div name="button_box" class="oe_button_box">'
+ buttons +
'</div>' +
'</form>',
res_id: 2,
config: {
device: {size_class: size_class},
},
});
};
var assertFormContainsNButtonsWithSizeClass = async function (size_class, n) {
var form = await createFormWithDeviceSizeClass(size_class);
assert.containsN(form, statButtonSelector, n, 'The form has the expected amount of buttons');
form.destroy();
};
await assertFormContainsNButtonsWithSizeClass(0, 2);
await assertFormContainsNButtonsWithSizeClass(1, 2);
await assertFormContainsNButtonsWithSizeClass(2, 2);
await assertFormContainsNButtonsWithSizeClass(3, 4);
await assertFormContainsNButtonsWithSizeClass(4, 7);
await assertFormContainsNButtonsWithSizeClass(5, 7);
await assertFormContainsNButtonsWithSizeClass(6, 7);
});
QUnit.test('can set bin_size to false in context', async function (assert){
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
context: {
bin_size: false,
},
mockRPC: function (route, args) {
assert.strictEqual(args.kwargs.context.bin_size, false,
"bin_size should always be in the context and should be false");
return this._super(route, args);
}
});
form.destroy();
});
QUnit.test('no focus set on form when closing many2one modal if lastActivatedFieldIndex is not set', async function (assert) {
assert.expect(8);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'<field name="p"/>' +
'<field name="timmy"/>' +
'<field name="product_ids"/>' +
'<field name="trululu"/>' +
'</form>',
res_id: 2,
archs: {
'partner,false,list': '<tree><field name="display_name"/></tree>',
'partner_type,false,list': '<tree><field name="name"/></tree>',
'partner,false,form': '<form><field name="trululu"/></form>',
'product,false,list': '<tree><field name="name"/></tree>',
},
mockRPC: function (route, args) {
if (args.method === 'get_formview_id') {
return Promise.resolve(false);
}
return this._super(route, args);
},
});
// set max-height to have scroll forcefully so that we can test scroll position after modal close
$('.o_content').css({'overflow': 'auto', 'max-height': '300px'});
// Open many2one modal, lastActivatedFieldIndex will not set as we directly click on external button
await testUtils.form.clickEdit(form);
assert.strictEqual($(".o_content").scrollTop(), 0, "scroll position should be 0");
form.$(".o_field_many2one[name='trululu'] .o_input").focus();
assert.notStrictEqual($(".o_content").scrollTop(), 0, "scroll position should not be 0");
await testUtils.dom.click(form.$('.o_external_button'));
// Close modal
await testUtils.dom.click($('.modal').last().find('button[class="btn-close"]'));
assert.notStrictEqual($(".o_content").scrollTop(), 0,
"scroll position should not be 0 after closing modal");
assert.containsNone(document.body, '.modal', 'There should be no modal');
assert.doesNotHaveClass($('body'), 'modal-open', 'Modal is not said opened');
assert.strictEqual(form.renderer.lastActivatedFieldIndex, -1,
"lastActivatedFieldIndex is -1");
assert.equal(document.activeElement, $('body')[0],
'body is focused, should not set focus on form widget');
assert.notStrictEqual(document.activeElement, form.$('.o_field_many2one[name="trululu"] .o_input'),
'field widget should not be focused when lastActivatedFieldIndex is -1');
form.destroy();
});
QUnit.test('in create mode, autofocus fields are focused', 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"/>' +
'<field name="foo" default_focus="1"/>' +
'</form>',
});
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"foo field should have focus");
form.destroy();
});
QUnit.test('create with false values', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="bar"/></group>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'create') {
assert.strictEqual(args.args[0].bar, false,
"the false value should be given as parameter");
}
return this._super(route, args);
},
});
await testUtils.form.clickSave(form);
form.destroy();
});
QUnit.test('autofocus first visible field', 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" invisible="1"/>' +
'<field name="foo"/>' +
'</form>',
});
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"foo field should have focus");
form.destroy();
});
QUnit.test('no autofocus with disable_autofocus option [REQUIRE FOCUS]', 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"/>' +
'<field name="foo"/>' +
'</form>',
viewOptions: {
disable_autofocus: true,
},
});
assert.notStrictEqual(document.activeElement, form.$('input[name="int_field"]')[0],
"int_field field should not have focus");
await form.update({});
assert.notStrictEqual(document.activeElement, form.$('input[name="int_field"]')[0],
"int_field field should not have focus");
form.destroy();
});
QUnit.test('open one2many form containing many2many_tags', async function (assert) {
assert.expect(4);
this.data.partner.records[0].product_ids = [37];
this.data.product.fields.partner_type_ids = {
string: "many2many partner_type", type: "many2many", relation: "partner_type",
};
this.data.product.records[0].partner_type_ids = [12, 14];
var form = await createView({
View: FormView,
model: 'partner',
res_id: 1,
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="product_ids">' +
'<tree create="0">' +
'<field name="display_name"/>' +
'<field name="partner_type_ids" widget="many2many_tags"/>' +
'</tree>' +
'<form string="Products">' +
'<sheet>' +
'<group>' +
'<label for="partner_type_ids"/>' +
'<div>' +
'<field name="partner_type_ids" widget="many2many_tags"/>' +
'</div>' +
'</group>' +
'</sheet>' +
'</form>' +
'</field>' +
'</group>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
});
var row = form.$('.o_field_one2many .o_legacy_list_view .o_data_row');
await testUtils.dom.click(row);
assert.verifySteps(['read', 'read', 'read'],
"there should be 3 read rpcs");
form.destroy();
});
QUnit.test('onchanges are applied before checking if it can be saved', async function (assert) {
assert.expect(4);
this.data.partner.onchanges.foo = function (obj) {};
this.data.partner.fields.foo.required = true;
var def = testUtils.makeTestPromise();
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: 2,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
assert.step(args.method);
if (args.method === 'onchange') {
return def.then(function () {
return result;
});
}
return result;
},
services: {
notification: {
notify: function (params) {
assert.step(params.type);
}
},
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name="foo"]'), '');
await testUtils.form.clickSave(form);
def.resolve();
await testUtils.nextTick();
assert.verifySteps(['read', 'onchange', 'danger']);
form.destroy();
});
QUnit.test('display toolbar', async function (assert) {
assert.expect(8);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
res_id: 1,
arch: '<form string="Partners">' +
'<group><field name="bar"/></group>' +
'</form>',
toolbar: {
action: [{
model_name: 'partner',
name: 'Action partner',
type: 'ir.actions.server',
usage: 'ir_actions_server',
}],
print: [],
},
viewOptions: {
hasActionMenus: true,
},
mockRPC: function (route, args) {
if (route === '/web/action/load') {
assert.strictEqual(args.context.active_id, 1,
"the active_id shoud be 1.");
assert.deepEqual(args.context.active_ids, [1],
"the active_ids should be an array with 1 inside.");
return Promise.resolve({});
}
return this._super.apply(this, arguments);
},
});
assert.containsNone(form, '.o_cp_action_menus .dropdown:contains(Print)');
assert.containsOnce(form, '.o_cp_action_menus .dropdown:contains(Action)');
await testUtils.controlPanel.toggleActionMenu(form);
assert.containsN(form, '.o_cp_action_menus .dropdown-item', 3, "there should be 3 actions");
assert.strictEqual(form.$('.o_cp_action_menus .dropdown-item:last').text().trim(), 'Action partner',
"the custom action should have 'Action partner' as name");
await testUtils.mock.intercept(form, 'do_action', function (event) {
var context = event.data.action.context.__contexts[1];
assert.strictEqual(context.active_id, 1,
"the active_id shoud be 1.");
assert.deepEqual(context.active_ids, [1],
"the active_ids should be an array with 1 inside.");
});
await testUtils.controlPanel.toggleMenuItem(form, "Action partner");
form.destroy();
});
QUnit.test('check interactions between multiple FormViewDialogs', async function (assert) {
assert.expect(8);
this.data.product.fields.product_ids = {
string: "one2many product", type: "one2many", relation: "product",
};
this.data.partner.records[0].product_id = 37;
var form = await createView({
View: FormView,
model: 'partner',
res_id: 1,
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="product_id"/>' +
'</group>' +
'</sheet>' +
'</form>',
archs: {
'product,false,form':
'<form string="Products">' +
'<sheet>' +
'<group>' +
'<field name="display_name"/>' +
'<field name="product_ids"/>' +
'</group>' +
'</sheet>' +
'</form>',
'product,false,list': '<tree><field name="display_name"/></tree>'
},
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/product/get_formview_id') {
return Promise.resolve(false);
} else if (args.method === 'write') {
assert.strictEqual(args.model, 'product',
"should write on product model");
assert.strictEqual(args.args[1].product_ids[0][2].display_name, 'xtv',
"display_name of the new object should be xtv");
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
// Open first dialog
await testUtils.dom.click(form.$('.o_external_button'));
assert.strictEqual($('.modal').length, 1,
"One FormViewDialog should be opened");
var $firstModal = $('.modal');
assert.strictEqual($('.modal .modal-title').first().text().trim(), 'Open: Product',
"dialog title should display the python field string as label");
assert.strictEqual($firstModal.find('input').val(), 'xphone',
"display_name should be correctly displayed");
// Open second dialog
await testUtils.dom.click($firstModal.find('.o_field_x2many_list_row_add a'));
assert.strictEqual($('.modal').length, 2,
"two FormViewDialogs should be opened");
var $secondModal = $('.modal:nth(1)');
// Add new value
await testUtils.fields.editInput($secondModal.find('input'), 'xtv');
await testUtils.dom.click($secondModal.find('.modal-footer button:first'));
assert.strictEqual($('.modal').length, 1,
"last opened dialog should be closed");
// Check that data in first dialog is correctly updated
assert.strictEqual($firstModal.find('tr.o_data_row td').text(), 'xtv',
"should have added a line with xtv as new record");
await testUtils.dom.click($firstModal.find('.modal-footer button:first'));
form.destroy();
});
QUnit.test('fields and record contexts are not mixed', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<field name="trululu" context="{\'test\': 1}"/>' +
'</group>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'name_search') {
assert.strictEqual(args.kwargs.context.test, 1,
"field's context should be sent");
assert.notOk('mainContext' in args.kwargs.context,
"record's context should not be sent");
}
return this._super.apply(this, arguments);
},
res_id: 2,
viewOptions: {
mode: 'edit',
context: {mainContext: 3},
},
});
await testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
form.destroy();
});
QUnit.test('do not activate an hidden tab when switching between records', async function (assert) {
assert.expect(6);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<notebook>' +
'<page string="Foo" attrs=\'{"invisible": [["id", "=", 2]]}\'>' +
'<field name="foo"/>' +
'</page>' +
'<page string="Bar">' +
'<field name="bar"/>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
viewOptions: {
ids: [1, 2],
index: 0,
},
res_id: 1,
});
assert.strictEqual(form.$('.o_notebook .nav-item:not(.o_invisible_modifier)').length, 2,
"both tabs should be visible");
assert.hasClass(form.$('.o_notebook .nav-link:first'),'active',
"first tab should be active");
// click on the pager to switch to the next record
await testUtils.controlPanel.pagerNext(form);
assert.strictEqual(form.$('.o_notebook .nav-item:not(.o_invisible_modifier)').length, 1,
"only the second tab should be visible");
assert.hasClass(form.$('.o_notebook .nav-item:not(.o_invisible_modifier) .nav-link'),'active',
"the visible tab should be active");
// click on the pager to switch back to the previous record
await testUtils.controlPanel.pagerPrevious(form);
assert.strictEqual(form.$('.o_notebook .nav-item:not(.o_invisible_modifier)').length, 2,
"both tabs should be visible again");
assert.hasClass(form.$('.o_notebook .nav-link:nth(1)'),'active',
"second tab should be active");
form.destroy();
});
QUnit.test('support anchor tags with action type', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<a type="action" name="42"><i class="fa fa-arrow-right"/> Click me !</a>' +
'</form>',
res_id: 1,
intercepts: {
do_action: function (event) {
assert.strictEqual(event.data.action, "42",
"should trigger do_action with correct action parameter");
}
}
});
await testUtils.dom.click(form.$('a[type="action"]'));
form.destroy();
});
QUnit.test('do not perform extra RPC to read invisible many2one fields', async function (assert) {
// This test isn't really meaningful anymore, since default_get and (first) onchange rpcs
// have been merged in a single onchange rpc, returning nameget for many2one fields. But it
// isn't really costly, and it still checks rpcs done when creating a new record with a m2o.
assert.expect(2);
this.data.partner.fields.trululu.default = 2;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="trululu" invisible="1"/>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
});
assert.verifySteps(['onchange'], "only one RPC should have been done");
form.destroy();
});
QUnit.test('do not perform extra RPC to read invisible x2many fields', async function (assert) {
assert.expect(2);
this.data.partner.records[0].p = [2]; // one2many
this.data.partner.records[0].product_ids = [37]; // one2many
this.data.partner.records[0].timmy = [12]; // many2many
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="p" invisible="1"/>' + // no inline view
'<field name="product_ids" invisible="1">' + // inline view
'<tree><field name="display_name"/></tree>' +
'</field>' +
'<field name="timmy" invisible="1" widget="many2many_tags"/>' + // no view
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
res_id: 1,
});
assert.verifySteps(['read'], "only one read should have been done");
form.destroy();
});
QUnit.test('default_order on x2many embedded view', async function (assert) {
assert.expect(11);
this.data.partner.fields.display_name.sortable = true;
this.data.partner.records[0].p = [1, 4];
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="p">' +
'<tree default_order="foo desc">' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
archs: {
'partner,false,form':
'<form string="Partner">' +
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
},
res_id: 1,
});
assert.ok(form.$('.o_field_one2many tbody tr:first td:contains(yop)').length,
"record 1 should be first");
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
assert.strictEqual($('.modal').length, 1,
"FormViewDialog should be opened");
await testUtils.fields.editInput($('.modal input[name="foo"]'), 'xop');
await testUtils.dom.click($('.modal-footer button:eq(1)'));
await testUtils.fields.editInput($('.modal input[name="foo"]'), 'zop');
await testUtils.dom.click($('.modal-footer button:first'));
// client-side sort
assert.ok(form.$('.o_field_one2many tbody tr:eq(0) td:contains(zop)').length,
"record zop should be first");
assert.ok(form.$('.o_field_one2many tbody tr:eq(1) td:contains(yop)').length,
"record yop should be second");
assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length,
"record xop should be third");
// server-side sort
await testUtils.form.clickSave(form);
assert.ok(form.$('.o_field_one2many tbody tr:eq(0) td:contains(zop)').length,
"record zop should be first");
assert.ok(form.$('.o_field_one2many tbody tr:eq(1) td:contains(yop)').length,
"record yop should be second");
assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length,
"record xop should be third");
// client-side sort on edit
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_one2many tbody tr:eq(1) td:contains(yop)'));
await testUtils.fields.editInput($('.modal input[name="foo"]'), 'zzz');
await testUtils.dom.click($('.modal-footer button:first'));
assert.ok(form.$('.o_field_one2many tbody tr:eq(0) td:contains(zzz)').length,
"record zzz should be first");
assert.ok(form.$('.o_field_one2many tbody tr:eq(1) td:contains(zop)').length,
"record zop should be second");
assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length,
"record xop should be third");
form.destroy();
});
QUnit.test('action context is used when evaluating domains', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="trululu" domain="[(\'id\', \'in\', context.get(\'product_ids\', []))]"/>' +
'</sheet>' +
'</form>',
res_id: 1,
viewOptions: {
context: {product_ids: [45,46,47]}
},
mockRPC: function (route, args) {
if (args.method === 'name_search') {
assert.deepEqual(args.kwargs.args[0], ['id', 'in', [45,46,47]],
"domain should be properly evaluated");
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('div[name="trululu"] input'));
form.destroy();
});
QUnit.test('form rendering with groups with col/colspan', async function (assert) {
assert.expect(45);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form>' +
'<sheet>' +
'<group col="6" class="parent_group">' +
'<group col="4" colspan="3" class="group_4">' +
'<div colspan="3"/>' +
'<div colspan="2"/><div/>' +
'<div colspan="4"/>' +
'</group>' +
'<group col="3" colspan="4" class="group_3">' +
'<group col="1" class="group_1">' +
'<div/><div/><div/>' +
'</group>' +
'<div/>' +
'<group col="3" class="field_group">' +
'<field name="foo" colspan="3"/>' +
'<div/><field name="bar" nolabel="1"/>' +
'<field name="qux"/>' +
'<field name="int_field" colspan="3" nolabel="1"/>' +
'<span/><field name="product_id"/>' +
'</group>' +
'</group>' +
'</group>' +
'<group>' +
'<field name="p">' +
'<tree>' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
var $parentGroup = form.$('.parent_group');
var $group4 = form.$('.group_4');
var $group3 = form.$('.group_3');
var $group1 = form.$('.group_1');
var $fieldGroup = form.$('.field_group');
// Verify outergroup/innergroup
assert.strictEqual($parentGroup[0].tagName, 'DIV', ".parent_group should be an outergroup");
assert.strictEqual($group4[0].tagName, 'TABLE', ".group_4 should be an innergroup");
assert.strictEqual($group3[0].tagName, 'DIV', ".group_3 should be an outergroup");
assert.strictEqual($group1[0].tagName, 'TABLE', ".group_1 should be an innergroup");
assert.strictEqual($fieldGroup[0].tagName, 'TABLE', ".field_group should be an innergroup");
// Verify .parent_group content
var $parentGroupChildren = $parentGroup.children();
assert.strictEqual($parentGroupChildren.length, 2, "there should be 2 groups in .parent_group");
assert.ok($parentGroupChildren.eq(0).is('.o_group_col_6'), "first .parent_group group should be 1/2 parent width");
assert.ok($parentGroupChildren.eq(1).is('.o_group_col_8'), "second .parent_group group should be 2/3 parent width");
// Verify .group_4 content
var $group4rows = $group4.find('> tbody > tr');
assert.strictEqual($group4rows.length, 3, "there should be 3 rows in .group_4");
var $group4firstRowTd = $group4rows.eq(0).children('td');
assert.strictEqual($group4firstRowTd.length, 1, "there should be 1 td in first row");
assert.hasAttrValue($group4firstRowTd, 'colspan', "3", "the first td colspan should be 3");
assert.strictEqual($group4firstRowTd.attr('style').substr(0, 9), "width: 75", "the first td should be 75% width");
assert.strictEqual($group4firstRowTd.children()[0].tagName, "DIV", "the first td should contain a div");
var $group4secondRowTds = $group4rows.eq(1).children('td');
assert.strictEqual($group4secondRowTds.length, 2, "there should be 2 tds in second row");
assert.hasAttrValue($group4secondRowTds.eq(0), 'colspan', "2", "the first td colspan should be 2");
assert.strictEqual($group4secondRowTds.eq(0).attr('style').substr(0, 9), "width: 50", "the first td be 50% width");
assert.hasAttrValue($group4secondRowTds.eq(1), 'colspan', undefined, "the second td colspan should be default one (1)");
assert.strictEqual($group4secondRowTds.eq(1).attr('style').substr(0, 9), "width: 25", "the second td be 75% width");
var $group4thirdRowTd = $group4rows.eq(2).children('td');
assert.strictEqual($group4thirdRowTd.length, 1, "there should be 1 td in third row");
assert.hasAttrValue($group4thirdRowTd, 'colspan', "4", "the first td colspan should be 4");
assert.strictEqual($group4thirdRowTd.attr('style').substr(0, 10), "width: 100", "the first td should be 100% width");
// Verify .group_3 content
assert.strictEqual($group3.children().length, 3, ".group_3 should have 3 children");
assert.strictEqual($group3.children('.o_group_col_4').length, 3, ".group_3 should have 3 children of 1/3 width");
// Verify .group_1 content
assert.strictEqual($group1.find('> tbody > tr').length, 3, "there should be 3 rows in .group_1");
// Verify .field_group content
var $fieldGroupRows = $fieldGroup.find('> tbody > tr');
assert.strictEqual($fieldGroupRows.length, 5, "there should be 5 rows in .field_group");
var $fieldGroupFirstRowTds = $fieldGroupRows.eq(0).children('td');
assert.strictEqual($fieldGroupFirstRowTds.length, 2, "there should be 2 tds in first row");
assert.hasClass($fieldGroupFirstRowTds.eq(0),'o_td_label', "first td should be a label td");
assert.hasAttrValue($fieldGroupFirstRowTds.eq(1), 'colspan', "2", "second td colspan should be given colspan (3) - 1 (label)");
assert.strictEqual($fieldGroupFirstRowTds.eq(1).attr('style').substr(0, 10), "width: 100", "second td width should be 100%");
var $fieldGroupSecondRowTds = $fieldGroupRows.eq(1).children('td');
assert.strictEqual($fieldGroupSecondRowTds.length, 2, "there should be 2 tds in second row");
assert.hasAttrValue($fieldGroupSecondRowTds.eq(0), 'colspan', undefined, "first td colspan should be default one (1)");
assert.strictEqual($fieldGroupSecondRowTds.eq(0).attr('style').substr(0, 9), "width: 33", "first td width should be 33.3333%");
assert.hasAttrValue($fieldGroupSecondRowTds.eq(1), 'colspan', undefined, "second td colspan should be default one (1)");
assert.strictEqual($fieldGroupSecondRowTds.eq(1).attr('style').substr(0, 9), "width: 33", "second td width should be 33.3333%");
var $fieldGroupThirdRowTds = $fieldGroupRows.eq(2).children('td'); // new row as label/field pair colspan is greater than remaining space
assert.strictEqual($fieldGroupThirdRowTds.length, 2, "there should be 2 tds in third row");
assert.hasClass($fieldGroupThirdRowTds.eq(0),'o_td_label', "first td should be a label td");
assert.hasAttrValue($fieldGroupThirdRowTds.eq(1), 'colspan', undefined, "second td colspan should be default one (1)");
assert.strictEqual($fieldGroupThirdRowTds.eq(1).attr('style').substr(0, 9), "width: 50", "second td should be 50% width");
var $fieldGroupFourthRowTds = $fieldGroupRows.eq(3).children('td');
assert.strictEqual($fieldGroupFourthRowTds.length, 1, "there should be 1 td in fourth row");
assert.hasAttrValue($fieldGroupFourthRowTds, 'colspan', "3", "the td should have a colspan equal to 3");
assert.strictEqual($fieldGroupFourthRowTds.attr('style').substr(0, 10), "width: 100", "the td should have 100% width");
var $fieldGroupFifthRowTds = $fieldGroupRows.eq(4).children('td'); // label/field pair can be put after the 1-colspan span
assert.strictEqual($fieldGroupFifthRowTds.length, 3, "there should be 3 tds in fourth row");
assert.strictEqual($fieldGroupFifthRowTds.eq(0).attr('style').substr(0, 9), "width: 50", "the first td should 50% width");
assert.hasClass($fieldGroupFifthRowTds.eq(1),'o_td_label', "the second td should be a label td");
assert.strictEqual($fieldGroupFifthRowTds.eq(2).attr('style').substr(0, 9), "width: 50", "the third td should 50% width");
form.destroy();
});
QUnit.test('outer and inner groups string attribute', async function (assert) {
assert.expect(5);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group string="parent group" class="parent_group">' +
'<group string="child group 1" class="group_1">' +
'<field name="bar"/>' +
'</group>' +
'<group string="child group 2" class="group_2">' +
'<field name="bar"/>' +
'</group>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
var $parentGroup = form.$('.parent_group');
var $group1 = form.$('.group_1');
var $group2 = form.$('.group_2');
assert.containsN(form, 'table.o_inner_group', 2,
"should contain two inner groups");
assert.strictEqual($group1.find('.o_horizontal_separator').length, 1,
"inner group should contain one string separator");
assert.strictEqual($group1.find('.o_horizontal_separator:contains(child group 1)').length, 1,
"first inner group should contain 'child group 1' string");
assert.strictEqual($group2.find('.o_horizontal_separator:contains(child group 2)').length, 1,
"second inner group should contain 'child group 2' string");
assert.strictEqual($parentGroup.find('> div.o_horizontal_separator:contains(parent group)').length, 1,
"outer group should contain 'parent group' string");
form.destroy();
});
QUnit.test('form group with newline tag inside', async function (assert) {
assert.expect(6);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form>' +
'<sheet>' +
'<group col="5" class="main_inner_group">' +
// col=5 otherwise the test is ok even without the
// newline code as this will render a <newline/> DOM
// element in the third column, leaving no place for
// the next field and its label on the same line.
'<field name="foo"/>' +
'<newline/>' +
'<field name="bar"/>' +
'<field name="qux"/>' +
'</group>' +
'<group col="3">' +
// col=3 otherwise the test is ok even without the
// newline code as this will render a <newline/> DOM
// element with the o_group_col_6 class, leaving no
// place for the next group on the same line.
'<group class="top_group">' +
'<div style="height: 200px;"/>' +
'</group>' +
'<newline/>' +
'<group class="bottom_group">' +
'<div/>' +
'</group>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
// Inner group
assert.containsN(form, '.main_inner_group > tbody > tr', 2,
"there should be 2 rows in the group");
assert.containsOnce(form, '.main_inner_group > tbody > tr:first > .o_td_label',
"there should be only one label in the first row");
assert.containsOnce(form, '.main_inner_group > tbody > tr:first .o_field_widget',
"there should be only one widget in the first row");
assert.containsN(form, '.main_inner_group > tbody > tr:last > .o_td_label', 2,
"there should be two labels in the second row");
assert.containsN(form, '.main_inner_group > tbody > tr:last .o_field_widget', 2,
"there should be two widgets in the second row");
// Outer group
assert.ok((form.$('.bottom_group').position().top - form.$('.top_group').position().top) >= 200,
"outergroup children should not be on the same line");
form.destroy();
});
QUnit.test('custom open record dialog title', async function (assert) {
assert.expect(1);
this.data.partner.records[0].p = [2];
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="p" widget="many2many" string="custom label">' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'<form>' +
'<field name="display_name"/>' +
'</form>' +
'</field>' +
'</form>',
session: {},
res_id: 1,
});
await testUtils.dom.click(form.$('.o_data_row:first'));
assert.strictEqual($('.modal .modal-title').first().text().trim(), 'Open: custom label',
"modal should use the python field string as title");
form.destroy();
});
QUnit.test('display translation alert', async function (assert) {
assert.expect(2);
this.data.partner.fields.foo.translate = true;
this.data.partner.fields.display_name.translate = true;
var multi_lang = _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"/>' +
'<field name="display_name"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name="foo"]'), "test");
await testUtils.form.clickSave(form);
assert.containsOnce(form, '.o_legacy_form_view .alert > div .oe_field_translate',
"should have single translation alert");
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name="display_name"]'), "test2");
await testUtils.form.clickSave(form);
assert.containsN(form, '.o_legacy_form_view .alert > div .oe_field_translate', 2,
"should have two translate fields in translation alert");
form.destroy();
_t.database.multi_lang = multi_lang;
});
QUnit.test('translation alerts are preserved on pager change', async function (assert) {
assert.expect(5);
this.data.partner.fields.foo.translate = true;
var multi_lang = _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>' +
'<field name="foo"/>' +
'</sheet>' +
'</form>',
viewOptions: {
ids: [1, 2],
index: 0,
},
res_id: 1,
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name="foo"]'), "test");
await testUtils.form.clickSave(form);
assert.containsOnce(form, '.o_legacy_form_view .alert > div', "should have a translation alert");
// click on the pager to switch to the next record
await testUtils.controlPanel.pagerNext(form);
assert.containsNone(form, '.o_legacy_form_view .alert > div', "should not have a translation alert");
// click on the pager to switch back to the previous record
await testUtils.controlPanel.pagerPrevious(form);
assert.containsOnce(form, '.o_legacy_form_view .alert > div', "should have a translation alert");
// remove translation alert by click X and check alert even after form reload
await testUtils.dom.click(form.$('.o_legacy_form_view .alert > .btn-close'));
assert.containsNone(form, '.o_legacy_form_view .alert > div', "should not have a translation alert");
await form.reload();
assert.containsNone(form, '.o_legacy_form_view .alert > div', "should not have a translation alert after reload");
form.destroy();
_t.database.multi_lang = multi_lang;
});
QUnit.test('translation alerts preserved on reverse breadcrumb', async function (assert) {
assert.expect(1);
serverData.models.partner.fields.foo.translate = true;
serverData.views = {
'partner,false,form': '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'</form>',
'partner,false,search': '<search></search>',
};
serverData.actions = {
1: {
id: 1,
name: 'Partner',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'form']],
},
};
const webClient = await createWebClient({ serverData });
patchWithCleanup(_t.database, {
multi_lang: true,
});
await doAction(webClient, 1);
$(target).find('input[name="foo"]').val("test").trigger("input");
await testUtils.dom.click(target.querySelector('.o_form_button_save'));
await legacyExtraNextTick();
assert.containsOnce(target, '.o_legacy_form_view .alert > div',
"should have a translation alert");
});
QUnit.test('translate event correctly handled with multiple controllers', async function (assert) {
assert.expect(3);
this.data.product.fields.name.translate = true;
this.data.partner.records[0].product_id = 37;
var nbTranslateCalls = 0;
var multi_lang = _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="product_id"/>' +
'</group>' +
'</sheet>' +
'</form>',
archs: {
'product,false,form': '<form>' +
'<sheet>' +
'<group>' +
'<field name="name"/>' +
'<field name="partner_type_id"/>' +
'</group>' +
'</sheet>' +
'</form>',
},
res_id: 1,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/product/get_formview_id') {
return Promise.resolve(false);
}
if (route === "/web/dataset/call_kw/product/get_field_translations") {
assert.deepEqual(args.args, [[37],"name"], "should translate the name field of the record");
nbTranslateCalls++;
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)"]]);
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('[name="product_id"] .o_external_button'));
assert.containsOnce($('.modal-body'), 'span.o_field_translate',
"there should be a translate button in the modal");
await testUtils.dom.click($('.modal-body span.o_field_translate'));
assert.strictEqual(nbTranslateCalls, 1, "should call_button translate once");
form.destroy();
_t.database.multi_lang = multi_lang;
});
QUnit.test('check the translate alert in the wizard', async function (assert) {
assert.expect(1);
// Check whether it is alert before the dialog closes
testUtils.mock.patch(ViewDialogs.FormViewDialog, {
close() {
assert.containsNone(this.$el, '.o_notification_box');
this._super(...arguments);
},
});
this.data.product.fields.name.translate = true;
const multi_lang = _t.database.multi_lang;
_t.database.multi_lang = true;
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `<form><field name="product_id"/></form>`,
archs: {
'product,false,form': `<form><field name="name"/></form>`,
},
res_id: 1,
});
await testUtils.form.clickEdit(form);
await testUtils.fields.many2one.createAndEdit('product_id', "Ralts");
await testUtils.dom.click($('.modal-footer button.btn-primary'));
form.destroy();
testUtils.mock.unpatch(ViewDialogs.FormViewDialog);
_t.database.multi_lang = multi_lang;
});
QUnit.test('buttons are disabled until status bar action is resolved', async function (assert) {
assert.expect(9);
var def = testUtils.makeTestPromise();
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<header>' +
'<button name="post" class="p" string="Confirm" type="object"/>' +
'<button name="some_method" class="s" string="Do it" type="object"/>' +
'</header>' +
'<sheet>' +
'<div name="button_box" class="oe_button_box">' +
'<button class="oe_stat_button" name="some_action" type="action">' +
'<field name="bar"/>' +
'</button>' +
'</div>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
intercepts: {
execute_action: function (event) {
return def.then(function() {
event.data.on_success();
});
}
},
});
assert.strictEqual(form.$buttons.find('button:not(:disabled)').length, 4,
"control panel buttons should be enabled");
assert.strictEqual(form.$('.o_form_statusbar button:not(:disabled)').length, 2,
"status bar buttons should be enabled");
assert.strictEqual(form.$('.oe_button_box button:not(:disabled)').length, 1,
"stat buttons should be enabled");
await testUtils.dom.clickFirst(form.$('.o_form_statusbar button'));
// The unresolved promise lets us check the state of the buttons
assert.strictEqual(form.$buttons.find('button:disabled').length, 4,
"control panel buttons should be disabled");
assert.containsN(form, '.o_form_statusbar button:disabled', 2,
"status bar buttons should be disabled");
assert.containsOnce(form, '.oe_button_box button:disabled',
"stat buttons should be disabled");
def.resolve();
await testUtils.nextTick();
assert.strictEqual(form.$buttons.find('button:not(:disabled)').length, 4,
"control panel buttons should be enabled");
assert.strictEqual(form.$('.o_form_statusbar button:not(:disabled)').length, 2,
"status bar buttons should be enabled");
assert.strictEqual(form.$('.oe_button_box button:not(:disabled)').length, 1,
"stat buttons should be enabled");
form.destroy();
});
QUnit.test('buttons are disabled until button box action is resolved', async function (assert) {
assert.expect(9);
var def = testUtils.makeTestPromise();
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<header>' +
'<button name="post" class="p" string="Confirm" type="object"/>' +
'<button name="some_method" class="s" string="Do it" type="object"/>' +
'</header>' +
'<sheet>' +
'<div name="button_box" class="oe_button_box">' +
'<button class="oe_stat_button" name="some_action" type="action">' +
'<field name="bar"/>' +
'</button>' +
'</div>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
intercepts: {
execute_action: function (event) {
return def.then(function() {
event.data.on_success();
});
}
},
});
assert.strictEqual(form.$buttons.find('button:not(:disabled)').length, 4,
"control panel buttons should be enabled");
assert.strictEqual(form.$('.o_form_statusbar button:not(:disabled)').length, 2,
"status bar buttons should be enabled");
assert.strictEqual(form.$('.oe_button_box button:not(:disabled)').length, 1,
"stat buttons should be enabled");
await testUtils.dom.click(form.$('.oe_button_box button'));
// The unresolved promise lets us check the state of the buttons
assert.strictEqual(form.$buttons.find('button:disabled').length, 4,
"control panel buttons should be disabled");
assert.containsN(form, '.o_form_statusbar button:disabled', 2,
"status bar buttons should be disabled");
assert.containsOnce(form, '.oe_button_box button:disabled',
"stat buttons should be disabled");
def.resolve();
await testUtils.nextTick();
assert.strictEqual(form.$buttons.find('button:not(:disabled)').length, 4,
"control panel buttons should be enabled");
assert.strictEqual(form.$('.o_form_statusbar button:not(:disabled)').length, 2,
"status bar buttons should be enabled");
assert.strictEqual(form.$('.oe_button_box button:not(:disabled)').length, 1,
"stat buttons should be enabled");
form.destroy();
});
QUnit.test('buttons with "confirm" attribute save before calling the method', async function (assert) {
assert.expect(9);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<header>' +
'<button name="post" class="p" string="Confirm" type="object" ' +
'confirm="Very dangerous. U sure?"/>' +
'</header>' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
intercepts: {
execute_action: function (event) {
assert.step('execute_action');
event.data.on_success();
},
},
});
// click on button, and cancel in confirm dialog
await testUtils.dom.click(form.$('.o_statusbar_buttons button'));
assert.ok(form.$('.o_statusbar_buttons button').prop('disabled'),
'button should be disabled');
await testUtils.dom.click($('.modal-footer button.btn-secondary'));
assert.ok(!form.$('.o_statusbar_buttons button').prop('disabled'),
'button should no longer be disabled');
assert.verifySteps(['onchange']);
// click on button, and click on ok in confirm dialog
await testUtils.dom.click(form.$('.o_statusbar_buttons button'));
assert.verifySteps([]);
await testUtils.dom.click($('.modal-footer button.btn-primary'));
assert.verifySteps(['create', 'read', 'execute_action']);
form.destroy();
});
QUnit.test('buttons with "confirm" attribute: click twice on "Ok"', async function (assert) {
assert.expect(7);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<header>
<button name="post" class="p" string="Confirm" type="object" confirm="U sure?"/>
</header>
</form>`,
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
intercepts: {
execute_action: function (event) {
assert.step('execute_action'); // should be called only once
event.data.on_success();
},
},
});
assert.verifySteps(["onchange"]);
await testUtils.dom.click(form.$('.o_statusbar_buttons button'));
assert.verifySteps([]);
testUtils.dom.click($('.modal-footer button.btn-primary'));
await Promise.resolve();
await testUtils.dom.click($('.modal-footer button.btn-primary'));
assert.verifySteps(['create', 'read', 'execute_action']);
form.destroy();
});
QUnit.test('buttons are disabled until action is resolved (in dialogs)', async function (assert) {
assert.expect(3);
var def = testUtils.makeTestPromise();
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="trululu"/>' +
'</sheet>' +
'</form>',
archs: {
'partner,false,form': '<form>' +
'<sheet>' +
'<div name="button_box" class="oe_button_box">' +
'<button class="oe_stat_button" name="some_action" type="action">' +
'<field name="bar"/>' +
'</button>' +
'</div>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
},
res_id: 1,
intercepts: {
execute_action: function (event) {
return def.then(function() {
event.data.on_success();
});
}
},
mockRPC: function (route, args) {
if (args.method === 'get_formview_id') {
return Promise.resolve(false);
}
return this._super.apply(this, arguments);
},
viewOptions: {
mode: 'edit',
},
});
await testUtils.dom.click(form.$('.o_external_button'));
assert.notOk($('.modal .oe_button_box button').attr('disabled'),
"stat buttons should be enabled");
await testUtils.dom.click($('.modal .oe_button_box button'));
assert.ok($('.modal .oe_button_box button').attr('disabled'),
"stat buttons should be disabled");
def.resolve();
await testUtils.nextTick();
assert.notOk($('.modal .oe_button_box button').attr('disabled'),
"stat buttons should be enabled");
form.destroy();
});
QUnit.test('multiple clicks on save should reload only once', async function (assert) {
assert.expect(5);
var def = testUtils.makeTestPromise();
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,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
assert.step(args.method);
if (args.method === "write") {
return def.then(function () {
return result;
});
} else {
return result;
}
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name="foo"]'), "test");
await testUtils.form.clickSave(form);
assert.ok(form.$buttons.find('.o_form_button_save').get(0).disabled);
def.resolve();
await testUtils.nextTick();
assert.verifySteps([
'read', // initial read to render the view
'write', // write on save
'read' // read on reload
]);
form.destroy();
});
QUnit.test('form view is not broken if save operation fails', 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"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'write' && args.args[1].foo === 'incorrect value') {
return Promise.reject();
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name="foo"]'), "incorrect value");
await testUtils.form.clickSave(form);
await testUtils.fields.editInput(form.$('input[name="foo"]'), "correct value");
await testUtils.form.clickSave(form);
assert.verifySteps([
'read', // initial read to render the view
'write', // write on save (it fails, does not trigger a read)
'write', // write on save (it works)
'read' // read on reload
]);
form.destroy();
});
QUnit.test('form view is not broken if save failed in readonly mode on field changed', async function (assert) {
assert.expect(10);
var failFlag = false;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<header><field name="trululu" widget="statusbar" clickable="true"/></header>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.step('write');
if (failFlag) {
return Promise.reject();
}
} else if (args.method === 'read') {
assert.step('read');
}
return this._super.apply(this, arguments);
},
});
var $selectedState = form.$('.o_statusbar_status button[data-value="4"]');
assert.ok($selectedState.hasClass('btn-primary') && $selectedState.hasClass('disabled'),
"selected status should be btn-primary and disabled");
failFlag = true;
var $clickableState = form.$('.o_statusbar_status button[data-value="1"]');
await testUtils.dom.click($clickableState);
var $lastActiveState = form.$('.o_statusbar_status button[data-value="4"]');
$selectedState = form.$('.o_statusbar_status button.btn-primary');
assert.strictEqual($selectedState[0], $lastActiveState[0],
"selected status is AAA record after save fail");
failFlag = false;
$clickableState = form.$('.o_statusbar_status button[data-value="1"]');
await testUtils.dom.click($clickableState);
var $lastClickedState = form.$('.o_statusbar_status button[data-value="1"]');
$selectedState = form.$('.o_statusbar_status button.btn-primary');
assert.strictEqual($selectedState[0], $lastClickedState[0],
"last clicked status should be active");
assert.verifySteps([
'read',
'write', // fails
'read', // must reload when saving fails
'write', // works
'read', // must reload when saving works
'read', // fixme: this read should not be necessary
]);
form.destroy();
});
QUnit.test('support password attribute', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo" password="True"/>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('span[name="foo"]').text(), '***',
"password should be displayed with stars");
await testUtils.form.clickEdit(form);
assert.strictEqual(form.$('input[name="foo"]').val(), 'yop',
"input value should be the password");
assert.strictEqual(form.$('input[name="foo"]').prop('type'), 'password',
"input should be of type password");
form.destroy();
});
QUnit.test('support autocomplete attribute', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="display_name" autocomplete="coucou"/>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
assert.hasAttrValue(form.$('input[name="display_name"]'), 'autocomplete', 'coucou',
"attribute autocomplete should be set");
form.destroy();
});
QUnit.test('input autocomplete attribute set to none by default', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="display_name"/>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
assert.hasAttrValue(form.$('input[name="display_name"]'), 'autocomplete', 'off',
"attribute autocomplete should be set to none by default");
form.destroy();
});
QUnit.test('context is correctly passed after save & new in FormViewDialog', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
res_id: 4,
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="product_ids"/>' +
'</group>' +
'</sheet>' +
'</form>',
archs: {
'product,false,form':
'<form string="Products">' +
'<sheet>' +
'<group>' +
'<field name="partner_type_id" ' +
'context="{\'color\': parent.id}"/>' +
'</group>' +
'</sheet>' +
'</form>',
'product,false,list': '<tree><field name="display_name"/></tree>'
},
mockRPC: function (route, args) {
if (args.method === 'name_search') {
assert.strictEqual(args.kwargs.context.color, 4,
"should use the correct context");
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.nextTick();
assert.strictEqual($('.modal').length, 1,
"One FormViewDialog should be opened");
// set a value on the m2o
await testUtils.fields.many2one.clickOpenDropdown('partner_type_id');
await testUtils.fields.many2one.clickHighlightedItem('partner_type_id');
await testUtils.dom.click($('.modal-footer button:eq(1)'));
await testUtils.nextTick();
await testUtils.dom.click($('.modal .o_field_many2one input'));
await testUtils.fields.many2one.clickHighlightedItem('partner_type_id');
await testUtils.dom.click($('.modal-footer button:first'));
await testUtils.nextTick();
form.destroy();
});
QUnit.test('render domain field widget without model', async function (assert) {
assert.expect(3);
this.data.partner.fields.model_name = { string: "Model name", type: "char" };
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="model_name"/>' +
'<field name="display_name" widget="domain" options="{\'model\': \'model_name\'}"/>' +
'</group>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'search_count') {
assert.strictEqual(args.model, 'test',
"should search_count on test");
if (!args.kwargs.domain) {
return Promise.reject({message:{
code: 200,
data: {},
message: "MockServer._getRecords: given domain has to be an array.",
}, event: $.Event()});
}
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.o_field_widget[name="display_name"]').text(), "Select a model to add a filter.",
"should contain an error message saying the model is missing");
await testUtils.fields.editInput(form.$('input[name="model_name"]'), "test");
assert.notStrictEqual(form.$('.o_field_widget[name="display_name"]').text(), "Select a model to add a filter.",
"should not contain an error message anymore");
form.destroy();
});
QUnit.test('readonly fields are not sent when saving', async function (assert) {
assert.expect(6);
// define an onchange on display_name to check that the value of readonly
// fields is correctly sent for onchanges
this.data.partner.onchanges = {
display_name: function () {},
p: function () {},
};
var checkOnchange = false;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="p">' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'<form string="Partners">' +
'<field name="display_name"/>' +
'<field name="foo" attrs="{\'readonly\': [[\'display_name\', \'=\', \'readonly\']]}"/>' +
'</form>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
if (checkOnchange && args.method === 'onchange') {
if (args.args[2] === 'display_name') { // onchange on field display_name
assert.strictEqual(args.args[1].foo, 'foo value',
"readonly fields value should be sent for onchanges");
} else { // onchange on field p
assert.deepEqual(args.args[1].p, [
[0, args.args[1].p[0][1], {display_name: 'readonly', foo: 'foo value'}]
], "readonly fields value should be sent for onchanges");
}
}
if (args.method === 'create') {
assert.deepEqual(args.args[0], {
p: [[0, args.args[0].p[0][1], {display_name: 'readonly'}]]
}, "should not have sent the value of the readonly field");
}
return this._super.apply(this, arguments);
},
});
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.nextTick();
assert.strictEqual($('.modal input.o_field_widget[name=foo]').length, 1,
'foo should be editable');
checkOnchange = true;
await testUtils.fields.editInput($('.modal .o_field_widget[name=foo]'), 'foo value');
await testUtils.fields.editInput($('.modal .o_field_widget[name=display_name]'), 'readonly');
assert.strictEqual($('.modal span.o_field_widget[name=foo]').length, 1,
'foo should be readonly');
await testUtils.dom.clickFirst($('.modal-footer .btn-primary'));
await testUtils.nextTick();
checkOnchange = false;
await testUtils.dom.click(form.$('.o_data_row'));
assert.strictEqual($('.modal .o_field_widget[name=foo]').text(), 'foo value',
"the edited value should have been kept");
await testUtils.dom.clickFirst($('.modal-footer .btn-primary'));
await testUtils.nextTick();
await testUtils.form.clickSave(form); // save the record
form.destroy();
});
QUnit.test('id is False in evalContext for new records', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="id"/>' +
'<field name="foo" attrs="{\'readonly\': [[\'id\', \'=\', False]]}"/>' +
'</form>',
});
assert.hasClass(form.$('.o_field_widget[name=foo]'),'o_readonly_modifier',
"foo should be readonly in 'Create' mode");
await testUtils.form.clickSave(form);
await testUtils.form.clickEdit(form);
assert.doesNotHaveClass(form.$('.o_field_widget[name=foo]'), 'o_readonly_modifier',
"foo should not be readonly anymore");
form.destroy();
});
QUnit.test('delete a duplicated record', async function (assert) {
assert.expect(5);
var newRecordID;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="display_name"/>' +
'</form>',
res_id: 1,
viewOptions: {hasActionMenus: true},
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'copy') {
return result.then(function (id) {
newRecordID = id;
return id;
});
}
if (args.method === 'unlink') {
assert.deepEqual(args.args[0], [newRecordID],
"should delete the newly created record");
}
return result;
},
});
// duplicate record 1
await testUtils.controlPanel.toggleActionMenu(form);
await testUtils.controlPanel.toggleMenuItem(form, "Duplicate");
assert.containsOnce(form, '.o_form_editable',
"form should be in edit mode");
assert.strictEqual(form.$('.o_field_widget').val(), 'first record (copy)',
"duplicated record should have correct name");
await testUtils.form.clickSave(form); // save duplicated record
// delete duplicated record
await testUtils.controlPanel.toggleActionMenu(form);
await testUtils.controlPanel.toggleMenuItem(form, "Delete");
assert.strictEqual($('.modal').length, 1, "should have opened a confirm dialog");
await testUtils.dom.click($('.modal-footer .btn-primary'));
assert.strictEqual(form.$('.o_field_widget').text(), 'first record',
"should have come back to previous record");
form.destroy();
});
QUnit.test('display tooltips for buttons', async function (assert) {
assert.expect(2);
var initialDebugMode = odoo.debug;
odoo.debug = true;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<header>' +
'<button name="some_method" class="oe_highlight" string="Button" type="object"/>' +
'</header>' +
'<button name="other_method" class="oe_highlight" string="Button2" type="object"/>' +
'</form>',
});
var $button = form.$('.o_form_statusbar button');
$button.tooltip('show', false);
$button.trigger($.Event('mouseenter'));
assert.strictEqual($('.tooltip .oe_tooltip_string').length, 1,
"should have rendered a tooltip");
$button.trigger($.Event('mouseleave'));
var $secondButton = form.$('button[name="other_method"]');
$secondButton.tooltip('show', false);
$secondButton.trigger($.Event('mouseenter'));
assert.strictEqual($('.tooltip .oe_tooltip_string').length, 1,
"should have rendered a tooltip");
$secondButton.trigger($.Event('mouseleave'));
$secondButton.tooltip('hide');
odoo.debug = initialDebugMode;
form.destroy();
});
QUnit.test('reload event is handled only once', async function (assert) {
// In this test, several form controllers are nested (two of them are
// opened in dialogs). When the users clicks on save in the last
// opened dialog, a 'reload' event is triggered up to reload the (direct)
// parent view. If this event isn't stopPropagated by the first controller
// catching it, it will crash when the other one will try to handle it,
// as this one doesn't know at all the dataPointID to reload.
assert.expect(11);
var arch = '<form>' +
'<field name="display_name"/>' +
'<field name="trululu"/>' +
'</form>';
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: arch,
archs: {
'partner,false,form': arch,
},
res_id: 2,
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'get_formview_id') {
return Promise.resolve(false);
}
return this._super.apply(this, arguments);
},
viewOptions: {
mode: 'edit',
},
});
await testUtils.dom.click(form.$('.o_external_button'));
await testUtils.dom.click($('.modal .o_external_button'));
await testUtils.fields.editInput($('.modal:nth(1) .o_field_widget[name=display_name]'), 'new name');
await testUtils.dom.click($('.modal:nth(1) footer .btn-primary').first());
assert.strictEqual($('.modal .o_field_widget[name=trululu] input').val(), 'new name',
"record should have been reloaded");
assert.verifySteps([
"read", // main record
"get_formview_id", // id of first form view opened in a dialog
"get_views", // arch of first form view opened in a dialog
"read", // first dialog
"get_formview_id", // id of second form view opened in a dialog
"get_views", // arch of second form view opened in a dialog
"read", // second dialog
"write", // save second dialog
"read", // reload first dialog
]);
form.destroy();
});
QUnit.test('process the context for inline subview', async function (assert) {
assert.expect(1);
this.data.partner.records[0].p = [2];
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'<field name="bar" invisible="context.get(\'hide_bar\', False)"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
viewOptions: {
context: {hide_bar: true},
},
});
assert.containsOnce(form, '.o_legacy_list_view thead tr th:not(.o_list_record_remove_header)',
"there should be only one column");
form.destroy();
});
QUnit.test('process the context for subview not inline', async function (assert) {
assert.expect(1);
this.data.partner.records[0].p = [2];
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="p"/>' +
'</form>',
archs: {
"partner,false,list": '<tree>' +
'<field name="foo"/>' +
'<field name="bar" invisible="context.get(\'hide_bar\', False)"/>' +
'</tree>',
},
res_id: 1,
viewOptions: {
context: {hide_bar: true},
},
});
assert.containsOnce(form, '.o_legacy_list_view thead tr th:not(.o_list_record_remove_header)',
"there should be only one column");
form.destroy();
});
QUnit.test('can toggle column in x2many in sub form view', async function (assert) {
assert.expect(2);
this.data.partner.records[2].p = [1,2];
this.data.partner.fields.foo.sortable = true;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="trululu"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/partner/get_formview_id') {
return Promise.resolve(false);
}
return this._super.apply(this, arguments);
},
archs: {
'partner,false,form': '<form string="Partners">' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'</tree>' +
'</field>' +
'</form>',
},
viewOptions: {mode: 'edit'},
});
await testUtils.dom.click(form.$('.o_external_button'));
assert.strictEqual($('.modal-body .o_legacy_form_view .o_legacy_list_view .o_data_cell').text(), "yopblip",
"table has some initial order");
await testUtils.dom.click($('.modal-body .o_legacy_form_view .o_legacy_list_view th.o_column_sortable'));
assert.strictEqual($('.modal-body .o_legacy_form_view .o_legacy_list_view .o_data_cell').text(), "blipyop",
"table is now sorted");
form.destroy();
});
QUnit.test('rainbowman attributes correctly passed on button click', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<header>' +
'<button name="action_won" string="Won" type="object" effect="{\'message\': \'Congrats!\'}"/>' +
'</header>' +
'</form>',
intercepts: {
execute_action: function (event) {
var effectDescription = pyUtils.py_eval(event.data.action_data.effect);
assert.deepEqual(effectDescription, {message: 'Congrats!'}, "should have correct effect description");
}
},
});
await testUtils.dom.click(form.$('.o_form_statusbar .btn-secondary'));
form.destroy();
});
QUnit.test('basic support for widgets', async function (assert) {
// This test could be removed as soon as we drop the support of legacy widgets (see test
// below, which is a duplicate of this one, but with an Owl Component instead).
assert.expect(1);
var MyWidget = Widget.extend({
init: function (parent, dataPoint) {
this.data = dataPoint.data;
},
start: function () {
this.$el.text(JSON.stringify(this.data));
},
});
widgetRegistry.add('test', MyWidget);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'<widget name="test"/>' +
'</form>',
});
assert.strictEqual(form.$('.o_widget').text(), '{"foo":"My little Foo Value","bar":false}',
"widget should have been instantiated");
form.destroy();
delete widgetRegistry.map.test;
});
QUnit.test('basic support for widgets (being Owl Components)', async function (assert) {
assert.expect(1);
class MyComponent extends LegacyComponent {
get value() {
return JSON.stringify(this.props.record.data);
}
}
MyComponent.template = xml`<div t-esc="value"/>`;
widgetRegistryOwl.add('test', MyComponent);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="foo"/>
<field name="bar"/>
<widget name="test"/>
</form>`,
});
assert.strictEqual(form.$('.o_widget').text(), '{"foo":"My little Foo Value","bar":false}');
form.destroy();
delete widgetRegistryOwl.map.test;
});
QUnit.test('attach document widget calls action with attachment ids', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
mockRPC: function (route, args) {
if (args.method === 'my_action') {
assert.deepEqual(args.kwargs.attachment_ids, [5, 2]);
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
arch: '<form>' +
'<widget name="attach_document" action="my_action"/>' +
'</form>',
});
var onFileLoadedEventName = form.$('.o_form_binary_form').attr('target');
// trigger _onFileLoaded function
$(window).trigger(onFileLoadedEventName, [{id: 5}, {id:2}]);
form.destroy();
});
QUnit.test('support header button as widgets on form statusbar', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<header>' +
'<widget name="attach_document" string="Attach document"/>' +
'</header>' +
'</form>',
});
assert.containsOnce(form, 'button.o_attachment_button',
"should have 1 attach_document widget in the statusbar");
assert.strictEqual(form.$('span.o_attach_document').text().trim(), 'Attach document',
"widget should have been instantiated");
form.destroy();
});
QUnit.test('basic support for widgets', async function (assert) {
assert.expect(1);
var MyWidget = Widget.extend({
init: function (parent, dataPoint) {
this.data = dataPoint.data;
},
start: function () {
this.$el.text(this.data.foo + "!");
},
updateState: function (dataPoint) {
this.$el.text(dataPoint.data.foo + "!");
},
});
widgetRegistry.add('test', MyWidget);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'<widget name="test"/>' +
'</form>',
});
await testUtils.fields.editInput(form.$('input[name="foo"]'), "I am alive");
assert.strictEqual(form.$('.o_widget').text(), 'I am alive!',
"widget should have been updated");
form.destroy();
delete widgetRegistry.map.test;
});
QUnit.test('bounce edit button in readonly mode', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<div class="oe_title">' +
'<field name="display_name"/>' +
'</div>' +
'</form>',
res_id: 1,
});
// in readonly
await testUtils.dom.click(form.$('div.oe_title'));
assert.hasClass(form.$('.o_form_button_edit'), 'o_catch_attention');
// in edit
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('[name="display_name"]'));
// await testUtils.nextTick();
assert.containsNone(form, 'button.o_catch_attention:visible');
form.destroy();
});
QUnit.test('proper stringification in debug mode tooltip', async function (assert) {
assert.expect(6);
var initialDebugMode = odoo.debug;
odoo.debug = true;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="product_id" context="{\'lang\': \'en_US\'}" ' +
'attrs=\'{"invisible": [["product_id", "=", 33]]}\' ' +
'widget="many2one" />' +
'</sheet>' +
'</form>',
});
var $field = form.$('[name="product_id"]');
$field.tooltip('show', true);
$field.trigger($.Event('mouseenter'));
assert.strictEqual($('.oe_tooltip_technical>li[data-item="context"]').length,
1, 'context should be present for this field');
assert.strictEqual($('.oe_tooltip_technical>li[data-item="context"]')[0].lastChild.wholeText.trim(),
"{'lang': 'en_US'}", "context should be properly stringified");
assert.strictEqual($('.oe_tooltip_technical>li[data-item="modifiers"]').length,
1, 'modifiers should be present for this field');
assert.strictEqual($('.oe_tooltip_technical>li[data-item="modifiers"]')[0].lastChild.wholeText.trim(),
'{"invisible":[["product_id","=",33]]}', "modifiers should be properly stringified");
assert.strictEqual($('.oe_tooltip_technical>li[data-item="widget"]').length,
1, 'widget should be present for this field');
assert.strictEqual($('.oe_tooltip_technical>li[data-item="widget"]')[0].lastChild.wholeText.trim(),
'Many2one (many2one)', "widget description should be correct");
$field.tooltip('hide');
odoo.debug = initialDebugMode;
form.destroy();
});
QUnit.test('autoresize of text fields is done when switching to edit mode', async function (assert) {
assert.expect(4);
this.data.partner.fields.text_field = { string: 'Text field', type: 'text' };
this.data.partner.fields.text_field.default = "some\n\nmulti\n\nline\n\ntext\n";
this.data.partner.records[0].text_field = "a\nb\nc\nd\ne\nf";
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="display_name"/>' +
'<field name="text_field"/>' +
'</sheet>' +
'</form>',
res_id: 1,
});
// switch to edit mode to ensure that autoresize is correctly done
await testUtils.form.clickEdit(form);
var height = form.$('.o_field_widget[name=text_field]').height();
// focus the field to manually trigger autoresize
form.$('.o_field_widget[name=text_field]').trigger('focus');
assert.strictEqual(form.$('.o_field_widget[name=text_field]').height(), height,
"autoresize should have been done automatically at rendering");
// next assert simply tries to ensure that the textarea isn't stucked to
// its minimal size, even after being focused
assert.ok(height > 80, "textarea should have an height of at least 80px");
// save and create a new record to ensure that autoresize is correctly done
await testUtils.form.clickSave(form);
await testUtils.form.clickCreate(form);
height = form.$('.o_field_widget[name=text_field]').height();
// focus the field to manually trigger autoresize
form.$('.o_field_widget[name=text_field]').trigger('focus');
assert.strictEqual(form.$('.o_field_widget[name=text_field]').height(), height,
"autoresize should have been done automatically at rendering");
assert.ok(height > 80, "textarea should have an height of at least 80px");
form.destroy();
});
QUnit.test('autoresize of text fields is done on notebook page show', async function (assert) {
assert.expect(5);
this.data.partner.fields.text_field = { string: 'Text field', type: 'text' };
this.data.partner.fields.text_field.default = "some\n\nmulti\n\nline\n\ntext\n";
this.data.partner.records[0].text_field = "a\nb\nc\nd\ne\nf";
this.data.partner.fields.text_field_empty = { string: 'Text field', type: 'text' };
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<notebook>' +
'<page string="First Page">' +
'<field name="foo"/>' +
'</page>' +
'<page string="Second Page">' +
'<field name="text_field"/>' +
'</page>' +
'<page string="Third Page">' +
'<field name="text_field_empty"/>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
assert.hasClass(form.$('.o_notebook .nav .nav-link:first()'), 'active');
await testUtils.dom.click(form.$('.o_notebook .nav .nav-link:nth(1)'));
assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(1)'), 'active');
var height = form.$('.o_field_widget[name=text_field]').height();
assert.ok(height > 80, "textarea should have an height of at least 80px");
await testUtils.dom.click(form.$('.o_notebook .nav .nav-link:nth(2)'));
assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(2)'), 'active');
var height = form.$('.o_field_widget[name=text_field_empty]').css('height');
assert.strictEqual(height, '50px', "empty textarea should have height of 50px");
form.destroy();
});
QUnit.test('check if the view destroys all widgets and instances', async function (assert) {
assert.expect(2);
var instanceNumber = 0;
await testUtils.mock.patch(mixins.ParentedMixin, {
init: function () {
instanceNumber++;
return this._super.apply(this, arguments);
},
destroy: function () {
if (!this.isDestroyed()) {
instanceNumber--;
}
return this._super.apply(this, arguments);
}
});
var params = {
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'<field name="int_field"/>' +
'<field name="qux"/>' +
'<field name="trululu"/>' +
'<field name="timmy"/>' +
'<field name="product_id"/>' +
'<field name="priority"/>' +
'<field name="state"/>' +
'<field name="date"/>' +
'<field name="datetime"/>' +
'<field name="product_ids"/>' +
'<field name="p">' +
'<tree default_order="foo desc">' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
archs: {
'partner,false,form':
'<form string="Partner">' +
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
"partner_type,false,list": '<tree><field name="name"/></tree>',
'product,false,list': '<tree><field name="display_name"/></tree>',
},
res_id: 1,
};
var form = await createView(params);
assert.ok(instanceNumber > 0);
form.destroy();
assert.strictEqual(instanceNumber, 0);
await testUtils.mock.unpatch(mixins.ParentedMixin);
});
QUnit.test('do not change pager when discarding current record', async function (assert) {
assert.expect(4);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
viewOptions: {
ids: [1, 2],
index: 0,
},
res_id: 2,
});
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), "2",
'pager should indicate that we are on second record');
assert.strictEqual(testUtils.controlPanel.getPagerSize(form), "2",
'pager should indicate that we are on second record');
await testUtils.form.clickEdit(form);
await testUtils.form.clickDiscard(form);
assert.strictEqual(testUtils.controlPanel.getPagerValue(form), "2",
'pager value should not have changed');
assert.strictEqual(testUtils.controlPanel.getPagerSize(form), "2",
'pager limit should not have changed');
form.destroy();
});
QUnit.test('Form view from ordered, grouped list view correct context', async function (assert) {
assert.expect(10);
this.data.partner.records[0].timmy = [12];
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="foo"/>' +
'<field name="timmy"/>' +
'</form>',
archs: {
'partner_type,false,list':
'<tree>' +
'<field name="name"/>' +
'</tree>',
},
viewOptions: {
// Simulates coming from a list view with a groupby and filter
context: {
orderedBy: [{name: 'foo', asc:true}],
group_by: ['foo'],
}
},
res_id: 1,
mockRPC: function (route, args) {
assert.step(args.model + ":" + args.method);
if (args.method === 'read') {
assert.ok(args.kwargs.context, 'context is present');
assert.notOk('orderedBy' in args.kwargs.context,
'orderedBy not in context');
assert.notOk('group_by' in args.kwargs.context,
'group_by not in context');
}
return this._super.apply(this, arguments);
}
});
assert.verifySteps(['partner_type:get_views', 'partner:read', 'partner_type:read']);
form.destroy();
});
QUnit.test('edition in form view on a "noCache" model', async function (assert) {
assert.expect(5);
await testUtils.mock.patch(BasicModel, {
noCacheModels: BasicModel.prototype.noCacheModels.concat(['partner']),
});
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="display_name"/>' +
'</sheet>' +
'</form>',
res_id: 1,
viewOptions: {
mode: 'edit',
},
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.step('write');
}
return this._super.apply(this, arguments);
},
});
core.bus.on('clear_cache', form, assert.step.bind(assert, 'clear_cache'));
await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'new value');
await testUtils.form.clickSave(form);
assert.verifySteps(['write', 'clear_cache']);
form.destroy();
await testUtils.mock.unpatch(BasicModel);
assert.verifySteps(['clear_cache']); // triggered by the test environment on destroy
});
QUnit.test('creation in form view on a "noCache" model', async function (assert) {
assert.expect(5);
await testUtils.mock.patch(BasicModel, {
noCacheModels: BasicModel.prototype.noCacheModels.concat(['partner']),
});
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="display_name"/>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'create') {
assert.step('create');
}
return this._super.apply(this, arguments);
},
});
core.bus.on('clear_cache', form, assert.step.bind(assert, 'clear_cache'));
await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'value');
await testUtils.form.clickSave(form);
assert.verifySteps(['create', 'clear_cache']);
form.destroy();
await testUtils.mock.unpatch(BasicModel);
assert.verifySteps(['clear_cache']); // triggered by the test environment on destroy
});
QUnit.test('deletion in form view on a "noCache" model', async function (assert) {
assert.expect(5);
await testUtils.mock.patch(BasicModel, {
noCacheModels: BasicModel.prototype.noCacheModels.concat(['partner']),
});
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="display_name"/>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'unlink') {
assert.step('unlink');
}
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
hasActionMenus: true,
},
});
core.bus.on('clear_cache', form, assert.step.bind(assert, 'clear_cache'));
await testUtils.controlPanel.toggleActionMenu(form);
await testUtils.controlPanel.toggleMenuItem(form, "Delete");
await testUtils.dom.click($('.modal-footer .btn-primary'));
assert.verifySteps(['unlink', 'clear_cache']);
form.destroy();
await testUtils.mock.unpatch(BasicModel);
assert.verifySteps(['clear_cache']); // triggered by the test environment on destroy
});
QUnit.test('reload currencies when writing on records of model res.currency', async function (assert) {
assert.expect(5);
this.data['res.currency'] = {
fields: {},
records: [{id: 1, display_name: "some currency"}],
};
var form = await createView({
View: FormView,
model: 'res.currency',
data: this.data,
arch: '<form><field name="display_name"/></form>',
res_id: 1,
viewOptions: {
mode: 'edit',
},
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
session: {
reloadCurrencies: function () {
assert.step('reload currencies');
},
},
});
await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'new value');
await testUtils.form.clickSave(form);
assert.verifySteps([
'read',
'write',
'reload currencies',
'read',
]);
form.destroy();
});
QUnit.test('keep editing after call_button fail', async function (assert) {
assert.expect(4);
var values;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form>' +
'<button name="post" class="p" string="Raise Error" type="object"/>' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="display_name"/>' +
'<field name="product_id"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
intercepts: {
execute_action: function (ev) {
assert.ok(true, 'the action is correctly executed');
ev.data.on_fail();
},
},
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.deepEqual(args.args[1].p[0][2], values);
}
return this._super.apply(this, arguments);
},
viewOptions: {
mode: 'edit',
},
});
// add a row and partially fill it
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.fields.editInput(form.$('input[name=display_name]'), 'abc');
// click button which will trigger_up 'execute_action' (this will save)
values = {
display_name: 'abc',
product_id: false,
};
await testUtils.dom.click(form.$('button.p'));
// edit the new row again and set a many2one value
await testUtils.dom.clickLast(form.$('.o_legacy_form_view .o_field_one2many .o_data_row .o_data_cell'));
await testUtils.nextTick();
await testUtils.fields.many2one.clickOpenDropdown('product_id');
await testUtils.fields.many2one.clickHighlightedItem('product_id');
assert.strictEqual(form.$('.o_field_many2one input').val(), 'xphone',
"value of the m2o should have been correctly updated");
values = {
product_id: 37,
};
await testUtils.form.clickSave(form);
form.destroy();
});
QUnit.test('asynchronous rendering of a widget tag', async function (assert) {
assert.expect(1);
var def1 = testUtils.makeTestPromise();
var MyWidget = Widget.extend({
willStart: function() {
return def1;
},
});
widgetRegistry.add('test', MyWidget);
const viewCreatedPromise = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<widget name="test"/>' +
'</form>',
}).then(function(form) {
assert.containsOnce(form, 'div.o_widget',
"there should be a div with widget class");
form.destroy();
delete widgetRegistry.map.test;
});
def1.resolve();
await viewCreatedPromise;
});
QUnit.test('no deadlock when saving with uncommitted changes', async function (assert) {
// Before saving a record, all field widgets are asked to commit their changes (new values
// that they wouldn't have sent to the model yet). This test is added alongside a bug fix
// ensuring that we don't end up in a deadlock when a widget actually has some changes to
// commit at that moment. By chance, this situation isn't reached when the user clicks on
// 'Save' (which is the natural way to save a record), because by clicking outside the
// widget, the 'change' event (this is mainly for InputFields) is triggered, and the widget
// notifies the model of its new value on its own initiative, before being requested to.
// In this test, we try to reproduce the deadlock situation by forcing the field widget to
// commit changes before the save. We thus manually call 'saveRecord', instead of clicking
// on 'Save'.
assert.expect(6);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="foo"/></form>',
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
// we set a fieldDebounce to precisely mock the behavior of the webclient: changes are
// not sent to the model at keystrokes, but when the input is left
fieldDebounce: 5000,
});
await testUtils.fields.editInput(form.$('input[name=foo]'), 'some foo value');
// manually save the record, to prevent the field widget to notify the model of its new
// value before being requested to
form.saveRecord();
await testUtils.nextTick();
assert.containsOnce(form, '.o_form_readonly', "form view should be in readonly");
assert.strictEqual(form.$('.o_legacy_form_view').text().trim(), 'some foo value',
"foo field should have correct value");
assert.verifySteps(['onchange', 'create', 'read']);
form.destroy();
});
QUnit.test('save record with onchange on one2many with required field', async function (assert) {
// in this test, we have a one2many with a required field, whose value is
// set by an onchange on another field ; we manually set the value of that
// first field, and directly click on Save (before the onchange RPC returns
// and sets the value of the required field)
assert.expect(6);
this.data.partner.fields.foo.default = undefined;
this.data.partner.onchanges = {
display_name: function (obj) {
obj.foo = obj.display_name ? 'foo value' : undefined;
},
};
var onchangeDef;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="display_name"/>' +
'<field name="foo" required="1"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'onchange') {
return Promise.resolve(onchangeDef).then(_.constant(result));
}
if (args.method === 'create') {
assert.step('create');
assert.strictEqual(args.args[0].p[0][2].foo, 'foo value',
"should have wait for the onchange to return before saving");
}
return result;
},
});
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), '',
"display_name should be the empty string by default");
assert.strictEqual(form.$('.o_field_widget[name=foo]').val(), '',
"foo should be the empty string by default");
onchangeDef = testUtils.makeTestPromise(); // delay the onchange
await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'some value');
await testUtils.form.clickSave(form);
assert.step('resolve');
onchangeDef.resolve();
await testUtils.nextTick();
assert.verifySteps(['resolve', 'create']);
form.destroy();
});
QUnit.test('call canBeRemoved while saving', async function (assert) {
assert.expect(10);
this.data.partner.onchanges = {
foo: function (obj) {
obj.display_name = obj.foo === 'trigger onchange' ? 'changed' : 'default';
},
};
var onchangeDef;
var createDef = testUtils.makeTestPromise();
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="display_name"/><field name="foo"/></form>',
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'onchange') {
return Promise.resolve(onchangeDef).then(_.constant(result));
}
if (args.method === 'create') {
return Promise.resolve(createDef).then(_.constant(result));
}
return result;
},
});
// edit foo to trigger a delayed onchange
onchangeDef = testUtils.makeTestPromise();
await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange');
assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'default');
// save (will wait for the onchange to return), and will be delayed as well
await testUtils.dom.click(form.$buttons.find('.o_form_button_save'));
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable');
assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'default');
// simulate a click on the breadcrumbs to leave the form view
form.canBeRemoved();
await testUtils.nextTick();
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable');
assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'default');
// unlock the onchange
onchangeDef.resolve();
await testUtils.nextTick();
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable');
assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), 'changed');
// unlock the create
createDef.resolve();
await testUtils.nextTick();
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_readonly');
assert.strictEqual(form.$('.o_field_widget[name=display_name]').text(), 'changed');
assert.containsNone(document.body, '.modal',
"should not display the 'Changes will be discarded' dialog");
form.destroy();
});
QUnit.test('call canBeRemoved twice', async function (assert) {
assert.expect(4);
let writeCalls = 0;
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="display_name"/><field name="foo"/></form>',
res_id: 1,
viewOptions: {
mode: 'edit',
},
mockRPC(route) {
if (route === '/web/dataset/call_kw/partner/write') {
writeCalls += 1;
}
return this._super(...arguments);
},
});
assert.containsOnce(form, '.o_form_editable');
await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value');
await form.canBeRemoved();
assert.containsNone(document.body, '.modal');
await form.canBeRemoved();
assert.containsNone(document.body, '.modal');
assert.strictEqual(writeCalls, 1, 'should save once');
form.destroy();
});
QUnit.test('domain returned by onchange is cleared on discard', async function (assert) {
assert.expect(5);
this.data.partner.onchanges = {
foo: function () {},
};
var domain = ['id', '=', 1];
var expectedDomain = domain;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="foo"/><field name="trululu"/></form>',
mockRPC: function (route, args) {
if (args.method === 'onchange' && args.args[0][0] === 1) {
// onchange returns a domain only on record 1
return Promise.resolve({
domain: {
trululu: domain,
},
});
}
if (args.method === 'name_search') {
assert.deepEqual(args.kwargs.args, expectedDomain);
}
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
ids: [1, 2],
mode: 'edit',
},
});
assert.strictEqual(form.$('input[name=foo]').val(), 'yop', "should be on record 1");
// change foo to trigger the onchange
await testUtils.fields.editInput(form.$('input[name=foo]'), 'new value');
// open many2one dropdown to check if the domain is applied
await testUtils.fields.many2one.clickOpenDropdown('trululu');
// switch to another record (should ask to discard changes, and reset the domain)
await testUtils.controlPanel.pagerNext(form);
assert.containsNone(document.body, '.modal', 'should not open modal');
assert.strictEqual(form.$('input[name=foo]').val(), 'blip', "should be on record 2");
// open many2one dropdown to check if the domain is applied
expectedDomain = [];
await testUtils.fields.many2one.clickOpenDropdown('trululu');
form.destroy();
});
QUnit.test('discard after a failed save', async function (assert) {
assert.expect(2);
serverData.views = {
'partner,false,form': '<form>' +
'<field name="date" required="true"/>' +
'<field name="foo" required="true"/>' +
'</form>',
'partner,false,kanban': '<kanban><templates><t t-name="kanban-box">' +
'</t></templates></kanban>',
'partner,false,search': '<search></search>',
};
const webClient = await createWebClient({ serverData });
await doAction(webClient, 1);
await testUtils.dom.click('.o_control_panel .o-kanban-button-new');
await legacyExtraNextTick();
//cannot save because there is a required field
await testUtils.dom.click('.o_control_panel .o_form_button_save');
await legacyExtraNextTick();
await testUtils.dom.click('.o_control_panel .o_form_button_cancel');
await legacyExtraNextTick();
assert.containsNone(target, '.o_legacy_form_view');
assert.containsOnce(target, '.o_legacy_kanban_view');
});
QUnit.test("one2many create record dialog shouldn't have a 'remove' button", async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="p">' +
'<kanban>' +
'<templates>' +
'<t t-name="kanban-box">' +
'<field name="foo"/>' +
'</t>' +
'</templates>' +
'</kanban>' +
'<form>' +
'<field name="foo"/>' +
'</form>' +
'</field>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickCreate(form);
await testUtils.dom.click(form.$('.o-kanban-button-new'));
assert.containsOnce(document.body, '.modal');
assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 0,
"shouldn't have a 'remove' button on new records");
form.destroy();
});
QUnit.test('edit a record in readonly and switch to edit before it is actually saved', async function (assert) {
assert.expect(3);
fieldRegistry.add("toggle_button", basicFields.FieldToggleBoolean);
registerCleanup(() => delete fieldRegistry.map.toggle_button);
const prom = testUtils.makeTestPromise();
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `<form>
<field name="foo"/>
<field name="bar" widget="toggle_button"/>
</form>`,
mockRPC: function (route, args) {
const result = this._super.apply(this, arguments);
if (args.method === 'write') { // delay the write RPC
assert.deepEqual(args.args[1], {bar: false});
return prom.then(_.constant(result));
}
return result;
},
res_id: 1,
});
// edit the record (in readonly) with toogle_button widget (and delay the write RPC)
await testUtils.dom.click(form.$('.o_field_widget[name=bar]'));
// switch to edit mode
await testUtils.form.clickEdit(form);
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_readonly'); // should wait for the RPC to return
// make write RPC return
prom.resolve();
await testUtils.nextTick();
assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable');
form.destroy();
});
QUnit.test('"bare" buttons in template should not trigger button click', async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<button string="Save" class="btn-primary" special="save"/>' +
'<button class="mybutton">westvleteren</button>' +
'</form>',
res_id: 2,
intercepts: {
execute_action: function () {
assert.step('execute_action');
},
},
});
await testUtils.dom.click(form.$('.o_legacy_form_view button.btn-primary'));
assert.verifySteps(['execute_action']);
await testUtils.dom.click(form.$('.o_legacy_form_view button.mybutton'));
assert.verifySteps([]);
form.destroy();
});
QUnit.test('form view with inline tree view with optional fields and local storage mock', async function (assert) {
assert.expect(12);
var Storage = RamStorage.extend({
getItem: function (key) {
assert.step('getItem ' + key);
return this._super.apply(this, arguments);
},
setItem: function (key, value) {
assert.step('setItem ' + key + ' to ' + value);
return this._super.apply(this, arguments);
},
});
var RamStorageService = AbstractStorageService.extend({
storage: new Storage(),
});
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="qux"/>' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'<field name="bar" optional="hide"/>' +
'</tree>' +
'</field>' +
'</form>',
services: {
local_storage: RamStorageService,
},
view_id: 27,
});
var localStorageKey = 'optional_fields,partner,form,27,p,list,undefined,bar,foo';
assert.verifySteps(['getItem ' + localStorageKey]);
assert.containsN(form, 'th', 2,
"should have 2 th, 1 for selector, 1 for foo column");
assert.ok(form.$('th:contains(Foo)').is(':visible'),
"should have a visible foo field");
assert.notOk(form.$('th:contains(Bar)').is(':visible'),
"should not have a visible bar field");
// optional fields
await testUtils.dom.click(form.$('table .o_optional_columns_dropdown_toggle'));
assert.containsN(form, 'div.o_optional_columns div.dropdown-item', 1,
"dropdown have 1 optional field");
// enable optional field
await testUtils.dom.click(form.$('div.o_optional_columns div.dropdown-item input'));
assert.verifySteps([
'setItem ' + localStorageKey + ' to ["bar"]',
'getItem ' + localStorageKey,
]);
assert.containsN(form, 'th', 3,
"should have 3 th, 1 for selector, 2 for columns");
assert.ok(form.$('th:contains(Foo)').is(':visible'),
"should have a visible foo field");
assert.ok(form.$('th:contains(Bar)').is(':visible'),
"should have a visible bar field");
form.destroy();
});
QUnit.test('form view with tree_view_ref with optional fields and local storage mock', async function (assert) {
assert.expect(12);
var Storage = RamStorage.extend({
getItem: function (key) {
assert.step('getItem ' + key);
return this._super.apply(this, arguments);
},
setItem: function (key, value) {
assert.step('setItem ' + key + ' to ' + value);
return this._super.apply(this, arguments);
},
});
var RamStorageService = AbstractStorageService.extend({
storage: new Storage(),
});
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="qux"/>' +
'<field name="p" context="{\'tree_view_ref\': \'34\'}"/>' +
'</form>',
archs: {
"partner,nope_not_this_one,list": '<tree>' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'</tree>',
"partner,34,list": '<tree>' +
'<field name="foo" optional="hide"/>' +
'<field name="bar"/>' +
'</tree>',
},
services: {
local_storage: RamStorageService,
},
view_id: 27,
});
var localStorageKey = 'optional_fields,partner,form,27,p,list,34,bar,foo';
assert.verifySteps(['getItem ' + localStorageKey]);
assert.containsN(form, 'th', 2,
"should have 2 th, 1 for selector, 1 for foo column");
assert.notOk(form.$('th:contains(Foo)').is(':visible'),
"should have a visible foo field");
assert.ok(form.$('th:contains(Bar)').is(':visible'),
"should not have a visible bar field");
// optional fields
await testUtils.dom.click(form.$('table .o_optional_columns_dropdown_toggle'));
assert.containsN(form, 'div.o_optional_columns div.dropdown-item', 1,
"dropdown have 1 optional field");
// enable optional field
await testUtils.dom.click(form.$('div.o_optional_columns div.dropdown-item input'));
assert.verifySteps([
'setItem ' + localStorageKey + ' to ["foo"]',
'getItem ' + localStorageKey,
]);
assert.containsN(form, 'th', 3,
"should have 3 th, 1 for selector, 2 for columns");
assert.ok(form.$('th:contains(Foo)').is(':visible'),
"should have a visible foo field");
assert.ok(form.$('th:contains(Bar)').is(':visible'),
"should have a visible bar field");
form.destroy();
});
QUnit.test('using tab in an empty required string field should not move to the next field', async function(assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="display_name" required="1" />' +
'<field name="foo" />' +
'</group>' +
'</sheet>' +
'</form>',
});
await 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="display_name"]')[0], document.activeElement,
"display_name should still be focused because it is empty and required");
assert.hasClass(form.$('input[name="display_name"]'), 'o_field_invalid',
"display_name should have the o_field_invalid class");
form.destroy();
});
QUnit.test('using tab in an empty required date field should not move to the next 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="date" required="1" />' +
'<field name="foo" />' +
'</group>' +
'</sheet>' +
'</form>',
});
await testUtils.dom.click(form.$('input[name=date]'));
assert.strictEqual(form.$('input[name="date"]')[0], document.activeElement,
"display_name should be focused");
form.$('input[name="date"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(form.$('input[name="date"]')[0], document.activeElement,
"date should still be focused because it is empty and required");
form.destroy();
});
QUnit.test('Edit button get the focus when pressing TAB from form', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<div class="oe_title">' +
'<field name="display_name"/>' +
'</div>' +
'</form>',
res_id: 1,
});
// in edit
await testUtils.form.clickEdit(form);
form.$('input[name="display_name"]').focus().trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(form.$buttons.find('.btn-primary:visible')[0], document.activeElement,
"the first primary button (save) should be focused");
form.destroy();
});
QUnit.test('In Edition mode, after navigating to the last field, the default button when pressing TAB is SAVE', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="state" invisible="1"/>' +
'<header>' +
'<button name="post" class="btn-primary firstButton" string="Confirm" type="object"/>' +
'<button name="post" class="btn-primary secondButton" string="Confirm2" type="object"/>' +
'</header>' +
'<sheet>' +
'<group>' +
'<div class="oe_title">' +
'<field name="display_name"/>' +
'</div>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
viewOptions: {
mode: 'edit',
},
});
form.$('input[name="display_name"]').focus().trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(form.$buttons.find('.o_form_button_save:visible')[0], document.activeElement,
"the save should be focused");
form.destroy();
});
QUnit.test('In READ mode, the default button with focus is the first primary button of the form', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="state" invisible="1"/>' +
'<header>' +
'<button name="post" class="btn-primary firstButton" string="Confirm" type="object"/>' +
'<button name="post" class="btn-primary secondButton" string="Confirm2" type="object"/>' +
'</header>' +
'<sheet>' +
'<group>' +
'<div class="oe_title">' +
'<field name="display_name"/>' +
'</div>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
});
assert.strictEqual(form.$('button.firstButton')[0], document.activeElement,
"by default the focus in edit mode should go to the first primary button of the form (not edit)");
form.destroy();
});
QUnit.test('In READ mode, the default button when pressing TAB is EDIT when there is no primary button on the form', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="state" invisible="1"/>' +
'<header>' +
'<button name="post" class="not-primary" string="Confirm" type="object"/>' +
'<button name="post" class="not-primary" string="Confirm2" type="object"/>' +
'</header>' +
'<sheet>' +
'<group>' +
'<div class="oe_title">' +
'<field name="display_name"/>' +
'</div>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
});
assert.strictEqual(form.$buttons.find('.o_form_button_edit')[0],document.activeElement,
"in read mode, when there are no primary buttons on the form, the default button with the focus should be edit");
form.destroy();
});
QUnit.test('In Edition mode, when an attribute is dynamically required (and not required), TAB should navigate to the next field', 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" attrs="{\'required\': [[\'bar\', \'=\', True]]}"/>' +
'<field name="bar"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 5,
viewOptions: {
mode: 'edit',
},
});
form.$('input[name="foo"]').focus();
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(form.$('div[name="bar"]>input')[0], document.activeElement, "foo is not required, so hitting TAB on foo should have moved the focus to BAR");
form.destroy();
});
QUnit.test('In Edition mode, when an attribute is dynamically required, TAB should stop on the field if it is required', 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" attrs="{\'required\': [[\'bar\', \'=\', True]]}"/>' +
'<field name="bar"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 5,
viewOptions: {
mode: 'edit',
},
});
await testUtils.dom.click(form.$('div[name="bar"]>input'));
form.$('input[name="foo"]').focus();
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement, "foo is required, so hitting TAB on foo should keep the focus on foo");
form.destroy();
});
QUnit.test('display tooltips for save and discard buttons', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo" />'+
'</form>',
});
form.$buttons.find('.o_form_buttons_edit').tooltip('show',false);
assert.strictEqual($('.tooltip .oe_tooltip_string').length, 1,
"should have rendered a tooltip");
form.$buttons.find('.o_form_buttons_edit').tooltip('hide');
form.destroy();
});
QUnit.test('if the focus is on the save button, hitting ENTER should save', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo" />'+
'</form>',
viewOptions: {
mode: 'edit',
},
mockRPC: function (route, args) {
if (args.method === 'create') {
assert.ok(true, "should call the /create route");
}
return this._super(route, args);
},
});
form.$buttons.find('.o_form_button_save')
.focus()
.trigger($.Event('keydown', {which: $.ui.keyCode.ENTER}));
await testUtils.nextTick();
form.destroy();
});
QUnit.test('if the focus is on the discard button, hitting ENTER should save', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo" />'+
'</form>',
viewOptions: {
mode: 'edit',
},
mockRPC: function (route, args) {
if (args.method === 'create') {
assert.ok(true, "should call the /create route");
}
return this._super(route, args);
},
});
form.$buttons.find('.o_form_button_cancel')
.focus()
.trigger($.Event('keydown', {which: $.ui.keyCode.ENTER}));
await testUtils.nextTick();
form.destroy();
});
QUnit.test('if the focus is on the save button, hitting ESCAPE should discard', async function (assert) {
assert.expect(0);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo" />'+
'</form>',
viewOptions: {
mode: 'edit',
},
mockRPC: function (route, args) {
if (args.method === 'create') {
throw new Error('Create should not be called');
}
return this._super(route, args);
},
});
form.$buttons.find('.o_form_button_save')
.focus()
.trigger($.Event('keydown', {which: $.ui.keyCode.ESCAPE}));
await testUtils.nextTick();
form.destroy();
});
QUnit.test('resequence list lines when discardable lines are present', async function (assert) {
assert.expect(8);
var onchangeNum = 0;
this.data.partner.onchanges = {
p: function (obj) {
onchangeNum++;
obj.foo = obj.p ? obj.p.length.toString() : "0";
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="foo"/>' +
'<field name="p"/>' +
'</form>',
archs: {
'partner,false,list':
'<tree editable="bottom">' +
'<field name="int_field" widget="handle"/>' +
'<field name="display_name" required="1"/>' +
'</tree>',
},
});
assert.strictEqual(onchangeNum, 1, "one onchange happens when form is opened");
assert.strictEqual(form.$('[name="foo"]').val(), "0", "onchange worked there is 0 line");
// Add one line
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
form.$('.o_field_one2many input:first').focus();
await testUtils.nextTick();
form.$('.o_field_one2many input:first').val('first line').trigger('input');
await testUtils.nextTick();
await testUtils.dom.click(form.$('input[name="foo"]'));
assert.strictEqual(onchangeNum, 2, "one onchange happens when a line is added");
assert.strictEqual(form.$('[name="foo"]').val(), "1", "onchange worked there is 1 line");
// Drag and drop second line before first one (with 1 draft and invalid line)
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.dom.dragAndDrop(
form.$('.ui-sortable-handle').eq(0),
form.$('.o_data_row').last(),
{position: 'bottom'}
);
assert.strictEqual(onchangeNum, 3, "one onchange happens when lines are resequenced");
assert.strictEqual(form.$('[name="foo"]').val(), "1", "onchange worked there is 1 line");
// Add a second line
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
form.$('.o_field_one2many input:first').focus();
await testUtils.nextTick();
form.$('.o_field_one2many input:first').val('second line').trigger('input');
await testUtils.nextTick();
await testUtils.dom.click(form.$('input[name="foo"]'));
assert.strictEqual(onchangeNum, 4, "one onchange happens when a line is added");
assert.strictEqual(form.$('[name="foo"]').val(), "2", "onchange worked there is 2 lines");
form.destroy();
});
QUnit.test('if the focus is on the discard button, hitting ESCAPE should discard', async function (assert) {
assert.expect(0);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo" />'+
'</form>',
viewOptions: {
mode: 'edit',
},
mockRPC: function (route, args) {
if (args.method === 'create') {
throw new Error('Create should not be called');
}
return this._super(route, args);
},
});
form.$buttons.find('.o_form_button_cancel')
.focus()
.trigger($.Event('keydown', {which: $.ui.keyCode.ESCAPE}));
await testUtils.nextTick();
form.destroy();
});
QUnit.test('if the focus is on the save button, hitting TAB should not move to the next button', async function (assert) {
assert.expect(1);
/*
this test has only one purpose: to say that it is normal that the focus stays within a button primary even after the TAB key has been pressed.
It is not possible here to execute the default action of the TAB on a button : https://stackoverflow.com/questions/32428993/why-doesnt-simulating-a-tab-keypress-move-focus-to-the-next-input-field
so writing a test that will always succeed is not useful.
*/
assert.ok("Behavior can't be tested");
});
QUnit.test('reload company when creating records of model res.company', async function (assert) {
assert.expect(6);
var form = await createView({
View: FormView,
model: 'res.company',
data: this.data,
arch: '<form><field name="name"/></form>',
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
intercepts: {
do_action: function (ev) {
assert.step('reload company');
assert.strictEqual(ev.data.action, "reload_context", "company view reloaded");
},
},
});
await testUtils.fields.editInput(form.$('input[name="name"]'), 'Test Company');
await testUtils.form.clickSave(form);
assert.verifySteps([
'onchange',
'create',
'reload company',
'read',
]);
form.destroy();
});
QUnit.test('reload company when writing on records of model res.company', async function (assert) {
assert.expect(6);
this.data['res.company'].records = [{
id: 1, name: "Test Company"
}];
var form = await createView({
View: FormView,
model: 'res.company',
data: this.data,
arch: '<form><field name="name"/></form>',
res_id: 1,
viewOptions: {
mode: 'edit',
},
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
intercepts: {
do_action: function (ev) {
assert.step('reload company');
assert.strictEqual(ev.data.action, "reload_context", "company view reloaded");
},
},
});
await testUtils.fields.editInput(form.$('input[name="name"]'), 'Test Company2');
await testUtils.form.clickSave(form);
assert.verifySteps([
'read',
'write',
'reload company',
'read',
]);
form.destroy();
});
QUnit.test('company_dependent field in form view, in multi company group', async function (assert) {
assert.expect(2);
this.data.partner.fields.product_id.company_dependent = true;
this.data.partner.fields.product_id.help = 'this is a tooltip';
this.data.partner.fields.foo.company_dependent = true;
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="foo"/>
<field name="product_id"/>
</group>
</form>`,
session: {
display_switch_company_menu: true,
},
});
const $productLabel = form.$('.o_form_label:eq(1)');
$productLabel.tooltip('show', false);
await testUtils.dom.triggerMouseEvent($productLabel, 'mouseover');
assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(),
"this is a tooltip\n\nValues set here are company-specific.");
await testUtils.dom.triggerMouseEvent($productLabel, 'mouseout');
const $fooLabel = form.$('.o_form_label:first');
$fooLabel.tooltip('show', false);
await testUtils.dom.triggerMouseEvent($fooLabel, 'mouseover');
assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(),
"Values set here are company-specific.");
await testUtils.dom.triggerMouseEvent($fooLabel, 'mouseout');
form.destroy();
});
QUnit.test('company_dependent field in form view, not in multi company group', async function (assert) {
assert.expect(1);
this.data.partner.fields.product_id.company_dependent = true;
this.data.partner.fields.product_id.help = 'this is a tooltip';
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="product_id"/>
</group>
</form>`,
session: {
display_switch_company_menu: false,
},
});
const $productLabel = form.$('.o_form_label');
$productLabel.tooltip('show', false);
await testUtils.dom.triggerMouseEvent($productLabel, 'mouseover');
assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "this is a tooltip");
await testUtils.dom.triggerMouseEvent($productLabel, 'mouseout');
form.destroy();
});
QUnit.test('do not call mounted twice on children', async function (assert) {
assert.expect(3);
class CustomFieldComponent extends FieldBoolean {
setup() {
onMounted(() => {
assert.step('mounted');
});
onWillUnmount(() => {
assert.step('willUnmount');
});
}
}
fieldRegistryOwl.add('custom', CustomFieldComponent);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `<form><field name="bar" widget="custom"/></form>`,
});
form.destroy();
delete fieldRegistryOwl.map.custom;
assert.verifySteps(['mounted', 'willUnmount']);
});
QUnit.test('Auto save: save when page changed', async function (assert) {
assert.expect(10);
serverData.actions[1] = {
id: 1,
name: 'Partner',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [false, 'form']],
};
serverData.views = {
'partner,false,list': `
<tree>
<field name="name"/>
</tree>
`,
'partner,false,form': `
<form>
<group>
<field name="name"/>
</group>
</form>
`,
'partner,false,search': '<search></search>',
};
const mockRPC = (route, args) => {
if (args.method === 'write') {
assert.deepEqual(args.args, [
[1],
{ name: "aaa" },
]);
}
};
const webClient = await createWebClient({ serverData , mockRPC });
await doAction(webClient, 1);
await testUtils.dom.click($(target).find('.o_data_row:first'));
await legacyExtraNextTick();
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partnerfirst record');
await testUtils.dom.click($(target).find('.o_form_button_edit'));
await testUtils.fields.editInput($(target).find('.o_field_widget[name="name"]'), 'aaa');
await testUtils.controlPanel.pagerNext(target);
await legacyExtraNextTick();
assert.containsOnce(target, '.o_form_editable');
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partnersecond record');
assert.strictEqual($(target).find('.o_field_widget[name="name"]').val(), 'name');
await testUtils.dom.click($(target).find('.o_form_button_cancel'));
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partnersecond record');
assert.strictEqual($(target).find('.o_field_widget[name="name"]').text(), 'name');
await testUtils.controlPanel.pagerPrevious(target);
await legacyExtraNextTick();
assert.containsOnce(target, '.o_form_readonly');
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partnerfirst record');
assert.strictEqual($(target).find('.o_field_widget[name="name"]').text(), 'aaa');
});
QUnit.test('Auto save: save when breadcrumb clicked', async function (assert) {
assert.expect(7);
serverData.actions[1] = {
id: 1,
name: 'Partner',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [false, 'form']],
};
serverData.views = {
'partner,false,list': `
<tree>
<field name="name"/>
</tree>
`,
'partner,false,form': `
<form>
<group>
<field name="name"/>
</group>
</form>
`,
'partner,false,search': '<search></search>',
};
const mockRPC = (route, args) => {
if (args.method === 'write') {
assert.deepEqual(args.args, [
[1],
{ name: "aaa" },
]);
}
};
const webClient = await createWebClient({ serverData , mockRPC });
await doAction(webClient, 1);
await testUtils.dom.click($(target).find('.o_data_row:first'));
await legacyExtraNextTick();
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partnerfirst record');
await testUtils.dom.click($(target).find('.o_form_button_edit'));
await testUtils.fields.editInput($(target).find('.o_field_widget[name="name"]'), 'aaa');
await testUtils.dom.click($(target).find('.breadcrumb-item.o_back_button'));
await legacyExtraNextTick();
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partner');
assert.strictEqual($(target).find('.o_field_cell[name="name"]:first').text(), 'aaa');
await testUtils.dom.click($(target).find('.o_data_row:first'));
await legacyExtraNextTick();
assert.containsOnce(target, '.o_form_readonly');
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partnerfirst record');
assert.strictEqual($(target).find('.o_field_widget[name="name"]').text(), 'aaa');
});
QUnit.test('Auto save: save when action changed', async function (assert) {
assert.expect(6);
serverData.actions[1] = {
id: 1,
name: 'Partner',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [false, 'form']],
};
serverData.actions[2] = {
id: 2,
name: 'Other action',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'kanban']],
};
serverData.views = {
'partner,false,list': `
<tree>
<field name="name"/>
</tree>
`,
'partner,false,form': `
<form>
<group>
<field name="name"/>
</group>
</form>
`,
'partner,false,search': '<search></search>',
'partner,false,kanban': `
<kanban>
<field name="name"/>
<templates>
<t t-name="kanban-box">
<div></div>
</t>
</templates>
</kanban>
`,
};
const mockRPC = (route, args) => {
if (args.method === 'write') {
assert.deepEqual(args.args, [
[1],
{ name: "aaa" },
]);
}
};
const webClient = await createWebClient({ serverData , mockRPC });
await doAction(webClient, 1);
await testUtils.dom.click($(target).find('.o_data_row:first'));
await legacyExtraNextTick();
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partnerfirst record');
await testUtils.dom.click($(target).find('.o_form_button_edit'));
await testUtils.fields.editInput($(target).find('.o_field_widget[name="name"]'), 'aaa');
await doAction(webClient, 2, { clearBreadcrumbs: true });
assert.strictEqual($(target).find('.breadcrumb').text(), 'Other action');
await doAction(webClient, 1, { clearBreadcrumbs: true });
await testUtils.dom.click($(target).find('.o_data_row:first'));
await legacyExtraNextTick();
assert.containsOnce(target, '.o_form_readonly');
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partnerfirst record');
assert.strictEqual($(target).find('.o_field_widget[name="name"]').text(), 'aaa');
});
QUnit.test('Auto save: save on closing tab/browser', async function (assert) {
assert.expect(2);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
</group>
</form>`,
res_id: 1,
mockRPC(route, { args, method, model }) {
if (method === 'write' && model === 'partner') {
assert.deepEqual(args, [
[1],
{ display_name: 'test' },
]);
}
return this._super(...arguments);
},
});
await testUtils.form.clickEdit(form);
assert.notStrictEqual(form.$('.o_field_widget[name="display_name"]').val(), 'test');
await testUtils.fields.editInput(form.$('.o_field_widget[name="display_name"]'), 'test');
window.dispatchEvent(new Event("beforeunload"));
await testUtils.nextTick();
form.destroy();
});
QUnit.test('Auto save: save on closing tab/browser (invalid field)', async function (assert) {
assert.expect(1);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name" required="1"/>
</group>
</form>`,
res_id: 1,
mockRPC(route, { args, method, model }) {
if (method === 'write' && model === 'partner') {
assert.step('save'); // should not be called
}
return this._super(...arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('.o_field_widget[name="display_name"]'), '');
window.dispatchEvent(new Event("beforeunload"));
await testUtils.nextTick();
assert.verifySteps([], 'should not save because of invalid field');
form.destroy();
});
QUnit.test('Auto save: save on closing tab/browser (not dirty)', async function (assert) {
assert.expect(1);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
</group>
</form>`,
res_id: 1,
mockRPC(route, { args, method, model }) {
if (method === 'write' && model === 'partner') {
assert.step('save'); // should not be called
}
return this._super(...arguments);
},
});
await testUtils.form.clickEdit(form);
window.dispatchEvent(new Event("beforeunload"));
await testUtils.nextTick();
assert.verifySteps([], 'should not save because we do not change anything');
form.destroy();
});
QUnit.test('Auto save: save on closing tab/browser (detached form)', async function (assert) {
assert.expect(3);
serverData.actions[1] = {
id: 1,
name: 'Partner',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [false, 'form']],
};
serverData.views = {
'partner,false,list': `
<tree>
<field name="display_name"/>
</tree>
`,
'partner,false,form': `
<form>
<group>
<field name="display_name"/>
</group>
</form>
`,
'partner,false,search': '<search></search>',
};
const mockRPC = (route, args) => {
if (args.method === 'write') {
assert.step('save');
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1);
// Click on a row to open a record
await testUtils.dom.click($(target).find('.o_data_row:first'));
await legacyExtraNextTick();
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partnerfirst record');
// Return in the list view to detach the form view
await testUtils.dom.click($(target).find('.o_back_button'));
await legacyExtraNextTick();
assert.strictEqual($(target).find('.breadcrumb').text(), 'Partner');
// Simulate tab/browser close in the list
window.dispatchEvent(new Event("beforeunload"));
await testUtils.nextTick();
// write rpc should not trigger because form view has been detached
// and list has nothing to save
assert.verifySteps([]);
});
QUnit.test('Auto save: save on closing tab/browser (onchanges)', async function (assert) {
assert.expect(1);
this.data.partner.onchanges = {
display_name: function (obj) {
obj.name = `copy: ${obj.display_name}`;
},
};
const def = testUtils.makeTestPromise();
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
<field name="name"/>
</group>
</form>`,
res_id: 1,
mockRPC(route, { args, method, model }) {
if (method === 'onchange' && model === 'partner') {
return def;
}
if (method === 'write' && model === 'partner') {
assert.deepEqual(args, [
[1],
{ display_name: 'test' },
]);
}
return this._super(...arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('.o_field_widget[name="display_name"]'), 'test');
window.dispatchEvent(new Event("beforeunload"));
await testUtils.nextTick();
form.destroy();
});
QUnit.test('Auto save: save on closing tab/browser (onchanges 2)', async function (assert) {
assert.expect(1);
this.data.partner.onchanges = {
display_name: function () {},
};
const def = testUtils.makeTestPromise();
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
<field name="name"/>
</group>
</form>`,
res_id: 1,
mockRPC(route, { args, method }) {
if (method === 'onchange') {
return def;
}
if (method === 'write') {
assert.deepEqual(args, [
[1],
{ display_name: 'test', name: 'test' },
]);
}
return this._super(...arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('.o_field_widget[name="display_name"]'), 'test');
await testUtils.fields.editInput(form.$('.o_field_widget[name="name"]'), 'test');
window.dispatchEvent(new Event("beforeunload"));
await testUtils.nextTick();
form.destroy();
});
QUnit.test('Auto save: save on closing tab/browser (pending change)', async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
fieldDebounce: 1000,
arch: `<form><field name="foo"/></form>`,
res_id: 1,
mockRPC(route, { args, method }) {
assert.step(method);
if (method === 'write') {
assert.deepEqual(args, [[1], { foo: 'test' }]);
}
return this._super(...arguments);
},
});
await testUtils.form.clickEdit(form);
// edit 'foo' but do not focusout -> the model isn't aware of the change
// until the 'beforeunload' event is triggered
form.$('.o_field_widget[name="foo"]').val('test');
await testUtils.dom.triggerEvent(form.$('.o_field_widget[name="foo"]'), 'input');
window.dispatchEvent(new Event("beforeunload"));
await testUtils.nextTick();
assert.verifySteps(['read', 'write']);
form.destroy();
});
QUnit.test('Auto save: save on closing tab/browser (onchanges + pending change)', async function (assert) {
assert.expect(5);
this.data.partner.onchanges = {
display_name: function (obj) {
obj.name = `copy: ${obj.display_name}`;
},
};
const def = testUtils.makeTestPromise();
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
fieldDebounce: 1000,
arch: `
<form>
<field name="display_name"/>
<field name="name"/>
<field name="foo"/>
</form>`,
res_id: 1,
mockRPC(route, { args, method }) {
assert.step(method);
if (method === 'onchange') {
return def;
}
if (method === 'write') {
assert.deepEqual(args, [
[1],
{ display_name: 'test', name: 'test', foo: 'test' },
]);
}
return this._super(...arguments);
},
});
await testUtils.form.clickEdit(form);
// edit 'display_name' and simulate a focusout (trigger the 'change' event)
// -> notifies the model of the change and performs the onchange
form.$('.o_field_widget[name="display_name"]').val('test');
await testUtils.dom.triggerEvent(form.$('.o_field_widget[name="display_name"]'), 'change');
// edit 'name' and simulate a focusout (trigger the 'change' event)
// -> waits for the mutex (i.e. the onchange) to notify the model
form.$('.o_field_widget[name="name"]').val('test');
await testUtils.dom.triggerEvent(form.$('.o_field_widget[name="name"]'), 'change');
// edit 'foo' but do not focusout -> the model isn't aware of the change
// until the 'beforeunload' event is triggered
form.$('.o_field_widget[name="foo"]').val('test');
await testUtils.dom.triggerEvent(form.$('.o_field_widget[name="foo"]'), 'input');
// trigger the 'beforeunload' event -> notifies the model directly and saves
window.dispatchEvent(new Event("beforeunload"));
await testUtils.nextTick();
assert.verifySteps(['read', 'onchange', 'write']);
form.destroy();
});
QUnit.test('Auto save: save on closing tab/browser (onchanges + invalid field)', async function (assert) {
assert.expect(3);
this.data.partner.onchanges = {
display_name: function (obj) {
obj.name = `copy: ${obj.display_name}`;
},
};
const def = testUtils.makeTestPromise();
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
<field name="name" required="1"/>
</group>
</form>`,
res_id: 1,
mockRPC(route, { method }) {
assert.step(method);
if (method === 'onchange') {
return def;
}
if (method === 'write') {
throw new Error('Should not save the record');
}
return this._super(...arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('.o_field_widget[name="display_name"]'), 'test');
await testUtils.fields.editInput(form.$('.o_field_widget[name="name"]'), '');
window.dispatchEvent(new Event("beforeunload"));
await testUtils.nextTick();
assert.verifySteps(['read', 'onchange']);
form.destroy();
});
QUnit.test('Auto save: click on save and save on closing tab/browser', async function (assert) {
const def = testUtils.makeTestPromise();
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
<field name="name" required="1"/>
</group>
</form>`,
res_id: 1,
async mockRPC(route, { method, model }) {
if (method === "write" && model === "partner") {
assert.step("write");
await def;
}
return this._super(...arguments);
},
});
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('.o_field_widget[name="display_name"]'), 'test');
await testUtils.form.clickSave(form);
window.dispatchEvent(new Event("beforeunload"));
await testUtils.nextTick();
def.resolve();
assert.verifySteps(['write']);
form.destroy();
});
QUnit.test('Quick Edition: click on a quick editable field', async function (assert) {
assert.expect(3);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
await testUtils.dom.click(form.$('.o_field_widget[name="display_name"]'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.strictEqual(document.activeElement, $('.o_field_widget[name="display_name"]')[0]);
form.destroy();
});
QUnit.test('Quick Edition: click on a non quick editable field', async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="priority" widget="priority"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(form, '.o_priority_star[aria-checked="true"]');
await testUtils.dom.click(form.$('.o_field_widget[name="priority"] a:first'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, '.o_priority_star[aria-checked="true"]');
form.destroy();
});
QUnit.test('Quick Edition: Label click', async function (assert) {
assert.expect(3);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="foo"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
await testUtils.dom.click(form.$('.o_form_label:first'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.strictEqual(document.activeElement, form.$('input.o_field_widget[name="foo"]')[0]);
form.destroy();
});
QUnit.test('Quick Edition: Label click (duplicated field)', async function (assert) {
assert.expect(8);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<div class="o_td_label" invisible="1">
<label for="foo" string="A"/>
</div>
<field name="foo" nolabel="1" invisible="1"/>
<div class="o_td_label">
<label for="foo" string="B"/>
</div>
<field name="foo" nolabel="1"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsN(form, '.o_form_label', 2);
assert.containsOnce(form, '.o_invisible_modifier .o_form_label');
await testUtils.dom.click(form.$('.o_form_label')[1]);
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsN(form, '.o_form_label', 2);
assert.containsOnce(form, '.o_invisible_modifier .o_form_label');
assert.containsOnce(form, 'input.o_field_widget[name="foo"]');
assert.strictEqual(document.activeElement, form.$('input.o_field_widget[name="foo"]')[0]);
form.destroy();
});
QUnit.test('Quick Edition: Checkbox click', async function (assert) {
assert.expect(11);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="bar"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, '.o_field_boolean input:checked');
assert.containsNone(form, '.o_field_boolean input:disabled');
await testUtils.dom.click(form.$('.o_field_widget[name="bar"]'));
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsNone(form, '.o_field_boolean input:checked');
await testUtils.form.clickSave(form);
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(form, '.o_field_boolean input:checked');
assert.containsNone(form, '.o_field_boolean input:disabled');
await testUtils.dom.click(form.$('.o_field_widget[name="bar"]'));
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsOnce(form, '.o_field_boolean input:checked');
assert.containsNone(form, '.o_field_boolean input:disabled');
form.destroy();
});
QUnit.test('Quick Edition: Checkbox click on label', async function (assert) {
assert.expect(8);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="bar"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, '.o_field_boolean input:checked');
await testUtils.dom.click(form.$('.o_td_label .o_form_label'));
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsNone(form, '.o_field_boolean input:checked');
await testUtils.form.clickSave(form);
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(form, '.o_field_boolean input:checked');
await testUtils.dom.click(form.$('.o_td_label .o_form_label'));
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsOnce(form, '.o_field_boolean input:checked');
form.destroy();
});
QUnit.test('Quick Edition: Readonly one2many list', async function (assert) {
assert.expect(4);
this.data.partner.records[0].p.push(2);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="p" attrs="{'readonly': True}">
<tree>
<field name="foo"/>
</tree>
</field>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(form, '.o_field_x2many_list_row_add',
'create line should not be displayed');
assert.containsNone(form, '.o_list_record_remove',
'remove buttons should not be displayed');
await testUtils.dom.click(form.$('.o_field_cell:first'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly',
'should not switch into edit mode');
form.destroy();
});
QUnit.test('Quick Edition: Readonly one2many list (non editable form)', async function (assert) {
assert.expect(7);
this.data.partner.records[0].p.push(2);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form edit="0">
<field name="p">
<tree>
<field name="foo"/>
</tree>
<form>
<field name="foo"/>
</form>
</field>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(document.body, '.modal');
assert.containsNone(form, '.o_field_x2many_list_row_add a', 'no add button should be displayed');
assert.containsNone(form, '.o_list_record_remove', 'no remove button should be displayed');
await testUtils.dom.click(form.$('.o_field_cell:first'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly', 'should not switch into edit mode');
assert.containsOnce(document.body, '.modal');
assert.containsOnce(document.body, '.modal span.o_field_widget[name="foo"]');
form.destroy();
});
QUnit.test('Quick Edition: Editable one2many list (click cell: editable)', async function (assert) {
assert.expect(3);
this.data.partner.records[0].p.push(2);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="p">
<tree editable="bottom">
<field name="foo"/>
</tree>
</field>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
await testUtils.dom.click(form.$('.o_field_cell[name="foo"]'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable',
'should switch into edit mode');
assert.strictEqual(document.activeElement, form.$('.o_field_cell[name="foo"] input')[0]);
form.destroy();
});
QUnit.test('Quick Edition: Editable one2many list (click cell: not editable)', async function (assert) {
assert.expect(5);
this.data.partner.records[0].p.push(2);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="p">
<tree>
<field name="foo"/>
</tree>
<form>
<field name="foo"/>
</form>
</field>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(document.body, '.modal');
await testUtils.dom.click(form.$('.o_field_cell[name="foo"]'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable',
'should switch into edit mode');
assert.containsOnce(document.body, '.modal');
assert.containsOnce(document.body, '.modal input.o_field_widget[name="foo"]');
form.destroy();
});
QUnit.test('Quick Edition: Editable one2many list (add a line: editable)', async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="p">
<tree editable="bottom">
<field name="foo"/>
</tree>
</field>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, '.o_field_x2many_list_row_add',
'create line should be displayed');
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable',
'should switch into edit mode');
assert.strictEqual(document.activeElement, form.$('.o_field_cell[name="foo"] input')[0]);
form.destroy();
});
QUnit.test('Quick Edition: Editable one2many list (add a line: not editable)', async function (assert) {
assert.expect(5);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="p">
<tree>
<field name="foo"/>
</tree>
<form>
<field name="foo"/>
</form>
</field>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(document.body, '.modal');
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable',
'should switch into edit mode');
assert.containsOnce(document.body, '.modal');
assert.containsOnce(document.body, '.modal input.o_field_widget[name="foo"]');
form.destroy();
});
QUnit.test('Quick Edition: Editable one2many list (drop a line: editable)', async function (assert) {
assert.expect(6);
this.data.partner.records[0].p = [1, 2];
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="p">
<tree editable="bottom">
<field name="foo"/>
</tree>
</field>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsN(form, '.o_list_record_remove', 2,
'remove buttons should be displayed');
assert.strictEqual(form.$('.o_field_cell[name="foo"]').text(), 'yopblip');
await testUtils.dom.click(form.$('.o_list_record_remove button')[0]);
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable',
'should switch into edit mode');
assert.containsOnce(form, '.o_data_row', 'only one record should remain');
assert.strictEqual(form.$('.o_field_cell[name="foo"]').text(), 'blip');
form.destroy();
});
QUnit.test('Quick Edition: Editable one2many list (drop a line: not editable)', async function (assert) {
assert.expect(6);
this.data.partner.records[0].p = [1, 2];
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="p">
<tree>
<field name="foo"/>
</tree>
</field>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsN(form, '.o_list_record_remove', 2,
'remove buttons should be displayed');
assert.strictEqual(form.$('.o_field_cell[name="foo"]').text(), 'yopblip');
await testUtils.dom.click(form.$('.o_list_record_remove button')[0]);
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable',
'should switch into edit mode');
assert.containsOnce(form, '.o_data_row', 'only one record should remain');
assert.strictEqual(form.$('.o_field_cell[name="foo"]').text(), 'blip');
form.destroy();
});
QUnit.test('Quick Edition: Date picker', async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="date"/>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
await testUtils.dom.click(form.$('.o_field_widget[name="date"]'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable',
'should switch into edit mode');
assert.strictEqual(document.activeElement, form.$('.o_field_widget[name="date"] input')[0]);
assert.containsOnce(document.body, '.bootstrap-datetimepicker-widget');
form.destroy();
});
QUnit.test('Quick Edition: Many2one', async function (assert) {
assert.expect(5);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="trululu"/>
</form>`,
res_id: 1,
mockRPC(route, { method }) {
assert.step(method);
return this._super(...arguments);
},
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
await testUtils.dom.click(form.$('.o_field_widget[name="trululu"]'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.verifySteps(['read', 'get_formview_action'])
form.destroy();
});
QUnit.test('Quick Edition: Many2Many', async function (assert) {
assert.expect(6);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="timmy">
<tree>
<field name="display_name"/>
</tree>
<form>
<field name="display_name"/>
</form>
</field>
</form>`,
archs: {
'partner_type,false,list': '<tree><field name="display_name"/></tree>',
'partner_type,false,search': '<search><field name="display_name"/></search>',
},
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, '.o_field_x2many_list_row_add',
'create line should be displayed');
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.nextTick(); // wait for quick edit
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable',
'should switch into edit mode');
assert.containsOnce(document.body, '.modal',
'should display a dialog');
assert.containsNone(form, '.o_field_many2many[name="timmy"] .o_data_row');
await testUtils.dom.click($('.modal .o_legacy_list_view .o_data_row')[0]);
assert.containsOnce(form, '.o_field_many2many[name="timmy"] .o_data_row');
form.destroy();
});
QUnit.test('Quick Edition: Many2Many checkbox', async function (assert) {
assert.expect(7);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="timmy" widget="many2many_checkboxes"/>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(form, 'input[type="checkbox"]:checked');
assert.containsNone(form, 'input[type="checkbox"]:disabled');
await testUtils.dom.click(form.$('.o_field_widget[name="timmy"] label:eq(1)'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsOnce(form, 'input[type="checkbox"]:checked');
assert.containsOnce(form, 'input[type="checkbox"]:eq(1):checked');
assert.containsNone(form, 'input[type="checkbox"]:disabled');
form.destroy();
});
QUnit.test('Quick Edition: Many2Many checkbox readonly', async function (assert) {
assert.expect(6);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="timmy" widget="many2many_checkboxes"
attrs="{'readonly': 1}"/>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(form, 'input[type="checkbox"]:checked');
assert.containsNone(form, 'input[type="checkbox"]:not(:disabled)');
await testUtils.dom.click(form.$('.o_field_widget[name="timmy"] label:eq(1)'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(form, 'input[type="checkbox"]:not(:disabled)');
assert.containsNone(form, 'input[type="checkbox"]:checked');
form.destroy();
});
QUnit.test('Quick Edition: Many2Many checkbox click on label', async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="timmy" widget="many2many_checkboxes"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(form, 'input[type="checkbox"]:checked');
await testUtils.dom.click(form.$('.o_td_label .o_form_label'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsNone(form, 'input[type="checkbox"]:checked');
form.destroy();
});
QUnit.test('Quick Edition: Many2one radio', async function (assert) {
assert.expect(6);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="trululu" widget="radio"/>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsNone(form, 'input[type="radio"]:eq(1):checked');
assert.containsNone(form, 'input[type="radio"]:disabled');
await testUtils.dom.click(form.$('.o_field_widget[name="trululu"] label:eq(1)'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsOnce(form, 'input[type="radio"]:eq(1):checked');
assert.containsNone(form, 'input[type="radio"]:disabled');
form.destroy();
});
QUnit.test('Quick Edition: Many2Many radio readonly', async function (assert) {
assert.expect(6);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="trululu" widget="radio"
attrs="{'readonly': 1}"/>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, 'input[type="radio"]:eq(2):checked');
assert.containsNone(form, 'input[type="radio"]:not(:disabled)');
await testUtils.dom.click(form.$('.o_field_widget[name="trululu"] label:eq(1)'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, 'input[type="radio"]:eq(2):checked');
assert.containsNone(form, 'input[type="radio"]:not(:disabled)');
form.destroy();
});
QUnit.test('Quick Edition: Many2one radio click on label', async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="trululu" widget="radio"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, 'input[type="radio"]:eq(2):checked');
await testUtils.dom.click(form.$('.o_td_label .o_form_label'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsOnce(form, 'input[type="radio"]:eq(2):checked');
form.destroy();
});
QUnit.test('Quick Edition: Selection radio click on value', async function (assert) {
assert.expect(5);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="state" widget="radio"/>
</group>
</form>`,
res_id: 1,
mockRPC: function (route, args) {
if (args.model === 'partner' && args.method === 'write') {
assert.step('Write');
}
return this._super(route, args);
},
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, 'input[type="radio"]:eq(0):checked');
// click on the last value
await testUtils.dom.click(form.$('.o_radio_item .o_form_label:contains(EF)'));
// should be switched in edit mode
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsOnce(form, 'input[type="radio"]:eq(2):checked');
assert.verifySteps([], "No write RPC done");
form.destroy();
});
QUnit.test('Quick Edition: non-editable form', async function (assert) {
assert.expect(3);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form edit="0">
<group>
<field name="foo"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
await testUtils.dom.click(form.$('.o_form_label'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
await testUtils.dom.click(form.$('.o_field_widget'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
form.destroy();
});
QUnit.test('Quick Edition: CopyToClipboard click on value', async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="foo" widget="CopyClipboardChar"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, '.o_clipboard_button');
await testUtils.dom.click(form.$('.o_field_copy'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
assert.containsNone(form, '.o_clipboard_button');
form.destroy();
});
QUnit.test('Quick Edition: CopyToClipboard click on copy button', async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="foo" widget="CopyClipboardChar"/>
</group>
</form>`,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, '.o_clipboard_button');
await testUtils.dom.click(form.$('.o_field_copy .o_clipboard_button'));
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.containsOnce(form, '.o_clipboard_button');
form.destroy();
});
QUnit.test('Quick Edition: selecting text of quick editable field', async function (assert) {
assert.expect(8);
const MULTI_CLICK_DELAY = 6498651354; // arbitrary large number to identify setTimeout calls
let quickEditCB;
let quickEditTimeoutId;
let nextId = 1;
const originalSetTimeout = window.setTimeout;
const originalClearTimeout = window.clearTimeout;
patchWithCleanup(window, {
setTimeout(fn, delay) {
if (delay === MULTI_CLICK_DELAY) {
quickEditCB = fn;
quickEditTimeoutId = `quick_edit_${nextId++}`;
return quickEditTimeoutId;
} else {
return originalSetTimeout(...arguments);
}
},
clearTimeout(id) {
if (id === quickEditTimeoutId) {
quickEditCB = undefined;
} else {
return originalClearTimeout(...arguments);
}
},
});
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
</group>
</form>`,
formMultiClickTime: MULTI_CLICK_DELAY,
res_id: 1,
});
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
// text selected by holding and dragging doesn't start quick edit
window.getSelection().removeAllRanges();
const range = document.createRange();
await range.selectNode(form.$('.o_field_widget[name="display_name"]')[0]);
window.getSelection().addRange(range);
await testUtils.dom.click(form.$('.o_field_widget[name="display_name"]'));
await testUtils.nextTick();
assert.strictEqual(quickEditCB, undefined, "no quickEdit callback should have been set");
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
// double click selecting text doesn't start quick edit
window.getSelection().removeAllRanges();
testUtils.dom.click(form.$('.o_field_widget[name="display_name"]'));
range.selectNode(form.$('.o_field_widget[name="display_name"]')[0]);
window.getSelection().addRange(range);
await testUtils.dom.click(form.$('.o_field_widget[name="display_name"]'));
await testUtils.nextTick();
assert.strictEqual(quickEditCB, undefined, "no quickEdit callback should have been set");
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
// quick edit happens after timeout
window.getSelection().removeAllRanges();
await testUtils.dom.click(form.$('.o_field_widget[name="display_name"]'));
await testUtils.nextTick();
assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
assert.ok(quickEditCB, "quickEdit callback should have been set");
quickEditCB();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
form.destroy();
});
QUnit.test('Quick Edition: do not bounce edit button when click on label', async function (assert) {
assert.expect(1);
const MULTI_CLICK_TIME = 50;
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
</group>
</form>`,
formMultiClickTime: MULTI_CLICK_TIME,
res_id: 1,
});
await testUtils.dom.click(form.$('.o_form_label'));
assert.containsNone(form, 'button.o_catch_attention:visible');
form.destroy();
});
QUnit.test('Quick Edition: do not bounce edit button when click on field char', async function (assert) {
assert.expect(1);
const MULTI_CLICK_TIME = 50;
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="display_name"/>
</group>
</form>`,
formMultiClickTime: MULTI_CLICK_TIME,
res_id: 1,
});
await testUtils.dom.click(form.$('.o_field_widget'));
assert.containsNone(form, 'button.o_catch_attention:visible');
form.destroy();
});
QUnit.test('Quick Edition: do not bounce edit button when click on field boolean', async function (assert) {
assert.expect(1);
const MULTI_CLICK_TIME = 50;
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<group>
<field name="bar"/>
</group>
</form>`,
formMultiClickTime: MULTI_CLICK_TIME,
res_id: 1,
});
await testUtils.dom.click(form.$('.o_field_widget'));
assert.containsNone(form, 'button.o_catch_attention:visible');
form.destroy();
});
QUnit.test("attach callbacks with long processing in __renderView", async function (assert) {
/**
* The main use case of this test is discuss, in which the FormRenderer
* __renderView method is overridden to perform asynchronous tasks (the
* update of the chatter Component) resulting in a delay between the
* appending of the new form content into its element and the
* "on_attach_callback" calls. This is the purpose of "__renderView"
* which is meant to do all the async work before the content is appended.
*/
assert.expect(11);
let testPromise = Promise.resolve();
const Renderer = FormRenderer.extend({
on_attach_callback() {
assert.step("form.on_attach_callback");
this._super(...arguments);
},
async __renderView() {
const _super = this._super.bind(this);
await testPromise;
return _super();
},
});
// Setup custom field widget
fieldRegistry.add("customwidget", AbstractField.extend({
className: "custom-widget",
on_attach_callback() {
assert.step("widget.on_attach_callback");
},
}));
const form = await createView({
arch: `<form><field name="bar" widget="customwidget"/></form>`,
data: this.data,
model: 'partner',
res_id: 1,
View: FormView.extend({
config: Object.assign({}, FormView.prototype.config, { Renderer }),
}),
});
assert.containsOnce(form, ".custom-widget");
assert.verifySteps([
"form.on_attach_callback", // Form attached
"widget.on_attach_callback", // Initial widget attached
]);
const initialWidget = form.$(".custom-widget")[0];
testPromise = testUtils.makeTestPromise();
await testUtils.form.clickEdit(form);
assert.containsOnce(form, ".custom-widget");
assert.strictEqual(initialWidget, form.$(".custom-widget")[0], "Widgets have yet to be replaced");
assert.verifySteps([]);
testPromise.resolve();
await testUtils.nextTick();
assert.containsOnce(form, ".custom-widget");
assert.notStrictEqual(initialWidget, form.$(".custom-widget")[0], "Widgets have been replaced");
assert.verifySteps([
"widget.on_attach_callback", // New widget attached
]);
form.destroy();
delete fieldRegistry.map.customwidget;
});
QUnit.test('field "length" with value 0: can apply onchange', async function (assert) {
assert.expect(1);
this.data.partner.fields.length = {string: 'Length', type: 'float', default: 0 };
this.data.partner.fields.foo.default = "foo default";
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="foo"/><field name="length"/></form>',
});
assert.strictEqual(form.$('input[name=foo]').val(), "foo default",
"should contain input with initial value");
form.destroy();
});
QUnit.test('field "length" with value 0: readonly fields are not sent when saving', async function (assert) {
assert.expect(3);
this.data.partner.fields.length = {string: 'Length', type: 'float', default: 0 };
this.data.partner.fields.foo.default = "foo default";
// define an onchange on display_name to check that the value of readonly
// fields is correctly sent for onchanges
this.data.partner.onchanges = {
display_name: function () {},
p: function () {},
};
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `<form string="Partners">
<field name="p">
<tree>
<field name="display_name"/>
</tree>
<form string="Partners">
<field name="length"/>
<field name="display_name"/>
<field name="foo" attrs="{\'readonly\': [[\'display_name\', \'=\', \'readonly\']]}"/>
</form>
</field>
</form>`,
mockRPC: function (route, args) {
if (args.method === 'create') {
assert.deepEqual(args.args[0], {
p: [[0, args.args[0].p[0][1], {length: 0, display_name: 'readonly'}]]
}, "should not have sent the value of the readonly field");
}
return this._super.apply(this, arguments);
},
});
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
assert.containsOnce(document.body, '.modal input.o_field_widget[name=foo]',
'foo should be editable');
await testUtils.fields.editInput($('.modal .o_field_widget[name=foo]'), 'foo value');
await testUtils.fields.editInput($('.modal .o_field_widget[name=display_name]'), 'readonly');
assert.containsOnce(document.body, '.modal span.o_field_widget[name=foo]',
'foo should be readonly');
await testUtils.dom.clickFirst($('.modal-footer .btn-primary'));
await testUtils.form.clickSave(form); // save the record
form.destroy();
});
});
});