odoo.define('web.list_tests', function (require) { "use strict"; var BasicModel = require('web.BasicModel'); var config = require('web.config'); var core = require('web.core'); var basicFields = require('web.basic_fields'); var FormView = require('web.FormView'); var ListView = require('web.ListView'); var mixins = require('web.mixins'); var NotificationService = require('web.NotificationService'); var testUtils = require('web.test_utils'); var widgetRegistry = require('web.widget_registry'); var Widget = require('web.Widget'); var _t = core._t; 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, group_operator: "sum"}, text: {string: "text field", type: "text"}, 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', async function (assert) { assert.expect(10); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); assert.doesNotHaveClass(list.$el, 'o_cannot_create', "should not have className 'o_cannot_create'"); // 3 th (1 for checkbox, 2 for columns) assert.containsN(list, 'th', 3, "should have 3 columns"); assert.strictEqual(list.$('td:contains(gnap)').length, 1, "should contain gnap"); assert.containsN(list, 'tbody tr', 4, "should have 4 rows"); assert.containsOnce(list, 'th.o_column_sortable', "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.isVisible(list.$buttons.find('.o_list_button_add')); assert.isNotVisible(list.$buttons.find('.o_list_button_save')); assert.isNotVisible(list.$buttons.find('.o_list_button_discard')); list.destroy(); }); QUnit.test('list with create="0"', async function (assert) { assert.expect(2); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); assert.hasClass(list.$el,'o_cannot_create', "should have className 'o_cannot_create'"); assert.containsNone(list.$buttons, '.o_list_button_add', "should not have the 'Create' button"); list.destroy(); }); QUnit.test('list with delete="0"', async function (assert) { assert.expect(4); var list = await createView({ View: ListView, model: 'foo', data: this.data, viewOptions: {hasSidebar: true}, arch: '', }); assert.isNotVisible(list.sidebar.$el, 'sidebar should be invisible'); assert.ok(list.$('tbody td.o_list_record_selector').length, 'should have at least one record'); await testUtils.dom.click(list.$('tbody td.o_list_record_selector:first input')); assert.isVisible(list.sidebar.$el, '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', async function (assert) { assert.expect(12); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); assert.containsN(list, 'th', 3, "should have 2 th"); assert.containsN(list, 'th', 3, "should have 3 th"); assert.containsOnce(list, 'td:contains(yop)', "should contain yop"); assert.isVisible(list.$buttons.find('.o_list_button_add'), "should have a visible Create button"); assert.isNotVisible(list.$buttons.find('.o_list_button_save'), "should not have a visible save button"); assert.isNotVisible(list.$buttons.find('.o_list_button_discard'), "should not have a visible discard button"); await testUtils.dom.click(list.$('td:not(.o_list_record_selector)').first()); assert.isNotVisible(list.$buttons.find('.o_list_button_add'), "should not have a visible Create button"); assert.isVisible(list.$buttons.find('.o_list_button_save'), "should have a visible save button"); assert.isVisible(list.$buttons.find('.o_list_button_discard'), "should have a visible discard button"); await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); assert.isVisible(list.$buttons.find('.o_list_button_add'), "should have a visible Create button"); assert.isNotVisible(list.$buttons.find('.o_list_button_save'), "should not have a visible save button"); assert.isNotVisible(list.$buttons.find('.o_list_button_discard'), "should not have a visible discard button"); list.destroy(); }); QUnit.test('editable rendering with handle', async function (assert) { assert.expect(5); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '' + '', }); assert.containsN(list, 'thead th', 4, "there should be 4 th"); assert.hasClass(list.$('thead th:eq(0)'), 'o_list_record_selector'); assert.hasClass(list.$('thead th:eq(1)'), 'o_handle_cell'); assert.strictEqual(list.$('thead th:eq(2)').attr('style'), "width: 50%;"); assert.strictEqual(list.$('thead th:eq(3)').attr('style'), "width: 50%;"); list.destroy(); }); QUnit.test('invisible columns are not displayed', async function (assert) { assert.expect(1); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '', }); // 1 th for checkbox, 1 for 1 visible column assert.containsN(list, 'th', 2, "should have 2 th"); list.destroy(); }); QUnit.test('record-depending invisible lines are correctly aligned', async function (assert) { assert.expect(4); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '' + '', }); assert.containsN(list, 'tbody tr:first td', 4, "there should be 4 cells in the first row"); assert.containsOnce(list, 'tbody td.o_invisible_modifier', "there should be 1 invisible bar cell"); assert.hasClass(list.$('tbody tr:first td:eq(2)'),'o_invisible_modifier', "the 3rd cell should be invisible"); assert.containsN(list, 'tbody tr:eq(0) td:visible', 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', async function (assert) { assert.expect(3); this.data.foo.fields.m2o.default = 2; var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '', mockRPC: function (route) { assert.step(_.last(route.split('/'))); return this._super.apply(this, arguments); }, }); await testUtils.dom.click(list.$buttons.find('.o_list_button_add')); assert.verifySteps(['search_read', 'default_get'], "no nameget should be done"); list.destroy(); }); QUnit.test('editable list datetimepicker destroy widget', async function (assert) { assert.expect(7); var eventPromise = testUtils.makeTestPromise(); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '', }); list.$el.on({ 'show.datetimepicker': async function () { assert.equal($('.bootstrap-datetimepicker-widget').length, 1, 'The datetimepicker is open'); assert.equal(list.$('.o_data_row').length, 5, 'There should be 5 rows'); assert.equal(list.$('.o_selected_row').length, 1, 'One row in edit mode'); await testUtils.fields.triggerKeydown(list.$('.o_datepicker_input'), 'escape'); assert.equal(list.$('.o_data_row').length, 4, 'There should be 4 rows'); assert.equal(list.$('.o_selected_row').length, 0, 'No row should be in edit mode'); eventPromise.resolve(); } }); assert.equal(list.$('.o_data_row').length, 4, 'There should be 4 rows'); assert.equal(list.$('.o_selected_row').length, 0, 'No row should be in edit mode'); await testUtils.dom.click(list.$buttons.find('.o_list_button_add')); await eventPromise; list.destroy(); }); QUnit.test('at least 4 rows are rendered, even if less data', async function (assert) { assert.expect(1); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', domain: [['bar', '=', true]], }); assert.containsN(list, 'tbody tr', 4, "should have 4 rows"); list.destroy(); }); QUnit.test('basic grouped list rendering', async function (assert) { assert.expect(4); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', 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.containsN(list, 'tr.o_group_header', 2, "should have 2 .o_group_header"); assert.containsN(list, 'th.o_group_name', 2, "should have 2 .o_group_name"); list.destroy(); }); QUnit.test('basic grouped list rendering 1 col without selector', async function (assert) { assert.expect(2); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', groupBy: ['bar'], hasSelectors: false, }); assert.strictEqual(list.$('.o_group_header:first').children().length, 1, "group header should have exactly 1 column"); assert.strictEqual(list.$('.o_group_header:first th').attr('colspan'), "1", "the header should span the whole table"); list.destroy(); }); QUnit.test('basic grouped list rendering 1 col with selector', async function (assert) { assert.expect(2); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', groupBy: ['bar'], hasSelectors: true, }); assert.strictEqual(list.$('.o_group_header:first').children().length, 1, "group header should have exactly 1 column"); assert.strictEqual(list.$('.o_group_header:first th').attr('colspan'), "2", "the header should span the whole table"); list.destroy(); }); QUnit.test('basic grouped list rendering 2 cols without selector', async function (assert) { assert.expect(2); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', groupBy: ['bar'], hasSelectors: false, }); assert.strictEqual(list.$('.o_group_header:first').children().length, 1, "group header should have exactly 1 column"); assert.strictEqual(list.$('.o_group_header:first th').attr('colspan'), "2", "the header should span the whole table"); list.destroy(); }); QUnit.test('basic grouped list rendering 2 col with selector', async function (assert) { assert.expect(2); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', groupBy: ['bar'], hasSelectors: true, }); assert.strictEqual(list.$('.o_group_header:first').children().length, 1, "group header should have exactly 1 column"); assert.strictEqual(list.$('.o_group_header:first th').attr('colspan'), "3", "the header should span the whole table"); list.destroy(); }); QUnit.test('basic grouped list rendering 7 cols with aggregates and selector', async function (assert) { assert.expect(4); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '' + '' + '' + '' + '' + '', groupBy: ['bar'], }); assert.strictEqual(list.$('.o_group_header:first').children().length, 5, "group header should have exactly 5 columns (one before first aggregate, one after last aggregate, and all in between"); assert.strictEqual(list.$('.o_group_header:first th').attr('colspan'), "3", "header name should span on the two first fields + selector (colspan 3)"); assert.containsN(list, '.o_group_header:first td', 3, "there should be 3 tds (aggregates + fields in between)"); assert.strictEqual(list.$('.o_group_header:first th:last').attr('colspan'), "2", "header last cell should span on the two last fields (to give space for the pager) (colspan 2)"); list.destroy(); }); QUnit.test('ordered list, sort attribute in context', async function (assert) { assert.expect(1); // Equivalent to saving a custom filter this.data.foo.fields.foo.sortable = true; this.data.foo.fields.date.sortable = true; var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '', }); // Descending order on Foo await testUtils.dom.click(list.$('th.o_column_sortable:contains("Foo")')); await testUtils.dom.click(list.$('th.o_column_sortable:contains("Foo")')); // Ascending order on Date await testUtils.dom.click(list.$('th.o_column_sortable:contains("Date")')); var listContext = list.getOwnedQueryParams(); assert.deepEqual(listContext, { orderedBy: [{ name: 'date', asc: true, }, { name: 'foo', asc: false, }] }, 'the list should have the right orderedBy in context'); list.destroy(); }); QUnit.test('Loading a filter with a sort attribute', async function (assert) { assert.expect(2); this.data.foo.fields.foo.sortable = true; this.data.foo.fields.date.sortable = true; var searchReads = 0; var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '', mockRPC: function (route, args) { if (route === '/web/dataset/search_read') { if (searchReads === 0) { assert.strictEqual(args.sort, 'date ASC, foo DESC', 'The sort attribute of the filter should be used by the initial search_read'); } else if (searchReads === 1) { assert.strictEqual(args.sort, 'date DESC, foo ASC', 'The sort attribute of the filter should be used by the next search_read'); } searchReads += 1; } return this._super.apply(this,arguments); }, intercepts: { load_filters: function (event) { return Promise.resolve([ { context: "{}", domain: "[]", id: 7, is_default: true, name: "My favorite", sort: "[\"date asc\", \"foo desc\"]", user_id: [2, "Mitchell Admin"], }, { context: "{}", domain: "[]", id: 8, is_default: false, name: "My second favorite", sort: "[\"date desc\", \"foo asc\"]", user_id: [2, "Mitchell Admin"], } ]).then(event.data.on_success); }, }, }); await testUtils.dom.click(list.$('.o_control_panel .o_search_options button.o_favorites_menu_button')); await testUtils.dom.click(list.$('.o_control_panel .o_search_options .o_favorites_menu .o_menu_item').eq(1)); list.destroy(); }); QUnit.test('many2one field rendering', async function (assert) { assert.expect(1); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); 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', async function (assert) { assert.expect(6); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', groupBy: ['foo'], }); await testUtils.dom.click(list.$('th.o_group_name:nth(1)')); await testUtils.nextTick(); assert.containsN(list, 'tbody:eq(1) tr', 2, "open group should contain 2 records"); assert.containsN(list, 'tbody', 3, "should contain 3 tbody"); assert.containsOnce(list, 'td:contains(9)', "should contain 9"); assert.containsOnce(list, 'td:contains(-4)', "should contain -4"); assert.containsOnce(list, 'td:contains(10)', "should contain 10"); assert.containsOnce(list, 'tr.o_group_header td:contains(10)', "but 10 should be in a header"); list.destroy(); }); QUnit.test('opening records when clicking on record', async function (assert) { assert.expect(3); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); testUtils.mock.intercept(list, "open_record", function () { assert.ok("list view should trigger 'open_record' event"); }); testUtils.dom.click(list.$('tr td:not(.o_list_record_selector)').first()); list.update({groupBy: ['foo']}); await testUtils.nextTick(); assert.containsN(list, 'tr.o_group_header', 3, "list should be grouped"); await testUtils.dom.click(list.$('th.o_group_name').first()); testUtils.dom.click(list.$('tr:not(.o_group_header) td:not(.o_list_record_selector)').first()); list.destroy(); }); QUnit.test('editable list view: readonly fields cannot be edited', async function (assert) { assert.expect(4); this.data.foo.fields.foo.readonly = true; var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '' + '', }); 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); await testUtils.dom.click($td); assert.hasClass($td.parent(),'o_selected_row', "row should be in edit mode"); assert.hasClass($td,'o_readonly_modifier', "foo cell should be readonly in edit mode"); assert.doesNotHaveClass($second_td, 'o_readonly_modifier', "bar cell should be editable"); assert.hasClass($third_td,'o_readonly_modifier', "int_field cell should be readonly in edit mode"); list.destroy(); }); QUnit.test('editable list view: line with no active element', async function (assert) { assert.expect(3); this.data.bar = { fields: { titi: {string: "Char", type: "char"}, grosminet: {string: "Bool", type: "boolean"}, }, records: [ {id: 1, titi: 'cui', grosminet: true}, {id: 2, titi: 'cuicui', grosminet: false}, ], }; this.data.foo.records[0].o2m = [1, 2]; var form = await createView({ View: FormView, model: 'foo', data: this.data, res_id: 1, viewOptions: { mode: 'edit' }, arch: '
'+ ''+ ''+ ''+ ''+ ''+ ''+ '
', mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[1], { o2m: [[1, 1, {grosminet: false}], [4, 2, false]], }); } return this._super.apply(this, arguments); }, }); var $td = form.$('.o_data_cell').first(); var $td2 = form.$('.o_data_cell').eq(1); assert.hasClass($td, 'o_readonly_modifier'); assert.hasClass($td2, 'o_boolean_toggle_cell'); await testUtils.dom.click($td); await testUtils.dom.click($td2.find('.o_boolean_toggle input')); await testUtils.nextTick(); await testUtils.form.clickSave(form); await testUtils.nextTick(); form.destroy(); }); QUnit.test('basic operations for editable list renderer', async function (assert) { assert.expect(2); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); var $td = list.$('td:not(.o_list_record_selector)').first(); assert.doesNotHaveClass($td.parent(), 'o_selected_row', "td should not be in edit mode"); await testUtils.dom.click($td); assert.hasClass($td.parent(),'o_selected_row', "td should be in edit mode"); list.destroy(); }); QUnit.test('editable list: add a line and discard', async function (assert) { assert.expect(11); testUtils.mock.patch(basicFields.FieldChar, { destroy: function () { assert.step('destroy'); this._super.apply(this, arguments); }, }); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', domain: [['foo', '=', 'yop']], }); assert.containsN(list, 'tbody tr', 4, "list should contain 4 rows"); assert.containsOnce(list, '.o_data_row', "list should contain one record (and thus 3 empty rows)"); assert.strictEqual(list.pager.$('.o_pager_value').text(), '1-1', "pager should be correct"); await testUtils.dom.click(list.$buttons.find('.o_list_button_add')); assert.containsN(list, 'tbody tr', 4, "list should still contain 4 rows"); assert.containsN(list, '.o_data_row', 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"); await testUtils.dom.click(list.$buttons.find('.o_list_button_discard')); assert.containsN(list, 'tbody tr', 4, "list should still contain 4 rows"); assert.containsOnce(list, '.o_data_row', "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.mock.unpatch(basicFields.FieldChar); list.destroy(); }); QUnit.test('field changes are triggered correctly', async function (assert) { assert.expect(2); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); var $td = list.$('td:not(.o_list_record_selector)').first(); var n = 0; testUtils.mock.intercept(list, "field_changed", function () { n += 1; }); await testUtils.dom.click($td); testUtils.fields.editInput($td.find('input'), 'abc'); assert.strictEqual(n, 1, "field_changed should have been triggered"); await testUtils.dom.click(list.$('td:not(.o_list_record_selector)').eq(2)); assert.strictEqual(n, 1, "field_changed should not have been triggered"); list.destroy(); }); QUnit.test('editable list view: basic char field edition', async function (assert) { assert.expect(4); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); var $td = list.$('td:not(.o_list_record_selector)').first(); await testUtils.dom.click($td); await testUtils.fields.editInput($td.find('input'), 'abc'); 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(); await testUtils.dom.click($next_row_td); assert.strictEqual(list.$('td:not(.o_list_record_selector)').first().text(), 'abc', 'changes should be saved correctly'); assert.doesNotHaveClass(list.$('tbody tr').first(), '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', async function (assert) { assert.expect(3); this.data.foo.fields.foo.sortable = true; var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', 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); }, }); await testUtils.dom.click(list.$('.o_data_cell:first')); await testUtils.fields.editInput(list.$('input[name="foo"]'), 'xyz'); await testUtils.dom.click(list.$('.o_column_sortable')); assert.hasClass(list.$('.o_data_row:first'),'o_selected_row', "first row should still be in edition"); await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); assert.doesNotHaveClass(list.$buttons, 'o-editing', "list buttons should be back to their readonly mode"); list.destroy(); }); QUnit.test('selection changes are triggered correctly', async function (assert) { assert.expect(8); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); 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.mock.intercept(list, "selection_changed", function () { n += 1; }); // tbody checkbox click testUtils.dom.click($tbody_selector); assert.strictEqual(n, 1, "selection_changed should have been triggered"); assert.ok($tbody_selector.is(':checked'), "selection checkbox should be checked"); testUtils.dom.click($tbody_selector); 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 testUtils.dom.click($thead_selector); assert.strictEqual(n, 3, "selection_changed should have been triggered"); assert.containsN(list, 'tbody .o_list_record_selector input:checked', list.$('tbody tr').length, "all selection checkboxes should be checked"); testUtils.dom.click($thead_selector); assert.strictEqual(n, 4, "selection_changed should have been triggered"); assert.containsNone(list, 'tbody .o_list_record_selector input:checked', "no selection checkbox should be checked"); list.destroy(); }); QUnit.test('selection is reset on reload', async function (assert) { assert.expect(5); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '', }); 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(); testUtils.dom.click($firstRowSelector); 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 await 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('selection is kept on render without reload', async function (assert) { assert.expect(6); var list = await createView({ View: ListView, model: 'foo', data: this.data, groupBy: ['foo'], viewOptions: {hasSidebar: true}, arch: '' + '' + '' + '', }); assert.isNotVisible(list.sidebar); // open blip grouping and check all lines await testUtils.dom.click(list.$('.o_group_header:contains("blip (2)")')); await testUtils.dom.click(list.$('.o_data_row:first input')); assert.isVisible(list.sidebar); // open yop grouping and verify blip are still checked await testUtils.dom.click(list.$('.o_group_header:contains("yop (1)")')); assert.containsOnce(list, '.o_data_row input:checked', "opening a grouping does not uncheck others"); assert.isVisible(list.sidebar); // close and open blip grouping and verify blip are unchecked await testUtils.dom.click(list.$('.o_group_header:contains("blip (2)")')); await testUtils.dom.click(list.$('.o_group_header:contains("blip (2)")')); assert.containsNone(list, '.o_data_row input:checked', "opening and closing a grouping uncheck its elements"); assert.isNotVisible(list.sidebar); list.destroy(); }); QUnit.test('aggregates are computed correctly', async function (assert) { assert.expect(4); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); 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"); testUtils.dom.click($tbody_selectors.first()); testUtils.dom.click($tbody_selectors.last()); assert.strictEqual(list.$('tfoot td:nth(2)').text(), "6", "total should be 6 as first and last records are selected"); testUtils.dom.click($thead_selector); 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 await 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', async function (assert) { assert.expect(4); var list = await createView({ View: ListView, model: 'foo', data: this.data, groupBy: ['m2o'], arch: '', }); 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:last()').text(), "23", "first group total should be 23"); assert.strictEqual($groupHeader2.find('td:last()').text(), "9", "second group total should be 9"); assert.strictEqual(list.$('tfoot td:last()').text(), "32", "total should be 32"); await testUtils.dom.click($groupHeader1); await testUtils.dom.click(list.$('tbody .o_list_record_selector input').first()); assert.strictEqual(list.$('tfoot td:last()').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', async function (assert) { assert.expect(2); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); assert.strictEqual(list.$('td[title="Sum"]').text(), "32", "current total should be 32"); await testUtils.dom.click(list.$('tr.o_data_row td.o_data_cell').first()); await testUtils.fields.editInput(list.$('td.o_data_cell input'), "15"); 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', async function (assert) { assert.expect(1); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '', }); 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', async function (assert) { assert.expect(10); var list = await createView({ View: ListView, model: 'foo', data: this.data, groupBy: ['foo'], arch: '', mockRPC: function (route, args) { if (args.method === 'web_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:last()').text(), '32', "total should be 32"); await testUtils.dom.click(list.$('.o_column_sortable')); assert.strictEqual(list.$('tfoot td:last()').text(), '32', "total should still be 32"); assert.strictEqual(list.$('tbody .o_list_number').text(), '51017', "order should be 5, 10, 17"); await testUtils.dom.click(list.$('.o_column_sortable')); assert.strictEqual(list.$('tbody .o_list_number').text(), '17105', "initial order should be 17, 10, 5"); assert.strictEqual(list.$('tfoot td:last()').text(), '32', "total should still be 32"); assert.verifySteps(['default order', 'int_field ASC', 'int_field DESC']); list.destroy(); }); QUnit.test('groups cannot be sorted on non-aggregable fields', async function (assert) { assert.expect(6); this.data.foo.fields.sort_field = {string: "sortable_field", type: "sting", sortable: true, default: "value"}; _.each(this.data.records, function (elem) { elem.sort_field = "value" + elem.id; }); this.data.foo.fields.foo.sortable = true; var list = await createView({ View: ListView, model: 'foo', data: this.data, groupBy: ['foo'], arch: '', mockRPC: function (route, args) { if (args.method === 'web_read_group') { assert.step(args.kwargs.orderby || 'default order'); } return this._super.apply(this, arguments); }, }); //we cannot sort by sort_field since it doesn't have a group_operator await testUtils.dom.click(list.$('.o_column_sortable:eq(2)')); //we can sort by int_field since it has a group_operator await testUtils.dom.click(list.$('.o_column_sortable:eq(1)')); //we keep previous order await testUtils.dom.click(list.$('.o_column_sortable:eq(2)')); //we can sort on foo since we are groupped by foo + previous order await testUtils.dom.click(list.$('.o_column_sortable:eq(0)')); assert.verifySteps([ 'default order', 'default order', 'int_field ASC', 'int_field ASC', 'foo ASC, int_field ASC' ]); list.destroy(); }); QUnit.test('properly apply onchange in simple case', async function (assert) { assert.expect(2); this.data.foo.onchanges = { foo: function (obj) { obj.int_field = obj.foo.length + 1000; }, }; var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); 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"); await testUtils.dom.click($foo_td); await testUtils.fields.editInput($foo_td.find('input'), 'tralala'); 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', async function (assert) { assert.expect(4); // Warning: this test is css dependant var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '' + '' + '', }); var startWidths = _.pluck(list.$('thead th'), 'offsetWidth'); var startWidth = list.$('table').addBack('table').width(); // start edition of first row await testUtils.dom.click(list.$('td:not(.o_list_record_selector)').first()); var editionWidths = _.pluck(list.$('thead th'), 'offsetWidth'); var editionWidth = list.$('table').addBack('table').width(); // leave edition await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); var readonlyWidths = _.pluck(list.$('thead th'), 'offsetWidth'); var readonlyWidth = list.$('table').addBack('table').width(); assert.strictEqual(editionWidth, startWidth, "table should have kept the same width when switching from readonly to edit mode"); assert.deepEqual(editionWidths, startWidths, "width of columns should remain unchanged when switching from readonly to edit mode"); assert.strictEqual(readonlyWidth, editionWidth, "table should have kept the same width when switching from edit to readonly mode"); assert.deepEqual(readonlyWidths, editionWidths, "width of columns should remain unchanged when switching from edit to readonly mode"); list.destroy(); }); QUnit.test('row height should not change when switching mode', async function (assert) { // Warning: this test is css dependant assert.expect(3); var multiLang = _t.database.multi_lang; _t.database.multi_lang = true; this.data.foo.fields.foo.translate = true; this.data.foo.fields.boolean = {type: 'boolean', string: 'Bool'}; var currencies = {}; _.each(this.data.res_currency.records, function (currency) { currencies[currency.id] = currency; }); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', session: { currencies: currencies, }, }); var startHeight = list.$('.o_data_row:first').height(); // start edition of first row await testUtils.dom.click(list.$('.o_data_row:first > td:not(.o_list_record_selector)').first()); assert.hasClass(list.$('.o_data_row:first'), 'o_selected_row'); var editionHeight = list.$('.o_data_row:first').height(); // leave edition await testUtils.dom.click(list.$buttons.find('.o_list_button_save')); var readonlyHeight = list.$('.o_data_row:first').height(); assert.strictEqual(startHeight, editionHeight); assert.strictEqual(startHeight, readonlyHeight); _t.database.multi_lang = multiLang; list.destroy(); }); QUnit.test('deleting one record', async function (assert) { assert.expect(5); var list = await createView({ View: ListView, model: 'foo', data: this.data, viewOptions: {hasSidebar: true}, arch: '', }); assert.isNotVisible(list.sidebar.$el, 'sidebar should be invisible'); assert.containsN(list, 'tbody td.o_list_record_selector', 4, "should have 4 records"); await testUtils.dom.click(list.$('tbody td.o_list_record_selector:first input')); assert.isVisible(list.sidebar.$el, 'sidebar should be visible'); await testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); await testUtils.dom.click(list.sidebar.$('a:contains(Delete)')); assert.hasClass($('body'),'modal-open', 'body should have modal-open clsss'); await testUtils.dom.click($('body .modal button span:contains(Ok)')); assert.containsN(list, 'tbody td.o_list_record_selector', 3, "should have 3 records"); list.destroy(); }); QUnit.test('archiving one record', async function (assert) { assert.expect(12); // add active field on foo model and make all records active this.data.foo.fields.active = {string: 'Active', type: 'boolean', default: true}; var list = await createView({ View: ListView, model: 'foo', data: this.data, viewOptions: {hasSidebar: true}, arch: '', mockRPC: function (route) { assert.step(route); return this._super.apply(this, arguments); }, }); assert.isNotVisible(list.sidebar.$el, 'sidebar should be invisible'); assert.containsN(list, 'tbody td.o_list_record_selector', 4, "should have 4 records"); testUtils.dom.click(list.$('tbody td.o_list_record_selector:first input')); assert.isVisible(list.sidebar.$el, 'sidebar should be visible'); assert.verifySteps(['/web/dataset/search_read']); testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); await testUtils.dom.click(list.sidebar.$('a:contains(Archive)')); assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed'); testUtils.dom.click($('.modal-footer .btn-secondary')); assert.containsN(list, 'tbody td.o_list_record_selector', 4, "still should have 4 records"); testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); await testUtils.dom.click(list.sidebar.$('a:contains(Archive)')); assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed'); await testUtils.dom.click($('.modal-footer .btn-primary')); assert.containsN(list, 'tbody td.o_list_record_selector', 3, "should have 3 records"); assert.verifySteps(['/web/dataset/call_kw/foo/write', '/web/dataset/search_read']); list.destroy(); }); QUnit.test('pager (ungrouped and grouped mode), default limit', async function (assert) { assert.expect(4); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', 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.isVisible(list.pager.$el, "pager should be visible"); assert.strictEqual(list.pager.state.size, 4, "pager's size should be 4"); await list.update({ groupBy: ['bar']}); assert.strictEqual(list.pager.state.size, 2, "pager's size should be 2"); list.destroy(); }); QUnit.test('can sort records when clicking on header', async function (assert) { assert.expect(9); this.data.foo.fields.foo.sortable = true; var nbSearchRead = 0; var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', 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; await testUtils.dom.click(list.$('thead th:contains(Foo)')); 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; await testUtils.dom.click(list.$('thead th:contains(Foo)')); 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', async function (assert) { assert.expect(3); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', 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', async function (assert) { assert.expect(3); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '', 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', async function (assert) { assert.expect(8); this.data.foo.records[0].o2m = [1, 3]; var form = await createView({ View: FormView, model: 'foo', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); 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]'); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.editInput($o2m.find('.o_field_widget'), "Value 2"); 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)"); await testUtils.form.clickSave(form); 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', async 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 = await createView({ View: FormView, model: 'foo', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); 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]'); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.editInput($o2m.find('.o_field_widget'), "Value 2"); 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)"); await testUtils.dom.click(form.$('.o_form_sheet_bg')); await testUtils.dom.click($o2m.find('.o_column_sortable')); 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"); await testUtils.dom.click($o2m.find('.o_column_sortable')); 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', async 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 = await createView({ View: FormView, model: 'foo', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // Change page await testUtils.dom.click(form.$('.o_field_widget[name=o2m] .o_pager_next')); 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"); await testUtils.dom.click(form.$('.o_column_sortable')); 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', async function (assert) { assert.expect(2); var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '' + '' + '