flectra/addons/web/static/tests/views/list_tests.js

3461 lines
135 KiB
JavaScript

flectra.define('web.list_tests', function (require) {
"use strict";
var config = require('web.config');
var basicFields = require('web.basic_fields');
var FormView = require('web.FormView');
var ListView = require('web.ListView');
var mixins = require('web.mixins');
var testUtils = require('web.test_utils');
var widgetRegistry = require('web.widget_registry');
var Widget = require('web.Widget');
var createView = testUtils.createView;
QUnit.module('Views', {
beforeEach: function () {
this.data = {
foo: {
fields: {
foo: {string: "Foo", type: "char"},
bar: {string: "Bar", type: "boolean"},
date: {string: "Some Date", type: "date"},
int_field: {string: "int_field", type: "integer", sortable: true},
qux: {string: "my float", type: "float"},
m2o: {string: "M2O field", type: "many2one", relation: "bar"},
o2m: {string: "O2M field", type: "one2many", relation: "bar"},
m2m: {string: "M2M field", type: "many2many", relation: "bar"},
amount: {string: "Monetary field", type: "monetary"},
currency_id: {string: "Currency", type: "many2one",
relation: "res_currency", default: 1},
datetime: {string: "Datetime Field", type: 'datetime'},
reference: {string: "Reference Field", type: 'reference', selection: [
["bar", "Bar"], ["res_currency", "Currency"], ["event", "Event"]]},
},
records: [
{
id: 1,
bar: true,
foo: "yop",
int_field: 10,
qux: 0.4,
m2o: 1,
m2m: [1, 2],
amount: 1200,
currency_id: 2,
date: "2017-01-25",
datetime: "2016-12-12 10:55:05",
reference: 'bar,1',
},
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13,
m2o: 2, m2m: [1, 2, 3], amount: 500, reference: 'res_currency,1'},
{id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3,
m2o: 1, m2m: [], amount: 300, reference: 'res_currency,2'},
{id: 4, bar: false, foo: "blip", int_field: -4, qux: 9,
m2o: 1, m2m: [1], amount: 0},
]
},
bar: {
fields: {},
records: [
{id: 1, display_name: "Value 1"},
{id: 2, display_name: "Value 2"},
{id: 3, display_name: "Value 3"},
]
},
res_currency: {
fields: {
symbol: {string: "Symbol", type: "char"},
position: {
string: "Position",
type: "selection",
selection: [['after', 'A'], ['before', 'B']],
},
},
records: [
{id: 1, display_name: "USD", symbol: '$', position: 'before'},
{id: 2, display_name: "EUR", symbol: '€', position: 'after'},
],
},
event: {
fields: {
id: {string: "ID", type: "integer"},
name: {string: "name", type: "char"},
},
records: [
{id: "2-20170808020000", name: "virtual"},
]
},
};
}
}, function () {
QUnit.module('ListView');
QUnit.test('simple readonly list', function (assert) {
assert.expect(10);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/><field name="int_field"/></tree>',
});
assert.notOk(list.$el.hasClass('o_cannot_create'),
"should not have className 'o_cannot_create'");
// 3 th (1 for checkbox, 2 for columns)
assert.strictEqual(list.$('th').length, 3, "should have 3 columns");
assert.strictEqual(list.$('td:contains(gnap)').length, 1, "should contain gnap");
assert.strictEqual(list.$('tbody tr').length, 4, "should have 4 rows");
assert.strictEqual(list.$('th.o_column_sortable').length, 1, "should have 1 sortable column");
assert.strictEqual(list.$('thead th:nth(2)').css('text-align'), 'right',
"header cells of integer fields should be right aligned");
assert.strictEqual(list.$('tbody tr:first td:nth(2)').css('text-align'), 'right',
"integer cells should be right aligned");
assert.ok(list.$buttons.find('.o_list_button_add').is(':visible'),
"should have a visible Create button");
assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'),
"should not have a visible save button");
assert.ok(!list.$buttons.find('.o_list_button_discard').is(':visible'),
"should not have a visible save button");
list.destroy();
});
QUnit.test('list with create="0"', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree create="0"><field name="foo"/></tree>',
});
assert.ok(list.$el.hasClass('o_cannot_create'),
"should have className 'o_cannot_create'");
assert.strictEqual(list.$buttons.find('.o_list_button_add').length, 0,
"should not have the 'Create' button");
list.destroy();
});
QUnit.test('list with delete="0"', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
viewOptions: {sidebar: true},
arch: '<tree delete="0"><field name="foo"/></tree>',
});
assert.ok(list.sidebar.$el.hasClass('o_hidden'), 'sidebar should be invisible');
assert.ok(list.$('tbody td.o_list_record_selector').length, 'should have at least one record');
list.$('tbody td.o_list_record_selector:first input').click();
assert.ok(!list.sidebar.$el.hasClass('o_hidden'), 'sidebar should be visible');
assert.notOk(list.sidebar.$('a:contains(Delete)').length, 'sidebar should not have Delete button');
list.destroy();
});
QUnit.test('simple editable rendering', function (assert) {
assert.expect(12);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
});
assert.strictEqual(list.$('th').length, 3, "should have 2 th");
assert.strictEqual(list.$('th').length, 3, "should have 3 th");
assert.strictEqual(list.$('td:contains(yop)').length, 1, "should contain yop");
assert.ok(list.$buttons.find('.o_list_button_add').is(':visible'),
"should have a visible Create button");
assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'),
"should not have a visible save button");
assert.ok(!list.$buttons.find('.o_list_button_discard').is(':visible'),
"should not have a visible discard button");
list.$('td:not(.o_list_record_selector)').first().click();
assert.ok(!list.$buttons.find('.o_list_button_add').is(':visible'),
"should not have a visible Create button");
assert.ok(list.$buttons.find('.o_list_button_save').is(':visible'),
"should have a visible save button");
assert.ok(list.$buttons.find('.o_list_button_discard').is(':visible'),
"should have a visible discard button");
list.$buttons.find('.o_list_button_save').click();
assert.ok(list.$buttons.find('.o_list_button_add').is(':visible'),
"should have a visible Create button");
assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'),
"should not have a visible save button");
assert.ok(!list.$buttons.find('.o_list_button_discard').is(':visible'),
"should not have a visible discard button");
list.destroy();
});
QUnit.test('invisible columns are not displayed', function (assert) {
assert.expect(1);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree>' +
'<field name="foo"/>' +
'<field name="bar" invisible="1"/>' +
'</tree>',
});
// 1 th for checkbox, 1 for 1 visible column
assert.strictEqual(list.$('th').length, 2, "should have 2 th");
list.destroy();
});
QUnit.test('record-depending invisible lines are correctly aligned', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree>' +
'<field name="foo"/>' +
'<field name="bar" attrs="{\'invisible\': [(\'id\',\'=\', 1)]}"/>' +
'<field name="int_field"/>' +
'</tree>',
});
assert.strictEqual(list.$('tbody tr:first td').length, 4,
"there should be 4 cells in the first row");
assert.strictEqual(list.$('tbody td.o_invisible_modifier').length, 1,
"there should be 1 invisible bar cell");
assert.ok(list.$('tbody tr:first td:eq(2)').hasClass('o_invisible_modifier'),
"the 3rd cell should be invisible");
assert.strictEqual(list.$('tbody tr:eq(0) td:visible').length, list.$('tbody tr:eq(1) td:visible').length,
"there should be the same number of visible cells in different rows");
list.destroy();
});
QUnit.test('do not perform extra RPC to read invisible many2one fields', function (assert) {
assert.expect(3);
this.data.foo.fields.m2o.default = 2;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="foo"/>' +
'<field name="m2o" invisible="1"/>' +
'</tree>',
mockRPC: function (route, args) {
assert.step(_.last(route.split('/')));
return this._super.apply(this, arguments);
},
});
list.$buttons.find('.o_list_button_add').click();
assert.verifySteps(['search_read', 'default_get'], "no nameget should be done");
list.destroy();
});
QUnit.test('at least 4 rows are rendered, even if less data', function (assert) {
assert.expect(1);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="bar"/></tree>',
domain: [['bar', '=', true]],
});
assert.strictEqual(list.$('tbody tr').length, 4, "should have 4 rows");
list.destroy();
});
QUnit.test('basic grouped list rendering', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/><field name="bar"/></tree>',
groupBy: ['bar'],
});
assert.strictEqual(list.$('th:contains(Foo)').length, 1, "should contain Foo");
assert.strictEqual(list.$('th:contains(Bar)').length, 1, "should contain Bar");
assert.strictEqual(list.$('tr.o_group_header').length, 2, "should have 2 .o_group_header");
assert.strictEqual(list.$('th.o_group_name').length, 2, "should have 2 .o_group_name");
list.destroy();
});
QUnit.test('many2one field rendering', function (assert) {
assert.expect(1);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="m2o"/></tree>',
});
assert.ok(list.$('td:contains(Value 1)').length,
"should have the display_name of the many2one");
list.destroy();
});
QUnit.test('grouped list view, with 1 open group', function (assert) {
assert.expect(6);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/><field name="int_field"/></tree>',
groupBy: ['foo'],
});
list.$('th.o_group_name').get(1).click();
assert.strictEqual(list.$('tbody:eq(1) tr').length, 2, "open group should contain 2 records");
assert.strictEqual(list.$('tbody').length, 3, "should contain 3 tbody");
assert.strictEqual(list.$('td:contains(9)').length, 1, "should contain 9");
assert.strictEqual(list.$('td:contains(-4)').length, 1, "should contain -4");
assert.strictEqual(list.$('td:contains(10)').length, 1, "should contain 10");
assert.strictEqual(list.$('tr.o_group_header td:contains(10)').length, 1, "but 10 should be in a header");
list.destroy();
});
QUnit.test('opening records when clicking on record', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/></tree>',
});
testUtils.intercept(list, "open_record", function () {
assert.ok("list view should trigger 'open_record' event");
});
list.$('tr td:not(.o_list_record_selector)').first().click();
list.update({groupBy: ['foo']});
assert.strictEqual(list.$('tr.o_group_header').length, 3, "list should be grouped");
list.$('th.o_group_name').first().click();
list.$('tr:not(.o_group_header) td:not(.o_list_record_selector)').first().click();
list.destroy();
});
QUnit.test('editable list view: readonly fields cannot be edited', function (assert) {
assert.expect(4);
this.data.foo.fields.foo.readonly = true;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom">' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'<field name="int_field" readonly="1"/>' +
'</tree>',
});
var $td = list.$('td:not(.o_list_record_selector)').first();
var $second_td = list.$('td:not(.o_list_record_selector)').eq(1);
var $third_td = list.$('td:not(.o_list_record_selector)').eq(2);
$td.click();
assert.ok($td.parent().hasClass('o_selected_row'),
"row should be in edit mode");
assert.ok($td.hasClass('o_readonly_modifier'),
"foo cell should be readonly in edit mode");
assert.ok(!$second_td.hasClass('o_readonly_modifier'),
"bar cell should be editable");
assert.ok($third_td.hasClass('o_readonly_modifier'),
"int_field cell should be readonly in edit mode");
list.destroy();
});
QUnit.test('basic operations for editable list renderer', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
});
var $td = list.$('td:not(.o_list_record_selector)').first();
assert.ok(!$td.parent().hasClass('o_selected_row'), "td should not be in edit mode");
$td.click();
assert.ok($td.parent().hasClass('o_selected_row'), "td should be in edit mode");
list.destroy();
});
QUnit.test('editable list: add a line and discard', function (assert) {
assert.expect(11);
testUtils.patch(basicFields.FieldChar, {
destroy: function () {
assert.step('destroy');
this._super.apply(this, arguments);
},
});
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
domain: [['foo', '=', 'yop']],
});
assert.strictEqual(list.$('tbody tr').length, 4,
"list should contain 4 rows");
assert.strictEqual(list.$('.o_data_row').length, 1,
"list should contain one record (and thus 3 empty rows)");
assert.strictEqual(list.pager.$('.o_pager_value').text(), '1-1',
"pager should be correct");
list.$buttons.find('.o_list_button_add').click();
assert.strictEqual(list.$('tbody tr').length, 4,
"list should still contain 4 rows");
assert.strictEqual(list.$('.o_data_row').length, 2,
"list should contain two record (and thus 2 empty rows)");
assert.strictEqual(list.pager.$('.o_pager_value').text(), '1-2',
"pager should be correct");
list.$buttons.find('.o_list_button_discard').click();
assert.strictEqual(list.$('tbody tr').length, 4,
"list should still contain 4 rows");
assert.strictEqual(list.$('.o_data_row').length, 1,
"list should contain one record (and thus 3 empty rows)");
assert.strictEqual(list.pager.$('.o_pager_value').text(), '1-1',
"pager should be correct");
assert.verifySteps(['destroy'],
"should have destroyed the widget of the removed line");
testUtils.unpatch(basicFields.FieldChar);
list.destroy();
});
QUnit.test('field changes are triggered correctly', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
});
var $td = list.$('td:not(.o_list_record_selector)').first();
var n = 0;
testUtils.intercept(list, "field_changed", function () {
n += 1;
});
$td.click();
$td.find('input').val('abc').trigger('input');
assert.strictEqual(n, 1, "field_changed should have been triggered");
list.$('td:not(.o_list_record_selector)').eq(2).click();
assert.strictEqual(n, 1, "field_changed should not have been triggered");
list.destroy();
});
QUnit.test('editable list view: basic char field edition', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
});
var $td = list.$('td:not(.o_list_record_selector)').first();
$td.click();
$td.find('input').val('abc').trigger('input');
assert.strictEqual($td.find('input').val(), 'abc', "char field has been edited correctly");
var $next_row_td = list.$('tbody tr:eq(1) td:not(.o_list_record_selector)').first();
$next_row_td.click(); // should trigger the save of the previous row
assert.strictEqual(list.$('td:not(.o_list_record_selector)').first().text(), 'abc',
'changes should be saved correctly');
assert.ok(!list.$('tbody tr').first().hasClass('o_selected_row'),
'saved row should be in readonly mode');
assert.strictEqual(this.data.foo.records[0].foo, 'abc',
"the edition should have been properly saved");
list.destroy();
});
QUnit.test('editable list view: save data when list sorting in edit mode', function (assert) {
assert.expect(3);
this.data.foo.fields.foo.sortable = true;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/></tree>',
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.deepEqual(args.args, [[1], {foo: 'xyz'}],
"should correctly save the edited record");
}
return this._super.apply(this, arguments);
}
});
list.$('.o_data_cell:first').click();
list.$('input[name="foo"]').val('xyz').trigger('input');
list.$('.o_column_sortable').click();
assert.ok(list.$('.o_data_row:first').hasClass('o_selected_row'),
"first row should still be in edition");
list.$buttons.find('.o_list_button_save').click();
assert.ok(!list.$buttons.hasClass('o-editing'),
"list buttons should be back to their readonly mode");
list.destroy();
});
QUnit.test('selection changes are triggered correctly', function (assert) {
assert.expect(8);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/><field name="bar"/></tree>',
});
var $tbody_selector = list.$('tbody .o_list_record_selector input').first();
var $thead_selector = list.$('thead .o_list_record_selector input');
var n = 0;
testUtils.intercept(list, "selection_changed", function () {
n += 1;
});
// tbody checkbox click
$tbody_selector.click();
assert.strictEqual(n, 1, "selection_changed should have been triggered");
assert.ok($tbody_selector.is(':checked'), "selection checkbox should be checked");
$tbody_selector.click();
assert.strictEqual(n, 2, "selection_changed should have been triggered");
assert.ok(!$tbody_selector.is(':checked'), "selection checkbox shouldn't be checked");
// head checkbox click
$thead_selector.click();
assert.strictEqual(n, 3, "selection_changed should have been triggered");
assert.strictEqual(list.$('tbody .o_list_record_selector input:checked').length,
list.$('tbody tr').length, "all selection checkboxes should be checked");
$thead_selector.click();
assert.strictEqual(n, 4, "selection_changed should have been triggered");
assert.strictEqual(list.$('tbody .o_list_record_selector input:checked').length, 0,
"no selection checkbox should be checked");
list.destroy();
});
QUnit.test('selection is reset on reload', function (assert) {
assert.expect(5);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree>' +
'<field name="foo"/>' +
'<field name="int_field" sum="Sum"/>' +
'</tree>',
});
assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32',
"total should be 32 (no record selected)");
// select first record
var $firstRowSelector = list.$('tbody .o_list_record_selector input').first();
$firstRowSelector.click();
assert.ok($firstRowSelector.is(':checked'), "first row should be selected");
assert.strictEqual(list.$('tfoot td:nth(2)').text(), '10',
"total should be 10 (first record selected)");
// reload
list.reload();
$firstRowSelector = list.$('tbody .o_list_record_selector input').first();
assert.notOk($firstRowSelector.is(':checked'),
"first row should no longer be selected");
assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32',
"total should be 32 (no more record selected)");
list.destroy();
});
QUnit.test('aggregates are computed correctly', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="int_field" sum="Sum"/></tree>',
});
var $tbody_selectors = list.$('tbody .o_list_record_selector input');
var $thead_selector = list.$('thead .o_list_record_selector input');
assert.strictEqual(list.$('tfoot td:nth(2)').text(), "32", "total should be 32");
$tbody_selectors.first().click();
$tbody_selectors.last().click();
assert.strictEqual(list.$('tfoot td:nth(2)').text(), "6",
"total should be 6 as first and last records are selected");
$thead_selector.click();
assert.strictEqual(list.$('tfoot td:nth(2)').text(), "32",
"total should be 32 as all records are selected");
// Let's update the view to dislay NO records
list.update({domain: ['&', ['bar', '=', false], ['int_field', '>', 0]]});
assert.strictEqual(list.$('tfoot td:nth(2)').text(), "0", "total should have been recomputed to 0");
list.destroy();
});
QUnit.test('aggregates are computed correctly in grouped lists', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
groupBy: ['m2o'],
arch: '<tree editable="bottom"><field name="int_field" sum="Sum"/></tree>',
});
var $groupHeader1 = list.$('.o_group_header').filter(function (index, el) {
return $(el).data('group').res_id === 1;
});
var $groupHeader2 = list.$('.o_group_header').filter(function (index, el) {
return $(el).data('group').res_id === 2;
});
assert.strictEqual($groupHeader1.find('td:nth(1)').text(), "23", "first group total should be 23");
assert.strictEqual($groupHeader2.find('td:nth(1)').text(), "9", "second group total should be 9");
assert.strictEqual(list.$('tfoot td:nth(2)').text(), "32", "total should be 32");
$groupHeader1.click();
list.$('tbody .o_list_record_selector input').first().click();
assert.strictEqual(list.$('tfoot td:nth(2)').text(), "10",
"total should be 10 as first record of first group is selected");
list.destroy();
});
QUnit.test('aggregates are updated when a line is edited', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="int_field" sum="Sum"/></tree>',
});
assert.strictEqual(list.$('td[title="Sum"]').text(), "32", "current total should be 32");
list.$('tr.o_data_row td.o_data_cell').first().click();
list.$('td.o_data_cell input').val("15").trigger("input");
assert.strictEqual(list.$('td[title="Sum"]').text(), "37",
"current total should now be 37");
list.destroy();
});
QUnit.test('aggregates are formatted according to field widget', function (assert) {
assert.expect(1);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree>' +
'<field name="foo"/>' +
'<field name="qux" widget="float_time" sum="Sum"/>' +
'</tree>',
});
assert.strictEqual(list.$('tfoot td:nth(2)').text(), '19:24',
"total should be formatted as a float_time");
list.destroy();
});
QUnit.test('groups can be sorted on aggregates', function (assert) {
assert.expect(10);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
groupBy: ['foo'],
arch: '<tree editable="bottom"><field name="int_field" sum="Sum"/></tree>',
mockRPC: function (route, args) {
if (args.method === 'read_group') {
assert.step(args.kwargs.orderby || 'default order');
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(list.$('tbody .o_list_number').text(), '10517',
"initial order should be 10, 5, 17");
assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32', "total should be 32");
list.$('.o_column_sortable').click(); // sort (int_field ASC)
assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32', "total should still be 32");
assert.strictEqual(list.$('tbody .o_list_number').text(), '51017',
"order should be 5, 10, 17");
list.$('.o_column_sortable').click(); // sort (int_field DESC)
assert.strictEqual(list.$('tbody .o_list_number').text(), '17105',
"initial order should be 17, 10, 5");
assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32', "total should still be 32");
assert.verifySteps(['default order', 'int_field ASC', 'int_field DESC']);
list.destroy();
});
QUnit.test('properly apply onchange in simple case', function (assert) {
assert.expect(2);
this.data.foo.onchanges = {
foo: function (obj) {
obj.int_field = obj.foo.length + 1000;
},
};
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top"><field name="foo"/><field name="int_field"/></tree>',
});
var $foo_td = list.$('td:not(.o_list_record_selector)').first();
var $int_field_td = list.$('td:not(.o_list_record_selector)').eq(1);
assert.strictEqual($int_field_td.text(), '10', "should contain initial value");
$foo_td.click();
$foo_td.find('input').val('tralala').trigger('input');
assert.strictEqual($int_field_td.find('input').val(), "1007",
"should contain input with onchange applied");
list.destroy();
});
QUnit.test('column width should not change when switching mode', function (assert) {
assert.expect(10);
// Warning: this test is css dependant
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="foo"/>' +
'<field name="int_field" readonly="1"/>' +
'<field name="m2o"/>' +
'<field name="m2m" widget="many2many_tags"/>' +
'</tree>',
});
var startWidths = _.pluck(list.$('thead th'), 'offsetWidth');
// start edition of first row
list.$('td:not(.o_list_record_selector)').first().click();
var editionWidths = _.pluck(list.$('thead th'), 'offsetWidth');
// leave edition
list.$buttons.find('.o_list_button_save').click();
var readonlyWidths = _.pluck(list.$('thead th'), 'offsetWidth');
for (var i = 0; i < startWidths.length; i++) {
assert.strictEqual(startWidths[i], editionWidths[i],
'width of columns should remain unchanged which switching from readonly to edit mode');
assert.strictEqual(editionWidths[i], readonlyWidths[i],
'width of columns should remain unchanged which switching from edit to readonly mode');
}
list.destroy();
});
QUnit.test('deleting one record', function (assert) {
assert.expect(5);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
viewOptions: {sidebar: true},
arch: '<tree><field name="foo"/></tree>',
});
assert.ok(list.sidebar.$el.hasClass('o_hidden'), 'sidebar should be invisible');
assert.strictEqual(list.$('tbody td.o_list_record_selector').length, 4, "should have 4 records");
list.$('tbody td.o_list_record_selector:first input').click();
assert.ok(!list.sidebar.$el.hasClass('o_hidden'), 'sidebar should be visible');
list.sidebar.$('a:contains(Delete)').click();
assert.ok($('body').hasClass('modal-open'), 'body should have modal-open clsss');
$('body .modal-dialog button span:contains(Ok)').click();
assert.strictEqual(list.$('tbody td.o_list_record_selector').length, 3, "should have 3 records");
list.destroy();
});
QUnit.test('archiving one record', function (assert) {
assert.expect(9);
// add active field on foo model and make all records active
this.data.foo.fields.active = {string: 'Active', type: 'boolean', default: true};
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
viewOptions: {sidebar: true},
arch: '<tree><field name="foo"/></tree>',
mockRPC: function (route) {
if (route === '/web/dataset/call_kw/ir.attachment/search_read') {
return $.when([]);
}
assert.step(route);
return this._super.apply(this, arguments);
},
});
assert.ok(list.sidebar.$el.hasClass('o_hidden'), 'sidebar should be invisible');
assert.strictEqual(list.$('tbody td.o_list_record_selector').length, 4, "should have 4 records");
list.$('tbody td.o_list_record_selector:first input').click();
assert.ok(!list.sidebar.$el.hasClass('o_hidden'), 'sidebar should be visible');
assert.verifySteps(['/web/dataset/search_read']);
list.sidebar.$('a:contains(Archive)').click();
assert.strictEqual(list.$('tbody td.o_list_record_selector').length, 3, "should have 3 records");
assert.verifySteps(['/web/dataset/search_read', '/web/dataset/call_kw/foo/write', '/web/dataset/search_read']);
list.destroy();
});
QUnit.test('pager (ungrouped and grouped mode), default limit', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/><field name="bar"/></tree>',
mockRPC: function (route, args) {
if (route === '/web/dataset/search_read') {
assert.strictEqual(args.limit, 80, "default limit should be 80 in List");
}
return this._super.apply(this, arguments);
},
});
assert.ok(!list.pager.$el.hasClass('o_hidden'), "pager should be visible");
assert.strictEqual(list.pager.state.size, 4, "pager's size should be 4");
list.update({ groupBy: ['bar']});
assert.ok(list.pager.$el.hasClass('o_hidden'), "pager should be invisible");
list.destroy();
});
QUnit.test('can sort records when clicking on header', function (assert) {
assert.expect(9);
this.data.foo.fields.foo.sortable = true;
var nbSearchRead = 0;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/><field name="bar"/></tree>',
mockRPC: function (route) {
if (route === '/web/dataset/search_read') {
nbSearchRead++;
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(nbSearchRead, 1, "should have done one search_read");
assert.ok(list.$('tbody tr:first td:contains(yop)').length,
"record 1 should be first");
assert.ok(list.$('tbody tr:eq(3) td:contains(blip)').length,
"record 3 should be first");
nbSearchRead = 0;
list.$('thead th:contains(Foo)').click();
assert.strictEqual(nbSearchRead, 1, "should have done one search_read");
assert.ok(list.$('tbody tr:first td:contains(blip)').length,
"record 3 should be first");
assert.ok(list.$('tbody tr:eq(3) td:contains(yop)').length,
"record 1 should be first");
nbSearchRead = 0;
list.$('thead th:contains(Foo)').click();
assert.strictEqual(nbSearchRead, 1, "should have done one search_read");
assert.ok(list.$('tbody tr:first td:contains(yop)').length,
"record 3 should be first");
assert.ok(list.$('tbody tr:eq(3) td:contains(blip)').length,
"record 1 should be first");
list.destroy();
});
QUnit.test('use default_order', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree default_order="foo"><field name="foo"/><field name="bar"/></tree>',
mockRPC: function (route, args) {
if (route === '/web/dataset/search_read') {
assert.strictEqual(args.sort, 'foo ASC',
"should correctly set the sort attribute");
}
return this._super.apply(this, arguments);
},
});
assert.ok(list.$('tbody tr:first td:contains(blip)').length,
"record 3 should be first");
assert.ok(list.$('tbody tr:eq(3) td:contains(yop)').length,
"record 1 should be first");
list.destroy();
});
QUnit.test('use more complex default_order', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree default_order="foo, bar desc, int_field">' +
'<field name="foo"/><field name="bar"/>' +
'</tree>',
mockRPC: function (route, args) {
if (route === '/web/dataset/search_read') {
assert.strictEqual(args.sort, 'foo ASC, bar DESC, int_field ASC',
"should correctly set the sort attribute");
}
return this._super.apply(this, arguments);
},
});
assert.ok(list.$('tbody tr:first td:contains(blip)').length,
"record 3 should be first");
assert.ok(list.$('tbody tr:eq(3) td:contains(yop)').length,
"record 1 should be first");
list.destroy();
});
QUnit.test('use default_order on editable tree: sort on save', function (assert) {
assert.expect(8);
this.data.foo.records[0].o2m = [1, 3];
var form = createView({
View: FormView,
model: 'foo',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="o2m">' +
'<tree editable="bottom" default_order="display_name">' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
"Value 1 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
"Value 3 should be second");
var $o2m = form.$('.o_field_widget[name=o2m]');
form.$('.o_field_x2many_list_row_add a').click();
$o2m.find('.o_field_widget').val("Value 2").trigger('input');
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
"Value 1 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
"Value 3 should be second");
assert.ok(form.$('tbody tr:eq(2) td input').val(),
"Value 2 should be third (shouldn't be sorted)");
form.$buttons.find('.o_form_button_save').click();
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
"Value 1 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 2)').length,
"Value 2 should be second (should be sorted after saving)");
assert.ok(form.$('tbody tr:eq(2) td:contains(Value 3)').length,
"Value 3 should be third");
form.destroy();
});
QUnit.test('use default_order on editable tree: sort on demand', function (assert) {
assert.expect(11);
this.data.foo.records[0].o2m = [1, 3];
this.data.bar.fields = {name: {string: "Name", type: "char", sortable: true}};
this.data.bar.records[0].name = "Value 1";
this.data.bar.records[2].name = "Value 3";
var form = createView({
View: FormView,
model: 'foo',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="o2m">' +
'<tree editable="bottom" default_order="name">' +
'<field name="name"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
"Value 1 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
"Value 3 should be second");
var $o2m = form.$('.o_field_widget[name=o2m]');
form.$('.o_field_x2many_list_row_add a').click();
$o2m.find('.o_field_widget').val("Value 2").trigger('input');
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
"Value 1 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
"Value 3 should be second");
assert.ok(form.$('tbody tr:eq(2) td input').val(),
"Value 2 should be third (shouldn't be sorted)");
form.$('.o_form_sheet_bg').click(); // validate the row before sorting
$o2m.find('.o_column_sortable').click(); // resort list after edition
assert.strictEqual(form.$('tbody tr:first').text(), 'Value 1',
"Value 1 should be first");
assert.strictEqual(form.$('tbody tr:eq(1)').text(), 'Value 2',
"Value 2 should be second (should be sorted after saving)");
assert.strictEqual(form.$('tbody tr:eq(2)').text(), 'Value 3',
"Value 3 should be third");
$o2m.find('.o_column_sortable').click();
assert.strictEqual(form.$('tbody tr:first').text(), 'Value 3',
"Value 3 should be first");
assert.strictEqual(form.$('tbody tr:eq(1)').text(), 'Value 2',
"Value 2 should be second (should be sorted after saving)");
assert.strictEqual(form.$('tbody tr:eq(2)').text(), 'Value 1',
"Value 1 should be third");
form.destroy();
});
QUnit.test('use default_order on editable tree: sort on demand in page', function (assert) {
assert.expect(4);
this.data.bar.fields = {name: {string: "Name", type: "char", sortable: true}};
var ids = [];
for (var i=0; i<45; i++) {
var id = 4 + i;
ids.push(id);
this.data.bar.records.push({
id: id,
name: "Value " + (id < 10 ? '0' : '') + id,
});
}
this.data.foo.records[0].o2m = ids;
var form = createView({
View: FormView,
model: 'foo',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="o2m">' +
'<tree editable="bottom" default_order="name">' +
'<field name="name"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
});
// Change page
form.$('.o_pager_next').click();
assert.strictEqual(form.$('tbody tr:first').text(), 'Value 44',
"record 44 should be first");
assert.strictEqual(form.$('tbody tr:eq(4)').text(), 'Value 48',
"record 48 should be last");
form.$('.o_column_sortable').click();
assert.strictEqual(form.$('tbody tr:first').text(), 'Value 08',
"record 48 should be first");
assert.strictEqual(form.$('tbody tr:eq(4)').text(), 'Value 04',
"record 44 should be first");
form.destroy();
});
QUnit.test('can display button in edit mode', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom">' +
'<field name="foo"/>' +
'<button name="notafield" type="object" icon="fa-asterisk" class="o_yeah"/>' +
'</tree>',
});
assert.ok(list.$('tbody button').length, "should have a button");
assert.ok(list.$('tbody button').hasClass('o_yeah'), "class should be set on the button");
list.destroy();
});
QUnit.test('can display a list with a many2many field', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree>' +
'<field name="m2m"/>' +
'</tree>',
mockRPC: function (route, args) {
assert.step(route);
return this._super(route, args);
},
});
assert.verifySteps(['/web/dataset/search_read'], "should have done 1 search_read");
assert.ok(list.$('td:contains(3 records)').length,
"should have a td with correct formatted value");
list.destroy();
});
QUnit.test('list without import button', function (assert) {
assert.expect(1);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/></tree>',
context: {
group_by_no_leaf: true,
}
});
assert.ok(!list.$buttons, "should not have any buttons");
list.destroy();
});
QUnit.test('display a tooltip on a field', function (assert) {
assert.expect(2);
var initialDebugMode = config.debug;
config.debug = false;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/></tree>',
});
// this is done to force the tooltip to show immediately instead of waiting
// 1000 ms. not totally academic, but a short test suite is easier to sell :(
list.$('th:not(.o_list_record_selector)').tooltip('show', false);
list.$('th:not(.o_list_record_selector)').trigger($.Event('mouseenter'));
assert.strictEqual($('.tooltip .oe_tooltip_string').length, 0, "should not have rendered a tooltip");
config.debug = true;
// it is necessary to rerender the list so tooltips can be properly created
list.reload();
list.$('th:not(.o_list_record_selector)').tooltip('show', false);
list.$('th:not(.o_list_record_selector)').trigger($.Event('mouseenter'));
assert.strictEqual($('.tooltip .oe_tooltip_string').length, 1, "should have rendered a tooltip");
config.debug = initialDebugMode;
list.destroy();
});
QUnit.test('support row decoration', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree decoration-info="int_field > 5">' +
'<field name="foo"/><field name="int_field"/>' +
'</tree>',
});
assert.strictEqual(list.$('tbody tr.text-info').length, 3,
"should have 3 columns with text-info class");
assert.strictEqual(list.$('tbody tr').length, 4, "should have 4 rows");
list.destroy();
});
QUnit.test('support row decoration (with unset numeric values)', function (assert) {
assert.expect(2);
this.data.foo.records = [];
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom" decoration-danger="int_field &lt; 0">' +
'<field name="int_field"/>' +
'</tree>',
});
list.$buttons.find('.o_list_button_add').click();
assert.strictEqual(list.$('tr.o_data_row.text-danger').length, 0,
"the data row should not have .text-danger decoration (int_field is unset)");
list.$('input[name="int_field"]').val('-3').trigger('input');
assert.strictEqual(list.$('tr.o_data_row.text-danger').length, 1,
"the data row should have .text-danger decoration (int_field is negative)");
list.destroy();
});
QUnit.test('support row decoration with date', function (assert) {
assert.expect(3);
this.data.foo.records[0].datetime = '2017-02-27 12:51:35';
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree decoration-info="datetime == \'2017-02-27 12:51:35\'" decoration-danger="datetime &gt; \'2017-02-27 12:51:35\' AND datetime &lt; \'2017-02-27 10:51:35\'">' +
'<field name="datetime"/><field name="int_field"/>' +
'</tree>',
});
assert.strictEqual(list.$('tbody tr.text-info').length, 1,
"should have 1 columns with text-info class with good datetime");
assert.strictEqual(list.$('tbody tr.text-danger').length, 0,
"should have 0 columns with text-danger class with wrong timezone datetime");
assert.strictEqual(list.$('tbody tr').length, 4, "should have 4 rows");
list.destroy();
});
QUnit.test('no content helper when no data', function (assert) {
assert.expect(5);
var records = this.data.foo.records;
this.data.foo.records = [];
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/></tree>',
viewOptions: {
action: {
help: '<p class="hello">click to add a partner</p>'
}
},
});
assert.strictEqual(list.$('.oe_view_nocontent').length, 1,
"should display the no content helper");
assert.strictEqual(list.$('table').length, 0, "should not have a table in the dom");
assert.strictEqual(list.$('.oe_view_nocontent p.hello:contains(add a partner)').length, 1,
"should have rendered no content helper from action");
this.data.foo.records = records;
list.reload();
assert.strictEqual(list.$('.oe_view_nocontent').length, 0,
"should not display the no content helper");
assert.strictEqual(list.$('table').length, 1, "should have a table in the dom");
list.destroy();
});
QUnit.test('no nocontent helper when no data and no help', function (assert) {
assert.expect(3);
this.data.foo.records = [];
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/></tree>',
});
assert.strictEqual(list.$('.oe_view_nocontent').length, 0,
"should not display the no content helper");
assert.strictEqual(list.$('tr.o_data_row').length, 0,
"should not have any data row");
assert.strictEqual(list.$('table').length, 1, "should have a table in the dom");
list.destroy();
});
QUnit.test('list view, editable, without data', function (assert) {
assert.expect(11);
this.data.foo.records = [];
this.data.foo.fields.date.default = "2017-02-10";
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree string="Phonecalls" editable="top">' +
'<field name="date"/>' +
'<field name="m2o"/>' +
'<field name="foo"/>' +
'<button type="object" icon="fa-plus-square" name="method"/>' +
'</tree>',
viewOptions: {
action: {
help: '<p class="hello">click to add a partner</p>'
}
},
mockRPC: function (route, args) {
if (args.method === 'create') {
assert.ok(true, "should have created a record");
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(list.$('.oe_view_nocontent').length, 1,
"should have a no content helper displayed");
assert.strictEqual(list.$('div.table-responsive').length, 0,
"should not have a div.table-responsive");
assert.strictEqual(list.$('table').length, 0, "should not have rendered a table");
list.$buttons.find('.o_list_button_add').click();
assert.strictEqual(list.$('.oe_view_nocontent').length, 0,
"should not have a no content helper displayed");
assert.strictEqual(list.$('table').length, 1, "should have rendered a table");
assert.strictEqual(list.$el.css('height'), list.$('div.table-responsive').css('height'),
"the div for the table should take the full height");
assert.ok(list.$('tbody tr:eq(0)').hasClass('o_selected_row'),
"the date field td should be in edit mode");
assert.strictEqual(list.$('tbody tr:eq(0) td:eq(1)').text().trim(), "",
"the date field td should not have any content");
assert.strictEqual(list.$('.o_list_button button').prop('disabled'), true,
"buttons should be disabled while the record is not yet created");
list.$buttons.find('.o_list_button_save').click();
assert.strictEqual(list.$('.o_list_button button').prop('disabled'), false,
"buttons should not be disabled once the record is created");
list.destroy();
});
QUnit.test('list view, editable, with a button', function (assert) {
assert.expect(1);
this.data.foo.records = [];
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree string="Phonecalls" editable="top">' +
'<field name="foo"/>' +
'<button string="abc" icon="fa-phone" type="object" name="schedule_another_phonecall"/>' +
'</tree>',
});
list.$buttons.find('.o_list_button_add').click();
assert.strictEqual(list.$('table button.o_icon_button i.fa-phone').length, 1,
"should have rendered a button");
list.destroy();
});
QUnit.test('list view with a button without icon', function (assert) {
assert.expect(1);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree string="Phonecalls" editable="top">' +
'<field name="foo"/>' +
'<button string="abc" type="object" name="schedule_another_phonecall"/>' +
'</tree>',
});
assert.strictEqual(list.$('table button').first().text(), 'abc',
"should have rendered a button with string attribute as label");
list.destroy();
});
QUnit.test('list view, editable, can discard', function (assert) {
assert.expect(5);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree string="Phonecalls" editable="top">' +
'<field name="foo"/>' +
'</tree>',
});
assert.strictEqual(list.$('td:not(.o_list_record_selector) input').length, 0, "no input should be in the table");
list.$('tbody td:not(.o_list_record_selector):first').click();
assert.strictEqual(list.$('td:not(.o_list_record_selector) input').length, 1, "first cell should be editable");
assert.ok(list.$buttons.find('.o_list_button_discard').is(':visible'),
"discard button should be visible");
list.$buttons.find('.o_list_button_discard').click();
assert.strictEqual(list.$('td:not(.o_list_record_selector) input').length, 0, "no input should be in the table");
assert.ok(!list.$buttons.find('.o_list_button_discard').is(':visible'),
"discard button should not be visible");
list.destroy();
});
QUnit.test('editable list view, click on the list to save', function (assert) {
assert.expect(3);
this.data.foo.fields.date.default = "2017-02-10";
this.data.foo.records = [];
var createCount = 0;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree string="Phonecalls" editable="top">' +
'<field name="date"/>' +
'</tree>',
mockRPC: function (route, args) {
if (args.method === 'create') {
createCount++;
}
return this._super.apply(this, arguments);
},
});
list.$buttons.find('.o_list_button_add').click();
list.$('div.table-responsive').click();
assert.strictEqual(createCount, 1, "should have created a record");
list.$buttons.find('.o_list_button_add').click();
list.$('tfoot').click();
assert.strictEqual(createCount, 2, "should have created a record");
list.$buttons.find('.o_list_button_add').click();
list.$('tbody tr').last().click();
assert.strictEqual(createCount, 3, "should have created a record");
list.destroy();
});
QUnit.test('click on a button in a list view', function (assert) {
assert.expect(9);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree>' +
'<field name="foo"/>' +
'<button string="a button" name="button_action" icon="fa-car" type="object"/>' +
'</tree>',
mockRPC: function (route) {
assert.step(route);
return this._super.apply(this, arguments);
},
intercepts: {
execute_action: function (event) {
assert.deepEqual(event.data.env.currentID, 1,
'should call with correct id');
assert.strictEqual(event.data.env.model, 'foo',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'button_action',
"should call correct method");
assert.strictEqual(event.data.action_data.type, 'object',
'should have correct type');
event.data.on_closed();
},
},
});
assert.strictEqual(list.$('.o_list_button').length, 4,
"there should be one button per row");
assert.strictEqual(list.$('.o_list_button:first .o_icon_button .fa.fa-car').length, 1,
'buttons should have correct icon');
list.$('.o_list_button:first > button').click(); // click on the button
assert.verifySteps(['/web/dataset/search_read', '/web/dataset/search_read'],
"should have reloaded the view (after the action is complete)");
list.destroy();
});
QUnit.test('invisible attrs in readonly and editable list', function (assert) {
assert.expect(5);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<button string="a button" name="button_action" icon="fa-car" ' +
'type="object" attrs="{\'invisible\': [(\'id\',\'=\', 1)]}"/>' +
'<field name="int_field"/>' +
'<field name="qux"/>' +
'<field name="foo" attrs="{\'invisible\': [(\'id\',\'=\', 1)]}"/>' +
'</tree>',
});
assert.equal(list.$('tbody tr:nth(0) td:nth(4)').html(), "",
"td that contains an invisible field should be empty");
assert.equal(list.$('tbody tr:nth(0) td:nth(1)').html(), "",
"td that contains an invisible button should be empty");
// edit first row
list.$('tbody tr:nth(0) td:nth(2)').click(); // click on first row to edit it
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(4) input.o_invisible_modifier').length, 1,
"td that contains an invisible field should not be empty in edition");
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > button.o_invisible_modifier').length, 1,
"td that contains an invisible button should not be empty in edition");
list.$buttons.find('.o_list_button_discard').click(); // leave edition
// click on the invisible field's cell to edit first row
list.$('tbody tr:nth(0) td:nth(4)').click();
assert.ok(list.$('tbody tr:nth(0)').hasClass('o_selected_row'),
"first row should be in edition");
list.destroy();
});
QUnit.test('monetary fields are properly rendered', function (assert) {
assert.expect(3);
var currencies = {};
_.each(this.data.res_currency.records, function (currency) {
currencies[currency.id] = currency;
});
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree>' +
'<field name="id"/>' +
'<field name="amount"/>' +
'<field name="currency_id" invisible="1"/>' +
'</tree>',
session: {
currencies: currencies,
},
});
assert.strictEqual(list.$('tbody tr:first td').length, 3,
"currency_id column should not be in the table");
assert.strictEqual(list.$('tbody tr:first td:nth(2)').text().replace(/\s/g, ' '),
'1200.00 €', "currency_id column should not be in the table");
assert.strictEqual(list.$('tbody tr:nth(1) td:nth(2)').text().replace(/\s/g, ' '),
'$ 500.00', "currency_id column should not be in the table");
list.destroy();
});
QUnit.test('simple list with date and datetime', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="date"/><field name="datetime"/></tree>',
session: {
getTZOffset: function () {
return 120;
},
},
});
assert.strictEqual(list.$('td:eq(1)').text(), "01/25/2017",
"should have formatted the date");
assert.strictEqual(list.$('td:eq(2)').text(), "12/12/2016 12:55:05",
"should have formatted the datetime");
list.destroy();
});
QUnit.test('edit a row by clicking on a readonly field', function (assert) {
assert.expect(8);
this.data.foo.fields.foo.readonly = true;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="int_field"/></tree>',
});
assert.ok(list.$('.o_data_row:first td:nth(1)').hasClass('o_readonly_modifier'),
"foo field cells should have class 'o_readonly_modifier'");
// edit the first row
list.$('.o_data_row:first td:nth(1)').click();
assert.ok(list.$('.o_data_row:first').hasClass('o_selected_row'),
"first row should be selected");
var $cell = list.$('.o_data_row:first td:nth(1)');
assert.ok($cell.hasClass('o_readonly_modifier') && $cell.parent().hasClass('o_selected_row'),
"foo field cells should have class 'o_readonly_modifier' and the row should be in edition");
assert.strictEqual(list.$('.o_data_row:first td:nth(1) span').text(), 'yop',
"a widget should have been rendered for readonly fields");
assert.ok(list.$('.o_data_row:first td:nth(2)').parent().hasClass('o_selected_row'),
"field 'int_field' should be in edition");
assert.strictEqual(list.$('.o_data_row:first td:nth(2) input').length, 1,
"a widget for field 'int_field should have been rendered'");
// click again on readonly cell of first line: nothing should have changed
list.$('.o_data_row:first td:nth(1)').click();
assert.ok(list.$('.o_data_row:first').hasClass('o_selected_row'),
"first row should be selected");
assert.strictEqual(list.$('.o_data_row:first td:nth(2) input').length, 1,
"a widget for field 'int_field' should have been rendered (only once)");
list.destroy();
});
QUnit.test('list view with nested groups', function (assert) {
assert.expect(42);
this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1});
this.data.foo.records.push({id: 6, foo: "blip", int_field: 5, m2o: 2});
var nbRPCs = {readGroup: 0, searchRead: 0};
var envIDs = []; // the ids that should be in the environment during this test
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="id"/><field name="int_field"/></tree>',
groupBy: ['m2o', 'foo'],
mockRPC: function (route, args) {
if (args.method === 'read_group') {
if (args.kwargs.groupby[0] === 'foo') { // nested read_group
// called twice (once when opening the group, once when sorting)
assert.deepEqual(args.kwargs.domain, [['m2o', '=', 1]],
"nested read_group should be called with correct domain");
}
nbRPCs.readGroup++;
} else if (route === '/web/dataset/search_read') {
// called twice (once when opening the group, once when sorting)
assert.deepEqual(args.domain, [['foo', '=', 'blip'], ['m2o', '=', 1]],
"nested search_read should be called with correct domain");
nbRPCs.searchRead++;
}
return this._super.apply(this, arguments);
},
intercepts: {
switch_view: function (event) {
assert.strictEqual(event.data.res_id, 4,
"'switch_view' event has been triggered");
},
env_updated: function (event) {
assert.deepEqual(event.data.ids, envIDs,
"should notify the environment with the correct ids");
},
},
});
assert.strictEqual(nbRPCs.readGroup, 1, "should have done one read_group");
assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
// basic rendering tests
assert.strictEqual(list.$('tbody').length, 1, "there should be 1 tbody");
assert.strictEqual(list.$('.o_group_header').length, 2,
"should contain 2 groups at first level");
assert.strictEqual(list.$('.o_group_name:first').text(), 'Value 1 (4)',
"group should have correct name and count");
assert.strictEqual(list.$('.o_group_name .fa-caret-right').length, 2,
"the carret of closed groups should be right");
assert.strictEqual(list.$('.o_group_name:first span').css('padding-left'),
'0px', "groups of level 1 should have a 0px padding-left");
assert.strictEqual(list.$('.o_group_header:first td:last').text(), '16',
"group aggregates are correctly displayed");
// open the first group
nbRPCs = {readGroup: 0, searchRead: 0};
list.$('.o_group_header:first').click();
assert.strictEqual(nbRPCs.readGroup, 1, "should have done one read_group");
assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
var $openGroup = list.$('tbody:nth(1)');
assert.strictEqual(list.$('.o_group_name:first').text(), 'Value 1 (4)',
"group should have correct name and count (of records, not inner subgroups)");
assert.strictEqual(list.$('tbody').length, 3, "there should be 3 tbodys");
assert.strictEqual(list.$('.o_group_name:first .fa-caret-down').length, 1,
"the carret of open groups should be down");
assert.strictEqual($openGroup.find('.o_group_header').length, 3,
"open group should contain 3 groups");
assert.strictEqual($openGroup.find('.o_group_name:nth(2)').text(), 'blip (2)',
"group should have correct name and count");
assert.strictEqual($openGroup.find('.o_group_name:nth(2) span').css('padding-left'),
'20px', "groups of level 2 should have a 20px padding-left");
assert.strictEqual($openGroup.find('.o_group_header:nth(2) td:last').text(), '-11',
"inner group aggregates are correctly displayed");
// open subgroup
nbRPCs = {readGroup: 0, searchRead: 0};
envIDs = [4, 5]; // the opened subgroup contains these two records
$openGroup.find('.o_group_header:nth(2)').click();
assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
var $openSubGroup = list.$('tbody:nth(2)');
assert.strictEqual(list.$('tbody').length, 4, "there should be 4 tbodys");
assert.strictEqual($openSubGroup.find('.o_data_row').length, 2,
"open subgroup should contain 2 data rows");
assert.strictEqual($openSubGroup.find('.o_data_row:first td:last').text(), '-4',
"first record in open subgroup should be res_id 4 (with int_field -4)");
// open a record (should trigger event 'open_record')
$openSubGroup.find('.o_data_row:first').click();
// sort by int_field (ASC) and check that open groups are still open
nbRPCs = {readGroup: 0, searchRead: 0};
envIDs = [5, 4]; // order of the records changed
list.$('thead th:last').click();
assert.strictEqual(nbRPCs.readGroup, 2, "should have done two read_groups");
assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
$openSubGroup = list.$('tbody:nth(2)');
assert.strictEqual(list.$('tbody').length, 4, "there should be 4 tbodys");
assert.strictEqual($openSubGroup.find('.o_data_row').length, 2,
"open subgroup should contain 2 data rows");
assert.strictEqual($openSubGroup.find('.o_data_row:first td:last').text(), '-7',
"first record in open subgroup should be res_id 5 (with int_field -7)");
// close first level group
nbRPCs = {readGroup: 0, searchRead: 0};
envIDs = []; // the group being closed, there is no more record in the environment
list.$('.o_group_header:nth(1)').click();
assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
assert.strictEqual(list.$('tbody').length, 1, "there should be 1 tbody");
assert.strictEqual(list.$('.o_group_header').length, 2,
"should contain 2 groups at first level");
assert.strictEqual(list.$('.o_group_name .fa-caret-right').length, 2,
"the carret of closed groups should be right");
list.destroy();
});
QUnit.test('grouped list on selection field at level 2', function (assert) {
assert.expect(4);
this.data.foo.fields.priority = {
string: "Priority",
type: "selection",
selection: [[1, "Low"], [2, "Medium"], [3, "High"]],
default: 1,
};
this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1, priority: 2});
this.data.foo.records.push({id: 6, foo: "blip", int_field: 5, m2o: 1, priority: 3});
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="id"/><field name="int_field"/></tree>',
groupBy: ['m2o', 'priority'],
});
assert.strictEqual(list.$('.o_group_header').length, 2,
"should contain 2 groups at first level");
// open the first group
list.$('.o_group_header:first').click();
var $openGroup = list.$('tbody:nth(1)');
assert.strictEqual($openGroup.find('tr').length, 3,
"should have 3 subgroups");
assert.strictEqual($openGroup.find('tr').length, 3,
"should have 3 subgroups");
assert.strictEqual($openGroup.find('.o_group_name:first').text(), 'Low (3)',
"should display the selection name in the group header");
list.destroy();
});
QUnit.test('grouped list with a pager in a group', function (assert) {
assert.expect(6);
this.data.foo.records[3].bar = true;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/><field name="bar"/></tree>',
groupBy: ['bar'],
viewOptions: {
limit: 3,
},
});
var headerHeight = list.$('.o_group_header').css('height');
// basic rendering checks
list.$('.o_group_header').click();
assert.strictEqual(list.$('.o_group_header').css('height'), headerHeight,
"height of group header shouldn't have changed");
assert.ok(list.$('.o_group_header td:last').hasClass('o_group_pager'),
"last cell of open group header should have classname 'o_group_header'");
assert.strictEqual(list.$('.o_pager_value').text(), '1-3',
"pager's value should be correct");
assert.strictEqual(list.$('.o_data_row').length, 3,
"open group should display 3 records");
// go to next page
list.$('.o_pager_next').click();
assert.strictEqual(list.$('.o_pager_value').text(), '4-4',
"pager's value should be correct");
assert.strictEqual(list.$('.o_data_row').length, 1,
"open group should display 1 record");
list.destroy();
});
QUnit.test('edition: create new line, then discard', function (assert) {
assert.expect(8);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
});
assert.strictEqual(list.$('tr.o_data_row').length, 4,
"should have 4 records");
assert.strictEqual(list.$buttons.find('.o_list_button_add:visible').length, 1,
"create button should be visible");
assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 0,
"discard button should be hidden");
list.$buttons.find('.o_list_button_add').click();
assert.strictEqual(list.$buttons.find('.o_list_button_add:visible').length, 0,
"create button should be hidden");
assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 1,
"discard button should be visible");
list.$buttons.find('.o_list_button_discard').click();
assert.strictEqual(list.$('tr.o_data_row').length, 4,
"should still have 4 records");
assert.strictEqual(list.$buttons.find('.o_list_button_add:visible').length, 1,
"create button should be visible again");
assert.strictEqual(list.$buttons.find('.o_list_button_discard:visible').length, 0,
"discard button should be hidden again");
list.destroy();
});
QUnit.test('invisible attrs on fields are re-evaluated on field change', function (assert) {
assert.expect(7);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch:
'<tree editable="top">' +
'<field name="foo" attrs="{\'invisible\': [[\'bar\', \'=\', True]]}"/>' +
'<field name="bar"/>' +
'</tree>',
});
assert.strictEqual(list.$('tbody td.o_invisible_modifier').length, 3,
"there should be 3 invisible foo cells in readonly mode");
// Make first line editable
list.$('tbody tr:nth(0) td:nth(1)').click();
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"].o_invisible_modifier').length, 1,
"the foo field widget should have been rendered as invisible");
list.$('tbody tr:nth(0) td:nth(2) input').click();
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"]:not(.o_invisible_modifier)').length, 1,
"the foo field widget should have been marked as non-invisible");
assert.strictEqual(list.$('tbody td.o_invisible_modifier').length, 2,
"the foo field widget parent cell should not be invisible anymore");
list.$('tbody tr:nth(0) td:nth(2) input').click();
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"].o_invisible_modifier').length, 1,
"the foo field widget should have been marked as invisible again");
assert.strictEqual(list.$('tbody td.o_invisible_modifier').length, 3,
"the foo field widget parent cell should now be invisible again");
// Reswitch the cell to editable and save the row
list.$('tbody tr:nth(0) td:nth(2) input').click();
list.$('thead').click();
assert.strictEqual(list.$('tbody td.o_invisible_modifier').length, 2,
"there should be 2 invisible foo cells in readonly mode");
list.destroy();
});
QUnit.test('readonly attrs on fields are re-evaluated on field change', function (assert) {
assert.expect(9);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch:
'<tree editable="top">' +
'<field name="foo" attrs="{\'readonly\': [[\'bar\', \'=\', True]]}"/>' +
'<field name="bar"/>' +
'</tree>',
});
assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 3,
"there should be 3 readonly foo cells in readonly mode");
// Make first line editable
list.$('tbody tr:nth(0) td:nth(1)').click();
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > span[name="foo"]').length, 1,
"the foo field widget should have been rendered as readonly");
list.$('tbody tr:nth(0) td:nth(2) input').click();
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"]').length, 1,
"the foo field widget should have been rerendered as editable");
assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 2,
"the foo field widget parent cell should not be readonly anymore");
list.$('tbody tr:nth(0) td:nth(2) input').click();
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > span[name="foo"]').length, 1,
"the foo field widget should have been rerendered as readonly");
assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 3,
"the foo field widget parent cell should now be readonly again");
list.$('tbody tr:nth(0) td:nth(2) input').click();
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"]').length, 1,
"the foo field widget should have been rerendered as editable again");
assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 2,
"the foo field widget parent cell should not be readonly again");
// Click outside to leave edition mode
list.$el.click();
assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 2,
"there should be 2 readonly foo cells in readonly mode");
list.destroy();
});
QUnit.test('required attrs on fields are re-evaluated on field change', function (assert) {
assert.expect(7);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch:
'<tree editable="top">' +
'<field name="foo" attrs="{\'required\': [[\'bar\', \'=\', True]]}"/>' +
'<field name="bar"/>' +
'</tree>',
});
assert.strictEqual(list.$('tbody td.o_required_modifier').length, 3,
"there should be 3 required foo cells in readonly mode");
// Make first line editable
list.$('tbody tr:nth(0) td:nth(1)').click();
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"].o_required_modifier').length, 1,
"the foo field widget should have been rendered as required");
list.$('tbody tr:nth(0) td:nth(2) input').click();
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"]:not(.o_required_modifier)').length, 1,
"the foo field widget should have been marked as non-required");
assert.strictEqual(list.$('tbody td.o_required_modifier').length, 2,
"the foo field widget parent cell should not be required anymore");
list.$('tbody tr:nth(0) td:nth(2) input').click();
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"].o_required_modifier').length, 1,
"the foo field widget should have been marked as required again");
assert.strictEqual(list.$('tbody td.o_required_modifier').length, 3,
"the foo field widget parent cell should now be required again");
// Reswitch the cell to editable and save the row
list.$('tbody tr:nth(0) td:nth(2) input').click();
list.$('thead').click();
assert.strictEqual(list.$('tbody td.o_required_modifier').length, 2,
"there should be 2 required foo cells in readonly mode");
list.destroy();
});
QUnit.test('leaving unvalid rows in edition', function (assert) {
assert.expect(4);
var warnings = 0;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch:
'<tree editable="bottom">' +
'<field name="foo" required="1"/>' +
'<field name="bar"/>' +
'</tree>',
intercepts: {
warning: function (ev) {
warnings++;
},
},
});
// Start first line edition
var $firstFooTd = list.$('tbody tr:nth(0) td:nth(1)');
$firstFooTd.click();
// Remove required foo field value
$firstFooTd.find('input').val("").trigger("input");
// Try starting other line edition
var $secondFooTd = list.$('tbody tr:nth(1) td:nth(1)');
$secondFooTd.click();
assert.strictEqual($firstFooTd.parent('.o_selected_row').length, 1,
"first line should still be in edition as invalid");
assert.strictEqual(list.$('tbody tr.o_selected_row').length, 1,
"no other line should be in edition");
assert.strictEqual($firstFooTd.find('input.o_field_invalid').length, 1,
"the required field should be marked as invalid");
assert.strictEqual(warnings, 1,
"a warning should have been displayed");
list.destroy();
});
QUnit.test('open a virtual id', function (assert) {
assert.expect(1);
var list = createView({
View: ListView,
model: 'event',
data: this.data,
arch: '<tree><field name="name"/></tree>',
});
testUtils.intercept(list, "switch_view", function (event) {
assert.deepEqual(event.data, {
'model': 'event',
'res_id': '2-20170808020000',
'view_type': 'form',
'mode': 'readonly'
},
"should trigger switch to the form view for the record virtual id");
});
list.$('td:contains(virtual)').click();
list.destroy();
});
QUnit.test('pressing enter on last line of editable list view', function (assert) {
assert.expect(7);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/></tree>',
mockRPC: function (route, args) {
assert.step(route);
return this._super.apply(this, arguments);
},
});
// click on 3rd line
list.$('td:contains(gnap)').click();
assert.ok(list.$('tr.o_data_row:eq(2)').hasClass('o_selected_row'),
"3rd row should be selected");
// press enter in input
list.$('tr.o_selected_row input').trigger({type: 'keydown', which: 13}); // enter
assert.ok(list.$('tr.o_data_row:eq(3)').hasClass('o_selected_row'),
"4rd row should be selected");
assert.notOk(list.$('tr.o_data_row:eq(2)').hasClass('o_selected_row'),
"3rd row should no longer be selected");
// press enter on last row
list.$('tr.o_selected_row input').trigger({type: 'keydown', which: 13}); // enter
assert.strictEqual(list.$('tr.o_data_row').length, 5, "should have created a 5th row");
assert.verifySteps(['/web/dataset/search_read', '/web/dataset/call_kw/foo/default_get']);
list.destroy();
});
QUnit.test('pressing tab on last cell of editable list view', function (assert) {
assert.expect(7);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="int_field"/></tree>',
mockRPC: function (route) {
assert.step(route);
return this._super.apply(this, arguments);
},
});
list.$('td:contains(blip)').last().click();
assert.strictEqual(document.activeElement.name, "foo",
"focus should be on an input with name = foo");
list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: 9}); // tab
assert.strictEqual(document.activeElement.name, "int_field",
"focus should be on an input with name = int_field");
list.$('tr.o_selected_row input[name="int_field"]').trigger({type: 'keydown', which: 9}); // tab
assert.ok(list.$('tr.o_data_row:eq(4)').hasClass('o_selected_row'),
"5th row should be selected");
assert.strictEqual(document.activeElement.name, "foo",
"focus should be on an input with name = foo");
assert.verifySteps(['/web/dataset/search_read', '/web/dataset/call_kw/foo/default_get']);
list.destroy();
});
QUnit.test('navigation with tab and read completes after default_get', function (assert) {
assert.expect(8);
var defaultGetDef = $.Deferred();
var readDef = $.Deferred();
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="int_field"/></tree>',
mockRPC: function (route, args) {
if (args.method) {
assert.step(args.method);
}
var result = this._super.apply(this, arguments);
if (args.method === 'read') {
return readDef.then(_.constant(result));
}
if (args.method === 'default_get') {
return defaultGetDef.then(_.constant(result));
}
return result;
},
});
list.$('td:contains(-4)').last().click();
list.$('tr.o_selected_row input[name="int_field"]').val('1234').trigger('input');
list.$('tr.o_selected_row input[name="int_field"]').trigger({type: 'keydown', which: 9}); // tab
defaultGetDef.resolve();
assert.strictEqual(list.$('tbody tr.o_data_row').length, 4,
"should have 4 data rows");
readDef.resolve();
assert.strictEqual(list.$('tbody tr.o_data_row').length, 5,
"should have 5 data rows");
assert.strictEqual(list.$('td:contains(1234)').length, 1,
"should have a cell with new value");
// we trigger a tab to move to the second cell in the current row. this
// operation requires that this.currentRow is properly set in the
// list editable renderer.
list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: 9}); // tab
assert.ok(list.$('tr.o_data_row:eq(4)').hasClass('o_selected_row'),
"5th row should be selected");
assert.verifySteps(['write', 'read', 'default_get']);
list.destroy();
});
QUnit.test('display toolbar', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'event',
data: this.data,
arch: '<tree><field name="name"/></tree>',
toolbar: {
action: [{
model_name: 'event',
name: 'Action event',
type: 'ir.actions.server',
usage: 'ir_actions_server',
}],
print: [],
},
viewOptions: {
sidebar: true,
},
});
var $dropdowns = $('.o_web_client .o_control_panel .btn-group .o_dropdown_toggler_btn');
assert.strictEqual($dropdowns.length, 2,
"there should be 2 dropdowns in the toolbar.");
var $actions = $('.o_web_client .o_control_panel .btn-group .dropdown-menu')[1].children;
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 event',
"the custom action should have 'Action event' as name");
list.destroy();
});
QUnit.test('edit list line after line deletion', function (assert) {
assert.expect(5);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top"><field name="foo"/><field name="int_field"/></tree>',
});
list.$('.o_data_row:nth(2) > td:not(.o_list_record_selector)').first().click();
assert.ok(list.$('.o_data_row:nth(2)').is('.o_selected_row'),
"third row should be in edition");
list.$buttons.find('.o_list_button_discard').click();
list.$buttons.find('.o_list_button_add').click();
assert.ok(list.$('.o_data_row:nth(0)').is('.o_selected_row'),
"first row should be in edition (creation)");
list.$buttons.find('.o_list_button_discard').click();
assert.strictEqual(list.$('.o_selected_row').length, 0,
"no row should be selected");
list.$('.o_data_row:nth(2) > td:not(.o_list_record_selector)').first().click();
assert.ok(list.$('.o_data_row:nth(2)').is('.o_selected_row'),
"third row should be in edition");
assert.strictEqual(list.$('.o_selected_row').length, 1,
"no other row should be selected");
list.destroy();
});
QUnit.test('inputs are disabled when unselecting rows', function (assert) {
assert.expect(1);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/></tree>',
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.strictEqual($input.prop('disabled'), true,
"input should be disabled");
}
return this._super.apply(this, arguments);
},
});
list.$('td:contains(gnap)').click();
var $input = list.$('tr.o_selected_row input[name="foo"]');
$input.val('lemon').trigger('input');
$input.trigger({type: 'keydown', which: $.ui.keyCode.DOWN});
list.destroy();
});
QUnit.test('navigation with tab and readonly field', function (assert) {
// This test makes sure that if we have 2 cells in a row, the first in
// edit mode, and the second one readonly, then if we press TAB when the
// focus is on the first, then the focus skip the readonly cells and
// directly goes to the next line instead.
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="int_field" readonly="1"/></tree>',
});
// click on first td and press TAB
list.$('td:contains(yop)').last().click();
list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.TAB});
assert.ok(list.$('tr.o_data_row:eq(1)').hasClass('o_selected_row'),
"2nd row should be selected");
// we do it again. This was broken because the this.currentRow variable
// was not properly set, and the second TAB could cause a crash.
list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.TAB});
assert.ok(list.$('tr.o_data_row:eq(2)').hasClass('o_selected_row'),
"3rd row should be selected");
list.destroy();
});
QUnit.test('navigation with tab on a list with create="0"', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom" create="0">' +
'<field name="display_name"/>' +
'</tree>',
});
assert.strictEqual(list.$('.o_data_row').length, 4,
"the list should contain 4 rows");
list.$('.o_data_row:nth(2) .o_data_cell:first').click();
assert.ok(list.$('.o_data_row:nth(2)').hasClass('o_selected_row'),
"third row should be in edition");
// Press 'Tab' -> should go to next line
list.$('.o_selected_row input').trigger({type: 'keydown', which: 9});
assert.ok(list.$('.o_data_row:nth(3)').hasClass('o_selected_row'),
"fourth row should be in edition");
// Press 'Tab' -> should go back to first line as the create action isn't available
list.$('.o_selected_row input').trigger({type: 'keydown', which: 9});
assert.ok(list.$('.o_data_row:first').hasClass('o_selected_row'),
"first row should be in edition");
list.destroy();
});
QUnit.test('navigation with tab on a one2many list with create="0"', function (assert) {
assert.expect(4);
this.data.foo.records[0].o2m = [1, 2];
var form = createView({
View: FormView,
model: 'foo',
data: this.data,
arch: '<form><sheet>' +
'<field name="o2m">' +
'<tree editable="bottom" create="0">' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</sheet></form>',
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$('.o_field_widget[name=o2m] .o_data_row').length, 2,
"there should be two records in the many2many");
form.$('.o_field_widget[name=o2m] .o_data_cell:first').click();
assert.ok(form.$('.o_field_widget[name=o2m] .o_data_row:first').hasClass('o_selected_row'),
"first row should be in edition");
// Press 'Tab' -> should go to next line
form.$('.o_field_widget[name=o2m] .o_selected_row input').trigger({type: 'keydown', which: 9});
assert.ok(form.$('.o_field_widget[name=o2m] .o_data_row:nth(1)').hasClass('o_selected_row'),
"second row should be in edition");
// Press 'Tab' -> should go back to first line as the create action isn't available
form.$('.o_field_widget[name=o2m] .o_selected_row input').trigger({type: 'keydown', which: 9});
assert.ok(form.$('.o_field_widget[name=o2m] .o_data_row:first').hasClass('o_selected_row'),
"first row should be in edition");
form.destroy();
});
QUnit.test('edition, then navigation with tab (with a readonly field)', function (assert) {
// This test makes sure that if we have 2 cells in a row, the first in
// edit mode, and the second one readonly, then if we edit and press TAB,
// (before debounce), the save operation is properly done (before
// selecting the next row)
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/><field name="int_field" readonly="1"/></tree>',
mockRPC: function (route, args) {
if (args.method) {
assert.step(args.method);
}
return this._super.apply(this, arguments);
},
fieldDebounce: 1,
});
// click on first td and press TAB
list.$('td:contains(yop)').click();
list.$('tr.o_selected_row input[name="foo"]').val('new value').trigger('input');
list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.TAB});
assert.strictEqual(list.$('tbody tr:first td:contains(new value)').length, 1,
"should have the new value visible in dom");
assert.verifySteps(["write", "read"]);
list.destroy();
});
QUnit.test('skip invisible fields when navigating list view with TAB', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom">' +
'<field name="foo"/>' +
'<field name="bar" invisible="1"/>' +
'<field name="int_field"/>' +
'</tree>',
res_id: 1,
});
list.$('td:contains(gnap)').click();
assert.strictEqual(list.$('input[name="foo"]')[0], document.activeElement,
"foo should be focused");
list.$('input[name="foo"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(list.$('input[name="int_field"]')[0], document.activeElement,
"int_field should be focused");
list.destroy();
});
QUnit.test('skip buttons when navigating list view with TAB (end)', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom">' +
'<field name="foo"/>' +
'<button name="kikou" string="Kikou" type="object"/>' +
'</tree>',
res_id: 1,
});
list.$('tbody tr:eq(2) td:eq(1)').click();
assert.strictEqual(list.$('tbody tr:eq(2) input[name="foo"]')[0], document.activeElement,
"foo should be focused");
list.$('tbody tr:eq(2) input[name="foo"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(list.$('tbody tr:eq(3) input[name="foo"]')[0], document.activeElement,
"next line should be selected");
list.destroy();
});
QUnit.test('skip buttons when navigating list view with TAB (middle)', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom">' +
// Adding a button column makes conversions between column and field position trickier
'<button name="kikou" string="Kikou" type="object"/>' +
'<field name="foo"/>' +
'<button name="kikou" string="Kikou" type="object"/>' +
'<field name="int_field"/>' +
'</tree>',
res_id: 1,
});
list.$('tbody tr:eq(2) td:eq(2)').click();
assert.strictEqual(list.$('tbody tr:eq(2) input[name="foo"]')[0], document.activeElement,
"foo should be focused");
list.$('tbody tr:eq(2) input[name="foo"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(list.$('tbody tr:eq(2) input[name="int_field"]')[0], document.activeElement,
"int_field should be focused");
list.destroy();
});
QUnit.test('navigation: moving down with keydown', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom"><field name="foo"/></tree>',
});
list.$('td:contains(yop)').click();
assert.ok(list.$('tr.o_data_row:eq(0)').hasClass('o_selected_row'),
"1st row should be selected");
list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.DOWN});
assert.ok(list.$('tr.o_data_row:eq(1)').hasClass('o_selected_row'),
"2nd row should be selected");
list.destroy();
});
QUnit.test('navigation: moving right with keydown from text field', function (assert) {
assert.expect(6);
this.data.foo.fields.foo.type = 'text';
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch:
'<tree editable="bottom">' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'</tree>',
});
list.$('td:contains(yop)').click();
var textarea = list.$('textarea[name="foo"]')[0];
assert.strictEqual(document.activeElement, textarea,
"textarea should be focused");
assert.strictEqual(textarea.selectionStart, 0,
"textarea selection start should be at the beginning");
assert.strictEqual(textarea.selectionEnd, 3,
"textarea selection end should be at the end");
textarea.selectionStart = 3; // Simulate browser keyboard right behavior (unselect)
assert.strictEqual(document.activeElement, textarea,
"textarea should still be focused");
assert.ok(textarea.selectionStart === 3 && textarea.selectionEnd === 3,
"textarea value ('yop') should not be selected and cursor should be at the end");
$(textarea).trigger({type: 'keydown', which: $.ui.keyCode.RIGHT});
assert.strictEqual(document.activeElement, list.$('[name="bar"] input')[0],
"next field (checkbox) should now be focused");
list.destroy();
});
QUnit.test('navigation: moving left/right with keydown', function (assert) {
assert.expect(8);
this.data.foo.fields.foo.type = 'text';
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch:
'<tree editable="bottom">' +
'<field name="m2m" widget="many2many_tags"/>' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'<field name="m2o"/>' +
'<field name="qux"/>' +
'</tree>',
});
list.$('td:contains(13)').click();
var $m2m = list.$('[name="m2m"] input');
var $foo = list.$('textarea[name="foo"]');
var $bar = list.$('[name="bar"] input');
var $m2o = list.$('[name="m2o"] input');
var $qux = list.$('input[name="qux"]');
assert.strictEqual(document.activeElement, $qux[0],
"'qux' input should be focused");
$qux[0].selectionEnd = 0; // Simulate browser keyboard left behavior (unselect)
$qux.trigger({type: 'keydown', which: $.ui.keyCode.LEFT});
assert.strictEqual(document.activeElement, $m2o[0],
"'m2o' input should be focused");
// forget unselecting and try leaving
$m2o.trigger({type: 'keydown', which: $.ui.keyCode.LEFT});
assert.strictEqual(document.activeElement, $m2o[0],
"'m2o' input should still be focused");
$m2o[0].selectionEnd = 0; // Simulate browser keyboard left behavior (unselect)
$m2o.trigger({type: 'keydown', which: $.ui.keyCode.LEFT});
assert.strictEqual(document.activeElement, $bar[0],
"'bar' input should be focused");
// no unselect here as it is a checkbox
$bar.trigger({type: 'keydown', which: $.ui.keyCode.LEFT});
assert.strictEqual(document.activeElement, $foo[0],
"'foo' input should be focused");
// forget unselecting and try leaving
$foo.trigger({type: 'keydown', which: $.ui.keyCode.LEFT});
assert.strictEqual(document.activeElement, $foo[0],
"'foo' input should still be focused");
$foo[0].selectionEnd = 0; // Simulate browser keyboard left behavior (unselect)
$foo.trigger({type: 'keydown', which: $.ui.keyCode.LEFT});
assert.strictEqual(document.activeElement, $m2m[0],
"'m2m' input should be focused");
$m2m[0].selectionStart = $m2m[0].value.length; // Simulate browser keyboard right behavior (unselect)
$m2m.trigger({type: 'keydown', which: $.ui.keyCode.RIGHT});
assert.strictEqual(document.activeElement, $foo[0],
"'foo' input should be focused");
list.destroy();
});
QUnit.test('discarding changes in a row properly updates the rendering', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch:
'<tree editable="top">' +
'<field name="foo"/>' +
'</tree>',
});
assert.strictEqual(list.$('.o_data_cell:first').text(), "yop",
"first cell should contain 'yop'");
list.$('.o_data_cell:first').click();
list.$('input[name="foo"]').val("hello").trigger('input');
list.$buttons.find('.o_list_button_discard').click();
assert.strictEqual($('.modal:visible').length, 1,
"a modal to ask for discard should be visible");
$('.modal:visible .btn-primary').click();
assert.strictEqual(list.$('.o_data_cell:first').text(), "yop",
"first cell should still contain 'yop'");
list.destroy();
});
QUnit.test('numbers in list are right-aligned', function (assert) {
assert.expect(2);
var currencies = {};
_.each(this.data.res_currency.records, function (currency) {
currencies[currency.id] = currency;
});
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch:
'<tree editable="top">' +
'<field name="foo"/>' +
'<field name="qux"/>' +
'<field name="amount" widget="monetary"/>' +
'<field name="currency_id" invisible="1"/>' +
'</tree>',
session: {
currencies: currencies,
},
});
var nbCellRight = _.filter(list.$('.o_data_row:first > .o_data_cell'), function (el) {
var style = window.getComputedStyle(el);
return style.textAlign === 'right';
}).length;
assert.strictEqual(nbCellRight, 2,
"there should be two right-aligned cells");
list.$('.o_data_cell:first').click();
var nbInputRight = _.filter(list.$('.o_data_row:first > .o_data_cell input'), function (el) {
var style = window.getComputedStyle(el);
return style.textAlign === 'right';
}).length;
assert.strictEqual(nbInputRight, 2,
"there should be two right-aligned input");
list.destroy();
});
QUnit.test('grouped list are not editable (ungrouped first)', function (assert) {
// Editable grouped list views are not supported, so the purpose of this
// test is to check that when a list view is grouped, its editable
// attribute is ignored
// In this test, the view isn't grouped at the beginning, so it is first
// editable, and then it is reloaded with a groupBy and is no longer
// editable
assert.expect(5);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top"><field name="foo"/><field name="bar"/></tree>',
intercepts: {
switch_view: function (event) {
var resID = event.data.res_id || false;
assert.step('switch view ' + event.data.view_type + ' ' + resID);
},
},
});
list.$('.o_data_cell:first').click();
assert.verifySteps([], 'no switch view should have been requested');
assert.strictEqual(list.$('.o_selected_row').length, 1,
"a row should be in edition");
// reload with groupBy
list.reload({groupBy: ['bar']});
// clicking on record should open the form view
list.$('.o_group_header:first').click();
list.$('.o_data_cell:first').click();
// clicking on create button should open the form view
list.$buttons.find('.o_list_button_add').click();
assert.verifySteps(['switch view form 1', 'switch view form false'],
'two switch view to form should have been requested');
list.destroy();
});
QUnit.test('grouped list are not editable (grouped first)', function (assert) {
// Editable grouped list views are not supported, so the purpose of this
// test is to check that when a list view is grouped, its editable
// attribute is ignored
// In this test, the view is grouped at the beginning, so it isn't
// editable, and then it is reloaded with no groupBy and becomes editable
assert.expect(6);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top"><field name="foo"/><field name="bar"/></tree>',
intercepts: {
switch_view: function (event) {
var resID = event.data.res_id || false;
assert.step('switch view ' + event.data.view_type + ' ' + resID);
},
},
groupBy: ['bar'],
});
// the view being grouped, it is not editable, so clicking on a record
// should open the form view
list.$('.o_group_header:first').click(); // open first group
list.$('.o_data_cell:first').click();
// for the same reason, clicking on 'Create' should open the form view
list.$buttons.find('.o_list_button_add').click();
assert.verifySteps(['switch view form 1', 'switch view form false'],
"two switch view to form should have been requested");
// reload without groupBy
list.reload({groupBy: []});
// as the view is no longer grouped, it is editable, so clicking on a
// row should switch it in edition
list.$('.o_data_cell:first').click();
assert.verifySteps(['switch view form 1', 'switch view form false'],
"no more switch view should have been requested");
assert.strictEqual(list.$('.o_selected_row').length, 1,
"a row should be in edition");
// clicking on the body should leave the edition
$('body').click();
assert.strictEqual(list.$('.o_selected_row').length, 0,
"the row should no longer be in edition");
list.destroy();
});
QUnit.test('field values are escaped', function (assert) {
assert.expect(1);
var value = '<script>throw Error();</script>';
this.data.foo.records[0].foo = value;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top"><field name="foo"/></tree>',
});
assert.strictEqual(list.$('.o_data_cell:first').text(), value,
"value should have been escaped");
list.destroy();
});
QUnit.test('pressing ESC discard the current line changes', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top"><field name="foo"/></tree>',
});
list.$buttons.find('.o_list_button_add').click();
list.$('input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.ESCAPE});
assert.strictEqual(list.$('tr.o_data_row').length, 4,
"should have 4 data row in list");
assert.strictEqual(list.$('tr.o_data_row.o_selected_row').length, 0,
"no rows should be selected");
assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'),
"should not have a visible save button");
list.destroy();
});
QUnit.test('pressing ESC discard the current line changes (with required)', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top"><field name="foo" required="1"/></tree>',
});
list.$buttons.find('.o_list_button_add').click();
list.$('input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.ESCAPE});
assert.strictEqual(list.$('tr.o_data_row').length, 4,
"should have 4 data row in list");
assert.strictEqual(list.$('tr.o_data_row.o_selected_row').length, 0,
"no rows should be selected");
assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'),
"should not have a visible save button");
list.destroy();
});
QUnit.test('field with password attribute', function (assert) {
assert.expect(2);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo" password="True"/></tree>',
});
assert.strictEqual(list.$('td.o_data_cell:eq(0)').text(), '***',
"should display string as password");
assert.strictEqual(list.$('td.o_data_cell:eq(1)').text(), '****',
"should display string as password");
list.destroy();
});
QUnit.test('list with handle widget', function (assert) {
assert.expect(11);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree>' +
'<field name="int_field" widget="handle"/>' +
'<field name="amount" widget="float" digits="[5,0]"/>' +
'</tree>',
mockRPC: function (route, args) {
if (route === '/web/dataset/resequence') {
assert.strictEqual(args.offset, -4,
"should write the sequence starting from the lowest current one");
assert.strictEqual(args.field, 'int_field',
"should write the right field as sequence");
assert.deepEqual(args.ids, [4, 2 , 3],
"should write the sequence in correct order");
return $.when();
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(list.$('tbody tr:eq(0) td:last').text(), '1200',
"default first record should have amount 1200");
assert.strictEqual(list.$('tbody tr:eq(1) td:last').text(), '500',
"default second record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '300',
"default third record should have amount 300");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '0',
"default fourth record should have amount 0");
// Drag and drop the fourth line in second position
testUtils.dragAndDrop(
list.$('.ui-sortable-handle').eq(3),
list.$('tbody tr').first(),
{position: 'bottom'}
);
assert.strictEqual(list.$('tbody tr:eq(0) td:last').text(), '1200',
"new first record should have amount 1200");
assert.strictEqual(list.$('tbody tr:eq(1) td:last').text(), '0',
"new second record should have amount 0");
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '500',
"new third record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '300',
"new fourth record should have amount 300");
list.destroy();
});
QUnit.test('editable list with handle widget', function (assert) {
assert.expect(12);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="int_field" widget="handle"/>' +
'<field name="amount" widget="float" digits="[5,0]"/>' +
'</tree>',
mockRPC: function (route, args) {
if (route === '/web/dataset/resequence') {
assert.strictEqual(args.offset, -4,
"should write the sequence starting from the lowest current one");
assert.strictEqual(args.field, 'int_field',
"should write the right field as sequence");
assert.deepEqual(args.ids, [4, 2, 3],
"should write the sequence in correct order");
return $.when();
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(list.$('tbody tr:eq(0) td:last').text(), '1200',
"default first record should have amount 1200");
assert.strictEqual(list.$('tbody tr:eq(1) td:last').text(), '500',
"default second record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '300',
"default third record should have amount 300");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '0',
"default fourth record should have amount 0");
// Drag and drop the fourth line in second position
testUtils.dragAndDrop(
list.$('.ui-sortable-handle').eq(3),
list.$('tbody tr').first(),
{position: 'bottom'}
);
assert.strictEqual(list.$('tbody tr:eq(0) td:last').text(), '1200',
"new first record should have amount 1200");
assert.strictEqual(list.$('tbody tr:eq(1) td:last').text(), '0',
"new second record should have amount 0");
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '500',
"new third record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '300',
"new fourth record should have amount 300");
list.$('tbody tr:eq(1) td:last').click();
assert.strictEqual(list.$('tbody tr:eq(1) td:last input').val(), '0',
"the edited record should be the good one");
list.destroy();
});
QUnit.test('editable list with handle widget with slow network', function (assert) {
assert.expect(15);
var def = $.Deferred();
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="int_field" widget="handle"/>' +
'<field name="amount" widget="float" digits="[5,0]"/>' +
'</tree>',
mockRPC: function (route, args) {
if (route === '/web/dataset/resequence') {
assert.strictEqual(args.offset, -4,
"should write the sequence starting from the lowest current one");
assert.strictEqual(args.field, 'int_field',
"should write the right field as sequence");
assert.deepEqual(args.ids, [4, 2, 3],
"should write the sequence in correct order");
return $.when(def);
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(list.$('tbody tr:eq(0) td:last').text(), '1200',
"default first record should have amount 1200");
assert.strictEqual(list.$('tbody tr:eq(1) td:last').text(), '500',
"default second record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '300',
"default third record should have amount 300");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '0',
"default fourth record should have amount 0");
// drag and drop the fourth line in second position
testUtils.dragAndDrop(
list.$('.ui-sortable-handle').eq(3),
list.$('tbody tr').first(),
{position: 'bottom'}
);
// edit moved row before the end of resequence
list.$('tbody tr:eq(3) td:last').click();
assert.strictEqual(list.$('tbody tr:eq(3) td:last input').length, 0,
"shouldn't edit the line before resequence");
def.resolve();
assert.strictEqual(list.$('tbody tr:eq(3) td:last input').length, 1,
"should edit the line after resequence");
assert.strictEqual(list.$('tbody tr:eq(3) td:last input').val(), '300',
"fourth record should have amount 300");
list.$('tbody tr:eq(3) td:last input').val(301).trigger('input');
list.$('tbody tr:eq(0) td:last').click();
list.$buttons.find('.o_list_button_save').click();
assert.strictEqual(list.$('tbody tr:eq(0) td:last').text(), '1200',
"first record should have amount 1200");
assert.strictEqual(list.$('tbody tr:eq(1) td:last').text(), '0',
"second record should have amount 1");
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '500',
"third record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '301',
"fourth record should have amount 301");
list.$('tbody tr:eq(3) td:last').click();
assert.strictEqual(list.$('tbody tr:eq(3) td:last input').val(), '301',
"fourth record should have amount 301");
list.destroy();
});
QUnit.test('multiple clicks on Add do not create invalid rows', function (assert) {
assert.expect(2);
this.data.foo.onchanges = {
m2o: function () {},
};
var def = $.Deferred();
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top"><field name="m2o" required="1"/></tree>',
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'onchange') {
return $.when(def).then(_.constant(result));
}
return result;
},
});
assert.strictEqual(list.$('.o_data_row').length, 4,
"should contain 4 records");
// click on Add twice, and delay the onchange
list.$buttons.find('.o_list_button_add').click();
list.$buttons.find('.o_list_button_add').click();
def.resolve();
assert.strictEqual(list.$('.o_data_row').length, 5,
"only one record should have been created");
list.destroy();
});
QUnit.test('reference field rendering', function (assert) {
assert.expect(4);
this.data.foo.records.push({
id: 5,
reference: 'res_currency,2',
});
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="reference"/></tree>',
mockRPC: function (route, args) {
if (args.method === 'name_get') {
assert.step(args.model);
}
return this._super.apply(this, arguments);
},
});
assert.verifySteps(['bar', 'res_currency'], "should have done 1 name_get by model in reference values");
assert.strictEqual(list.$('tbody td').text(), "Value 1USDEUREUR",
"should have the display_name of the reference");
list.destroy();
});
QUnit.test('editable list view: contexts are correctly sent', function (assert) {
assert.expect(6);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="foo"/>' +
'</tree>',
mockRPC: function (route, args) {
var context;
if (route === '/web/dataset/search_read') {
context = args.context;
} else {
context = args.kwargs.context;
}
assert.strictEqual(context.active_field, 2, "context should be correct");
assert.strictEqual(context.someKey, 'some value', "context should be correct");
return this._super.apply(this, arguments);
},
session: {
user_context: {someKey: 'some value'},
},
viewOptions: {
context: {active_field: 2},
},
});
list.$('.o_data_cell:first').click();
list.$('.o_field_widget[name=foo]').val('abc').trigger('input');
list.$buttons.find('.o_list_button_save').click();
list.destroy();
});
QUnit.test('list grouped by date:month', function (assert) {
assert.expect(1);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="date"/></tree>',
groupBy: ['date:month'],
});
assert.strictEqual(list.$('tbody').text(), "January 2017 (1)Undefined (3)",
"the group names should be correct");
list.destroy();
});
QUnit.test('grouped list edition with toggle_button widget', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="bar" widget="toggle_button"/></tree>',
groupBy: ['m2o'],
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.deepEqual(args.args[1], {bar: false},
"should write the correct value");
}
return this._super.apply(this, arguments);
},
});
list.$('.o_group_header:first').click(); // open the first group
assert.strictEqual(list.$('.o_data_row:first .o_toggle_button_success').length, 1,
"boolean value of the first record should be true");
list.$('.o_data_row:first .o_icon_button').click(); // toggle the value
assert.strictEqual(list.$('.o_data_row:first .text-muted:not(.o_toggle_button_success)').length, 1,
"boolean button should have been updated");
list.destroy();
});
QUnit.test('grouped list view, indentation for empty group', function (assert) {
assert.expect(3);
this.data.foo.fields.priority = {
string: "Priority",
type: "selection",
selection: [[1, "Low"], [2, "Medium"], [3, "High"]],
default: 1,
};
this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1, priority: 2});
this.data.foo.records.push({id: 6, foo: "blip", int_field: 5, m2o: 1, priority: 3});
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="id"/></tree>',
groupBy: ['priority', 'm2o'],
mockRPC: function (route, args) {
// Override of the read_group to display the row even if there is no record in it,
// to mock the behavihour of some fields e.g stage_id on the sale order.
if (args.method === 'read_group' && args.kwargs.groupby[0] === "m2o") {
return $.when([
{
id: 8,
m2o:[1,"Value 1"],
m2o_count: 0
}, {
id: 2,
m2o:[2,"Value 2"],
m2o_count: 1
}
]);
}
return this._super.apply(this, arguments);
},
});
// open the first group
list.$('.o_group_header:first').click();
assert.strictEqual(list.$('th.o_group_name').eq(1).children().length, 1,
"There should be an empty element creating the indentation for the subgroup.");
assert.strictEqual(list.$('th.o_group_name').eq(1).children().eq(0).hasClass('fa'), true,
"The first element of the row name should have the fa class");
assert.strictEqual(list.$('th.o_group_name').eq(1).children().eq(0).is('span'), true,
"The first element of the row name should be a span");
list.destroy();
});
QUnit.test('basic support for widgets', function (assert) {
assert.expect(1);
var MyWidget = Widget.extend({
init: function (parent, dataPoint) {
this.data = dataPoint.data;
},
start: function () {
this.$el.text(JSON.stringify(this.data));
},
});
widgetRegistry.add('test', MyWidget);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/><field name="int_field"/><widget name="test"/></tree>',
});
assert.strictEqual(list.$('.o_widget').first().text(), '{"foo":"yop","int_field":10,"id":1}',
"widget should have been instantiated");
list.destroy();
delete widgetRegistry.map.test;
});
QUnit.test('use the limit attribute in arch', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree limit="2"><field name="foo"/></tree>',
mockRPC: function (route, args) {
assert.strictEqual(args.limit, 2,
'should use the correct limit value');
return this._super.apply(this, arguments);
},
});
assert.strictEqual(list.pager.$el.text().trim(), '1-2 / 4',
"pager should be correct");
assert.strictEqual(list.$('.o_data_row').length, 2,
'should display 2 data rows');
list.destroy();
});
QUnit.test('check if the view destroys all widgets and instances', function (assert) {
assert.expect(1);
var instanceNumber = 0;
testUtils.patch(mixins.ParentedMixin, {
init: function () {
instanceNumber++;
return this._super.apply(this, arguments);
},
destroy: function () {
if (!this.isDestroyed()) {
instanceNumber--;
}
return this._super.apply(this, arguments);
}
});
var params = {
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree string="Partners">' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'<field name="date"/>' +
'<field name="int_field"/>' +
'<field name="qux"/>' +
'<field name="m2o"/>' +
'<field name="o2m"/>' +
'<field name="m2m"/>' +
'<field name="amount"/>' +
'<field name="currency_id"/>' +
'<field name="datetime"/>' +
'<field name="reference"/>' +
'</tree>',
};
var list = createView(params);
list.destroy();
var initialInstanceNumber = instanceNumber;
instanceNumber = 0;
list = createView(params);
// call destroy function of controller to ensure that it correctly destroys everything
list.__destroy();
assert.strictEqual(instanceNumber, initialInstanceNumber+1, "every widget must be destroyed exept the parent");
list.destroy();
testUtils.unpatch(mixins.ParentedMixin);
});
QUnit.test('concurrent reloads finishing in inverse order', function (assert) {
assert.expect(3);
var blockSearchRead = false;
var def = $.Deferred();
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/></tree>',
mockRPC: function (route) {
var result = this._super.apply(this, arguments);
if (route === '/web/dataset/search_read' && blockSearchRead) {
return $.when(def).then(_.constant(result));
}
return result;
},
});
assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4,
"list view should contain 4 records");
// reload with a domain (this request is blocked)
blockSearchRead = true;
list.reload({domain: [['foo', '=', 'yop']]});
assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4,
"list view should still contain 4 records (search_read being blocked)");
// reload without the domain
blockSearchRead = false;
list.reload({domain: []});
// unblock the RPC
def.resolve();
assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4,
"list view should still contain 4 records");
list.destroy();
});
});
});