Files
odoo_source/addons/web/static/tests/views/form_tests.js
T
Aaron Bohy 1e8f6d01ff [FIX] web,*: send correct context on button clicked
*account_asset

When a button of type 'action' is clicked (execute a given action),
special keys must be set in the context (active_id, active_ids and
active_model). They must be computed regarding the record containing
the clicked button, i.e. if we are in a modal, it must be the id
and model of the record displayed in the modal.

Before this rev., we always sent the id and model of the record
displayed in the background (i.e. the id and model of the url).

This caused a bug in MRP that could be reproduced as follows:
- open a manufacturing order in form view
- click on edit
- click on one of the line of the one2many, and click on the green
  icon to edit a product
- click on the update product quantity button on top of it
- the product field must be correctly filled, which was not the
  case before this rev.
2017-08-10 08:48:32 +02:00

5573 lines
218 KiB
JavaScript

odoo.define('web.form_tests', function (require) {
"use strict";
var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var _t = core._t;
var createView = testUtils.createView;
var createAsyncView = testUtils.createAsyncView;
QUnit.module('Views', {
beforeEach: function () {
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"},
},
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",
}],
onchanges: {},
},
product: {
fields: {
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},
]
},
};
}
}, function () {
QUnit.module('FormView');
QUnit.test('simple form rendering', function (assert) {
assert.expect(12);
var form = 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.strictEqual(form.$('div.test').length, 1,
"should contain a div with some html");
assert.strictEqual(form.$('div.test').css('opacity'), "0.5",
"should keep the inline style on html elements");
assert.strictEqual(form.$('label:contains(Foo)').length, 1,
"should contain label Foo");
assert.strictEqual(form.$('span:contains(blip)').length, 1,
"should contain span with field value");
assert.strictEqual(form.$('.o_group .o_group:first').attr('style'), 'background-color: red',
"should apply style attribute on groups");
assert.strictEqual(form.$('.o_field_widget[name=foo]').attr('style'), 'color: blue',
"should apply style attribute on fields");
assert.strictEqual(form.$('label:contains(something_id)').length, 0,
"should not contain f3 string description");
assert.strictEqual(form.$('label:contains(f3_description)').length, 1,
"should contain custom f3 string description");
assert.strictEqual(form.$('div.o_field_one2many table').length, 1,
"should render a one2many relation");
assert.strictEqual(form.$('tbody td:not(.o_list_record_selector) .o_checkbox input:checked').length, 1,
"1 checkboxes should be checked");
assert.strictEqual(form.get('title'), "second record",
"title should be display_name of record");
assert.strictEqual(form.$('label.o_form_label_empty:contains(timmy)').length, 0,
"the many2many label shouldn't be marked as empty");
form.destroy();
});
QUnit.test('only necessary fields are fetched with correct context', function (assert) {
assert.expect(2);
var form = 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', function (assert) {
assert.expect(1);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('table.o_inner_group').length, 1,
"should contain an inner group");
form.destroy();
});
QUnit.test('invisible fields are properly hidden', function (assert) {
assert.expect(3);
var form = 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"/>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('label.o_invisible_modifier:contains(Foo)').length, 1,
"should not contain label Foo");
assert.strictEqual(form.$('span.o_invisible_modifier:contains(yop)').length, 1,
"should not contain span with field value");
assert.strictEqual(form.$('.o_field_widget.o_invisible_modifier:contains(0.4)').length, 1,
"field qux should be invisible");
form.destroy();
});
QUnit.test('invisible elements are properly hidden', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<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.strictEqual(form.$('.o_notebook li.o_invisible_modifier a:contains(invisible)').length, 1,
"should not display tab invisible");
assert.strictEqual(form.$('table.o_inner_group.o_invisible_modifier td:contains(invgroup)').length, 1,
"should not display invisible groups");
form.destroy();
});
QUnit.test('invisible attrs on fields are re-evaluated on field change', 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 = 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,
});
form.$buttons.find('.o_form_button_edit').click();
assert.ok(form.$('.foo_field').hasClass('o_invisible_modifier'), 'should not display foo field');
assert.ok(form.$('.bar_field').hasClass('o_invisible_modifier'), 'should not display bar field');
// set a value on the m2o
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
form.$('.o_field_many2one input').click();
$dropdown.find('li:first()').click();
assert.ok(!form.$('.foo_field').hasClass('o_invisible_modifier'), 'should display foo field');
form.destroy();
});
QUnit.test('properly handle modifiers and attributes on notebook tags', function (assert) {
assert.expect(2);
var form = 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.ok(form.$('.o_notebook').hasClass('o_invisible_modifier'),
'the notebook should handle modifiers (invisible)');
assert.ok(form.$('.o_notebook').hasClass('new_class'),
'the notebook should handle attributes');
form.destroy();
});
QUnit.test('invisible attrs on first notebook page', function (assert) {
assert.expect(6);
var form = 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,
});
form.$buttons.find('.o_form_button_edit').click();
assert.ok(form.$('.o_notebook .nav li:first()').hasClass('active'),
'first tab should be active');
assert.ok(!form.$('.o_notebook .nav li:first()').hasClass('o_invisible_modifier'),
'first tab should be visible');
// set a value on the m2o
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
form.$('.o_field_many2one input').click();
$dropdown.find('li:first()').click();
assert.ok(!form.$('.o_notebook .nav li:first()').hasClass('active'),
'first tab should not be active');
assert.ok(form.$('.o_notebook .nav li:first()').hasClass('o_invisible_modifier'),
'first tab should be invisible');
assert.ok(form.$('.o_notebook .nav li:nth(1)').hasClass('active'),
'second tab should be active');
assert.ok(form.$('.o_notebook .tab-content .tab-pane:nth(1)').hasClass('active'),
'second page should be active');
form.destroy();
});
QUnit.test('first notebook page invisible', function (assert) {
assert.expect(2);
var form = 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 li:first()').is(':visible'),
'first tab should be invisible');
assert.ok(form.$('.o_notebook .nav li:nth(1)').hasClass('active'),
'second tab should be active');
form.destroy();
});
QUnit.test('invisible attrs on group are re-evaluated on field change', function (assert) {
assert.expect(2);
var form = 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
});
assert.strictEqual(form.$('div.o_group:visible').length, 1, "should display the group");
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_boolean input').click();
assert.strictEqual(form.$('div.o_group:hidden').length, 1, "should not display the group");
form.destroy();
});
QUnit.test('invisible attrs with zero value in domain and unset value in data', function (assert) {
assert.expect(1);
this.data.partner.fields.int_field.type = 'monetary';
var form = 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.notOk(form.$('div.hello').is(':visible'),
"attrs invisible should have been computed and applied");
form.destroy();
});
QUnit.test('rendering stat buttons', function (assert) {
assert.expect(3);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<div name="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.strictEqual(form.$('button.oe_stat_button').length, 2,
"should have 2 stat buttons");
assert.strictEqual(form.$('button.oe_stat_button.o_invisible_modifier').length, 1,
"should have 1 invisible stat buttons");
var count = 0;
testUtils.intercept(form, "execute_action", function () {
count++;
});
form.$('.oe_stat_button').first().click();
assert.strictEqual(count, 1, "should have triggered a execute action");
form.destroy();
});
QUnit.test('label uses the string attribute', function (assert) {
assert.expect(1);
var form = 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.strictEqual(form.$('label.o_form_label:contains(customstring)').length, 1,
"should have 1 label with correct string");
form.destroy();
});
QUnit.test('readonly attrs on fields are re-evaluated on field change', function (assert) {
assert.expect(3);
var form = 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,
});
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('span[name="foo"]').length, 1,
"the foo field widget should be readonly");
form.$('.o_field_boolean input').click();
assert.strictEqual(form.$('input[name="foo"]').length, 1,
"the foo field widget should have been rerendered to now be editable");
form.$('.o_field_boolean input').click();
assert.strictEqual(form.$('span[name="foo"]').length, 1,
"the foo field widget should have been rerendered to now be readonly again");
form.destroy();
});
QUnit.test('empty fields have o_form_empty class in readonly mode', 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 = 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.strictEqual(form.$('.o_field_widget.o_field_empty').length, 2,
"should have 2 empty fields with correct class");
assert.strictEqual(form.$('.o_form_label_empty').length, 2,
"should have 2 muted labels (for the empty fieds) in readonly");
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_field_empty').length, 1,
"in edit mode, only empty readonly fields should have the o_field_empty class");
assert.strictEqual(form.$('.o_form_label_empty').length, 1,
"in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class");
form.$('input[name="foo"]').val("test").trigger("input");
assert.strictEqual(form.$('.o_field_empty').length, 0,
"after readonly modifier change, the o_field_empty class should have been removed");
assert.strictEqual(form.$('.o_form_label_empty').length, 0,
"after readonly modifier change, the o_form_label_empty class should have been removed");
form.$('input[name="foo"]').val("hello").trigger("input");
assert.strictEqual(form.$('.o_field_empty').length, 1,
"after value changed to false for a readonly field, the o_field_empty class should have been added");
assert.strictEqual(form.$('.o_form_label_empty').length, 1,
"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', 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].int_field = false; // 1 is record with id=2
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
'<field name="int_field" attrs="{\'readonly\': [[\'foo\', \'=\', \'readonly\']]}"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
});
assert.strictEqual(form.$('.o_field_widget.o_field_empty').length, 1,
"should have 1 empty field with correct class");
assert.strictEqual(form.$('.o_form_label_empty').length, 1,
"should have 1 muted label (for the empty fied) in readonly");
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_field_empty').length, 0,
"in edit mode, only empty readonly fields should have the o_field_empty class");
assert.strictEqual(form.$('.o_form_label_empty').length, 0,
"in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class");
form.$('input[name="foo"]').val("readonly").trigger("input"); // int_field is now rerendered as readonly
form.$('input[name="foo"]').val("edit").trigger("input"); // int_field is now rerendered as editable
form.$('input[name="int_field"]').val('1').trigger("input"); // int_field is now set
form.$('input[name="foo"]').val("readonly").trigger("input"); // int_field is now rerendered as readonly
assert.strictEqual(form.$('.o_field_empty').length, 0,
"there still should not be any empty class on fields as the readonly one is now set");
assert.strictEqual(form.$('.o_form_label_empty').length, 0,
"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', function (assert) {
assert.expect(2);
this.data.partner.fields.product_id.readonly = true;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<group>' +
'<field name="product_id"/>' +
'</group>' +
'</group>' +
'</sheet>' +
'</form>',
});
assert.strictEqual(form.$('.o_form_label_empty').length, 0,
"no empty class on label");
assert.strictEqual(form.$('.o_field_empty').length, 0,
"no empty class on field");
form.destroy();
});
QUnit.test('form view can switch to edit mode', function (assert) {
assert.expect(9);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'),
'form view should be .o_form_readonly');
assert.ok(form.$buttons.find('.o_form_buttons_view').is(':visible'),
'readonly buttons should be visible');
assert.ok(!form.$buttons.find('.o_form_buttons_edit').is(':visible'),
'edit buttons should not be visible');
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode');
assert.ok(form.$('.o_form_view').hasClass('o_form_editable'),
'form view should be .o_form_editable');
assert.ok(!form.$('.o_form_view').hasClass('o_form_readonly'),
'form view should not be .o_form_readonly');
assert.ok(!form.$buttons.find('.o_form_buttons_view').is(':visible'),
'readonly buttons should not be visible');
assert.ok(form.$buttons.find('.o_form_buttons_edit').is(':visible'),
'edit buttons should be visible');
form.destroy();
});
QUnit.test('required attrs on fields are re-evaluated on field change', function (assert) {
assert.expect(3);
var form = 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,
});
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input[name="foo"].o_required_modifier').length, 1,
"the foo field widget should be required");
form.$('.o_field_boolean input').click();
assert.strictEqual(form.$('input[name="foo"]:not(.o_required_modifier)').length, 1,
"the foo field widget should now have been marked as non-required");
form.$('.o_field_boolean input').click();
assert.strictEqual(form.$('input[name="foo"].o_required_modifier').length, 1,
"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', function (assert) {
assert.expect(2);
this.data.partner.fields.foo.required = true;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('span.o_required_modifier').length, 1,
"should have 1 span with o_required_modifier class");
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input.o_required_modifier').length, 1,
"in edit mode, should have 1 input with o_required_modifier");
form.destroy();
});
QUnit.test('required float fields works as expected', function (assert) {
assert.expect(10);
this.data.partner.fields.qux.required = true;
var form = 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.ok(form.$('input[name="qux"]').hasClass('o_required_modifier'),
"qux input is flagged as required");
assert.strictEqual(form.$('input[name="qux"]').val(), "0.0",
"qux input is 0 by default (float field)");
form.$buttons.find('.o_form_button_save').click();
assert.notOk(form.$('input[name="qux"]').hasClass('o_field_invalid'),
"qux input is not displayed as invalid");
form.$buttons.find('.o_form_button_edit').click();
form.$('input[name="qux"]').val("1").trigger('input');
form.$buttons.find('.o_form_button_save').click();
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input[name="qux"]').val(), "1.0",
"qux input is properly formatted");
assert.verifySteps(['default_get', 'create', 'read', 'write', 'read']);
form.destroy();
});
QUnit.test('separators', function (assert) {
assert.expect(1);
var form = 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.strictEqual(form.$('div.o_horizontal_separator').length, 1,
"should contain a separator div");
form.destroy();
});
QUnit.test('invisible attrs on separators', function (assert) {
assert.expect(1);
var form = 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.strictEqual(form.$('div.o_horizontal_separator').hasClass('o_invisible_modifier'), true,
"separator div should be hidden");
form.destroy();
});
QUnit.test('buttons in form view', function (assert) {
assert.expect(7);
var rpcCount = 0;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="state" invisible="1"/>' +
'<header>' +
'<button name="post" class="p" states="ab,ef" 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>',
res_id: 2,
mockRPC: function () {
rpcCount++;
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('button.btn.btn-sm div.fa.fa-check').length, 1,
"should contain a button with correct content");
assert.strictEqual(form.$('.o_form_statusbar button').length, 2,
"should have 2 buttons in the statusbar");
assert.strictEqual(form.$('.o_form_statusbar button:visible').length, 1,
"should have only 1 visible button in the statusbar");
testUtils.intercept(form, 'execute_action', function (event) {
assert.strictEqual(event.data.action_data.name, "post",
"should trigger execute_action with correct method name");
assert.deepEqual(event.data.env.currentID, 2, "should have correct id in event data");
event.data.on_success();
event.data.on_closed();
});
rpcCount = 0;
form.$('.o_form_statusbar button.p').click();
assert.strictEqual(rpcCount, 1, "should have done 1 rpcs to reload");
testUtils.intercept(form, 'execute_action', function (event) {
event.data.on_fail();
});
form.$('.o_form_statusbar button.s').click();
assert.strictEqual(rpcCount, 2, "should have done 2 rpcs to reload");
form.destroy();
});
QUnit.test('buttons in form view, new record', function (assert) {
// this simulates a situation similar to the settings forms.
assert.expect(7);
var resID;
var form = 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);
},
});
testUtils.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();
});
form.$('.o_form_statusbar button.p').click();
assert.verifySteps(['default_get', 'create', 'read', 'execute_action', 'read']);
form.destroy();
});
QUnit.test('buttons in form view, new record, with field id in view', 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 = 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);
},
});
testUtils.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();
});
form.$('.o_form_statusbar button.p').click();
assert.verifySteps(['default_get', 'create', 'read', 'execute_action', 'read']);
form.destroy();
});
QUnit.test('change and save char', function (assert) {
assert.expect(6);
var form = 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.strictEqual(form.$('span:contains(blip)').length, 1,
"should contain span with field value");
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode');
form.$('input').val("tralala").trigger('input');
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
assert.strictEqual(form.$('span:contains(tralala)').length, 1,
"should contain span with field value");
form.destroy();
});
QUnit.test('properly reload data from server', function (assert) {
assert.expect(1);
var form = 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,
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input').val("tralala").trigger('input');
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('span:contains(apple)').length, 1,
"should contain span with field value");
form.destroy();
});
QUnit.test('properly apply onchange in simple case', function (assert) {
assert.expect(2);
this.data.partner.onchanges = {
foo: function (obj) {
obj.int_field = obj.foo.length + 1000;
},
};
var form = 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,
});
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input').eq(1).val(), "9",
"should contain input with initial value");
form.$('input').first().val("tralala").trigger('input');
assert.strictEqual(form.$('input').eq(1).val(), "1007",
"should contain input with onchange applied");
form.destroy();
});
QUnit.test('onchange send only the present fields to the server', 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 = 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.display_name": "", "p.product_id": "", "timmy": "", "timmy.name": ""},
"should send only the fields used in the views");
}
return this._super(route, args);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input:first').val("tralala").trigger('input');
form.destroy();
});
QUnit.test('onchange only send present fields value', function (assert) {
assert.expect(1);
this.data.partner.onchanges.foo = function (obj) {};
var form = 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") {
assert.deepEqual(args.args[1], {
display_name: "first record",
foo: "tralala",
id: 1,
p: [[0, false, {"display_name": "valid line", "qux": 12.4}]]
}, "should send the values for the present fields");
}
return this._super(route, args);
},
});
form.$buttons.find('.o_form_button_edit').click();
// add a o2m row
form.$('.o_field_x2many_list_row_add a').click();
form.$('.o_field_one2many input:first').focus();
form.$('.o_field_one2many input:first').val('valid line').trigger('input');
form.$('.o_field_one2many input:last').focus();
form.$('.o_field_one2many input:last').val('12.4').trigger('input');
// trigger an onchange by modifying foo
form.$('input[name="foo"]:first').val("tralala").trigger('input');
form.destroy();
});
QUnit.test('evaluate in python field options', 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 = 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', function (assert) {
assert.expect(5);
var form = 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;
form.$buttons.find('.o_form_button_create').click();
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");
form.$buttons.find('.o_form_button_save').click();
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', function (assert) {
assert.expect(4);
this.data.partner.onchanges.foo = function () {};
var form = 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.display_name': '',
'p.foo': '1'
}, "onchangeSpec should be correct (with sub fields)");
}
return this._super.apply(this, arguments);
},
});
assert.verifySteps(['default_get', 'onchange']);
form.destroy();
});
QUnit.test('sidebar is hidden when switching to edit mode', function (assert) {
assert.expect(3);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'</form>',
viewOptions: {sidebar: true},
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'search_read' && args.model === 'ir.attachment') {
return $.when([]);
}
return this._super.apply(this, arguments);
},
});
assert.ok(!form.sidebar.$el.hasClass('o_hidden'), 'sidebar should be visible');
form.$buttons.find('.o_form_button_edit').click();
assert.ok(form.sidebar.$el.hasClass('o_hidden'), 'sidebar should be invisible');
form.$buttons.find('.o_form_button_cancel').click();
assert.ok(!form.sidebar.$el.hasClass('o_hidden'), 'sidebar should be visible');
form.destroy();
});
QUnit.test('basic default record', function (assert) {
assert.expect(2);
this.data.partner.fields.foo.default = "default foo value";
var count = 0;
var form = 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').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', 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}], // create a new value
[0, 0, {foo: "new foo2", product_id: 37}], // create a new value
];
var nameGetCount = 0;
var form = 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.ok(form.$('td:contains(new foo1)').length,
"should have new foo1 value in one2many");
assert.ok(form.$('td:contains(new foo2)').length,
"should have new foo2 value in one2many");
assert.ok(form.$('td:contains(xphone)').length,
"should have a cell with the name field 'product_id', set to xphone");
assert.strictEqual(nameGetCount, 1, "should have done only 1 nameget");
form.destroy();
});
QUnit.test('make default record with non empty many2one', function (assert) {
var done = assert.async();
assert.expect(2);
this.data.partner.fields.trululu.default = 4;
var nameGetCount = 0;
createAsyncView({
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++;
var result = this._super.apply(this, arguments);
return concurrency.delay(1).then(function () {
return result;
});
}
return this._super.apply(this, arguments);
},
}).then(function (form) {
assert.ok(form.$('input').val(), 'aaa',
'default value should be correctly displayed');
assert.strictEqual(nameGetCount, 1, 'should have done one name_get');
form.destroy();
done();
});
});
QUnit.test('form view properly change its title', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.get('title'), 'first record',
"should have the display name of the record as title");
form.$buttons.find('.o_form_button_create').click();
assert.strictEqual(form.get('title'), _t("New"),
"should have the display name of the record as title");
form.destroy();
});
QUnit.test('can duplicate a record', function (assert) {
assert.expect(3);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
viewOptions: {sidebar: true},
mockRPC: function (route, args) {
if (args.method === 'search_read' && args.model === 'ir.attachment') {
return $.when([]);
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.get('title'), 'first record',
"should have the display name of the record as title");
form.sidebar.$('a:contains(Duplicate)').click();
assert.strictEqual(form.get('title'), 'first record (copy)',
"should have duplicated the record");
assert.strictEqual(form.mode, "edit", 'should be in edit mode');
form.destroy();
});
QUnit.test('cannot duplicate a record', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners" duplicate="false">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
viewOptions: {sidebar: true},
mockRPC: function (route, args) {
if (args.method === 'search_read' && args.model === 'ir.attachment') {
return $.when([]);
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.get('title'), 'first record',
"should have the display name of the record as title");
assert.ok(form.sidebar.$('a:contains(Duplicate)').length === 0,
"should not contains a 'Duplicate' action");
form.destroy();
});
QUnit.test('buttons in footer are moved to $buttons if necessary', function (assert) {
// not sure about this test...
assert.expect(2);
var form = 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: {footer_to_buttons: true},
});
assert.ok(form.$buttons.find('button.infooter').length, "footer button should be in footer");
assert.ok(!form.$('button.infooter').length, "footer button should not be in form");
form.destroy();
});
QUnit.test('clicking on stat buttons in edit mode', function (assert) {
assert.expect(9);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<div name="button_box">' +
'<button class="oe_stat_button">' +
'<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);
},
});
form.$buttons.find('.o_form_button_edit').click();
var count = 0;
testUtils.intercept(form, "execute_action", function (event) {
event.stopPropagation();
count++;
});
form.$('.oe_stat_button').first().click();
assert.strictEqual(count, 1, "should have triggered a execute action");
assert.strictEqual(form.mode, "edit", "form view should be in edit mode");
form.$('input').val("tralala").trigger('input');
form.$('.oe_stat_button').first().click();
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', function (assert) {
assert.expect(2);
var form = 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.getTitle(), 'second record',
"should have correct display_name");
form.$buttons.find('.o_form_button_edit').click();
form.$('input[name="name"]').val('some other name').trigger('input');
form.$('.oe_stat_button').first().click();
assert.strictEqual(form.getTitle(), 'GOLDORAK',
"should have correct display_name");
form.destroy();
});
QUnit.test('buttons with attr "special" do not trigger a save', function (assert) {
assert.expect(4);
var executeActionCount = 0;
var writeCount = 0;
var form = 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="Discard" class="btn-default" special="cancel"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'write') {
writeCount++;
}
return this._super(route, args);
}
});
testUtils.intercept(form, "execute_action", function () {
executeActionCount++;
});
form.$buttons.find('.o_form_button_edit').click();
// make the record dirty
form.$('input').val("tralala").trigger('input');
form.$('button').eq(0).click();
assert.strictEqual(writeCount, 1, "should have triggered a write");
assert.strictEqual(executeActionCount, 1, "should have triggered a execute action");
form.$buttons.find('.o_form_button_edit').click();
form.$('input').val("abcdef").trigger('input');
form.$('button').eq(1).click();
assert.strictEqual(writeCount, 1, "should not have triggered a write");
assert.strictEqual(executeActionCount, 2, "should have triggered a execute action");
form.destroy();
});
QUnit.test('missing widgets do not crash', function (assert) {
assert.expect(1);
this.data.partner.fields.foo.type = 'new field type without widget';
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('.o_field_widget').length, 1, "should have rendered an abstract field");
form.destroy();
});
QUnit.test('nolabel', function (assert) {
assert.expect(6);
var form = 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.strictEqual(form.$("label").length, 2, "should have rendered only two label");
assert.strictEqual(form.$("label").first().text(), "Product",
"one should be the one for the product field");
assert.strictEqual(form.$("label").eq(1).text(), "Bar",
"one should be the one for the bar field");
assert.strictEqual(form.$('.firstgroup td').first().attr('colspan'), undefined,
"foo td should have a default colspan (1)");
assert.strictEqual(form.$('.secondgroup tr').length, 2,
"int_field and qux should have same tr");
assert.strictEqual(form.$('.secondgroup tr:first td').length, 2,
"product_id field should be on its own tr");
form.destroy();
});
QUnit.test('many2one in a one2many', function (assert) {
assert.expect(1);
this.data.partner.records[0].p = [2];
this.data.partner.records[1].product_id = 37;
var form = 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.strictEqual(form.$('td:contains(xphone)').length, 1,
"should display the name of the many2one");
form.destroy();
});
QUnit.test('discard changes on a non dirty form view', function (assert) {
assert.expect(4);
var nbWrite = 0;
var form = 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
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input').val(), 'yop', 'input should contain yop');
// click on discard
form.$buttons.find('.o_form_button_cancel').click();
assert.ok(!$('.modal:visible').length, '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', function (assert) {
assert.expect(7);
var nbWrite = 0;
var form = 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
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input').val(), 'yop', 'input should contain yop');
form.$('input').val('new value').trigger('input');
assert.strictEqual(form.$('input').val(), 'new value', 'input should contain new value');
// click on discard and cancel the confirm request
form.$buttons.find('.o_form_button_cancel').click();
assert.ok($('.modal').length, 'a confirm modal should be displayed');
$('.modal .modal-footer .btn-default').click(); // click on cancel
assert.strictEqual(form.$('input').val(), 'new value', 'input should still contain new value');
// click on discard and confirm
form.$buttons.find('.o_form_button_cancel').click();
assert.ok($('.modal').length, 'a confirm modal should be displayed');
$('.modal .modal-footer .btn-primary').click(); // click on confirm
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)', 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 = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="date"></field></form>',
intercepts: {
switch_to_previous_view: function (event) {
form.update({}, {reload: false});
}
},
});
form.$buttons.find('.o_form_button_cancel').click();
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('span:contains(2017)').length, 1,
"should have a span with the year somewhere");
form.destroy();
});
QUnit.test('discard changes on relational data on new record', function (assert) {
assert.expect(3);
var nbWrite = 0;
var form = 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>',
mockRPC: function (route) {
if (route === '/web/dataset/call_kw/partner/write') {
nbWrite++;
}
return this._super.apply(this, arguments);
},
intercepts: {
switch_to_previous_view: function (event) {
assert.ok(true, "should have sent correct event");
// simulate the response from the view 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});
}
},
});
// switch to edit mode and edit the p field
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_x2many_list_row_add a').click();
form.$('.o_field_many2one input').click();
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
$dropdown.find('li:first()').click();
assert.strictEqual(form.$('input').val(), 'xphone', 'input should contain xphone');
// click on discard and confirm
form.$buttons.find('.o_form_button_cancel').click();
$('.modal .modal-footer .btn-primary').click(); // click on confirm
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', function (assert) {
assert.expect(3);
this.data.partner.fields.foo.default = "ABC";
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
intercepts: {
switch_to_previous_view: function () {
assert.ok(true, "should have sent correct event");
}
}
});
// switch to edit mode and edit the foo field
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input').val(), 'ABC', 'input should contain ABC');
form.$buttons.find('.o_form_button_cancel').click();
assert.strictEqual($('.modal').length, 0,
'there should not be a confirm modal');
form.destroy();
});
QUnit.test('discard changes on a new (dirty) form view', function (assert) {
assert.expect(8);
this.data.partner.fields.foo.default = "ABC";
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
intercepts: {
switch_to_previous_view: function (event) {
assert.ok(true, "should have sent correct event");
// simulate the response from the view 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');
form.$('input').val('DEF').trigger('input');
// discard the changes and check it has properly been discarded
form.$buttons.find('.o_form_button_cancel').click();
assert.strictEqual($('.modal').length, 1,
'there should be a confirm modal');
assert.strictEqual(form.$('input').val(), 'DEF', 'input should be DEF');
$('.modal .modal-footer .btn-primary').click(); // click on confirm
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)
form.$('input').val('GHI').trigger('input');
form.$buttons.find('.o_form_button_cancel').click();
assert.strictEqual(form.$('input').val(), 'GHI', 'input should be GHI');
$('.modal .modal-footer .btn-primary').click(); // click on confirm
assert.strictEqual(form.$('input').val(), 'ABC', 'input should now be ABC');
form.destroy();
});
QUnit.test('discard changes on a duplicated record', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
res_id: 1,
viewOptions: {sidebar: true},
mockRPC: function (route, args) {
if (args.method === 'search_read' && args.model === 'ir.attachment') {
return $.when([]);
}
return this._super.apply(this, arguments);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input').val("tralala").trigger('input');
form.$buttons.find('.o_form_button_save').click();
form.sidebar.$('a:contains(Duplicate)').click();
assert.strictEqual(form.$('input').val(), 'tralala', 'input should contain ABC');
form.$buttons.find('.o_form_button_cancel').click();
assert.strictEqual($('.modal').length, 0,
'there should not be a confirm modal');
form.destroy();
});
QUnit.test('switching to another record from a dirty one', function (assert) {
assert.expect(11);
var nbWrite = 0;
var form = 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(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
assert.strictEqual(form.pager.$('.o_pager_limit').text(), "2", 'pager limit should be 2');
// switch to edit mode
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input').val(), 'yop', 'input should contain yop');
// edit the foo field
form.$('input').val('new value').trigger('input');
assert.strictEqual(form.$('input').val(), 'new value', 'input should contain new value');
// click on the pager to switch to the next record and cancel the confirm request
form.pager.$('.o_pager_next').click(); // click on next
assert.ok($('.modal').length, 'a confirm modal should be displayed');
$('.modal .modal-footer .btn-default').click(); // click on cancel
assert.strictEqual(form.$('input').val(), 'new value', 'input should still contain new value');
assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should still be 1');
// click on the pager to switch to the next record and confirm
form.pager.$('.o_pager_next').click(); // click on next
assert.ok($('.modal').length, 'a confirm modal should be displayed');
$('.modal .modal-footer .btn-primary').click(); // click on confirm
assert.strictEqual(form.$('input').val(), 'blip', 'input should contain blip');
assert.strictEqual(form.pager.$('.o_pager_value').text(), "2", 'pager value should be 2');
assert.strictEqual(nbWrite, 0, 'no write RPC should have been done');
form.destroy();
});
QUnit.test('handling dirty state: switching to another record', function (assert) {
assert.expect(12);
var form = 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(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
// switch to edit mode
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input').val(), 'yop', 'input should contain yop');
// edit the foo field
form.$('input').val('new value').trigger('input');
assert.strictEqual(form.$('input').val(), 'new value', 'input should contain new value');
form.$buttons.find('.o_form_button_save').click();
// click on the pager to switch to the next record and cancel the confirm request
form.pager.$('.o_pager_next').click(); // click on next
assert.strictEqual($('.modal:visible').length, 0, 'no confirm modal should be displayed');
assert.strictEqual(form.pager.$('.o_pager_value').text(), "2", 'pager value should be 2');
assert.strictEqual(form.$('.o_priority .fa-star-o').length, 2,
'priority widget should have been rendered with correct value');
// edit the value in readonly
form.$('.o_priority .fa-star-o:first').click(); // click on the first star
assert.strictEqual(form.$('.o_priority .fa-star').length, 1,
'priority widget should have been updated');
form.pager.$('.o_pager_next').click(); // click on next
assert.strictEqual($('.modal:visible').length, 0, 'no confirm modal should be displayed');
assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
// switch to edit mode
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input').val(), 'new value', 'input should contain yop');
// edit the foo field
form.$('input').val('wrong value').trigger('input');
form.$buttons.find('.o_form_button_cancel').click();
assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed');
$('.modal .modal-footer .btn-primary').click(); // click on confirm
form.pager.$('.o_pager_next').click(); // click on next
assert.strictEqual(form.pager.$('.o_pager_value').text(), "2", 'pager value should be 2');
form.destroy();
});
QUnit.test('restore local state when switching to another record', function (assert) {
assert.expect(4);
var form = 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
form.$('.o_notebook li:eq(1) a').click();
assert.notOk(form.$('.o_notebook li:eq(0)').hasClass('active'),
"first tab should not be active");
assert.ok(form.$('.o_notebook li:eq(1)').hasClass('active'),
"second tab should be active");
// click on the pager to switch to the next record
form.pager.$('.o_pager_next').click();
assert.notOk(form.$('.o_notebook li:eq(0)').hasClass('active'),
"first tab should not be active");
assert.ok(form.$('.o_notebook li:eq(1)').hasClass('active'),
"second tab should be active");
form.destroy();
});
QUnit.test('pager is hidden in create mode', function (assert) {
assert.expect(7);
var form = 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.ok(form.pager.$el.is(':visible'), "pager should be visible");
assert.strictEqual(form.pager.$('.o_pager_value').text(), "1",
"current pager value should be 1");
assert.strictEqual(form.pager.$('.o_pager_limit').text(), "2",
"current pager limit should be 1");
form.$buttons.find('.o_form_button_create').click();
assert.notOk(form.pager.$el.is(':visible'), "pager should not be visible");
form.$buttons.find('.o_form_button_save').click();
assert.ok(form.pager.$el.is(':visible'), "pager should be visible");
assert.strictEqual(form.pager.$('.o_pager_value').text(), "3",
"current pager value should be 3");
assert.strictEqual(form.pager.$('.o_pager_limit').text(), "3",
"current pager limit should be 3");
form.destroy();
});
QUnit.test('switching to another record, in readonly mode', function (assert) {
assert.expect(5);
var pushStateCount = 0;
var form = 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(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
form.pager.$('.o_pager_next').click(); // click on next
assert.strictEqual(form.pager.$('.o_pager_value').text(), "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', function (assert) {
assert.expect(4);
var form = 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.strictEqual(form.$('span.foo_field').length, 1, "should have a span foo field");
assert.ok(!form.$('span.foo_field').is(':visible'),
"foo field should not be visible");
form.$buttons.find('.o_form_button_create').click();
assert.strictEqual(form.$('input.foo_field').length, 1,
"should have a visible input for foo field");
assert.ok(form.$('input.foo_field').is(':visible'),
"foo field should be visible");
form.destroy();
});
QUnit.test('empty readonly fields are visible on new records', 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 = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet><group>' +
'<field name="foo"/>' +
'</group></sheet>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('.o_field_empty').length, 1,
'readonly field should be invisible on an existing record');
form.$buttons.find('.o_form_button_create').click();
assert.strictEqual(form.$('.o_field_empty').length, 0,
'readonly field should be visible on a new record');
form.destroy();
});
QUnit.test('all group children have correct layout classname', function (assert) {
assert.expect(2);
var form = 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.ok(form.$('.inner_group').hasClass('o_group_col_6'),
"inner groups should have classname 'o_group_col_6'");
assert.ok(form.$('.inner_div').hasClass('o_group_col_6'),
"divs inside groups should have classname 'o_group_col_6'");
form.destroy();
});
QUnit.test('deleting a record', function (assert) {
assert.expect(8);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
viewOptions: {
ids: [1, 2, 4],
index: 0,
sidebar: true,
},
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'search_read' && args.model === 'ir.attachment') {
return $.when([]);
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
assert.strictEqual(form.pager.$('.o_pager_limit').text(), "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 sidebar
form.sidebar.$('button.o_dropdown_toggler_btn').click();
form.sidebar.$('a:contains(Delete)').click();
assert.ok($('.modal').length, 'a confirm modal should be displayed');
// confirm the delete
$('.modal .modal-footer button.btn-primary').click();
assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
assert.strictEqual(form.pager.$('.o_pager_limit').text(), "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', function (assert) {
assert.expect(6);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners"><field name="foo"></field></form>',
viewOptions: {
ids: [1],
index: 0,
sidebar: true,
},
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'search_read' && args.model === 'ir.attachment') {
return $.when([]);
}
assert.step(args.method);
return this._super.apply(this, arguments);
}
});
// open sidebar, click on delete and confirm
form.sidebar.$('button.o_dropdown_toggler_btn').click();
form.sidebar.$('a:contains(Delete)').click();
testUtils.intercept(form, 'do_action', function (event) {
assert.strictEqual(event.data.action, 'history_back',
"should trigger an history back action");
});
assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed');
$('.modal .modal-footer button.btn-primary').click();
assert.strictEqual($('.modal').length, 0, 'no confirm modal should be displayed');
assert.verifySteps(['read', 'unlink']);
form.destroy();
});
QUnit.test('empty required fields cannot be saved', function (assert) {
assert.expect(5);
this.data.partner.fields.foo.required = true;
delete this.data.partner.fields.foo.default;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group><field name="foo"/></group>' +
'</form>',
});
testUtils.intercept(form, 'warning', function (event) {
assert.strictEqual(event.data.title, 'The following fields are invalid:',
"should have a warning with correct title");
assert.strictEqual(event.data.message, '<ul><li>Foo</li></ul>',
"should have a warning with correct message");
});
form.$buttons.find('.o_form_button_save').click();
assert.ok(form.$('label').hasClass('o_field_invalid'),
"label should be tagged as invalid");
assert.ok(form.$('input').hasClass('o_field_invalid'),
"input should be tagged as invalid");
form.$('input').val("tralala").trigger('input');
assert.strictEqual(form.$('.o_field_invalid').length, 0,
"nothing should be marked as invalid");
form.destroy();
});
QUnit.test('changes in a readonly form view are saved directly', function (assert) {
assert.expect(10);
var nbWrite = 0;
var form = 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.strictEqual(form.$('.o_priority .o_priority_star').length, 2,
'priority widget should have been rendered');
assert.strictEqual(form.$('.o_priority .fa-star-o').length, 2,
'priority widget should have been rendered with correct value');
// edit the value in readonly
form.$('.o_priority .fa-star-o:first').click(); // click on the first star
assert.strictEqual(nbWrite, 1, 'should have saved directly');
assert.strictEqual(form.$('.o_priority .fa-star').length, 1,
'priority widget should have been updated');
// switch to edit mode and edit the value again
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_priority .o_priority_star').length, 2,
'priority widget should have been correctly rendered');
assert.strictEqual(form.$('.o_priority .fa-star').length, 1,
'priority widget should have correct value');
form.$('.o_priority .fa-star-o:first').click(); // click on the second star
assert.strictEqual(nbWrite, 1, 'should not have saved directly');
assert.strictEqual(form.$('.o_priority .fa-star').length, 2,
'priority widget should have been updated');
// save
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(nbWrite, 2, 'should not have saved directly');
assert.strictEqual(form.$('.o_priority .fa-star').length, 2,
'priority widget should have correct value');
form.destroy();
});
QUnit.test('display a dialog if onchange result is a warning', function (assert) {
assert.expect(5);
this.data.partner.onchanges = { foo: true };
var form = 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 $.when({
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.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");
},
},
});
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input[name=int_field]').val(), '9',
"'int_field' value should be 9 before the change");
form.$('input').first().val("tralala").trigger('input');
assert.strictEqual(form.$('input[name=int_field]').val(), '10',
"the onchange should have been correctly applied");
form.destroy();
});
QUnit.test('can create record even if onchange returns a warning', function (assert) {
assert.expect(2);
this.data.partner.onchanges = { foo: true };
var form = 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 $.when({
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.step('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', function (assert) {
assert.expect(2);
this.data.partner.onchanges = { foo: true };
var form = 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 $.when({
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
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('tr.o_data_row').length, 0,
"should not have added a line");
form.destroy();
});
QUnit.test('attrs are properly transmitted to new records', function (assert) {
assert.expect(2);
// this test checks that the fieldsInfo have been transmitted to the
// load function when creating a new record
var terminology = {
string_true: "Production Environment",
hover_true: "Switch to test environment",
string_false: "Test Environment",
hover_false: "Switch to production environment"
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group>' +
'<field name="bar" widget="boolean_button" options=\'{"terminology": ' +
JSON.stringify(terminology) + '}\'/>' +
'</group>' +
'</form>',
res_id: 2,
});
assert.strictEqual(form.$('.o_stat_text.o_not_hover:contains(Production Environment)').length, 1,
"button should contain correct string");
form.$buttons.find('.o_form_button_create').click();
assert.strictEqual(form.$('.o_stat_text.o_not_hover:contains(Test Environment)').length, 1,
"button should contain correct string");
form.destroy();
});
QUnit.test('button box is not rendered in create mode', function (assert) {
assert.expect(3);
var form = 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.strictEqual(form.$('.oe_stat_button').length, 1,
"button box should be displayed in readonly");
// edit mode
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.oe_stat_button').length, 1,
"button box should be displayed in edit on an existing record");
// create mode (leave edition first!)
form.$buttons.find('.o_form_button_cancel').click();
form.$buttons.find('.o_form_button_create').click();
assert.strictEqual(form.$('.oe_stat_button').length, 0,
"button box should not be displayed when creating a new record");
form.destroy();
});
QUnit.test('properly apply onchange on one2many fields', 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 = 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.strictEqual(form.$('.o_field_one2many .o_data_row').length, 1,
"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
form.$buttons.find('.o_form_button_edit').click();
form.$('input').val('let us trigger an onchange').trigger('input');
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('update many2many value in one2many after onchange', 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 = 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");
form.$buttons.find('.o_form_button_edit').click();
form.$('input').val("tralala").trigger('input');
assert.strictEqual($('div[name="p"] .o_data_row td').text().trim(), "goldNo records",
"should have proper initial content");
form.destroy();
});
QUnit.test('properly apply onchange on many2many fields', function (assert) {
assert.expect(14);
this.data.partner.onchanges = {
foo: function (obj) {
obj.timmy = [
[5],
[4, 12],
[4, 14],
];
},
};
var form = 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.strictEqual(form.$('.o_field_many2many .o_data_row').length, 0,
"there should be no many2many record linked at first");
// switch to edit mode
form.$buttons.find('.o_form_button_edit').click();
form.$('input').val('let us trigger an onchange').trigger('input');
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");
form.$buttons.find('.o_form_button_save').click();
assert.verifySteps(['read', 'onchange', 'read', 'write', 'read', 'read']);
form.destroy();
});
QUnit.test('onchanges on date(time) fields', 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 = 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: {
tzOffset: 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");
form.$buttons.find('.o_form_button_edit').click();
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
form.$('.o_field_widget[name="foo"]').val("coucou").trigger('input');
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', 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 = $.Deferred();
var form = 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;
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input').first().val("1").trigger('input');
assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet");
form.$('input').first().val("12").trigger('input');
assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet");
return waitForFinishedOnChange().then(function () {
assert.strictEqual(onchangeNbr, 1, "one onchange has been called");
// add something in the input, then focus another input
form.$('input').first().val("123").trigger('input');
form.$('input').first().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 = $.Deferred();
return concurrency.delay(0);
});
}
});
QUnit.test('onchanges are not sent for invalid values', function (assert) {
assert.expect(6);
this.data.partner.onchanges = {
int_field: function (obj) {
obj.foo = String(obj.int_field);
},
};
var form = 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);
},
});
form.$buttons.find('.o_form_button_edit').click();
// edit int_field, and check that an onchange has been applied
form.$('input[name="int_field"]').val("123").trigger('input');
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
form.$('input[name="int_field"]').val("123a").trigger('input');
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
form.$buttons.find('.o_form_button_save').click();
assert.ok(form.$('input[name="int_field"]').hasClass('o_field_invalid'),
"input int_field is marked as invalid");
assert.verifySteps(['read', 'onchange']);
form.destroy();
});
QUnit.test('rpc complete after destroying parent', function (assert) {
// We just test that there is no crash in this situation
assert.expect(0);
var form = 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();
}
}
});
form.$('.o_form_button_update').click();
});
QUnit.test('onchanges that complete after discarding', function (assert) {
assert.expect(4);
var def1 = $.Deferred();
this.data.partner.onchanges = {
foo: function (obj) {
obj.int_field = obj.foo.length + 1000;
},
};
var form = 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");
form.$buttons.find('.o_form_button_edit').click();
// edit a value
form.$('input').first().val("1234").trigger('input');
// discard changes
form.$buttons.find('.o_form_button_cancel').click();
$('.modal .modal-footer .btn-primary').click();
assert.strictEqual(form.$('span[name="foo"]').text(), "blip",
"field foo should still be displayed to initial value");
// complete the onchange
def1.resolve();
assert.strictEqual(form.$('span[name="foo"]').text(), "blip",
"field foo should still be displayed to initial value");
form.destroy();
});
QUnit.test('discarding before save returns', function (assert) {
assert.expect(4);
var def = $.Deferred();
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<group><field name="foo"/></group>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'write') {
return def.then(_.constant(result));
}
return result;
},
viewOptions: {
mode: 'edit',
},
});
form.$('input').val("1234").trigger('input');
// save the value and discard directly
form.$buttons.find('.o_form_button_save').click();
form.discardChanges(); // Simulate click on breadcrumb
assert.strictEqual(form.$('.o_field_widget[name="foo"]').val(), "1234",
"field foo should still contain new value");
assert.strictEqual($('.modal').length, 0,
"Confirm dialog should not be displayed");
// complete the write
def.resolve();
assert.strictEqual($('.modal').length, 0,
"Confirm dialog should not be displayed");
assert.strictEqual(form.$('.o_field_widget[name="foo"]').text(), "1234",
"value should have been saved and rerendered in readonly");
form.destroy();
});
QUnit.test('unchanged relational data is sent for onchanges', function (assert) {
assert.expect(1);
this.data.partner.records[1].p = [4];
this.data.partner.onchanges = {
foo: function (obj) {
obj.int_field = obj.foo.length + 1000;
},
};
var form = 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);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input:first').val('trigger an onchange').trigger('input');
form.destroy();
});
QUnit.test('onchanges on unknown fields of o2m are ignored', 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 = 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 $.when({
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);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input:first').val('trigger an onchange').trigger('input');
assert.strictEqual(form.$('.o_data_row td:first').text(), 'foo changed',
"onchange should have been correctly applied on field in o2m list");
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('onchange value are not discarded on o2m edition', function (assert) {
assert.expect(4);
this.data.partner.records[1].p = [4];
this.data.partner.onchanges = {
foo: function () {},
};
var form = 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 $.when({
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);
},
});
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_data_row td:first').text(), 'My little Foo Value',
"the initial value should be the default one");
form.$('input:first').val('trigger an onchange').trigger('input');
assert.strictEqual(form.$('.o_data_row td:first').text(), 'foo changed',
"onchange should have been correctly applied on field in o2m list");
form.$('.o_data_row').click(); // edit the o2m in the dialog
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)', 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 = 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,
});
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_data_row td:first').text(), 'My little Foo Value',
"the initial value should be the default one");
form.$('.o_data_row td:nth(1)').click(); // edit the o2m inline
form.$('.o_data_row input:nth(1)').val(77).trigger('input');
assert.strictEqual(form.$('.o_data_row input:first').val(), '[blip] 77',
"onchange should have been correctly applied");
// create a new o2m record
form.$('.o_field_x2many_list_row_add a').click();
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)', 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 = 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,
});
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_data_row td:first').text(), 'My little Foo Value',
"the initial value should be the default one");
form.$('.o_data_row td:first').click(); // edit the o2m in a dialog
$('.modal input:nth(1)').val(77).trigger('input');
assert.strictEqual($('.modal input:first').val(), '[blip] 77',
"onchange should have been correctly applied");
$('.modal .modal-footer .btn-primary').click(); // save the dialog
assert.strictEqual(form.$('.o_data_row td:first').text(), '[blip] 77',
"onchange should have been correctly applied");
// create a new o2m record
form.$('.o_field_x2many_list_row_add a').click();
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");
$('.modal .modal-footer .btn-primary').click(); // save the dialog
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', function (assert) {
assert.expect(2);
this.data.partner.onchanges = {
foo: function () {},
};
var form = 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.args[4].test, 1,
"the context of the field triggering the onchange should be given");
assert.strictEqual(args.args[4].int_ctx, undefined,
"the context of other fields should not be given");
}
return this._super.apply(this, arguments);
},
res_id: 2,
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input:first').val('coucou').trigger('input');
form.destroy();
});
QUnit.test('navigation with tab key in form view', function (assert) {
assert.expect(3);
var form = 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
form.$buttons.find('.o_form_button_edit').click();
// focus first input, trigger tab
form.$('input[name="foo"]').focus();
form.$('input[name="foo"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.ok($.contains(form.$('div[name="bar"]')[0], document.activeElement),
"bar checkbox should be focused");
form.$('div[name="bar"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(form.$('input[name="display_name"]')[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}));
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', function (assert) {
// The behavior of the phone widget is completely altered by crm_voip so
// this test fails if crm_voip is installed. The enterprise module is
// responsible for testing its own behavior in its own tests.
if ('crm.voip' in odoo.__DEBUG__.services) {
assert.expect(0);
return;
}
assert.expect(3);
this.data.partner.records[1].product_id = 37;
var form = 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}));
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}));
assert.strictEqual(form.$('[name="display_name"]')[0], document.activeElement,
"display_name should be focused (emails are focusable but phone aren't)");
// 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}));
assert.strictEqual(document.activeElement, form.$('[name="trululu"]')[0],
"first many2one should be focused");
form.destroy();
});
QUnit.test('skip invisible fields when navigating with TAB', function (assert) {
assert.expect(1);
this.data.partner.records[0].bar = true;
var form = 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,
});
form.$buttons.find('.o_form_button_edit').click();
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('clicking on a stat button with a context', function (assert) {
assert.expect(1);
var form = 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");
},
},
});
form.$('.oe_stat_button').click();
form.destroy();
});
QUnit.test('clicking on a stat button with no context', function (assert) {
assert.expect(1);
var form = 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");
},
},
});
form.$('.oe_stat_button').click();
form.destroy();
});
QUnit.test('diplay a stat button outside a buttonbox', function (assert) {
assert.expect(3);
var form = 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.strictEqual(form.$('button .o_field_widget').length, 1,
"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 than the field value");
form.destroy();
});
QUnit.test('diplay something else than a button in a buttonbox', function (assert) {
assert.expect(3);
var form = 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.strictEqual(form.$('.oe_button_box .oe_stat_button').length, 1,
"button box should only contain one button");
assert.strictEqual(form.$('.oe_button_box label').length, 1,
"button box should only contain one label");
form.destroy();
});
QUnit.test('one2many default value creation', function (assert) {
assert.expect(1);
this.data.partner.records[0].product_ids = [37];
var form = 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 === 'default_get') {
return $.when({
product_ids: [[0, 0, {
name: 'xdroid',
partner_type_id: 12,
}]]
});
}
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);
},
});
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('many2manys inside one2manys are saved correctly', function (assert) {
assert.expect(1);
var form = 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, false, {
display_name: false,
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
form.$('.o_field_x2many_list_row_add a').click();
form.$('.o_many2many_tags_cell').click();
form.$('.o_field_many2one input').click();
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
$dropdown.find('li:first()').click(); // select the first tag
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('*_view_ref in context are passed correctly', function (assert) {
var done = assert.async();
assert.expect(4);
createAsyncView({
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.eval();
assert.strictEqual(context.tree_view_ref, 'module.tree_view_ref',
"context should contain tree_view_ref");
assert.notOk('some_context' in context,
"should not send record's context to load p's fields_view");
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(function (form) {
// reload to check that the record's context hasn't been modified
form.reload();
form.destroy();
done();
});
});
QUnit.test('readonly fields with modifiers may be saved', 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 overriden 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 = 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
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input[name="foo"]').length, 1,
"foo field should be editable");
form.$('input[name="foo"]').val('New foo value').trigger('input');
form.$buttons.find('.o_form_button_save').click();
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('check if id and active_id are defined', function (assert) {
assert.expect(2);
var form = 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 === 'default_get' && args.args[0][0] === 'trululu') {
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);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_x2many_list_row_add a').click();
form.destroy();
});
QUnit.test('modifiers are considered on multiple <footer/> tags', function (assert) {
assert.expect(2);
var form = 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: {
footer_to_buttons: true,
mode: 'edit',
},
});
assert.deepEqual(getVisibleButtonTexts(), ["Hello", "World"],
"only the first button section should be visible");
form.$(".o_field_boolean input").click();
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('footers are not duplicated on rerender', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form>' +
'<field name="foo"/>' +
'<footer>' +
'<button>Hello</button>' +
'</footer>' +
'</form>',
res_id: 1,
viewOptions: {
footer_to_buttons: true,
},
});
assert.strictEqual(form.$('footer').length, 0,
"footer should have been moved outside of the form view");
form.reload();
assert.strictEqual(form.$('footer').length, 0,
"footer should still have been moved outside of the form view");
form.destroy();
});
QUnit.test('render stat button with string inline', function (assert) {
assert.expect(1);
var form = 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_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', function (assert) {
assert.expect(1);
var done = assert.async();
testUtils.createAsyncView({
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.strictEqual(form.$('.ace_editor').length, 1,
"should have waited for ace to load its dependencies");
form.destroy();
done();
});
});
QUnit.test('open one2many form containing one2many', function (assert) {
assert.expect(8);
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 = 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_list_view .o_data_row');
assert.strictEqual(row.children()[1].textContent, '1 record',
"the cell should contains the number of record: 1");
row.click();
var modal_row = $('.modal-body .o_form_sheet .o_field_one2many .o_list_view .o_data_row');
assert.strictEqual(modal_row.children().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', 'read', 'read'],
"there should be 4 read rpcs");
form.destroy();
});
QUnit.test('in edit mode, first field is focused', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
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', function (assert) {
assert.expect(1);
var form = 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,
});
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"foo field should have focus");
form.destroy();
});
QUnit.test('in create mode, autofocus fields are focused', function (assert) {
assert.expect(1);
var form = 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', function (assert) {
assert.expect(1);
var form = 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);
},
});
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('autofocus first visible field', function (assert) {
assert.expect(1);
var form = 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('open one2many form containing many2many_tags', 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 = 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_list_view .o_data_row');
row.click();
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', function (assert) {
assert.expect(4);
this.data.partner.onchanges.foo = function (obj) {};
this.data.partner.fields.foo.required = true;
var def = $.Deferred();
var form = 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;
},
intercepts: {
warning: function () {
assert.step('warning');
},
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input[name="foo"]').val('').trigger("input");
form.$buttons.find('.o_form_button_save').click();
def.resolve();
assert.verifySteps(['read', 'onchange', 'warning']);
form.destroy();
});
QUnit.test('display toolbar', function (assert) {
assert.expect(7);
var form = 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: {
sidebar: 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 $.when({});
}
if (args.method === 'search_read' && args.model === 'ir.attachment') {
return $.when([]);
}
return this._super.apply(this, arguments);
},
});
// The toolbar displayed changes if the module 'document' is installed.
// So if the module is installed, the assertion checks for 3 dropdowns
// if not only 2 dropdowns are displayed.
var $dropdowns = $('.o_web_client .o_control_panel .btn-group .o_dropdown_toggler_btn');
var $actions = $('.o_web_client .o_control_panel .btn-group .dropdown-menu')[1].children;
if ('document.document' in odoo.__DEBUG__.services) {
assert.strictEqual($dropdowns.length, 3,
"there should be 3 dropdowns (print, attachment, action) in the toolbar.");
$actions = $('.o_web_client .o_control_panel .btn-group .dropdown-menu')[2].children;
} else {
assert.strictEqual($dropdowns.length, 2,
"there should be 2 dropdowns (print, action) in the toolbar.");
}
assert.strictEqual($actions.length, 3,
"there should be 3 actions");
var $customAction = $('.o_web_client .o_control_panel .btn-group .dropdown-menu li a')[2];
assert.strictEqual($customAction.text.trim(), 'Action partner',
"the custom action should have 'Action partner' as name");
testUtils.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.");
});
$customAction.click();
form.destroy();
});
QUnit.test('check interactions between multiple FormViewDialogs', 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 = 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 $.when(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);
},
});
form.$buttons.find('.o_form_button_edit').click();
// Open first dialog
form.$('.o_external_button').click();
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
$firstModal.find('.o_field_x2many_list_row_add a').click();
assert.strictEqual($('.modal').length, 2,
"two FormViewDialogs should be opened");
var $secondModal = $('.modal:nth(1)');
// Add new value
$secondModal.find('input').val('xtv').trigger('input');
$secondModal.find('.modal-footer button:first').click(); // Save & close
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");
$firstModal.find('.modal-footer button:first').click(); // Save & close
form.destroy();
});
QUnit.test('fields and record contexts are not mixed', function (assert) {
assert.expect(2);
var form = 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},
},
});
form.$('input:first').click(); // trigger the name_search
form.destroy();
});
QUnit.test('do not activate an hidden tab when switching between records', function (assert) {
assert.expect(6);
var form = 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 ul li:not(.o_invisible_modifier)').length, 2,
"both tabs should be visible");
assert.ok(form.$('.o_notebook ul li:first').hasClass('active'),
"first tab should be active");
// click on the pager to switch to the next record
form.pager.$('.o_pager_next').click();
assert.strictEqual(form.$('.o_notebook ul li:not(.o_invisible_modifier)').length, 1,
"only the second tab should be visible");
assert.ok(form.$('.o_notebook ul li:not(.o_invisible_modifier)').hasClass('active'),
"the visible tab should be active");
// click on the pager to switch back to the previous record
form.pager.$('.o_pager_previous').click();
assert.strictEqual(form.$('.o_notebook ul li:not(.o_invisible_modifier)').length, 2,
"both tabs should be visible again");
assert.ok(form.$('.o_notebook ul li:nth(1)').hasClass('active'),
"second tab should be active");
form.destroy();
});
QUnit.test('support anchor tags with action type', function (assert) {
assert.expect(1);
var form = 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");
}
}
});
form.$('a[type="action"]').click();
form.destroy();
});
QUnit.test('do not perform extra RPC to read invisible many2one fields', function (assert) {
assert.expect(2);
this.data.partner.fields.trululu.default = 2;
var form = 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(['default_get'], "only one RPC should have been done");
form.destroy();
});
QUnit.test('do not perform extra RPC to read invisible x2many fields', 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 = 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', function (assert) {
assert.expect(11);
this.data.partner.fields.display_name.sortable = true;
this.data.partner.records[0].p = [1, 4];
var form = 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");
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual($('.modal').length, 1,
"FormViewDialog should be opened");
$('.modal input[name="foo"]').val('xop').trigger("input");
$('.modal .modal-footer button:eq(1)').click(); // Save & new
$('.modal input[name="foo"]').val('zop').trigger("input");
$('.modal .modal-footer button:first').click(); // Save & close
// 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
form.$buttons.find('.o_form_button_save').click();
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
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_one2many tbody tr:eq(1) td:contains(yop)').click();
$('.modal input[name="foo"]').val('zzz').trigger("input");
$('.modal .modal-footer button:first').click(); // Save
assert.ok(form.$('.o_field_one2many tbody tr:eq(0) td:contains(zzz)').length,
"record zzz should be first");
assert.ok(form.$('.o_field_one2many tbody tr:eq(1) td:contains(zop)').length,
"record zop should be second");
assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length,
"record xop should be third");
form.destroy();
});
QUnit.test('action context is used when evaluating domains', function (assert) {
assert.expect(1);
var form = 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);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('div[name="trululu"] input').click();
form.destroy();
});
QUnit.test('form rendering with groups with col/colspan', function (assert) {
assert.expect(46);
var form = 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.strictEqual($group4firstRowTd.attr('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.strictEqual($group4secondRowTds.eq(0).attr('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.strictEqual($group4secondRowTds.eq(1).attr('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.strictEqual($group4thirdRowTd.attr('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.ok($fieldGroupFirstRowTds.eq(0).hasClass('o_td_label'), "first td should be a label td");
assert.strictEqual($fieldGroupFirstRowTds.eq(1).attr('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.strictEqual($fieldGroupSecondRowTds.eq(0).attr('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.strictEqual($fieldGroupSecondRowTds.eq(1).attr('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.ok($fieldGroupThirdRowTds.eq(0).hasClass('o_td_label'), "first td should be a label td");
assert.strictEqual($fieldGroupThirdRowTds.eq(1).attr('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.strictEqual($fieldGroupFourthRowTds.attr('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.ok($fieldGroupFifthRowTds.eq(1).hasClass('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");
// Verify that one2many list table hasn't been impacted
assert.strictEqual(form.$('.o_field_one2many th:first').attr('style'), undefined,
"o2m list columns should have no width harcoded");
form.destroy();
});
QUnit.test('form group with newline tag inside', function (assert) {
assert.expect(6);
var form = 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.strictEqual(form.$('.main_inner_group > tbody > tr').length, 2,
"there should be 2 rows in the group");
assert.strictEqual(form.$('.main_inner_group > tbody > tr:first > .o_td_label').length, 1,
"there should be only one label in the first row");
assert.strictEqual(form.$('.main_inner_group > tbody > tr:first .o_field_widget').length, 1,
"there should be only one widget in the first row");
assert.strictEqual(form.$('.main_inner_group > tbody > tr:last > .o_td_label').length, 2,
"there should be two labels in the second row");
assert.strictEqual(form.$('.main_inner_group > tbody > tr:last .o_field_widget').length, 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', function (assert) {
assert.expect(1);
this.data.partner.records[0].p = [2];
var form = 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,
});
form.$('.o_data_row:first').click();
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', function (assert) {
assert.expect(1);
this.data.partner.fields.foo.translate = true;
var multi_lang = _t.database.multi_lang;
_t.database.multi_lang = true;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input[name="foo"]').val("test").trigger("input");
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,"should have a translation alert");
form.destroy();
_t.database.multi_lang = multi_lang;
});
QUnit.test('translate event correctly handled with multiple controllers', 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 = 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 $.when(false);
} else if (route === "/web/dataset/call_button" && args.method === 'translate_fields') {
assert.deepEqual(args.args, ["product",37,"name",{}], 'should call "call_button" route');
nbTranslateCalls++;
return $.when();
}
return this._super.apply(this, arguments);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('[name="product_id"] .o_external_button').click();
assert.strictEqual($('.modal-body .o_field_translate').length, 1,
"there should be a translate button in the modal");
$('.modal-body .o_field_translate').click();
assert.strictEqual(nbTranslateCalls, 1, "should call_button translate once");
form.destroy();
_t.database.multi_lang = multi_lang;
});
QUnit.test('buttons are disabled until status bar action is resolved', function (assert) {
assert.expect(9);
var def = $.Deferred();
var form = 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">' +
'<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");
form.$('.o_form_statusbar button').click();
// The unresolved deferred lets us check the state of the buttons
assert.strictEqual(form.$buttons.find('button:disabled').length, 4,
"control panel buttons should be disabled");
assert.strictEqual(form.$('.o_form_statusbar button:disabled').length, 2,
"status bar buttons should be disabled");
assert.strictEqual(form.$('.oe_button_box button:disabled').length, 1,
"stat buttons should be disabled");
def.resolve();
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', function (assert) {
assert.expect(9);
var def = $.Deferred();
var form = 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">' +
'<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");
form.$('.oe_button_box button').click();
// The unresolved deferred lets us check the state of the buttons
assert.strictEqual(form.$buttons.find('button:disabled').length, 4,
"control panel buttons should be disabled");
assert.strictEqual(form.$('.o_form_statusbar button:disabled').length, 2,
"status bar buttons should be disabled");
assert.strictEqual(form.$('.oe_button_box button:disabled').length, 1,
"stat buttons should be disabled");
def.resolve();
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 action is resolved (in dialogs)', function (assert) {
assert.expect(3);
var def = $.Deferred();
var form = 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">' +
'<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 $.when(false);
}
return this._super.apply(this, arguments);
},
viewOptions: {
mode: 'edit',
},
});
form.$('.o_external_button').click();
assert.notOk($('.modal .oe_button_box button').attr('disabled'),
"stat buttons should be enabled");
$('.modal .oe_button_box button').click();
assert.ok($('.modal .oe_button_box button').attr('disabled'),
"stat buttons should be disabled");
def.resolve();
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', function (assert) {
assert.expect(4);
var def = $.Deferred();
var form = 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;
}
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input[name="foo"]').val("test").trigger("input");
form.$buttons.find('.o_form_button_save').click();
form.$buttons.find('.o_form_button_save').click();
def.resolve();
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', function (assert) {
assert.expect(5);
var form = 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 $.Deferred().reject();
}
return this._super.apply(this, arguments);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input[name="foo"]').val("incorrect value").trigger("input");
form.$buttons.find('.o_form_button_save').click();
form.$('input[name="foo"]').val("correct value").trigger("input");
form.$buttons.find('.o_form_button_save').click();
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('support password attribute', function (assert) {
assert.expect(3);
var form = 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");
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input[name="foo"]').val(), '***',
"password should be displayed with stars");
assert.strictEqual(form.$('input[name="foo"]').prop('type'), 'password',
"input should be of type password");
form.destroy();
});
QUnit.test('support autocomplete attribute', function (assert) {
assert.expect(3);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="display_name" autocomplete="coucou"/>' +
'<field name="foo" password="True"/>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input[name="foo"]').val(), '***',
"password should be displayed with stars");
assert.strictEqual(form.$('input[name="display_name"]').prop('autocomplete'), 'coucou',
"attribute autocomplete should be set");
assert.strictEqual(form.$('input[name="foo"]').prop('autocomplete'), 'new-password',
"attribute autocomplete should be set to 'new-password' on password input");
form.destroy();
});
QUnit.test('context is correctly passed after save & new in FormViewDialog', function (assert) {
assert.expect(3);
var form = 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);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual($('.modal').length, 1,
"One FormViewDialog should be opened");
// set a value on the m2o
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
$('.modal .o_field_many2one input').click();
$dropdown.find('li:first()').click();
$('.modal .modal-footer button:eq(1)').click(); // Save & new
$('.modal .o_field_many2one input').click();
$('.modal .modal-footer button:first').click(); // Save & close
form.destroy();
});
QUnit.test('render domain field widget without model', function (assert) {
assert.expect(3);
this.data.partner.fields.model_name = { string: "Model name", type: "char" };
var form = 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 $.Deferred().reject({
code: 200,
data: {},
message: "MockServer._getRecords: given domain has to be an array.",
}, $.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");
form.$('input[name="model_name"]').val("test").trigger("input");
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('wow effect', function (assert) {
assert.expect(3);
this.data.partner.fields.model_name = { string: "Model name", type: "char" };
var clickCount = 0;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<button name="test" string="Mark Won" type="object" class="o_wow"/>' +
'</form>',
intercepts: {
execute_action: function (event) {
if (clickCount === 0) {
event.data.on_success();
} else {
event.data.on_fail();
}
clickCount++;
},
show_wow: function () {
assert.step('wow');
},
},
});
form.$('button.o_wow').click();
assert.verifySteps(['wow']);
form.$('button.o_wow').click();
assert.verifySteps(['wow']);
form.destroy();
});
QUnit.test('readonly fields are not sent when saving', 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 = 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, false, {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, false, {display_name: 'readonly'}]]
}, "should not have sent the value of the readonly field");
}
return this._super.apply(this, arguments);
},
});
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual($('.modal input.o_field_widget[name=foo]').length, 1,
'foo should be editable');
checkOnchange = true;
$('.modal .o_field_widget[name=foo]').val('foo value').trigger('input');
$('.modal .o_field_widget[name=display_name]').val('readonly').trigger('input');
assert.strictEqual($('.modal span.o_field_widget[name=foo]').length, 1,
'foo should be readonly');
$('.modal .modal-footer .btn-primary').click(); // close the modal
checkOnchange = false;
form.$('.o_data_row').click(); // re-open previous record
assert.strictEqual($('.modal .o_field_widget[name=foo]').text(), 'foo value',
"the edited value should have been kept");
$('.modal .modal-footer .btn-primary').click(); // close the modal
form.$buttons.find('.o_form_button_save').click(); // save the record
form.destroy();
});
QUnit.test('id is False in evalContext for new records', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="id"/>' +
'<field name="foo" attrs="{\'readonly\': [[\'id\', \'=\', False]]}"/>' +
'</form>',
});
assert.ok(form.$('.o_field_widget[name=foo]').hasClass('o_readonly_modifier'),
"foo should be readonly in 'Create' mode");
form.$buttons.find('.o_form_button_save').click();
form.$buttons.find('.o_form_button_edit').click();
assert.notOk(form.$('.o_field_widget[name=foo]').hasClass('o_readonly_modifier'),
"foo should not be readonly anymore");
form.destroy();
});
QUnit.test('delete a duplicated record', function (assert) {
assert.expect(5);
var newRecordID;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="display_name"/>' +
'</form>',
res_id: 1,
viewOptions: {sidebar: true},
mockRPC: function (route, args) {
if (args.method === 'search_read' && args.model === 'ir.attachment') {
// rpcs done by the sidebar
return $.when([]);
}
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;
},
});
form.sidebar.$('a:contains(Duplicate)').click(); // duplicate record 1
assert.strictEqual(form.$('.o_form_editable').length, 1,
"form should be in edit mode");
assert.strictEqual(form.$('.o_field_widget').val(), 'first record (copy)',
"duplicated record should have correct name");
form.$buttons.find('.o_form_button_save').click(); // save duplicated record
form.sidebar.$('a:contains(Delete)').click(); // delete duplicated record
assert.strictEqual($('.modal').length, 1, "should have opened a confirm dialog");
$('.modal .modal-footer .btn-primary').click(); // confirm
assert.strictEqual(form.$('.o_field_widget').text(), 'first record',
"should have come back to previous record");
form.destroy();
});
QUnit.test('display tooltips for buttons', function (assert) {
assert.expect(1);
var initialDebugMode = config.debug;
config.debug = true;
var form = 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>' +
'</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");
config.debug = initialDebugMode;
form.destroy();
});
});
});