odoo.define('web.kanban_tests', function (require) { "use strict"; var AbstractField = require('web.AbstractField'); var fieldRegistry = require('web.field_registry'); const FormRenderer = require("web.FormRenderer"); var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar'); var kanbanExamplesRegistry = require('web.kanban_examples_registry'); var KanbanRenderer = require('web.KanbanRenderer'); var KanbanView = require('web.KanbanView'); var mixins = require('web.mixins'); var testUtils = require('web.test_utils'); var Widget = require('web.Widget'); var widgetRegistry = require('web.widget_registry'); const widgetRegistryOwl = require('web.widgetRegistry'); const { LegacyComponent } = require("@web/legacy/legacy_component"); const { registry } = require('@web/core/registry'); const legacyViewRegistry = require('web.view_registry'); var makeTestPromise = testUtils.makeTestPromise; var nextTick = testUtils.nextTick; const cpHelpers = require('@web/../tests/search/helpers'); var createView = testUtils.createView; const { Markup } = require("web.utils"); const { markup, xml } = require("@odoo/owl"); QUnit.module('LegacyViews', { before: function () { this._initialKanbanProgressBarAnimate = KanbanColumnProgressBar.prototype.ANIMATE; KanbanColumnProgressBar.prototype.ANIMATE = false; }, after: function () { KanbanColumnProgressBar.prototype.ANIMATE = this._initialKanbanProgressBarAnimate; }, beforeEach: function () { registry.category("views").remove("kanban"); // remove new kanban from registry legacyViewRegistry.add("kanban", KanbanView); // add legacy kanban -> will be wrapped and added to new registry this.data = { partner: { fields: { foo: {string: "Foo", type: "char"}, bar: {string: "Bar", type: "boolean"}, int_field: {string: "int_field", type: "integer", sortable: true}, qux: {string: "my float", type: "float"}, product_id: {string: "something_id", type: "many2one", relation: "product"}, category_ids: { string: "categories", type: "many2many", relation: 'category'}, state: { string: "State", type: "selection", selection: [["abc", "ABC"], ["def", "DEF"], ["ghi", "GHI"]]}, date: {string: "Date Field", type: 'date'}, datetime: {string: "Datetime Field", type: 'datetime'}, image: {string: "Image", type: "binary"}, displayed_image_id: {string: "cover", type: "many2one", relation: "ir.attachment"}, currency_id: {string: "Currency", type: "many2one", relation: "currency", default: 1}, salary: {string: "Monetary field", type: "monetary"}, }, records: [ {id: 1, bar: true, foo: "yop", int_field: 10, qux: 0.4, product_id: 3, state: "abc", category_ids: [], 'image': 'R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA==', salary: 1750, currency_id: 1}, {id: 2, bar: true, foo: "blip", int_field: 9, qux: 13, product_id: 5, state: "def", category_ids: [6], salary: 1500, currency_id: 1}, {id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3, product_id: 3, state: "ghi", category_ids: [7], salary: 2000, currency_id: 2}, {id: 4, bar: false, foo: "blip", int_field: -4, qux: 9, product_id: 5, state: "ghi", category_ids: [], salary: 2222, currency_id: 1}, ] }, product: { fields: { id: {string: "ID", type: "integer"}, name: {string: "Display Name", type: "char"}, }, records: [ {id: 3, name: "hello"}, {id: 5, name: "xmo"}, ] }, category: { fields: { name: {string: "Category Name", type: "char"}, color: {string: "Color index", type: "integer"}, }, records: [ {id: 6, name: "gold", color: 2}, {id: 7, name: "silver", color: 5}, ] }, 'ir.attachment': { fields: { mimetype: {type: "char"}, name: {type: "char"}, res_model: {type: "char"}, res_id: {type: "integer"}, }, records: [ {id: 1, name: "1.png", mimetype: 'image/png', res_model: 'partner', res_id: 1}, {id: 2, name: "2.png", mimetype: 'image/png', res_model: 'partner', res_id: 2}, ] }, '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'}, ], }, }; }, }, function () { QUnit.module('KanbanView (legacy)'); QUnit.test('basic ungrouped rendering', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '
' + '' + '' + '
' + '
', mockRPC: function (route, args) { assert.ok(args.context.bin_size, "should not request direct binary payload"); return this._super(route, args); }, }); assert.hasClass(kanban.$('.o_legacy_kanban_view'), 'o_kanban_ungrouped'); assert.hasClass(kanban.$('.o_legacy_kanban_view'), 'o_kanban_test'); assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 4); assert.containsN(kanban,'.o_kanban_ghost', 6); assert.containsOnce(kanban, '.o_kanban_record:contains(gnap)'); kanban.destroy(); }); QUnit.test('basic grouped rendering', async function (assert) { assert.expect(13); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], mockRPC: function (route, args) { if (args.method === 'web_read_group') { // the lazy option is important, so the server can fill in // the empty groups assert.ok(args.kwargs.lazy, "should use lazy read_group"); } return this._super(route, args); }, }); assert.hasClass(kanban.$('.o_legacy_kanban_view'), 'o_kanban_grouped'); assert.hasClass(kanban.$('.o_legacy_kanban_view'), 'o_kanban_test'); assert.containsN(kanban, '.o_kanban_group', 2); assert.containsOnce(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record'); assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 3); // check available actions in kanban header's config dropdown assert.containsOnce(kanban, '.o_kanban_header:first .o_kanban_config .o_kanban_toggle_fold'); assert.containsNone(kanban, '.o_kanban_header:first .o_kanban_config .o_column_edit'); assert.containsNone(kanban, '.o_kanban_header:first .o_kanban_config .o_column_delete'); assert.containsNone(kanban, '.o_kanban_header:first .o_kanban_config .o_column_archive_records'); assert.containsNone(kanban, '.o_kanban_header:first .o_kanban_config .o_column_unarchive_records'); // the next line makes sure that reload works properly. It looks useless, // but it actually test that a grouped local record can be reloaded without // changing its result. await kanban.reload(kanban); assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 3); kanban.destroy(); }); QUnit.test('basic grouped rendering with active field (archivable by default)', async function (assert) { // var done = assert.async(); assert.expect(9); // add active field on partner model and make all records active this.data.partner.fields.active = {string: 'Active', type: 'char', default: true}; var envIDs = [1, 2, 3, 4]; // the ids that should be in the environment during this test var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '
' + '
', groupBy: ['bar'], mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/action_archive') { var partnerIDS = args.args[0]; var records = this.data.partner.records _.each(partnerIDS, function(partnerID) { _.find(records, function (record) { return record.id === partnerID; }).active = false; }) this.data.partner.records[0].active; return Promise.resolve(); } return this._super.apply(this, arguments); }, }); // check archive/restore all actions in kanban header's config dropdown assert.containsOnce(kanban, '.o_kanban_header:first .o_kanban_config .o_column_archive_records'); assert.containsOnce(kanban, '.o_kanban_header:first .o_kanban_config .o_column_unarchive_records'); assert.deepEqual(kanban.exportState().resIds, envIDs); // archive the records of the first column assert.containsN(kanban, '.o_kanban_group:last .o_kanban_record', 3); testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last')); await testUtils.dom.click(kanban.$('.o_kanban_group:last .o_column_archive_records')); assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed"); await testUtils.modal.clickButton('Cancel'); assert.containsN(kanban, '.o_kanban_group:last .o_kanban_record', 3, "still last column should contain 3 records"); testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last')); await testUtils.dom.click(kanban.$('.o_kanban_group:last .o_column_archive_records')); assert.ok($('.modal').length, 'a confirm modal should be displayed'); await testUtils.modal.clickButton('Ok'); assert.containsNone(kanban, '.o_kanban_group:last .o_kanban_record', "last column should not contain any records"); envIDs = [4]; assert.deepEqual(kanban.exportState().resIds, envIDs); kanban.destroy(); }); QUnit.test('basic grouped rendering with active field and archive enabled (archivable true)', async function (assert) { assert.expect(7); // add active field on partner model and make all records active this.data.partner.fields.active = {string: 'Active', type: 'char', default: true}; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '
' + '
', groupBy: ['bar'], mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/action_archive') { var partnerIDS = args.args[0]; var records = this.data.partner.records _.each(partnerIDS, function(partnerID) { _.find(records, function (record) { return record.id === partnerID; }).active = false; }) this.data.partner.records[0].active; return Promise.resolve(); } return this._super.apply(this, arguments); }, }); // check archive/restore all actions in kanban header's config dropdown assert.ok(kanban.$('.o_kanban_header:first .o_kanban_config .o_column_archive_records').length, "should be able to archive all the records"); assert.ok(kanban.$('.o_kanban_header:first .o_kanban_config .o_column_unarchive_records').length, "should be able to restore all the records"); // archive the records of the first column assert.containsN(kanban, '.o_kanban_group:last .o_kanban_record', 3, "last column should contain 3 records"); testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last')); await testUtils.dom.click(kanban.$('.o_kanban_group:last .o_column_archive_records')); assert.ok($('.modal').length, 'a confirm modal should be displayed'); await testUtils.modal.clickButton('Cancel'); // Click on 'Cancel' assert.containsN(kanban, '.o_kanban_group:last .o_kanban_record', 3, "still last column should contain 3 records"); testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last')); await testUtils.dom.click(kanban.$('.o_kanban_group:last .o_column_archive_records')); assert.ok($('.modal').length, 'a confirm modal should be displayed'); await testUtils.modal.clickButton('Ok'); // Click on 'Ok' assert.containsNone(kanban, '.o_kanban_group:last .o_kanban_record', "last column should not contain any records"); kanban.destroy(); }); QUnit.test('basic grouped rendering with active field and hidden archive buttons (archivable false)', async function (assert) { assert.expect(2); // add active field on partner model and make all records active this.data.partner.fields.active = {string: 'Active', type: 'char', default: true}; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '
' + '
', groupBy: ['bar'], }); // check archive/restore all actions in kanban header's config dropdown assert.strictEqual( kanban.$('.o_kanban_header:first .o_kanban_config .o_column_archive_records').length, 0, "should not be able to archive all the records"); assert.strictEqual( kanban.$('.o_kanban_header:first .o_kanban_config .o_column_unarchive_records').length, 0, "should not be able to restore all the records"); kanban.destroy(); }); QUnit.test("m2m grouped rendering with active field and archive enabled (archivable true)", async function (assert) { assert.expect(7); // add active field on partner model and make all records active this.data.partner.fields.active = { string: 'Active', type: 'char', default: true }; // more many2many data this.data.partner.records[0].category_ids = [6, 7]; this.data.partner.records[3].foo = "blork"; this.data.partner.records[3].category_ids = []; const kanban = await createView({ View: KanbanView, model: "partner", data: this.data, arch: `
`, groupBy: ["category_ids"], }); assert.containsN(kanban, ".o_kanban_group", 3); assert.containsOnce(kanban, ".o_kanban_group:nth-child(1) .o_kanban_record"); assert.containsN(kanban, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); assert.containsN(kanban, ".o_kanban_group:nth-child(3) .o_kanban_record", 2); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group")].map(el => el.innerText.replace(/\s/g, " ")), ["None blork", "gold yopblip", "silver yopgnap"], ); // check archive/restore all actions in kanban header's config dropdown // despite the fact that the kanban view is configured to be archivable, // the actions should not be there as it is grouped by an m2m field. assert.containsNone(kanban, ".o_kanban_header .o_kanban_config .o_column_archive_records", "should not be able to archive all the records"); assert.containsNone(kanban, ".o_kanban_header .o_kanban_config .o_column_unarchive_records", "should not be able to unarchive all the records"); kanban.destroy(); }); QUnit.test('context can be used in kanban template', async function (assert) { assert.expect(2); var form = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '' + '' + '
' + '
' + '
' + '
', context: {some_key: 1}, domain: [['id', '=', 1]], }); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1, "there should be one record"); assert.strictEqual(form.$('.o_kanban_record span:contains(yop)').length, 1, "condition in the kanban template should have been correctly evaluated"); form.destroy(); }); QUnit.test('pager should be hidden in grouped mode', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], }); assert.containsNone(kanban, '.o_pager'); kanban.destroy(); }); QUnit.test('pager, ungrouped, with default limit', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
', mockRPC: function (route, args) { assert.strictEqual(args.limit, 40, "default limit should be 40 in Kanban"); return this._super.apply(this, arguments); }, }); assert.containsOnce(kanban, '.o_pager'); assert.strictEqual(testUtils.controlPanel.getPagerSize(kanban), "4", "pager's size should be 4"); kanban.destroy(); }); QUnit.test('pager, ungrouped, with limit given in options', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
', mockRPC: function (route, args) { assert.strictEqual(args.limit, 2, "limit should be 2"); return this._super.apply(this, arguments); }, viewOptions: { limit: 2, }, }); assert.strictEqual(testUtils.controlPanel.getPagerValue(kanban), "1-2", "pager's limit should be 2"); assert.strictEqual(testUtils.controlPanel.getPagerSize(kanban), "4", "pager's size should be 4"); kanban.destroy(); }); QUnit.test('pager, ungrouped, with limit set on arch and given in options', async function (assert) { assert.expect(3); // the limit given in the arch should take the priority over the one given in options var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
', mockRPC: function (route, args) { assert.strictEqual(args.limit, 3, "limit should be 3"); return this._super.apply(this, arguments); }, viewOptions: { limit: 2, }, }); assert.strictEqual(testUtils.controlPanel.getPagerValue(kanban), "1-3", "pager's limit should be 3"); assert.strictEqual(testUtils.controlPanel.getPagerSize(kanban), "4", "pager's size should be 4"); kanban.destroy(); }); QUnit.test('pager, ungrouped, deleting all records from last page should move to previous page', async function (assert) { assert.expect(5); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
Delete
`, }); assert.strictEqual(testUtils.controlPanel.getPagerValue(kanban), "1-3", "should have 3 records on current page"); assert.strictEqual(testUtils.controlPanel.getPagerSize(kanban), "4", "should have 4 records"); // move to next page await testUtils.controlPanel.pagerNext(kanban); assert.strictEqual(testUtils.controlPanel.getPagerValue(kanban), "4-4", "should be on second page"); // delete a record await testUtils.dom.click(kanban.$('.o_kanban_record:first a:first')); await testUtils.dom.click($('.modal-footer button:first')); assert.strictEqual(testUtils.controlPanel.getPagerValue(kanban), "1-3", "should have 1 page only"); assert.strictEqual(testUtils.controlPanel.getPagerSize(kanban), "3", "should have 4 records"); kanban.destroy(); }); QUnit.test('create in grouped on m2o', async function (assert) { assert.expect(5); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); assert.hasClass(kanban.$('.o_legacy_kanban_view'),'ui-sortable', "columns are sortable when grouped by a m2o field"); assert.hasClass(kanban.$buttons.find('.o-kanban-button-new'),'btn-primary', "'create' button should be btn-primary for grouped kanban with at least one column"); assert.hasClass(kanban.$('.o_legacy_kanban_view > div:last'),'o_column_quick_create', "column quick create should be enabled when grouped by a many2one field)"); await testUtils.kanban.clickCreate(kanban); // Click on 'Create' assert.hasClass(kanban.$('.o_kanban_group:first() > div:nth(1)'),'o_kanban_quick_create', "clicking on create should open the quick_create in the first column"); assert.ok(kanban.$('span.o_column_title:contains(hello)').length, "should have a column title with a value from the many2one"); kanban.destroy(); }); QUnit.test('create in grouped on char', async function (assert) { assert.expect(4); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['foo'], }); assert.doesNotHaveClass(kanban.$('.o_legacy_kanban_view'), 'ui-sortable', "columns aren't sortable when not grouped by a m2o field"); assert.containsN(kanban, '.o_kanban_group', 3, "should have " + 3 + " columns"); assert.strictEqual(kanban.$('.o_kanban_group:first() .o_column_title').text(), "yop", "'yop' column should be the first column"); assert.doesNotHaveClass(kanban.$('.o_legacy_kanban_view > div:last'), 'o_column_quick_create', "column quick create should be disabled when not grouped by a many2one field)"); kanban.destroy(); }); QUnit.test('prevent deletion when grouped by many2many field', async function (assert) { assert.expect(2); this.data.partner.records[0].category_ids = [6, 7]; this.data.partner.records[3].category_ids = [7]; const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
delete
`, groupBy: ['category_ids'], }); assert.containsNone(kanban, '.thisisdeletable', "records should not be deletable"); await kanban.reload({ groupBy: ['foo'] }); assert.containsN(kanban, '.thisisdeletable', 4, "records should be deletable"); kanban.destroy(); }); QUnit.test('quick create record without quick_create_view', async function (assert) { assert.expect(16); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], mockRPC: function (route, args) { assert.step(args.method || route); if (args.method === 'name_create') { assert.strictEqual(args.args[0], 'new partner', "should send the correct value"); } return this._super.apply(this, arguments); }, }); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should contain one record"); // click on 'Create' -> should open the quick create in the first column await testUtils.kanban.clickCreate(kanban); var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); assert.strictEqual($quickCreate.length, 1, "should have a quick create element in the first column"); assert.strictEqual($quickCreate.find('.o_legacy_form_view.o_xxs_form_view').length, 1, "should have rendered an XXS form view"); assert.strictEqual($quickCreate.find('input').length, 1, "should have only one input"); assert.hasClass($quickCreate.find('input'), 'o_required_modifier', "the field should be required"); assert.strictEqual($quickCreate.find('input[placeholder=Title]').length, 1, "input placeholder should be 'Title'"); // fill the quick create and validate await testUtils.fields.editInput($quickCreate.find('input'), 'new partner'); await testUtils.dom.click($quickCreate.find('button.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column should contain two records"); assert.verifySteps([ 'web_read_group', // initial read_group '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) 'onchange', // quick create 'name_create', // should perform a name_create to create the record 'read', // read the created record 'onchange', // reopen the quick create automatically ]); kanban.destroy(); }); QUnit.test('quick create record with quick_create_view', async function (assert) { assert.expect(19); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '' + '' + '', }, groupBy: ['bar'], mockRPC: function (route, args) { assert.step(args.method || route); if (args.method === 'create') { assert.deepEqual(args.args[0], { foo: 'new partner', int_field: 4, state: 'def', }, "should send the correct values"); } return this._super.apply(this, arguments); }, }); assert.containsOnce(kanban, '.o_control_panel', 'should have one control panel'); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should contain one record"); // click on 'Create' -> should open the quick create in the first column await testUtils.kanban.clickCreate(kanban); var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); assert.strictEqual($quickCreate.length, 1, "should have a quick create element in the first column"); assert.strictEqual($quickCreate.find('.o_legacy_form_view.o_xxs_form_view').length, 1, "should have rendered an XXS form view"); assert.containsOnce(kanban, '.o_control_panel', 'should not have instantiated an extra control panel'); assert.strictEqual($quickCreate.find('input').length, 2, "should have two inputs"); assert.strictEqual($quickCreate.find('.o_field_widget').length, 3, "should have rendered three widgets"); // fill the quick create and validate await testUtils.fields.editInput($quickCreate.find('.o_field_widget[name=foo]'), 'new partner'); await testUtils.fields.editInput($quickCreate.find('.o_field_widget[name=int_field]'), '4'); await testUtils.dom.click($quickCreate.find('.o_field_widget[name=state] .o_priority_star:first')); await testUtils.dom.click($quickCreate.find('button.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column should contain two records"); assert.verifySteps([ 'web_read_group', // initial read_group '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) 'get_views', // form view in quick create 'onchange', // quick create 'create', // should perform a create to create the record 'read', // read the created record 'get_views', // form view in quick create (is actually in cache) 'onchange', // reopen the quick create automatically ]); kanban.destroy(); }); QUnit.test('quick create record in grouped on m2o (no quick_create_view)', async function (assert) { assert.expect(12); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { assert.step(args.method || route); if (args.method === 'name_create') { assert.strictEqual(args.args[0], 'new partner', "should send the correct value"); assert.deepEqual(args.kwargs.context, { default_product_id: 3, default_qux: 2.5, }, "should send the correct context"); } return this._super.apply(this, arguments); }, viewOptions: { context: {default_qux: 2.5}, }, }); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column should contain two records"); // click on 'Create', fill the quick create and validate await testUtils.kanban.clickCreate(kanban); var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); await testUtils.fields.editInput($quickCreate.find('input'), 'new partner'); await testUtils.dom.click($quickCreate.find('button.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 3, "first column should contain three records"); assert.verifySteps([ 'web_read_group', // initial read_group '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) 'onchange', // quick create 'name_create', // should perform a name_create to create the record 'read', // read the created record 'onchange', // reopen the quick create automatically ]); kanban.destroy(); }); QUnit.test('quick create record in grouped on m2o (with quick_create_view)', async function (assert) { assert.expect(14); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '' + '' + '', }, groupBy: ['product_id'], mockRPC: function (route, args) { assert.step(args.method || route); if (args.method === 'create') { assert.deepEqual(args.args[0], { foo: 'new partner', int_field: 4, state: 'def', }, "should send the correct values"); assert.deepEqual(args.kwargs.context, { default_product_id: 3, default_qux: 2.5, }, "should send the correct context"); } return this._super.apply(this, arguments); }, viewOptions: { context: {default_qux: 2.5}, }, }); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column should contain two records"); // click on 'Create', fill the quick create and validate await testUtils.kanban.clickCreate(kanban); var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); await testUtils.fields.editInput($quickCreate.find('.o_field_widget[name=foo]'), 'new partner'); await testUtils.fields.editInput($quickCreate.find('.o_field_widget[name=int_field]'), '4'); await testUtils.dom.click($quickCreate.find('.o_field_widget[name=state] .o_priority_star:first')); await testUtils.dom.click($quickCreate.find('button.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 3, "first column should contain three records"); assert.verifySteps([ 'web_read_group', // initial read_group '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) 'get_views', // form view in quick create 'onchange', // quick create 'create', // should perform a create to create the record 'read', // read the created record 'get_views', // form view in quick create (is actually in cache) 'onchange', // reopen the quick create automatically ]); kanban.destroy(); }); QUnit.test('quick create record with default values and onchanges', async function (assert) { assert.expect(10); this.data.partner.fields.int_field.default = 4; this.data.partner.onchanges = { foo: function (obj) { if (obj.foo) { obj.int_field = 8; } }, }; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '' + '', }, groupBy: ['bar'], mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); // click on 'Create' -> should open the quick create in the first column await testUtils.kanban.clickCreate(kanban); var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); assert.strictEqual($quickCreate.length, 1, "should have a quick create element in the first column"); assert.strictEqual($quickCreate.find('.o_field_widget[name=int_field]').val(), '4', "default value should be set"); // fill the 'foo' field -> should trigger the onchange await testUtils.fields.editInput($quickCreate.find('.o_field_widget[name=foo]'), 'new partner'); assert.strictEqual($quickCreate.find('.o_field_widget[name=int_field]').val(), '8', "onchange should have been triggered"); assert.verifySteps([ 'web_read_group', // initial read_group '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) 'get_views', // form view in quick create 'onchange', // quick create 'onchange', // onchange due to 'foo' field change ]); kanban.destroy(); }); QUnit.test('quick create record with quick_create_view: modifiers', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '' + '' + '
' + '
' + '' + '
' + '
' + '', groupBy: ['foo'], }); // Quick create kanban record await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); var $quickAdd = kanban.$('.o_kanban_quick_create'); $quickAdd.find('.o_input').val('Test'); await testUtils.dom.click($quickAdd.find('.o_kanban_add')); // Select state in kanban await testUtils.dom.click(kanban.$('.o_status').first()); await testUtils.dom.click(kanban.$('.o_selection .dropdown-item:first')); assert.hasClass(kanban.$('.o_status').first(),'o_status_green', "Kanban state should be done (Green)"); kanban.destroy(); }); QUnit.test('window resize should not change quick create form size', async function (assert) { assert.expect(2); testUtils.mock.patch(FormRenderer, { start: function () { this._super.apply(this, arguments); window.addEventListener("resize", this._applyFormSizeClass.bind(this)); }, }); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], }); // click to add an element and cancel the quick creation by pressing ESC await testUtils.dom.click(kanban.el.querySelector('.o_kanban_header .o_kanban_quick_add i')); const quickCreate = kanban.el.querySelector('.o_kanban_quick_create'); assert.hasClass(quickCreate.querySelector('.o_legacy_form_view'), "o_xxs_form_view"); // trigger window resize explicitly to call _applyFormSizeClass window.dispatchEvent(new Event('resize')); assert.hasClass(quickCreate.querySelector('.o_legacy_form_view'), 'o_xxs_form_view'); kanban.destroy(); testUtils.mock.unpatch(FormRenderer); }); QUnit.test('quick create record: cancel and validate without using the buttons', async function (assert) { assert.expect(9); var nbRecords = 4; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], }); assert.strictEqual(kanban.exportState().resIds.length, nbRecords); // click to add an element and cancel the quick creation by pressing ESC await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); var $quickCreate = kanban.$('.o_kanban_quick_create'); assert.strictEqual($quickCreate.length, 1, "should have a quick create element"); $quickCreate.find('input').trigger($.Event('keydown', { keyCode: $.ui.keyCode.ESCAPE, which: $.ui.keyCode.ESCAPE, })); assert.containsNone(kanban, '.o_kanban_quick_create', "should have destroyed the quick create element"); // click to add and element and click outside, should cancel the quick creation await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); await testUtils.dom.click(kanban.$('.o_kanban_group .o_kanban_record:first')); assert.containsNone(kanban, '.o_kanban_quick_create', "the quick create should be destroyed when the user clicks outside"); // click to input and drag the mouse outside, should not cancel the quick creation await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); $quickCreate = kanban.$('.o_kanban_quick_create'); await testUtils.dom.triggerMouseEvent($quickCreate.find('input'), 'mousedown'); await testUtils.dom.click(kanban.$('.o_kanban_group .o_kanban_record:first').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "the quick create should not have been destroyed after clicking outside"); // click to really add an element await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); $quickCreate = kanban.$('.o_kanban_quick_create'); await testUtils.fields.editInput($quickCreate.find('input'), 'new partner'); // clicking outside should no longer destroy the quick create as it is dirty await testUtils.dom.click(kanban.$('.o_kanban_group .o_kanban_record:first')); assert.containsOnce(kanban, '.o_kanban_quick_create', "the quick create should not have been destroyed"); // confirm by pressing ENTER nbRecords = 5; $quickCreate.find('input').trigger($.Event('keydown', { keyCode: $.ui.keyCode.ENTER, which: $.ui.keyCode.ENTER, })); await nextTick(); assert.strictEqual(this.data.partner.records.length, 5, "should have created a partner"); assert.strictEqual(_.last(this.data.partner.records).name, "new partner", "should have correct name"); assert.strictEqual(kanban.exportState().resIds.length, nbRecords); kanban.destroy(); }); QUnit.test('quick create record: validate with ENTER', async function (assert) { // in this test, we accurately mock the behavior of the webclient by specifying a // fieldDebounce > 0, meaning that the changes in an InputField aren't notified to the model // on 'input' events, but they wait for the 'change' event (or a call to 'commitChanges', // e.g. triggered by a navigation event) // in this scenario, the call to 'commitChanges' actually does something (i.e. it notifies // the new value of the char field), whereas it does nothing if the changes are notified // directly assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', archs: { 'partner,some_view_ref,form': '' + '' + '' + '', }, groupBy: ['bar'], fieldDebounce: 5000, }); assert.containsN(kanban, '.o_kanban_record', 4, "should have 4 records at the beginning"); // add an element and confirm by pressing ENTER await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); await testUtils.kanban.quickCreate(kanban, 'new partner', 'foo'); // triggers a navigation event, leading to the 'commitChanges' and record creation assert.containsN(kanban, '.o_kanban_record', 5, "should have created a new record"); assert.strictEqual(kanban.$('.o_kanban_quick_create input[name=foo]').val(), '', "quick create should now be empty"); kanban.destroy(); }); QUnit.test('quick create record: prevent multiple adds with ENTER', async function (assert) { assert.expect(9); var prom = makeTestPromise(); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '' + '', }, groupBy: ['bar'], // add a fieldDebounce to accurately simulate what happens in the webclient: the field // doesn't notify the BasicModel that it has changed directly, as it waits for the user // to focusout or navigate (e.g. by pressing ENTER) fieldDebounce: 5000, mockRPC: function (route, args) { var result = this._super.apply(this, arguments); if (args.method === 'create') { assert.step('create'); return prom.then(function () { return result; }); } return result; }, }); assert.containsN(kanban, '.o_kanban_record', 4, "should have 4 records at the beginning"); // add an element and press ENTER twice await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); var enterEvent = { keyCode: $.ui.keyCode.ENTER, which: $.ui.keyCode.ENTER, }; await testUtils.fields.editAndTrigger( kanban.$('.o_kanban_quick_create').find('input[name=foo]'), 'new partner', ['input', $.Event('keydown', enterEvent), $.Event('keydown', enterEvent)] ); assert.containsN(kanban, '.o_kanban_record', 4, "should not have created the record yet"); assert.strictEqual(kanban.$('.o_kanban_quick_create input[name=foo]').val(), 'new partner', "quick create should not be empty yet"); assert.hasClass(kanban.$('.o_kanban_quick_create'), 'o_disabled', "quick create should be disabled"); prom.resolve(); await nextTick(); assert.containsN(kanban, '.o_kanban_record', 5, "should have created a new record"); assert.strictEqual(kanban.$('.o_kanban_quick_create input[name=foo]').val(), '', "quick create should now be empty"); assert.doesNotHaveClass(kanban.$('.o_kanban_quick_create'), 'o_disabled', "quick create should be enabled"); assert.verifySteps(['create']); kanban.destroy(); }); QUnit.test('quick create record: prevent multiple adds with Add clicked', async function (assert) { assert.expect(9); var prom = makeTestPromise(); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '' + '', }, groupBy: ['bar'], mockRPC: function (route, args) { var result = this._super.apply(this, arguments); if (args.method === 'create') { assert.step('create'); return prom.then(function () { return result; }); } return result; }, }); assert.containsN(kanban, '.o_kanban_record', 4, "should have 4 records at the beginning"); // add an element and click 'Add' twice await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create').find('input[name=foo]'), 'new partner'); await testUtils.dom.click(kanban.$('.o_kanban_quick_create').find('.o_kanban_add')); await testUtils.dom.click(kanban.$('.o_kanban_quick_create').find('.o_kanban_add')); assert.containsN(kanban, '.o_kanban_record', 4, "should not have created the record yet"); assert.strictEqual(kanban.$('.o_kanban_quick_create input[name=foo]').val(), 'new partner', "quick create should not be empty yet"); assert.hasClass(kanban.$('.o_kanban_quick_create'),'o_disabled', "quick create should be disabled"); prom.resolve(); await nextTick(); assert.containsN(kanban, '.o_kanban_record', 5, "should have created a new record"); assert.strictEqual(kanban.$('.o_kanban_quick_create input[name=foo]').val(), '', "quick create should now be empty"); assert.doesNotHaveClass(kanban.$('.o_kanban_quick_create'), 'o_disabled', "quick create should be enabled"); assert.verifySteps(['create']); kanban.destroy(); }); QUnit.test('quick create record: prevent multiple adds with ENTER, with onchange', async function (assert) { assert.expect(13); this.data.partner.onchanges = { foo: function (obj) { obj.int_field += (obj.foo ? 3 : 0); }, }; var shouldDelayOnchange = false; var prom = makeTestPromise(); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '' + '', }, groupBy: ['bar'], mockRPC: function (route, args) { var result = this._super.apply(this, arguments); if (args.method === 'onchange') { assert.step('onchange'); if (shouldDelayOnchange) { return Promise.resolve(prom).then(function () { return result; }); } } if (args.method === 'create') { assert.step('create'); assert.deepEqual(_.pick(args.args[0], 'foo', 'int_field'), { foo: 'new partner', int_field: 3, }); } return result; }, // add a fieldDebounce to accurately simulate what happens in the webclient: the field // doesn't notify the BasicModel that it has changed directly, as it waits for the user // to focusout or navigate (e.g. by pressing ENTER) fieldDebounce: 5000, }); assert.containsN(kanban, '.o_kanban_record', 4, "should have 4 records at the beginning"); // add an element and press ENTER twice await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); shouldDelayOnchange = true; var enterEvent = { keyCode: $.ui.keyCode.ENTER, which: $.ui.keyCode.ENTER, }; await testUtils.fields.editAndTrigger( kanban.$('.o_kanban_quick_create').find('input[name=foo]'), 'new partner', ['input', $.Event('keydown', enterEvent), $.Event('keydown', enterEvent)] ); assert.containsN(kanban, '.o_kanban_record', 4, "should not have created the record yet"); assert.strictEqual(kanban.$('.o_kanban_quick_create input[name=foo]').val(), 'new partner', "quick create should not be empty yet"); assert.hasClass(kanban.$('.o_kanban_quick_create'),'o_disabled', "quick create should be disabled"); prom.resolve(); await nextTick(); assert.containsN(kanban, '.o_kanban_record', 5, "should have created a new record"); assert.strictEqual(kanban.$('.o_kanban_quick_create input[name=foo]').val(), '', "quick create should now be empty"); assert.doesNotHaveClass(kanban.$('.o_kanban_quick_create'), 'o_disabled', "quick create should be enabled"); assert.verifySteps([ 'onchange', // default_get 'onchange', // new partner 'create', 'onchange', // default_get ]); kanban.destroy(); }); QUnit.test('quick create record: click Add to create, with delayed onchange', async function (assert) { assert.expect(13); this.data.partner.onchanges = { foo: function (obj) { obj.int_field += (obj.foo ? 3 : 0); }, }; var shouldDelayOnchange = false; var prom = makeTestPromise(); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '' + '', }, groupBy: ['bar'], mockRPC: function (route, args) { var result = this._super.apply(this, arguments); if (args.method === 'onchange') { assert.step('onchange'); if (shouldDelayOnchange) { return Promise.resolve(prom).then(function () { return result; }); } } if (args.method === 'create') { assert.step('create'); assert.deepEqual(_.pick(args.args[0], 'foo', 'int_field'), { foo: 'new partner', int_field: 3, }); } return result; }, }); assert.containsN(kanban, '.o_kanban_record', 4, "should have 4 records at the beginning"); // add an element and click 'add' await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); shouldDelayOnchange = true; await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create').find('input[name=foo]'), 'new partner'); await testUtils.dom.click(kanban.$('.o_kanban_quick_create').find('.o_kanban_add')); assert.containsN(kanban, '.o_kanban_record', 4, "should not have created the record yet"); assert.strictEqual(kanban.$('.o_kanban_quick_create input[name=foo]').val(), 'new partner', "quick create should not be empty yet"); assert.hasClass(kanban.$('.o_kanban_quick_create'),'o_disabled', "quick create should be disabled"); prom.resolve(); // the onchange returns await nextTick(); assert.containsN(kanban, '.o_kanban_record', 5, "should have created a new record"); assert.strictEqual(kanban.$('.o_kanban_quick_create input[name=foo]').val(), '', "quick create should now be empty"); assert.doesNotHaveClass(kanban.$('.o_kanban_quick_create'), 'o_disabled', "quick create should be enabled"); assert.verifySteps([ 'onchange', // default_get 'onchange', // new partner 'create', 'onchange', // default_get ]); kanban.destroy(); }); QUnit.test('quick create when first column is folded', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], }); assert.doesNotHaveClass(kanban.$('.o_kanban_group:first'), 'o_column_folded', "first column should not be folded"); // fold the first column testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:first')); await testUtils.dom.click(kanban.$('.o_kanban_group:first .o_kanban_toggle_fold')); assert.hasClass(kanban.$('.o_kanban_group:first'),'o_column_folded', "first column should be folded"); // click on 'Create' to open the quick create in the first column await testUtils.kanban.clickCreate(kanban); assert.doesNotHaveClass(kanban.$('.o_kanban_group:first'), 'o_column_folded', "first column should no longer be folded"); var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); assert.strictEqual($quickCreate.length, 1, "should have added a quick create element in first column"); // fold again the first column testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:first')); await testUtils.dom.click(kanban.$('.o_kanban_group:first .o_kanban_toggle_fold')); assert.hasClass(kanban.$('.o_kanban_group:first'),'o_column_folded', "first column should be folded"); assert.containsNone(kanban, '.o_kanban_quick_create', "there should be no more quick create"); kanban.destroy(); }); QUnit.test('quick create record: cancel when not dirty', async function (assert) { assert.expect(11); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], }); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should contain one record"); // click to add an element await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "should have open the quick create widget"); // click again to add an element -> should have kept the quick create open await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "should have kept the quick create open"); // click outside: should remove the quick create await testUtils.dom.click(kanban.$('.o_kanban_group .o_kanban_record:first')); assert.containsNone(kanban, '.o_kanban_quick_create', "the quick create should not have been destroyed"); // click to reopen the quick create await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "should have open the quick create widget"); // press ESC: should remove the quick create kanban.$('.o_kanban_quick_create input').trigger($.Event('keydown', { keyCode: $.ui.keyCode.ESCAPE, which: $.ui.keyCode.ESCAPE, })); assert.containsNone(kanban, '.o_kanban_quick_create', "quick create widget should have been removed"); // click to reopen the quick create await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "should have open the quick create widget"); // click on 'Discard': should remove the quick create await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); await testUtils.dom.click(kanban.$('.o_kanban_group .o_kanban_record:first')); assert.containsNone(kanban, '.o_kanban_quick_create', "the quick create should be destroyed when the user clicks outside"); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should still contain one record"); // click to reopen the quick create await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "should have open the quick create widget"); // clicking on the quick create itself should keep it open await testUtils.dom.click(kanban.$('.o_kanban_quick_create')); assert.containsOnce(kanban, '.o_kanban_quick_create', "the quick create should not have been destroyed when clicked on itself"); kanban.destroy(); }); QUnit.test('quick create record: cancel when modal is opened', async function (assert) { assert.expect(3); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '', }, groupBy: ['bar'], }); // click to add an element await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "should have open the quick create widget"); kanban.$('.o_kanban_quick_create input') .val('test') .trigger('keyup') .trigger('focusout'); await nextTick(); // When focusing out of the many2one, a modal to add a 'product' will appear. // The following assertions ensures that a click on the body element that has 'modal-open' // will NOT close the quick create. // This can happen when the user clicks out of the input because of a race condition between // the focusout of the m2o and the global 'click' handler of the quick create. // Check odoo/odoo#61981 for more details. const $body = kanban.$el.closest('body'); assert.hasClass($body, 'modal-open', "modal should be opening after m2o focusout"); await testUtils.dom.click($body); assert.containsOnce(kanban, '.o_kanban_quick_create', "quick create should stay open while modal is opening"); kanban.destroy(); }); QUnit.test('quick create record: cancel when dirty', async function (assert) { assert.expect(7); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], }); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should contain one record"); // click to add an element and edit it await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "should have open the quick create widget"); var $quickCreate = kanban.$('.o_kanban_quick_create'); await testUtils.fields.editInput($quickCreate.find('input'), 'some value'); // click outside: should not remove the quick create await testUtils.dom.click(kanban.$('.o_kanban_group .o_kanban_record:first')); assert.containsOnce(kanban, '.o_kanban_quick_create', "the quick create should not have been destroyed"); // press ESC: should remove the quick create $quickCreate.find('input').trigger($.Event('keydown', { keyCode: $.ui.keyCode.ESCAPE, which: $.ui.keyCode.ESCAPE, })); assert.containsNone(kanban, '.o_kanban_quick_create', "quick create widget should have been removed"); // click to reopen quick create and edit it await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "should have open the quick create widget"); $quickCreate = kanban.$('.o_kanban_quick_create'); await testUtils.fields.editInput($quickCreate.find('input'), 'some value'); // click on 'Discard': should remove the quick create await testUtils.dom.click(kanban.$('.o_kanban_quick_create .o_kanban_cancel')); assert.containsNone(kanban, '.o_kanban_quick_create', "the quick create should be destroyed when the user clicks outside"); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should still contain one record"); kanban.destroy(); }); QUnit.test('quick create record and edit in grouped mode', async function (assert) { assert.expect(6); var newRecordID; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', mockRPC: function (route, args) { var def = this._super.apply(this, arguments); if (args.method === 'name_create') { def.then(function (result) { newRecordID = result[0]; }); } return def; }, groupBy: ['bar'], intercepts: { switch_view: function (event) { assert.strictEqual(event.data.mode, "edit", "should trigger 'open_record' event in edit mode"); assert.strictEqual(event.data.res_id, newRecordID, "should open the correct record"); }, }, }); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should contain one record"); // click to add and edit an element var $quickCreate = kanban.$('.o_kanban_quick_create'); await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); $quickCreate = kanban.$('.o_kanban_quick_create'); await testUtils.fields.editInput($quickCreate.find('input'), 'new partner'); await testUtils.dom.click($quickCreate.find('button.o_kanban_edit')); assert.strictEqual(this.data.partner.records.length, 5, "should have created a partner"); assert.strictEqual(_.last(this.data.partner.records).name, "new partner", "should have correct name"); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column should now contain two records"); kanban.destroy(); }); QUnit.test('quick create several records in a row', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], }); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should contain one record"); // click to add an element, fill the input and press ENTER await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "the quick create should be open"); await testUtils.kanban.quickCreate(kanban, 'new partner 1'); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column should now contain two records"); assert.containsOnce(kanban, '.o_kanban_quick_create', "the quick create should still be open"); // create a second element in a row await testUtils.kanban.quickCreate(kanban, 'new partner 2'); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 3, "first column should now contain three records"); assert.containsOnce(kanban, '.o_kanban_quick_create', "the quick create should still be open"); kanban.destroy(); }); QUnit.test('quick create is disabled until record is created and read', async function (assert) { assert.expect(6); var prom = makeTestPromise(); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], mockRPC: function (route, args) { var result = this._super.apply(this, arguments); if (args.method === 'read') { return prom.then(_.constant(result)); } return result; }, }); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should contain one record"); // click to add a record, and add two in a row (first one will be delayed) await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first()); assert.containsOnce(kanban, '.o_kanban_quick_create', "the quick create should be open"); await testUtils.kanban.quickCreate(kanban, 'new partner 1'); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should still contain one record"); assert.containsOnce(kanban, '.o_kanban_quick_create.o_disabled', "quick create should be disabled"); prom.resolve(); await nextTick(); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column should now contain two records"); assert.strictEqual(kanban.$('.o_kanban_quick_create:not(.o_disabled)').length, 1, "quick create should be enabled"); kanban.destroy(); }); QUnit.test('quick create record fail in grouped by many2one', async function (assert) { assert.expect(8); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', archs: { 'partner,false,form': '
' + '' + '' + '', }, groupBy: ['product_id'], mockRPC: function (route, args) { if (args.method === 'name_create') { return Promise.reject({ message: { code: 200, data: {}, message: "Odoo server error", }, event: $.Event() }); } return this._super.apply(this, arguments); }, }); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "there should be 2 records in first column"); await testUtils.kanban.clickCreate(kanban); // Click on 'Create' assert.hasClass(kanban.$('.o_kanban_group:first() > div:nth(1)'),'o_kanban_quick_create', "clicking on create should open the quick_create in the first column"); await testUtils.kanban.quickCreate(kanban, 'test'); assert.strictEqual($('.modal .o_legacy_form_view.o_form_editable').length, 1, "a form view dialog should have been opened (in edit)"); assert.strictEqual($('.modal .o_field_many2one input').val(), 'hello', "the correct product_id should already be set"); // specify a name and save await testUtils.fields.editInput($('.modal input[name=foo]'), 'test'); await testUtils.modal.clickButton('Save'); assert.strictEqual($('.modal').length, 0, "the modal should be closed"); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 3, "there should be 3 records in first column"); var $firstRecord = kanban.$('.o_kanban_group:first .o_kanban_record:first'); assert.strictEqual($firstRecord.text(), 'test', "the first record of the first column should be the new one"); assert.strictEqual(kanban.$('.o_kanban_quick_create:not(.o_disabled)').length, 1, "quick create should be enabled"); kanban.destroy(); }); QUnit.test('quick create record is re-enabled after discard on failure', async function (assert) { assert.expect(4); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', archs: { 'partner,false,form': '
' + '' + '' + '', }, groupBy: ['product_id'], mockRPC: function (route, args) { if (args.method === 'name_create') { return Promise.reject({ message: { code: 200, data: {}, message: "Odoo server error", }, event: $.Event() }); } return this._super.apply(this, arguments); } }); await testUtils.kanban.clickCreate(kanban); assert.containsOnce(kanban, '.o_kanban_quick_create', "should have a quick create widget"); await testUtils.kanban.quickCreate(kanban, 'test'); assert.strictEqual($('.modal .o_legacy_form_view.o_form_editable').length, 1, "a form view dialog should have been opened (in edit)"); await testUtils.modal.clickButton('Discard'); assert.strictEqual($('.modal').length, 0, "the modal should be closed"); assert.strictEqual(kanban.$('.o_kanban_quick_create:not(.o_disabled)').length, 1, "quick create widget should have been re-enabled"); kanban.destroy(); }); QUnit.test('quick create record fails in grouped by char', async function (assert) { assert.expect(7); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', archs: { 'partner,false,form': '
' + '' + '', }, mockRPC: function (route, args) { if (args.method === 'name_create') { return Promise.reject({ message: { code: 200, data: {}, message: "Odoo server error", }, event: $.Event() }); } if (args.method === 'create') { assert.deepEqual(args.args[0], {foo: 'yop'}, "should write the correct value for foo"); assert.deepEqual(args.kwargs.context, {default_foo: 'yop', default_name: 'test'}, "should send the correct default value for foo"); } return this._super.apply(this, arguments); }, groupBy: ['foo'], }); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "there should be 1 record in first column"); await testUtils.dom.click(kanban.$('.o_kanban_header:first .o_kanban_quick_add i')); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create input'), 'test'); await testUtils.dom.click(kanban.$('.o_kanban_add')); assert.strictEqual($('.modal .o_legacy_form_view.o_form_editable').length, 1, "a form view dialog should have been opened (in edit)"); assert.strictEqual($('.modal .o_field_widget[name=foo]').val(), 'yop', "the correct default value for foo should already be set"); await testUtils.modal.clickButton('Save'); assert.strictEqual($('.modal').length, 0, "the modal should be closed"); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "there should be 2 records in first column"); kanban.destroy(); }); QUnit.test('quick create record fails in grouped by selection', async function (assert) { assert.expect(7); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', archs: { 'partner,false,form': '
' + '' + '', }, mockRPC: function (route, args) { if (args.method === 'name_create') { return Promise.reject({ message: { code: 200, data: {}, message: "Odoo server error", }, event: $.Event() }); } if (args.method === 'create') { assert.deepEqual(args.args[0], {state: 'abc'}, "should write the correct value for state"); assert.deepEqual(args.kwargs.context, {default_state: 'abc', default_name: 'test'}, "should send the correct default value for state"); } return this._super.apply(this, arguments); }, groupBy: ['state'], }); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "there should be 1 record in first column"); await testUtils.dom.click(kanban.$('.o_kanban_header:first .o_kanban_quick_add i')); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create input'), 'test'); await testUtils.dom.click(kanban.$('.o_kanban_add')); assert.strictEqual($('.modal .o_legacy_form_view.o_form_editable').length, 1, "a form view dialog should have been opened (in edit)"); assert.strictEqual($('.modal .o_field_widget[name=state]').val(), '"abc"', "the correct default value for state should already be set"); await testUtils.modal.clickButton('Save'); assert.strictEqual($('.modal').length, 0, "the modal should be closed"); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "there should be 2 records in first column"); kanban.destroy(); }); QUnit.test('quick create record in empty grouped kanban', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (args.method === 'web_read_group') { // override read_group to return empty groups, as this is // the case for several models (e.g. project.task grouped // by stage_id) var result = { groups: [ {__domain: [['product_id', '=', 3]], product_id_count: 0}, {__domain: [['product_id', '=', 5]], product_id_count: 0}, ], length: 2, }; return Promise.resolve(result); } return this._super.apply(this, arguments); }, }); assert.containsN(kanban, '.o_kanban_group', 2, "there should be 2 columns"); assert.containsNone(kanban, '.o_kanban_record', "both columns should be empty"); await testUtils.kanban.clickCreate(kanban); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_quick_create', "should have opened the quick create in the first column"); kanban.destroy(); }); QUnit.test('quick create record in grouped on date(time) field', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['date'], intercepts: { switch_view: function (ev) { assert.deepEqual(_.pick(ev.data, 'res_id', 'view_type'), { res_id: undefined, view_type: 'form', }, "should trigger an event to open the form view (twice)"); }, }, }); assert.containsNone(kanban, '.o_kanban_header .o_kanban_quick_add i', "quick create should be disabled when grouped on a date field"); // clicking on CREATE in control panel should not open a quick create await testUtils.kanban.clickCreate(kanban); assert.containsNone(kanban, '.o_kanban_quick_create', "should not have opened the quick create widget"); await kanban.reload({groupBy: ['datetime']}); assert.containsNone(kanban, '.o_kanban_header .o_kanban_quick_add i', "quick create should be disabled when grouped on a datetime field"); // clicking on CREATE in control panel should not open a quick create await testUtils.kanban.clickCreate(kanban); assert.containsNone(kanban, '.o_kanban_quick_create', "should not have opened the quick create widget"); kanban.destroy(); }); QUnit.test('quick create record if grouped on date(time) field with attribute allow_group_range_value: true', async function (assert) { assert.expect(6); this.data.partner.records[0].date = '2017-01-08'; this.data.partner.records[1].date = '2017-01-09'; this.data.partner.records[2].date = '2017-01-08'; this.data.partner.records[3].date = '2017-01-10'; this.data.partner.records[0].datetime = '2017-01-08 10:55:05'; this.data.partner.records[1].datetime = '2017-01-09 11:31:10'; this.data.partner.records[2].datetime = '2017-01-08 09:20:25'; this.data.partner.records[3].datetime = '2017-01-10 08:05:51'; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '
' + '
' + '
', archs: { 'partner,quick_form,form': '
' + '' + '' + '', }, groupBy: ['date'], }); assert.containsOnce(kanban, '.o_kanban_header .o_kanban_quick_add i', "quick create should be enabled when grouped on a non-readonly date field"); // clicking on CREATE in control panel should open a quick create await testUtils.kanban.clickCreate(kanban); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_quick_create', "should have opened the quick create in the first column"); assert.strictEqual(kanban.$( ".o_kanban_group:first .o_kanban_quick_create .o_datepicker_input[name=date]" ).val(), "01/31/2017"); await kanban.reload({groupBy: ['datetime']}); assert.containsOnce(kanban, '.o_kanban_header .o_kanban_quick_add i', "quick create should be enabled when grouped on a non-readonly datetime field"); // clicking on CREATE in control panel should open a quick create await testUtils.kanban.clickCreate(kanban); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_quick_create', "should have opened the quick create in the first column"); assert.strictEqual(kanban.$( ".o_kanban_group:first .o_kanban_quick_create .o_datepicker_input[name=datetime]" ).val(), "01/31/2017 23:59:59"); kanban.destroy(); }); QUnit.test('quick create record feature is properly enabled/disabled at reload', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['foo'], }); assert.containsN(kanban, '.o_kanban_header .o_kanban_quick_add i', 3, "quick create should be enabled when grouped on a char field"); await kanban.reload({groupBy: ['date']}); assert.containsNone(kanban, '.o_kanban_header .o_kanban_quick_add i', "quick create should now be disabled (grouped on date field)"); await kanban.reload({groupBy: ['bar']}); assert.containsN(kanban, '.o_kanban_header .o_kanban_quick_add i', 2, "quick create should be enabled again (grouped on boolean field)"); kanban.destroy(); }); QUnit.test('quick create record in grouped by char field', async function (assert) { assert.expect(4); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['foo'], mockRPC: function (route, args) { if (args.method === 'name_create') { assert.deepEqual(args.kwargs.context, {default_foo: 'yop'}, "should send the correct default value for foo"); } return this._super.apply(this, arguments); }, }); assert.containsN(kanban, '.o_kanban_header .o_kanban_quick_add i', 3, "quick create should be enabled when grouped on a char field"); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should contain 1 record"); await testUtils.dom.click(kanban.$('.o_kanban_header:first .o_kanban_quick_add i')); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create input'), 'new record'); await testUtils.dom.click(kanban.$('.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column should now contain 2 records"); kanban.destroy(); }); QUnit.test('quick create record in grouped by boolean field', async function (assert) { assert.expect(4); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['bar'], mockRPC: function (route, args) { if (args.method === 'name_create') { assert.deepEqual(args.kwargs.context, {default_bar: true}, "should send the correct default value for bar"); } return this._super.apply(this, arguments); }, }); assert.containsN(kanban, '.o_kanban_header .o_kanban_quick_add i', 2, "quick create should be enabled when grouped on a boolean field"); assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 3, "second column (true) should contain 3 records"); await testUtils.dom.click(kanban.$('.o_kanban_header:nth(1) .o_kanban_quick_add i')); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create input'), 'new record'); await testUtils.dom.click(kanban.$('.o_kanban_add')); assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 4, "second column (true) should now contain 4 records"); kanban.destroy(); }); QUnit.test('quick create record in grouped on selection field', async function (assert) { assert.expect(4); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', mockRPC: function (route, args) { if (args.method === 'name_create') { assert.deepEqual(args.kwargs.context, {default_state: 'abc'}, "should send the correct default value for bar"); } return this._super.apply(this, arguments); }, groupBy: ['state'], }); assert.containsN(kanban, '.o_kanban_header .o_kanban_quick_add i', 3, "quick create should be enabled when grouped on a selection field"); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column (abc) should contain 1 record"); await testUtils.dom.click(kanban.$('.o_kanban_header:first .o_kanban_quick_add i')); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create input'), 'new record'); await testUtils.dom.click(kanban.$('.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column (abc) should contain 2 records"); kanban.destroy(); }); QUnit.test('quick create record in grouped by char field (within quick_create_view)', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '', }, groupBy: ['foo'], mockRPC: function (route, args) { if (args.method === 'create') { assert.deepEqual(args.args[0], {foo: 'yop'}, "should write the correct value for foo"); assert.deepEqual(args.kwargs.context, {default_foo: 'yop'}, "should send the correct default value for foo"); } return this._super.apply(this, arguments); }, }); assert.containsN(kanban, '.o_kanban_header .o_kanban_quick_add i', 3, "quick create should be enabled when grouped on a char field"); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column should contain 1 record"); await testUtils.dom.click(kanban.$('.o_kanban_header:first .o_kanban_quick_add i')); assert.strictEqual(kanban.$('.o_kanban_quick_create input').val(), 'yop', "should have set the correct foo value by default"); await testUtils.dom.click(kanban.$('.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column should now contain 2 records"); kanban.destroy(); }); QUnit.test('quick create record in grouped by boolean field (within quick_create_view)', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '', }, groupBy: ['bar'], mockRPC: function (route, args) { if (args.method === 'create') { assert.deepEqual(args.args[0], {bar: true}, "should write the correct value for bar"); assert.deepEqual(args.kwargs.context, {default_bar: true}, "should send the correct default value for bar"); } return this._super.apply(this, arguments); }, }); assert.containsN(kanban, '.o_kanban_header .o_kanban_quick_add i', 2, "quick create should be enabled when grouped on a boolean field"); assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 3, "second column (true) should contain 3 records"); await testUtils.dom.click(kanban.$('.o_kanban_header:nth(1) .o_kanban_quick_add i')); assert.ok(kanban.$('.o_kanban_quick_create .o_field_boolean input').is(':checked'), "should have set the correct bar value by default"); await testUtils.dom.click(kanban.$('.o_kanban_add')); assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 4, "second column (true) should now contain 4 records"); kanban.destroy(); }); QUnit.test('quick create record in grouped by selection field (within quick_create_view)', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '', }, groupBy: ['state'], mockRPC: function (route, args) { if (args.method === 'create') { assert.deepEqual(args.args[0], {state: 'abc'}, "should write the correct value for state"); assert.deepEqual(args.kwargs.context, {default_state: 'abc'}, "should send the correct default value for state"); } return this._super.apply(this, arguments); }, }); assert.containsN(kanban, '.o_kanban_header .o_kanban_quick_add i', 3, "quick create should be enabled when grouped on a selection field"); assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record', "first column (abc) should contain 1 record"); await testUtils.dom.click(kanban.$('.o_kanban_header:first .o_kanban_quick_add i')); assert.strictEqual(kanban.$('.o_kanban_quick_create select').val(), '"abc"', "should have set the correct state value by default"); await testUtils.dom.click(kanban.$('.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2, "first column (abc) should now contain 2 records"); kanban.destroy(); }); QUnit.test('quick create record while adding a new column', async function (assert) { assert.expect(10); var def = testUtils.makeTestPromise(); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { var result = this._super.apply(this, arguments); if (args.method === 'name_create' && args.model === 'product') { return def.then(_.constant(result)); } return result; }, }); assert.containsN(kanban, '.o_kanban_group', 2); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 2); // add a new column assert.containsOnce(kanban, '.o_column_quick_create'); assert.isNotVisible(kanban.$('.o_column_quick_create input')); await testUtils.dom.click(kanban.$('.o_quick_create_folded')); assert.isVisible(kanban.$('.o_column_quick_create input')); await testUtils.fields.editInput(kanban.$('.o_column_quick_create input'), 'new column'); await testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group', 2); // click to add a new record await testUtils.dom.click(kanban.$buttons.find('.o-kanban-button-new')); // should wait for the column to be created (and view to be re-rendered // before opening the quick create assert.containsNone(kanban, '.o_kanban_quick_create'); // unlock column creation def.resolve(); await testUtils.nextTick(); assert.containsN(kanban, '.o_kanban_group', 3); assert.containsOnce(kanban, '.o_kanban_quick_create'); // quick create record in first column await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create input'), 'new record'); await testUtils.dom.click(kanban.$('.o_kanban_quick_create .o_kanban_add')); assert.containsN(kanban, '.o_kanban_group:first .o_kanban_record', 3); kanban.destroy(); }); QUnit.test('close a column while quick creating a record', async function (assert) { assert.expect(6); const def = testUtils.makeTestPromise(); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, archs: { 'partner,some_view_ref,form': '
', }, groupBy: ['product_id'], async mockRPC(route, args) { const result = this._super(...arguments); if (args.method === 'get_views') { await def; } return result; }, }); assert.containsN(kanban, '.o_kanban_group', 2); assert.containsNone(kanban, '.o_column_folded'); // click to quick create a new record in the first column (this operation is delayed) await testUtils.dom.click(kanban.$('.o_kanban_group:first .o_kanban_quick_add')); assert.containsNone(kanban, '.o_legacy_form_view'); // click to fold the first column await testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:first')); await testUtils.dom.click(kanban.$('.o_kanban_group:first .o_kanban_toggle_fold')); assert.containsOnce(kanban, '.o_column_folded'); def.resolve(); await testUtils.nextTick(); assert.containsNone(kanban, '.o_legacy_form_view'); assert.containsOnce(kanban, '.o_column_folded'); kanban.destroy(); }); QUnit.test('quick create record: open on a column while another column has already one', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); // Click on quick create in first column await testUtils.dom.click(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_quick_add')); assert.containsOnce(kanban, '.o_kanban_quick_create'); assert.containsOnce(kanban.$('.o_kanban_group:nth-child(1)'), '.o_kanban_quick_create'); // Click on quick create in second column await testUtils.dom.click(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_quick_add')); assert.containsOnce(kanban, '.o_kanban_quick_create'); assert.containsOnce(kanban.$('.o_kanban_group:nth-child(2)'), '.o_kanban_quick_create'); // Click on quick create in first column once again await testUtils.dom.click(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_quick_add')); assert.containsOnce(kanban, '.o_kanban_quick_create'); assert.containsOnce(kanban.$('.o_kanban_group:nth-child(1)'), '.o_kanban_quick_create'); kanban.destroy(); }); QUnit.test('many2many_tags in kanban views', async function (assert) { assert.expect(12); this.data.partner.records[0].category_ids = [6, 7]; this.data.partner.records[1].category_ids = [7, 8]; this.data.category.records.push({ id: 8, name: "hello", color: 0, }); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '' + '' + '' + '
' + '
' + '
', mockRPC: function (route) { assert.step(route); return this._super.apply(this, arguments); }, intercepts: { switch_view: function (event) { assert.deepEqual(_.pick(event.data, 'mode', 'model', 'res_id', 'view_type'), { mode: 'readonly', model: 'partner', res_id: 1, view_type: 'form', }, "should trigger an event to open the clicked record in a form view"); }, }, }); var $first_record = kanban.$('.o_kanban_record:first()'); assert.strictEqual($first_record.find('.o_field_many2manytags .o_tag').length, 2, 'first record should contain 2 tags'); assert.hasClass($first_record.find('.o_tag:first()'),'o_tag_color_2', 'first tag should have color 2'); assert.verifySteps(['/web/dataset/search_read', '/web/dataset/call_kw/category/read'], 'two RPC should have been done (one search read and one read for the m2m)'); // Checks that second records has only one tag as one should be hidden (color 0) assert.strictEqual(kanban.$('.o_kanban_record').eq(1).find('.o_tag').length, 1, 'there should be only one tag in second record'); // Write on the record using the priority widget to trigger a re-render in readonly await testUtils.dom.click(kanban.$('.o_field_widget.o_priority a.o_priority_star.fa-star-o').first()); assert.verifySteps([ '/web/dataset/call_kw/partner/write', '/web/dataset/call_kw/partner/read', '/web/dataset/call_kw/category/read' ], 'five RPCs should have been done (previous 2, 1 write (triggers a re-render), same 2 at re-render'); assert.strictEqual(kanban.$('.o_kanban_record:first()').find('.o_field_many2manytags .o_tag').length, 2, 'first record should still contain only 2 tags'); // click on a tag (should trigger switch_view) await testUtils.dom.click(kanban.$('.o_tag:contains(gold):first')); kanban.destroy(); }); QUnit.test('Do not open record when clicking on `a` with `href`', async function (assert) { assert.expect(5); this.data.partner.records = [ { id: 1, foo: 'yop' }, ]; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '
' + 'test link' + '
' + '
' + '
' + '
' + '
', intercepts: { // when clicking on a record in kanban view, // it switches to form view. switch_view: function () { throw new Error("should not switch view"); }, }, doNotDisableAHref: true, }); var $record = kanban.$('.o_kanban_record:not(.o_kanban_ghost)'); assert.strictEqual($record.length, 1, "should display a kanban record"); var $testLink = $record.find('a'); assert.strictEqual($testLink.length, 1, "should contain a link in the kanban record"); assert.ok(!!$testLink[0].href, "link inside kanban record should have non-empty href"); // Prevent the browser default behaviour when clicking on anything. // This includes clicking on a `` with `href`, so that it does not // change the URL in the address bar. // Note that we should not specify a click listener on 'a', otherwise // it may influence the kanban record global click handler to not open // the record. $(document.body).on('click.o_test', function (ev) { assert.notOk(ev.isDefaultPrevented(), "should not prevented browser default behaviour beforehand"); assert.strictEqual(ev.target, $testLink[0], "should have clicked on the test link in the kanban record"); ev.preventDefault(); }); await testUtils.dom.click($testLink); $(document.body).off('click.o_test'); kanban.destroy(); }); QUnit.test('Open record when clicking on widget field', async function (assert) { assert.expect(2); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, archs: { 'product,false,form': '
', }, intercepts: { switch_view(ev) { assert.deepEqual({ res_id: ev.data.res_id, view_type: ev.data.view_type, }, { res_id: 1, view_type: 'form', }, "should trigger an event to open the form view"); }, }, }); assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 4); kanban.$(".oe_kanban_global_click .o_field_monetary[name=salary]:eq(0)").click(); kanban.destroy(); }); QUnit.test('o2m loaded in only one batch', async function (assert) { assert.expect(9); this.data.subtask = { fields: { name: {string: 'Name', type: 'char'} }, records: [ {id: 1, name: "subtask #1"}, {id: 2, name: "subtask #2"}, ] }; this.data.partner.fields.subtask_ids = { string: 'Subtasks', type: 'one2many', relation: 'subtask' }; this.data.partner.records[0].subtask_ids = [1]; this.data.partner.records[1].subtask_ids = [2]; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); await kanban.reload(); assert.verifySteps([ 'web_read_group', '/web/dataset/search_read', '/web/dataset/search_read', 'read', 'web_read_group', '/web/dataset/search_read', '/web/dataset/search_read', 'read', ]); kanban.destroy(); }); QUnit.test('m2m loaded in only one batch', async function (assert) { assert.expect(9); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); await kanban.reload(kanban); assert.verifySteps([ 'web_read_group', '/web/dataset/search_read', '/web/dataset/search_read', 'read', 'web_read_group', '/web/dataset/search_read', '/web/dataset/search_read', 'read', ]); kanban.destroy(); }); QUnit.test('fetch reference in only one batch', async function (assert) { assert.expect(9); this.data.partner.records[0].ref_product = 'product,3'; this.data.partner.records[1].ref_product = 'product,5'; this.data.partner.fields.ref_product = { string: "Reference Field", type: 'reference', }; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); await kanban.reload(); assert.verifySteps([ 'web_read_group', '/web/dataset/search_read', '/web/dataset/search_read', 'name_get', 'web_read_group', '/web/dataset/search_read', '/web/dataset/search_read', 'name_get', ]); kanban.destroy(); }); QUnit.test('wait x2manys batch fetches to re-render', async function (assert) { assert.expect(7); var done = assert.async(); var def = Promise.resolve(); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { var result = this._super(route, args); if (args.method === 'read') { return def.then(function() { return result; }); } return result; }, }); def = testUtils.makeTestPromise(); assert.containsN(kanban, '.o_tag', 2); assert.containsN(kanban, '.o_kanban_group', 2); kanban.update({groupBy: ['state']}); def.then(async function () { assert.containsN(kanban, '.o_kanban_group', 2); await testUtils.nextTick(); assert.containsN(kanban, '.o_kanban_group', 3); assert.containsN(kanban, '.o_tag', 2, 'Should display 2 tags after update'); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_tag').text(), 'gold', 'First category should be \'gold\''); assert.strictEqual(kanban.$('.o_kanban_group:eq(2) .o_tag').text(), 'silver', 'Second category should be \'silver\''); kanban.destroy(); done(); }); await testUtils.nextTick(); def.resolve(); }); QUnit.test('can drag and drop a record from one column to the next', async function (assert) { assert.expect(9); // @todo: remove this resequenceDef whenever the jquery upgrade branch // is merged. This is currently necessary to simulate the reality: we // need the click handlers to be executed after the end of the drag and // drop operation, not before. var resequenceDef = testUtils.makeTestPromise(); var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test this.data.partner.fields.sequence = {type: 'number', string: "Sequence"}; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + 'edit' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { assert.ok(true, "should call resequence"); return resequenceDef.then(_.constant(true)); } return this._super(route, args); }, }); assert.containsN(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record', 2); assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2); assert.containsN(kanban, '.thisiseditable', 4); assert.deepEqual(kanban.exportState().resIds, envIDs); var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); var $group = kanban.$('.o_kanban_group:nth-child(2)'); envIDs = [3, 2, 4, 1]; // first record of first column moved to the bottom of second column await testUtils.dom.dragAndDrop($record, $group, {withTrailingClick: true}); resequenceDef.resolve(); await testUtils.nextTick(); assert.containsOnce(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record'); assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 3); assert.containsN(kanban, '.thisiseditable', 4); assert.deepEqual(kanban.exportState().resIds, envIDs); resequenceDef.resolve(); kanban.destroy(); }); QUnit.test('drag and drop a record, grouped by selection', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
' + '
', groupBy: ['state'], mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { assert.ok(true, "should call resequence"); return Promise.resolve(true); } if (args.model === 'partner' && args.method === 'write') { assert.deepEqual(args.args[1], {state: 'def'}); } return this._super(route, args); }, }); assert.containsOnce(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record'); assert.containsOnce(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record'); var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); var $group = kanban.$('.o_kanban_group:nth-child(2)'); await testUtils.dom.dragAndDrop($record, $group); await nextTick(); // wait for resequence after drag and drop assert.containsNone(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record'); assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2); kanban.destroy(); }); QUnit.test('prevent drag and drop of record if grouped by readonly', async function (assert) { assert.expect(12); this.data.partner.fields.foo.readonly = true; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '' + '' + '
' + '
' + '
', mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { return Promise.resolve(); } if (args.model === 'partner' && args.method === 'write') { throw new Error('should not be draggable'); } return this._super(route, args); }, }); // simulate an update coming from the searchview, with another groupby given await kanban.update({groupBy: ['state']}); assert.containsOnce(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record'); assert.containsOnce(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record'); // drag&drop a record in another column var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); var $group = kanban.$('.o_kanban_group:nth-child(2)'); await testUtils.dom.dragAndDrop($record, $group); await nextTick(); // wait for resequence after drag and drop // should not be draggable assert.containsOnce(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record'); assert.containsOnce(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record'); // simulate an update coming from the searchview, with another groupby given await kanban.update({groupBy: ['foo']}); assert.containsOnce(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record'); assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2); // drag&drop a record in another column $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); $group = kanban.$('.o_kanban_group:nth-child(2)'); await testUtils.dom.dragAndDrop($record, $group); await nextTick(); // wait for resequence after drag and drop // should not be draggable assert.containsOnce(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record'); assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2); // drag&drop a record in the same column var $record1 = kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(0)'); var $record2 = kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(1)'); assert.strictEqual($record1.text(), "blipDEF", "first record should be DEF"); assert.strictEqual($record2.text(), "blipGHI", "second record should be GHI"); await testUtils.dom.dragAndDrop($record2, $record1, {position: 'top'}); // should still be able to resequence assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(0)').text(), "blipGHI", "records should have been resequenced"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(1)').text(), "blipDEF", "records should have been resequenced"); kanban.destroy(); }); QUnit.test('prevent drag and drop if grouped by date/datetime field', async function (assert) { assert.expect(10); this.data.partner.records[0].date = '2017-01-08'; this.data.partner.records[1].date = '2017-01-09'; this.data.partner.records[2].date = '2017-02-08'; this.data.partner.records[3].date = '2017-02-10'; this.data.partner.records[0].datetime = '2017-01-08 10:55:05'; this.data.partner.records[1].datetime = '2017-01-09 11:31:10'; this.data.partner.records[2].datetime = '2017-02-08 09:20:25'; this.data.partner.records[3].datetime = '2017-02-10 08:05:51'; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '
' + '
', groupBy: ['date:month'], }); assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have 2 columns"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2, "1st column should contain 2 records of January month"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2, "2nd column should contain 2 records of February month"); // drag&drop a record in another column var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); var $group = kanban.$('.o_kanban_group:nth-child(2)'); await testUtils.dom.dragAndDrop($record, $group); // should not drag&drop record assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length , 2, "Should remain same records in first column(2 records)"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2, "Should remain same records in 2nd column(2 record)"); await kanban.reload({groupBy: ['datetime:month']}); assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have 2 columns"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2, "1st column should contain 2 records of January month"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2, "2nd column should contain 2 records of February month"); // drag&drop a record in another column $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); $group = kanban.$('.o_kanban_group:nth-child(2)'); await testUtils.dom.dragAndDrop($record, $group); // should not drag&drop record assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length , 2, "Should remain same records in first column(2 records)"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2, "Should remain same records in 2nd column(2 record)"); kanban.destroy(); }); QUnit.test('prevent drag and drop if grouped by many2many field', async function (assert) { assert.expect(13); this.data.partner.records[0].category_ids = [6, 7]; this.data.partner.records[3].category_ids = [7]; const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['category_ids'], }); assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have 2 columns"); assert.strictEqual(kanban.$('.o_kanban_group:first .o_column_title').text(), 'gold', 'first column should have correct title'); assert.strictEqual(kanban.$('.o_kanban_group:last .o_column_title').text(), 'silver', 'second column should have correct title'); assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').text(), 'yopblip', "first column should have 2 records"); assert.strictEqual(kanban.$('.o_kanban_group:last .o_kanban_record').text(), 'yopgnapblip', "second column should have 3 records"); // drag&drop a record in another column let $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); let $group = kanban.$('.o_kanban_group:nth-child(2)'); await testUtils.dom.dragAndDrop($record, $group); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2, "1st column should contain 2 records"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 3, "2nd column should contain 3 records"); // Sanity check: groupby a non m2m field and check dragdrop is working await kanban.reload({ groupBy: ["state"] }); assert.strictEqual(kanban.$('.o_kanban_group').length, 3, "should have 3 columns"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group .o_column_title")].map((el) => el.innerText), ["ABC", "DEF", "GHI"], "columns should have correct title" ); assert.containsOnce(kanban, ".o_kanban_group:first-child .o_kanban_record", "first column should have 1 record"); assert.containsN(kanban, ".o_kanban_group:last-child .o_kanban_record", 2, "last column should have 2 records"); $record = kanban.$('.o_kanban_group:first-child .o_kanban_record:first'); $group = kanban.$('.o_kanban_group:last-child'); await testUtils.dom.dragAndDrop($record, $group); assert.containsNone(kanban, ".o_kanban_group:first-child .o_kanban_record", "first column should not contain records"); assert.containsN(kanban, ".o_kanban_group:last-child .o_kanban_record", 3, "last column should contain 3 records"); kanban.destroy(); }); QUnit.test('drag and drop record if grouped by date/time field with attribute allow_group_range_value: true', async function (assert) { assert.expect(14); this.data.partner.records[0].date = '2017-01-08'; this.data.partner.records[1].date = '2017-01-09'; this.data.partner.records[2].date = '2017-02-08'; this.data.partner.records[3].date = '2017-02-10'; this.data.partner.records[0].datetime = '2017-01-08 10:55:05'; this.data.partner.records[1].datetime = '2017-01-09 11:31:10'; this.data.partner.records[2].datetime = '2017-02-08 09:20:25'; this.data.partner.records[3].datetime = '2017-02-10 08:05:51'; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '' + '
' + '
' + '
' + '
', groupBy: ['date:month'], mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { assert.ok(true, "should call resequence"); return Promise.resolve(true); } if (args.model === 'partner' && args.method === 'write') { if ("date" in args.args[1]) { assert.deepEqual(args.args[1], {date: '2017-02-28'}); } else if ("datetime" in args.args[1]) { assert.deepEqual(args.args[1], {datetime: '2017-02-28 23:59:59'}); } } return this._super(route, args); }, }); assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have 2 columns"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2, "1st column should contain 2 records of January month"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2, "2nd column should contain 2 records of February month"); var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); var $group = kanban.$('.o_kanban_group:nth-child(2)'); await testUtils.dom.dragAndDrop($record, $group); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length , 1, "Should only have one record remaining"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 3, "Should now have 3 records"); await kanban.reload({groupBy: ['datetime:month']}); assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have 2 columns"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2, "1st column should contain 2 records of January month"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2, "2nd column should contain 2 records of February month"); $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); $group = kanban.$('.o_kanban_group:nth-child(2)'); await testUtils.dom.dragAndDrop($record, $group); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length , 1, "Should only have one record remaining"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 3, "Should now have 3 records"); kanban.destroy(); }); QUnit.test('completely prevent drag and drop if records_draggable set to false', async function (assert) { assert.expect(6); var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['product_id'], }); // testing initial state assert.containsN(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record', 2); assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2); assert.deepEqual(kanban.exportState().resIds, envIDs); // attempt to drag&drop a record in another column var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); var $group = kanban.$('.o_kanban_group:nth-child(2)'); await testUtils.dom.dragAndDrop($record, $group, {withTrailingClick: true}); // should not drag&drop record assert.containsN(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record', 2, "First column should still contain 2 records"); assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 2, "Second column should still contain 2 records"); assert.deepEqual(kanban.exportState().resIds, envIDs, "Records should not have moved"); kanban.destroy(); }); QUnit.test('prevent drag and drop of record if onchange fails', async function (assert) { assert.expect(4); this.data.partner.onchanges = { product_id: function (obj) {} }; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/onchange') { return Promise.reject({}); } return this._super(route, args); }, }); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2, "column should contain 2 records"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 2, "column should contain 2 records"); // drag&drop a record in another column var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first'); var $group = kanban.$('.o_kanban_group:nth-child(2)'); await testUtils.dom.dragAndDrop($record, $group); // should not be dropped, card should reset back to first column assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2, "column should now contain 2 records"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 2, "column should contain 2 records"); kanban.destroy(); }); QUnit.test('kanban view with default_group_by', async function (assert) { assert.expect(7); this.data.partner.records.product_id = 1; this.data.product.records.push({id: 1, display_name: "third product"}); var readGroupCount = 0; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/web_read_group') { readGroupCount++; var correctGroupBy; if (readGroupCount === 2) { correctGroupBy = ['product_id']; } else { correctGroupBy = ['bar']; } // this is done three times assert.ok(_.isEqual(args.kwargs.groupby, correctGroupBy), "groupby args should be correct"); } return this._super.apply(this, arguments); }, }); assert.hasClass(kanban.$('.o_legacy_kanban_view'), 'o_kanban_grouped'); assert.containsN(kanban, '.o_kanban_group', 2, "should have " + 2 + " columns"); // simulate an update coming from the searchview, with another groupby given await kanban.update({groupBy: ['product_id']}); assert.containsN(kanban, '.o_kanban_group', 2, "should now have " + 3 + " columns"); // simulate an update coming from the searchview, removing the previously set groupby await kanban.update({groupBy: []}); assert.containsN(kanban, '.o_kanban_group', 2, "should have " + 2 + " columns again"); kanban.destroy(); }); QUnit.test('kanban view not groupable', async function (assert) { assert.expect(3); const searchMenuTypesOriginal = KanbanView.prototype.searchMenuTypes; KanbanView.prototype.searchMenuTypes = ['filter', 'favorite']; const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, archs: { 'partner,false,search': ` `, }, mockRPC: function (route, args) { if (args.method === 'read_group') { throw new Error("Should not do a read_group RPC"); } return this._super.apply(this, arguments); }, context: { search_default_itsName: 1, }, }); assert.doesNotHaveClass(kanban.$('.o_legacy_kanban_view'), 'o_kanban_grouped'); assert.containsNone(kanban, '.o_control_panel div.o_search_options div.o_group_by_menu'); assert.deepEqual(cpHelpers.getFacetTexts(kanban.el), []); kanban.destroy(); KanbanView.prototype.searchMenuTypes = searchMenuTypesOriginal; }); QUnit.test('kanban view with create=False', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
', }); assert.ok(!kanban.$buttons || !kanban.$buttons.find('.o-kanban-button-new').length, "Create button shouldn't be there"); kanban.destroy(); }); QUnit.test('clicking on a link triggers correct event', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '
' + '', }); testUtils.mock.intercept(kanban, 'switch_view', function (event) { assert.deepEqual(event.data, { view_type: 'form', res_id: 1, mode: 'edit', model: 'partner', }); }); await testUtils.dom.click(kanban.$('a').first()); kanban.destroy(); }); QUnit.test('environment is updated when (un)folding groups', async function (assert) { assert.expect(3); var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); assert.deepEqual(kanban.exportState().resIds, envIDs); // fold the second group and check that the res_ids it contains are no // longer in the environment envIDs = [1, 3]; await testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last')); await testUtils.dom.click(kanban.$('.o_kanban_group:last .o_kanban_toggle_fold')); assert.deepEqual(kanban.exportState().resIds, envIDs); // re-open the second group and check that the res_ids it contains are // back in the environment envIDs = [1, 3, 2, 4]; await testUtils.dom.click(kanban.$('.o_kanban_group:last')); assert.deepEqual(kanban.exportState().resIds, envIDs); kanban.destroy(); }); QUnit.test('create a column in grouped on m2o', async function (assert) { assert.expect(14); var nbRPCs = 0; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { nbRPCs++; if (args.method === 'name_create') { assert.ok(true, "should call name_create"); } //Create column will call resequence to set column order if (route === '/web/dataset/resequence') { assert.ok(true, "should call resequence"); return Promise.resolve(true); } return this._super(route, args); }, }); assert.containsOnce(kanban, '.o_column_quick_create', "should have a quick create column"); assert.notOk(kanban.$('.o_column_quick_create input').is(':visible'), "the input should not be visible"); await testUtils.dom.click(kanban.$('.o_quick_create_folded')); assert.ok(kanban.$('.o_column_quick_create input').is(':visible'), "the input should be visible"); // discard the column creation and click it again await kanban.$('.o_column_quick_create input').trigger($.Event('keydown', { keyCode: $.ui.keyCode.ESCAPE, which: $.ui.keyCode.ESCAPE, })); assert.notOk(kanban.$('.o_column_quick_create input').is(':visible'), "the input should not be visible after discard"); await testUtils.dom.click(kanban.$('.o_quick_create_folded')); assert.ok(kanban.$('.o_column_quick_create input').is(':visible'), "the input should be visible"); await kanban.$('.o_column_quick_create input').val('new value').trigger('input'); await testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); assert.strictEqual(kanban.$('.o_kanban_group:last span:contains(new value)').length, 1, "the last column should be the newly created one"); assert.ok(_.isNumber(kanban.$('.o_kanban_group:last').data('id')), 'the created column should have the correct id'); assert.doesNotHaveClass(kanban.$('.o_kanban_group:last'), 'o_column_folded', 'the created column should not be folded'); // fold and unfold the created column, and check that no RPC is done (as there is no record) nbRPCs = 0; await testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last')); await testUtils.dom.click(kanban.$('.o_kanban_group:last .o_kanban_toggle_fold')); assert.hasClass(kanban.$('.o_kanban_group:last'),'o_column_folded', 'the created column should now be folded'); await testUtils.dom.click(kanban.$('.o_kanban_group:last')); assert.doesNotHaveClass(kanban.$('.o_kanban_group:last'), 'o_column_folded'); assert.strictEqual(nbRPCs, 0, 'no rpc should have been done when folding/unfolding'); // quick create a record await testUtils.kanban.clickCreate(kanban); assert.hasClass(kanban.$('.o_kanban_group:first() > div:nth(1)'),'o_kanban_quick_create', "clicking on create should open the quick_create in the first column"); kanban.destroy(); }); QUnit.test('auto fold group when reach the limit', async function (assert) { assert.expect(9); var data = this.data; for (var i = 0; i < 12; i++) { data.product.records.push({ id: (8 + i), name: ("column"), }); data.partner.records.push({ id: (20 + i), foo: ("dumb entry"), product_id: (8 + i), }); } var kanban = await createView({ View: KanbanView, model: 'partner', data: data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (args.method === 'web_read_group') { return this._super.apply(this, arguments).then(function (result) { result.groups[2].__fold = true; result.groups[8].__fold = true; return result; }); } return this._super(route, args); }, }); // we look if column are fold/unfold according what is expected assert.doesNotHaveClass(kanban.$('.o_kanban_group:nth-child(2)'), 'o_column_folded'); assert.doesNotHaveClass(kanban.$('.o_kanban_group:nth-child(4)'), 'o_column_folded'); assert.doesNotHaveClass(kanban.$('.o_kanban_group:nth-child(10)'), 'o_column_folded'); assert.hasClass(kanban.$('.o_kanban_group:nth-child(3)'), 'o_column_folded'); assert.hasClass(kanban.$('.o_kanban_group:nth-child(9)'), 'o_column_folded'); // we look if columns are actually fold after we reached the limit assert.hasClass(kanban.$('.o_kanban_group:nth-child(13)'), 'o_column_folded'); assert.hasClass(kanban.$('.o_kanban_group:nth-child(14)'), 'o_column_folded'); // we look if we have the right count of folded/unfolded column assert.containsN(kanban, '.o_kanban_group:not(.o_column_folded)', 10); assert.containsN(kanban, '.o_kanban_group.o_column_folded', 4); kanban.destroy(); }); QUnit.test('hide and display help message (ESC) in kanban quick create', async function (assert) { assert.expect(2); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); await testUtils.dom.click(kanban.$('.o_quick_create_folded')); assert.ok(kanban.$('.o_discard_msg').is(':visible'), 'the ESC to discard message is visible'); // click outside the column (to lose focus) await testUtils.dom.clickFirst(kanban.$('.o_kanban_header')); assert.notOk(kanban.$('.o_discard_msg').is(':visible'), 'the ESC to discard message is no longer visible'); kanban.destroy(); }); QUnit.test('delete a column in grouped on m2o', async function (assert) { assert.expect(37); testUtils.mock.patch(KanbanRenderer, { _renderGrouped: function () { this._super.apply(this, arguments); // set delay and revert animation time to 0 so dummy drag and drop works if (this.$el.sortable('instance')) { this.$el.sortable('option', {delay: 0, revert: 0}); } }, }); var resequencedIDs; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { resequencedIDs = args.ids; assert.strictEqual(_.reject(args.ids, _.isNumber).length, 0, "column resequenced should be existing records with IDs"); return Promise.resolve(true); } if (args.method) { assert.step(args.method); } return this._super(route, args); }, }); // check the initial rendering assert.containsN(kanban, '.o_kanban_group', 2, "should have two columns"); assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 3, 'first column should be [3, "hello"]'); assert.strictEqual(kanban.$('.o_kanban_group:last').data('id'), 5, 'second column should be [5, "xmo"]'); assert.strictEqual(kanban.$('.o_kanban_group:last .o_column_title').text(), 'xmo', 'second column should have correct title'); assert.containsN(kanban, '.o_kanban_group:last .o_kanban_record', 2, "second column should have two records"); // check available actions in kanban header's config dropdown assert.ok(kanban.$('.o_kanban_group:first .o_kanban_toggle_fold').length, "should be able to fold the column"); assert.ok(kanban.$('.o_kanban_group:first .o_column_edit').length, "should be able to edit the column"); assert.ok(kanban.$('.o_kanban_group:first .o_column_delete').length, "should be able to delete the column"); assert.ok(!kanban.$('.o_kanban_group:first .o_column_archive_records').length, "should not be able to archive all the records"); assert.ok(!kanban.$('.o_kanban_group:first .o_column_unarchive_records').length, "should not be able to restore all the records"); // delete second column (first cancel the confirm request, then confirm) testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last')); await testUtils.dom.click(kanban.$('.o_kanban_group:last .o_column_delete')); assert.ok($('.modal').length, 'a confirm modal should be displayed'); await testUtils.modal.clickButton('Cancel'); // click on cancel assert.strictEqual(kanban.$('.o_kanban_group:last').data('id'), 5, 'column [5, "xmo"] should still be there'); testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last')); await testUtils.dom.click(kanban.$('.o_kanban_group:last .o_column_delete')); assert.ok($('.modal').length, 'a confirm modal should be displayed'); await testUtils.modal.clickButton('Ok'); // click on confirm assert.strictEqual(kanban.$('.o_kanban_group:last').data('id'), 3, 'last column should now be [3, "hello"]'); assert.containsN(kanban, '.o_kanban_group', 2, "should still have two columns"); assert.ok(!_.isNumber(kanban.$('.o_kanban_group:first').data('id')), 'first column should have no id (Undefined column)'); // check available actions on 'Undefined' column assert.ok(kanban.$('.o_kanban_group:first .o_kanban_toggle_fold').length, "should be able to fold the column"); assert.ok(!kanban.$('.o_kanban_group:first .o_column_delete').length, 'Undefined column could not be deleted'); assert.ok(!kanban.$('.o_kanban_group:first .o_column_edit').length, 'Undefined column could not be edited'); assert.ok(!kanban.$('.o_kanban_group:first .o_column_archive_records').length, "Records of undefined column could not be archived"); assert.ok(!kanban.$('.o_kanban_group:first .o_column_unarchive_records').length, "Records of undefined column could not be restored"); assert.verifySteps(['web_read_group', 'unlink', 'web_read_group']); assert.strictEqual(kanban.renderer.widgets.length, 2, "the old widgets should have been correctly deleted"); // test column drag and drop having an 'Undefined' column await testUtils.dom.dragAndDrop( kanban.$('.o_column_title:first'), kanban.$('.o_column_title:last'), {position: 'right'} ); assert.strictEqual(resequencedIDs, undefined, "resequencing require at least 2 not Undefined columns"); await testUtils.dom.click(kanban.$('.o_column_quick_create .o_quick_create_folded')); kanban.$('.o_column_quick_create input').val('once third column'); await testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); var newColumnID = kanban.$('.o_kanban_group:last').data('id'); await testUtils.dom.dragAndDrop( kanban.$('.o_column_title:first'), kanban.$('.o_column_title:last'), {position: 'right'} ); assert.deepEqual([3, newColumnID], resequencedIDs, "moving the Undefined column should not affect order of other columns"); await testUtils.dom.dragAndDrop( kanban.$('.o_column_title:first'), kanban.$('.o_column_title:nth(1)'), {position: 'right'} ); await nextTick(); // wait for resequence after drag and drop assert.deepEqual([newColumnID, 3], resequencedIDs, "moved column should be resequenced accordingly"); assert.verifySteps(['name_create', 'read', 'read', 'read']); kanban.destroy(); testUtils.mock.unpatch(KanbanRenderer); }); QUnit.test('create a column, delete it and create another one', async function (assert) { assert.expect(5); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); assert.containsN(kanban, '.o_kanban_group', 2, "should have two columns"); await testUtils.dom.click(kanban.$('.o_column_quick_create .o_quick_create_folded')); kanban.$('.o_column_quick_create input').val('new column 1'); await testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group', 3, "should have two columns"); testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:last')); await testUtils.dom.click(kanban.$('.o_kanban_group:last .o_column_delete')); await testUtils.modal.clickButton('Ok'); assert.containsN(kanban, '.o_kanban_group', 2, "should have twos columns"); await testUtils.dom.click(kanban.$('.o_column_quick_create .o_quick_create_folded')); kanban.$('.o_column_quick_create input').val('new column 2'); await testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); assert.containsN(kanban, '.o_kanban_group', 3, "should have three columns"); assert.strictEqual(kanban.$('.o_kanban_group:last span:contains(new column 2)').length, 1, "the last column should be the newly created one"); kanban.destroy(); }); QUnit.test('edit a column in grouped on m2o', async function (assert) { assert.expect(12); var nbRPCs = 0; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], archs: { 'product,false,form': '
', }, mockRPC: function (route, args) { nbRPCs++; return this._super(route, args); }, }); assert.strictEqual(kanban.$('.o_kanban_group[data-id=5] .o_column_title').text(), 'xmo', 'title of the column should be "xmo"'); // edit the title of column [5, 'xmo'] and close without saving testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group[data-id=5]')); await testUtils.dom.click(kanban.$('.o_kanban_group[data-id=5] .o_column_edit')); assert.containsOnce(document.body, '.modal .o_form_editable', "a form view should be open in a modal"); assert.strictEqual($('.modal .o_form_editable input').val(), 'xmo', 'the name should be "xmo"'); await testUtils.fields.editInput($('.modal .o_form_editable input'), 'ged'); // change the value nbRPCs = 0; await testUtils.dom.click($('.modal-header .btn-close')); assert.containsNone(document.body, '.modal'); assert.strictEqual(kanban.$('.o_kanban_group[data-id=5] .o_column_title').text(), 'xmo', 'title of the column should still be "xmo"'); assert.strictEqual(nbRPCs, 0, 'no RPC should have been done'); // edit the title of column [5, 'xmo'] and discard testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group[data-id=5]')); await testUtils.dom.click(kanban.$('.o_kanban_group[data-id=5] .o_column_edit')); await testUtils.fields.editInput($('.modal .o_form_editable input'), 'ged'); // change the value nbRPCs = 0; await testUtils.modal.clickButton('Discard'); assert.containsNone(document.body, '.modal'); assert.strictEqual(kanban.$('.o_kanban_group[data-id=5] .o_column_title').text(), 'xmo', 'title of the column should still be "xmo"'); assert.strictEqual(nbRPCs, 0, 'no RPC should have been done'); // edit the title of column [5, 'xmo'] and save testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group[data-id=5]')); await testUtils.dom.click(kanban.$('.o_kanban_group[data-id=5] .o_column_edit')); await testUtils.fields.editInput($('.modal .o_form_editable input'), 'ged'); // change the value nbRPCs = 0; await testUtils.modal.clickButton('Save'); // click on save assert.ok(!$('.modal').length, 'the modal should be closed'); assert.strictEqual(kanban.$('.o_kanban_group[data-id=5] .o_column_title').text(), 'ged', 'title of the column should be "ged"'); assert.strictEqual(nbRPCs, 4, 'should have done 1 write, 1 read_group and 2 search_read'); kanban.destroy(); }); QUnit.test('edit a column propagates right context', async function (assert) { assert.expect(4); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], archs: { 'product,false,form': '
', }, session: {user_context: {lang: 'brol'}}, mockRPC: function (route, args) { let context; if (route === '/web/dataset/search_read' && args.model === 'partner') { context = args.context; assert.strictEqual(context.lang, 'brol', 'lang is present in context for partner operations'); } if (args.model === 'product') { context = args.kwargs.context; assert.strictEqual(context.lang, 'brol', 'lang is present in context for product operations'); } return this._super.apply(this, arguments); }, }); testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group[data-id=5]')); await testUtils.dom.click(kanban.$('.o_kanban_group[data-id=5] .o_column_edit')); kanban.destroy(); }); QUnit.test('quick create column should be opened if there is no column', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], domain: [['foo', '=', 'norecord']], }); assert.containsNone(kanban, '.o_kanban_group'); assert.containsOnce(kanban, '.o_column_quick_create'); assert.ok(kanban.$('.o_column_quick_create input').is(':visible'), "the quick create should be opened"); kanban.destroy(); }); QUnit.test('quick create column should not be closed on widnow click if there is no column', async function (assert) { assert.expect(4); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['product_id'], domain: [['foo', '=', 'norecord']], }); assert.containsNone(kanban, '.o_kanban_group'); assert.containsOnce(kanban, '.o_column_quick_create'); assert.ok(kanban.$('.o_column_quick_create input').is(':visible'), "the quick create should be opened"); // click outside should not discard quick create column await testUtils.dom.click(kanban.$('.o_kanban_example_background_container')); assert.ok(kanban.$('.o_column_quick_create input').is(':visible'), "the quick create should still be opened"); kanban.destroy(); }); QUnit.test('quick create several columns in a row', async function (assert) { assert.expect(10); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); assert.containsN(kanban, '.o_kanban_group', 2, "should have two columns"); assert.containsOnce(kanban, '.o_column_quick_create', "should have a ColumnQuickCreate widget"); assert.containsOnce(kanban, '.o_column_quick_create .o_quick_create_folded:visible', "the ColumnQuickCreate should be folded"); assert.containsNone(kanban, '.o_column_quick_create .o_quick_create_unfolded:visible', "the ColumnQuickCreate should be folded"); // add a new column await testUtils.dom.click(kanban.$('.o_column_quick_create .o_quick_create_folded')); assert.containsNone(kanban, '.o_column_quick_create .o_quick_create_folded:visible', "the ColumnQuickCreate should be unfolded"); assert.containsOnce(kanban, '.o_column_quick_create .o_quick_create_unfolded:visible', "the ColumnQuickCreate should be unfolded"); kanban.$('.o_column_quick_create input').val('New Column 1'); await testUtils.dom.click(kanban.$('.o_column_quick_create .btn-primary')); assert.containsN(kanban, '.o_kanban_group', 3, "should now have three columns"); // add another column assert.containsNone(kanban, '.o_column_quick_create .o_quick_create_folded:visible', "the ColumnQuickCreate should still be unfolded"); assert.containsOnce(kanban, '.o_column_quick_create .o_quick_create_unfolded:visible', "the ColumnQuickCreate should still be unfolded"); kanban.$('.o_column_quick_create input').val('New Column 2'); await testUtils.dom.click(kanban.$('.o_column_quick_create .btn-primary')); assert.containsN(kanban, '.o_kanban_group', 4, "should now have four columns"); kanban.destroy(); }); QUnit.test('quick create column and examples', async function (assert) { assert.expect(12); kanbanExamplesRegistry.add('test', { examples:[{ name: "A first example", columns: ["Column 1", "Column 2", "Column 3"], description: "A weak description.", }, { name: "A second example", columns: ["Col 1", "Col 2"], description: Markup`A fantastic description.` }], }); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); assert.containsOnce(kanban, '.o_column_quick_create', "should have a ColumnQuickCreate widget"); // open the quick create await testUtils.dom.click(kanban.$('.o_column_quick_create .o_quick_create_folded')); assert.containsOnce(kanban, '.o_column_quick_create .o_kanban_examples:visible', "should have a link to see examples"); // click to see the examples await testUtils.dom.click(kanban.$('.o_column_quick_create .o_kanban_examples')); assert.strictEqual($('.modal .o_legacy_kanban_examples_dialog').length, 1, "should have open the examples dialog"); assert.strictEqual($('.modal .o_kanban_examples_dialog_nav li').length, 2, "should have two examples (in the menu)"); assert.strictEqual($('.modal .o_kanban_examples_dialog_nav a').text(), ' A first example A second example ', "example names should be correct"); assert.strictEqual($('.modal .o_kanban_examples_dialog_content .tab-pane').length, 2, "should have two examples"); const $panes = $('.modal .o_kanban_examples_dialog_content .tab-pane'); var $firstPane = $panes.eq(0); assert.strictEqual($firstPane.find('.o_kanban_examples_group').length, 3, "there should be 3 stages in the first example"); assert.strictEqual($firstPane.find('h6').text(), 'Column 1Column 2Column 3', "column titles should be correct"); assert.strictEqual($firstPane.find('.o_kanban_examples_description').html().trim(), "A <b>weak</b> description.", "An escaped description should be displayed"); var $secondPane = $panes.eq(1); assert.strictEqual($secondPane.find('.o_kanban_examples_group').length, 2, "there should be 2 stages in the second example"); assert.strictEqual($secondPane.find('h6').text(), 'Col 1Col 2', "column titles should be correct"); assert.strictEqual($secondPane.find('.o_kanban_examples_description').html().trim(), "A fantastic description.", "A formatted description should be displayed."); kanban.destroy(); delete kanbanExamplesRegistry.map['test']; }); QUnit.test("quick create column's apply button's display text", async function (assert) { assert.expect(1); const applyExamplesText = 'Use This For My Test'; kanbanExamplesRegistry.add('test', { applyExamplesText: applyExamplesText, examples:[{ name: "A first example", columns: ["Column 1", "Column 2", "Column 3"], }, { name: "A second example", columns: ["Col 1", "Col 2"], }], }); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); // open the quick create await testUtils.dom.click(kanban.$('.o_column_quick_create .o_quick_create_folded')); // click to see the examples await testUtils.dom.click(kanban.$('.o_column_quick_create .o_kanban_examples')); const $primaryActionButton = $('.modal footer.modal-footer button.btn-primary > span'); assert.strictEqual($primaryActionButton.text(), applyExamplesText, 'the primary button should display the value of applyExamplesText'); kanban.destroy(); }); QUnit.test('quick create column and examples background with ghostColumns titles', async function (assert) { assert.expect(4); this.data.partner.records = []; kanbanExamplesRegistry.add('test', { ghostColumns: ["Ghost 1", "Ghost 2", "Ghost 3", "Ghost 4"], examples:[{ name: "A first example", columns: ["Column 1", "Column 2", "Column 3"], }, { name: "A second example", columns: ["Col 1", "Col 2"], }], }); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); assert.containsOnce(kanban, '.o_kanban_example_background', "should have ExamplesBackground when no data"); assert.strictEqual(kanban.$('.o_kanban_examples_group h6').text(), 'Ghost 1Ghost 2Ghost 3Ghost 4', "ghost title should be correct"); assert.containsOnce(kanban, '.o_column_quick_create', "should have a ColumnQuickCreate widget"); assert.containsOnce(kanban, '.o_column_quick_create .o_kanban_examples:visible', "should not have a link to see examples as there is no examples registered"); kanban.destroy(); }); QUnit.test('quick create column and examples background without ghostColumns titles', async function (assert) { assert.expect(4); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); assert.containsOnce(kanban, '.o_kanban_example_background', "should have ExamplesBackground when no data"); assert.strictEqual(kanban.$('.o_kanban_examples_group h6').text(), 'Column 1Column 2Column 3Column 4', "ghost title should be correct"); assert.containsOnce(kanban, '.o_column_quick_create', "should have a ColumnQuickCreate widget"); assert.containsNone(kanban, '.o_column_quick_create .o_kanban_examples:visible', "should not have a link to see examples as there is no examples registered"); kanban.destroy(); }); QUnit.test('nocontent helper after adding a record (kanban with progressbar)', async function (assert) { assert.expect(3); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['product_id'], domain: [['foo', '=', 'abcd']], mockRPC: function (route, args) { if (args.method === 'web_read_group') { const result = { groups: [ { __domain: [['product_id', '=', 3]], product_id_count: 0, product_id: [3, 'hello'] }, ], }; return Promise.resolve(result); } return this._super.apply(this, arguments); }, viewOptions: { action: { help: "No content helper", }, }, }); assert.containsOnce(kanban, '.o_view_nocontent', "the nocontent helper is displayed"); // add a record await testUtils.dom.click(kanban.$('.o_kanban_quick_add')); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create .o_input'), 'twilight sparkle'); await testUtils.dom.click(kanban.$('button.o_kanban_add')); assert.containsNone(kanban, '.o_view_nocontent', "the nocontent helper is not displayed after quick create"); // cancel quick create await testUtils.dom.click(kanban.$('button.o_kanban_cancel')); assert.containsNone(kanban, '.o_view_nocontent', "the nocontent helper is not displayed after cancelling the quick create"); kanban.destroy(); }); QUnit.test('if view was not grouped at start, it can be grouped and ungrouped', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', }); assert.doesNotHaveClass(kanban.$('.o_legacy_kanban_view'), 'o_kanban_grouped'); await kanban.update({groupBy: ['product_id']}); assert.hasClass(kanban.$('.o_legacy_kanban_view'),'o_kanban_grouped'); await kanban.update({groupBy: []}); assert.doesNotHaveClass(kanban.$('.o_legacy_kanban_view'), 'o_kanban_grouped'); kanban.destroy(); }); QUnit.test('no content helper when archive all records in kanban group', async function (assert) { assert.expect(3); // add active field on partner model to have archive option this.data.partner.fields.active = {string: 'Active', type: 'boolean', default: true}; // remove last records to have only one column this.data.partner.records = this.data.partner.records.slice(0, 3); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, viewOptions: { action: { help: "click to add a partner", } }, groupBy: ['bar'], mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/action_archive') { const partnerIDS = args.args[0]; const records = this.data.partner.records; _.each(partnerIDS, function (partnerID) { _.find(records, function (record) { return record.id === partnerID; }).active = false; }); return Promise.resolve(); } return this._super.apply(this, arguments); }, }); // check that the (unique) column contains 3 records assert.containsN(kanban, '.o_kanban_group:last .o_kanban_record', 3); // archive the records of the last column testUtils.kanban.toggleGroupSettings($(kanban.el.querySelector('.o_kanban_group'))); // we should change the helper await testUtils.dom.click(kanban.el.querySelector('.o_kanban_group .o_column_archive_records')); assert.containsOnce(document.body, '.modal'); await testUtils.modal.clickButton('Ok'); // check no content helper is exist assert.containsOnce(kanban, '.o_view_nocontent'); kanban.destroy(); }); QUnit.test('no content helper when no data', async function (assert) { assert.expect(3); var records = this.data.partner.records; this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '
' + '' + '' + '
' + '
', viewOptions: { action: { help: markup('

click to add a partner

'), } }, }); assert.containsOnce(kanban, '.o_view_nocontent', "should display the no content helper"); assert.strictEqual(kanban.$('.o_view_nocontent p.hello:contains(add a partner)').length, 1, "should have rendered no content helper from action"); this.data.partner.records = records; await kanban.reload(); assert.containsNone(kanban, '.o_view_nocontent', "should not display the no content helper"); kanban.destroy(); }); QUnit.test('no nocontent helper for grouped kanban with empty groups', async function (assert) { assert.expect(2); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (args.method === 'web_read_group') { // override read_group to return empty groups, as this is // the case for several models (e.g. project.task grouped // by stage_id) return this._super.apply(this, arguments).then(function (result) { _.each(result.groups, function (group) { group[args.kwargs.groupby[0] + '_count'] = 0; }); return result; }); } return this._super.apply(this, arguments); }, viewOptions: { action: { help: "No content helper", }, }, }); assert.containsN(kanban, '.o_kanban_group', 2, "there should be two columns"); assert.containsNone(kanban, '.o_kanban_record', "there should be no records"); kanban.destroy(); }); QUnit.test('no nocontent helper for grouped kanban with no records', async function (assert) { assert.expect(4); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['product_id'], viewOptions: { action: { help: "No content helper", }, }, }); assert.containsNone(kanban, '.o_kanban_group', "there should be no columns"); assert.containsNone(kanban, '.o_kanban_record', "there should be no records"); assert.containsNone(kanban, '.o_view_nocontent', "there should be no nocontent helper (we are in 'column creation mode')"); assert.containsOnce(kanban, '.o_column_quick_create', "there should be a column quick create"); kanban.destroy(); }); QUnit.test('no nocontent helper is shown when no longer creating column', async function (assert) { assert.expect(3); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['product_id'], viewOptions: { action: { help: "No content helper", }, }, }); assert.containsNone(kanban, '.o_view_nocontent', "there should be no nocontent helper (we are in 'column creation mode')"); // creating a new column kanban.$('.o_column_quick_create .o_input').val('applejack'); await testUtils.dom.click(kanban.$('.o_column_quick_create .o_kanban_add')); assert.containsNone(kanban, '.o_view_nocontent', "there should be no nocontent helper (still in 'column creation mode')"); // leaving column creation mode kanban.$('.o_column_quick_create .o_input').trigger($.Event('keydown', { keyCode: $.ui.keyCode.ESCAPE, which: $.ui.keyCode.ESCAPE, })); assert.containsOnce(kanban, '.o_view_nocontent', "there should be a nocontent helper"); kanban.destroy(); }); QUnit.test('no nocontent helper is hidden when quick creating a column', async function (assert) { assert.expect(2); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (args.method === 'web_read_group') { var result = { groups: [ {__domain: [['product_id', '=', 3]], product_id_count: 0, product_id: [3, 'hello']}, ], length: 1, }; return Promise.resolve(result); } return this._super.apply(this, arguments); }, viewOptions: { action: { help: "No content helper", }, }, }); assert.containsOnce(kanban, '.o_view_nocontent', "there should be a nocontent helper"); await testUtils.dom.click(kanban.$('.o_kanban_add_column')); assert.containsNone(kanban, '.o_view_nocontent', "there should be no nocontent helper (we are in 'column creation mode')"); kanban.destroy(); }); QUnit.test('remove nocontent helper after adding a record', async function (assert) { assert.expect(2); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (args.method === 'web_read_group') { var result = { groups: [ {__domain: [['product_id', '=', 3]], product_id_count: 0, product_id: [3, 'hello']}, ], length: 1, }; return Promise.resolve(result); } return this._super.apply(this, arguments); }, viewOptions: { action: { help: "No content helper", }, }, }); assert.containsOnce(kanban, '.o_view_nocontent', "there should be a nocontent helper"); // add a record await testUtils.dom.click(kanban.$('.o_kanban_quick_add')); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create .o_input'), 'twilight sparkle'); await testUtils.dom.click(kanban.$('.o_kanban_quick_create button.o_kanban_add')); assert.containsNone(kanban, '.o_view_nocontent', "there should be no nocontent helper (there is now one record)"); kanban.destroy(); }); QUnit.test('remove nocontent helper when adding a record', async function (assert) { assert.expect(2); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (args.method === 'web_read_group') { var result = { groups: [ {__domain: [['product_id', '=', 3]], product_id_count: 0, product_id: [3, 'hello']}, ], length: 1, }; return Promise.resolve(result); } return this._super.apply(this, arguments); }, viewOptions: { action: { help: "No content helper", }, }, }); assert.containsOnce(kanban, '.o_view_nocontent', "there should be a nocontent helper"); // add a record await testUtils.dom.click(kanban.$('.o_kanban_quick_add')); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create .o_input'), 'twilight sparkle'); assert.containsNone(kanban, '.o_view_nocontent', "there should be no nocontent helper (there is now one record)"); kanban.destroy(); }); QUnit.test('nocontent helper is displayed again after canceling quick create', async function (assert) { assert.expect(1); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (args.method === 'web_read_group') { var result = { groups: [ {__domain: [['product_id', '=', 3]], product_id_count: 0, product_id: [3, 'hello']}, ], length: 1, }; return Promise.resolve(result); } return this._super.apply(this, arguments); }, viewOptions: { action: { help: "No content helper", }, }, }); // add a record await testUtils.dom.click(kanban.$('.o_kanban_quick_add')); await testUtils.dom.click(kanban.$('.o_legacy_kanban_view')); assert.containsOnce(kanban, '.o_view_nocontent', "there should be again a nocontent helper"); kanban.destroy(); }); QUnit.test('nocontent helper for grouped kanban with no records with no group_create', async function (assert) { assert.expect(4); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['product_id'], viewOptions: { action: { help: "No content helper", }, }, }); assert.containsNone(kanban, '.o_kanban_group', "there should be no columns"); assert.containsNone(kanban, '.o_kanban_record', "there should be no records"); assert.containsNone(kanban, '.o_view_nocontent', "there should not be a nocontent helper"); assert.containsNone(kanban, '.o_column_quick_create', "there should not be a column quick create"); kanban.destroy(); }); QUnit.test('empty grouped kanban with sample data and no columns', async function (assert) { assert.expect(3); this.data.partner.records = []; const kanban = await createView({ arch: `
`, data: this.data, groupBy: ['product_id'], model: 'partner', View: KanbanView, viewOptions: { action: { help: "No content helper", }, }, }); assert.containsNone(kanban, '.o_view_nocontent'); assert.containsOnce(kanban, '.o_quick_create_unfolded'); assert.containsOnce(kanban, '.o_kanban_example_background_container'); kanban.destroy(); }); QUnit.test('empty grouped kanban with sample data and click quick create', async function (assert) { assert.expect(11); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['product_id'], async mockRPC(route, { kwargs, method }) { const result = await this._super(...arguments); if (method === 'web_read_group') { // override read_group to return empty groups, as this is // the case for several models (e.g. project.task grouped // by stage_id) result.groups.forEach(group => { group[`${kwargs.groupby[0]}_count`] = 0; }); } return result; }, viewOptions: { action: { help: "No content helper", }, }, }); assert.containsN(kanban, '.o_kanban_group', 2, "there should be two columns"); assert.hasClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsOnce(kanban, '.o_view_nocontent'); assert.containsN(kanban, '.o_kanban_record', 16, "there should be 8 sample records by column"); await testUtils.dom.click(kanban.$('.o_kanban_quick_add:first')); assert.doesNotHaveClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsNone(kanban, '.o_kanban_record'); assert.containsNone(kanban, '.o_view_nocontent'); assert.containsOnce(kanban.$('.o_kanban_group:first'), '.o_kanban_quick_create'); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create .o_input'), 'twilight sparkle'); await testUtils.dom.click(kanban.$('.o_kanban_quick_create button.o_kanban_add')); assert.doesNotHaveClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsOnce(kanban.$('.o_kanban_group:first'), '.o_kanban_record'); assert.containsNone(kanban, '.o_view_nocontent'); kanban.destroy(); }); QUnit.test('empty grouped kanban with sample data and cancel quick create', async function (assert) { assert.expect(12); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['product_id'], async mockRPC(route, { kwargs, method }) { const result = await this._super(...arguments); if (method === 'web_read_group') { // override read_group to return empty groups, as this is // the case for several models (e.g. project.task grouped // by stage_id) result.groups.forEach(group => { group[`${kwargs.groupby[0]}_count`] = 0; }); } return result; }, viewOptions: { action: { help: "No content helper", }, }, }); assert.containsN(kanban, '.o_kanban_group', 2, "there should be two columns"); assert.hasClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsOnce(kanban, '.o_view_nocontent'); assert.containsN(kanban, '.o_kanban_record', 16, "there should be 8 sample records by column"); await testUtils.dom.click(kanban.$('.o_kanban_quick_add:first')); assert.doesNotHaveClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsNone(kanban, '.o_kanban_record'); assert.containsNone(kanban, '.o_view_nocontent'); assert.containsOnce(kanban.$('.o_kanban_group:first'), '.o_kanban_quick_create'); await testUtils.dom.click(kanban.$('.o_legacy_kanban_view')); assert.doesNotHaveClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsNone(kanban, '.o_kanban_quick_create'); assert.containsNone(kanban, '.o_kanban_record'); assert.containsOnce(kanban, '.o_view_nocontent'); kanban.destroy(); }); QUnit.test('empty grouped kanban with sample data: keyboard navigation', async function (assert) { assert.expect(5); const kanban = await createView({ arch: `
`, data: this.data, groupBy: ['product_id'], model: 'partner', View: KanbanView, async mockRPC(route, { kwargs, method }) { const result = await this._super(...arguments); if (method === 'web_read_group') { result.groups.forEach(g => g.product_id_count = 0); } return result; }, }); // Check keynav is disabled assert.hasClass( kanban.el.querySelector('.o_kanban_record'), 'o_sample_data_disabled' ); assert.hasClass( kanban.el.querySelector('.o_kanban_toggle_fold'), 'o_sample_data_disabled' ); assert.containsNone(kanban.renderer, '[tabindex]:not([tabindex="-1"])'); assert.hasClass(document.activeElement, 'o_searchview_input'); await testUtils.fields.triggerKeydown(document.activeElement, 'down'); assert.hasClass(document.activeElement, 'o_searchview_input'); kanban.destroy(); }); QUnit.test('empty kanban with sample data', async function (assert) { assert.expect(6); this.data.partner.records = []; const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, viewOptions: { action: { help: "No content helper", }, }, }); assert.hasClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 10, "there should be 10 sample records"); assert.containsOnce(kanban, '.o_view_nocontent'); await kanban.reload({ domain: [['id', '<', 0]]}); assert.doesNotHaveClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsNone(kanban, '.o_kanban_record:not(.o_kanban_ghost)'); assert.containsOnce(kanban, '.o_view_nocontent'); kanban.destroy(); }); QUnit.test('empty grouped kanban with sample data and many2many_tags', async function (assert) { assert.expect(6); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['product_id'], async mockRPC(route, { kwargs, method }) { assert.step(method || route); const result = await this._super(...arguments); if (method === 'web_read_group') { // override read_group to return empty groups, as this is // the case for several models (e.g. project.task grouped // by stage_id) result.groups.forEach(group => { group[`${kwargs.groupby[0]}_count`] = 0; }); } return result; }, }); assert.containsN(kanban, '.o_kanban_group', 2, "there should be 2 'real' columns"); assert.hasClass(kanban.$el, 'o_legacy_view_sample_data'); assert.ok(kanban.$('.o_kanban_record').length >= 1, "there should be sample records"); assert.ok(kanban.$('.o_field_many2manytags .o_tag').length >= 1, "there should be tags"); assert.verifySteps(["web_read_group"], "should not read the tags"); kanban.destroy(); }); QUnit.test('sample data does not change after reload with sample data', async function (assert) { assert.expect(4); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['product_id'], async mockRPC(route, { kwargs, method }) { const result = await this._super(...arguments); if (method === 'web_read_group') { // override read_group to return empty groups, as this is // the case for several models (e.g. project.task grouped // by stage_id) result.groups.forEach(group => { group[`${kwargs.groupby[0]}_count`] = 0; }); } return result; }, }); const columns = kanban.el.querySelectorAll('.o_kanban_group'); assert.ok(columns.length >= 1, "there should be at least 1 sample column"); assert.hasClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsN(kanban, '.o_kanban_record', 16); const kanbanText = kanban.el.innerText; await kanban.reload(); assert.strictEqual(kanbanText, kanban.el.innerText, "the content should be the same after reloading the view"); kanban.destroy(); }); QUnit.test('non empty kanban with sample data', async function (assert) { assert.expect(5); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, viewOptions: { action: { help: "No content helper", }, }, }); assert.doesNotHaveClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 4); assert.containsNone(kanban, '.o_view_nocontent'); await kanban.reload({ domain: [['id', '<', 0]]}); assert.doesNotHaveClass(kanban.$el, 'o_legacy_view_sample_data'); assert.containsNone(kanban, '.o_kanban_record:not(.o_kanban_ghost)'); kanban.destroy(); }); QUnit.test('empty grouped kanban with sample data: add a column', async function (assert) { assert.expect(6); const kanban = await createView({ arch: `
`, data: this.data, groupBy: ['product_id'], model: 'partner', View: KanbanView, async mockRPC(route, { method }) { const result = await this._super(...arguments); if (method === 'web_read_group') { result.groups = this.data.product.records.map(r => { return { product_id: [r.id, r.display_name], product_id_count: 0, __domain: ['product_id', '=', r.id], // This domain shouldn't be valid. }; }); result.length = result.groups.length; } return result; }, }); assert.hasClass(kanban, 'o_legacy_view_sample_data'); assert.containsN(kanban, '.o_kanban_group', 2); assert.ok(kanban.$('.o_kanban_record').length > 0, 'should contain sample records'); await testUtils.dom.click(kanban.el.querySelector('.o_kanban_add_column')); await testUtils.fields.editInput(kanban.el.querySelector('.o_kanban_header input'), "Yoohoo"); await testUtils.dom.click(kanban.el.querySelector('.btn.o_kanban_add')); assert.hasClass(kanban, 'o_legacy_view_sample_data'); assert.containsN(kanban, '.o_kanban_group', 3); assert.ok(kanban.$('.o_kanban_record').length > 0, 'should contain sample records'); kanban.destroy(); }); QUnit.test('empty grouped kanban with sample data: cannot fold a column', async function (assert) { // folding a column in grouped kanban with sample data is disabled, for the sake of simplicity assert.expect(5); const kanban = await createView({ arch: `
`, data: this.data, groupBy: ['product_id'], model: 'partner', View: KanbanView, async mockRPC(route, { kwargs, method }) { const result = await this._super(...arguments); if (method === 'web_read_group') { // override read_group to return a single, empty group result.groups = result.groups.slice(0, 1); result.groups[0][`${kwargs.groupby[0]}_count`] = 0; result.length = 1; } return result; }, }); assert.hasClass(kanban, 'o_legacy_view_sample_data'); assert.containsOnce(kanban, '.o_kanban_group'); assert.ok(kanban.$('.o_kanban_record').length > 0, 'should contain sample records'); await testUtils.dom.click(kanban.el.querySelector('.o_kanban_config > a')); assert.hasClass(kanban.el.querySelector('.o_kanban_config .o_kanban_toggle_fold'), 'o_sample_data_disabled'); assert.hasClass(kanban.el.querySelector('.o_kanban_config .o_kanban_toggle_fold'), 'disabled'); kanban.destroy(); }); QUnit.skip('empty grouped kanban with sample data: fold/unfold a column', async function (assert) { // folding/unfolding of grouped kanban with sample data is currently disabled assert.expect(8); const kanban = await createView({ arch: `
`, data: this.data, groupBy: ['product_id'], model: 'partner', View: KanbanView, async mockRPC(route, { kwargs, method }) { const result = await this._super(...arguments); if (method === 'web_read_group') { // override read_group to return a single, empty group result.groups = result.groups.slice(0, 1); result.groups[0][`${kwargs.groupby[0]}_count`] = 0; result.length = 1; } return result; }, }); assert.hasClass(kanban, 'o_legacy_view_sample_data'); assert.containsOnce(kanban, '.o_kanban_group'); assert.ok(kanban.$('.o_kanban_record').length > 0, 'should contain sample records'); // Fold the column await testUtils.dom.click(kanban.el.querySelector('.o_kanban_config > a')); await testUtils.dom.click(kanban.el.querySelector('.dropdown-item.o_kanban_toggle_fold')); assert.containsOnce(kanban, '.o_kanban_group'); assert.hasClass(kanban.$('.o_kanban_group'), 'o_column_folded'); // Unfold the column await testUtils.dom.click(kanban.el.querySelector('.o_kanban_group.o_column_folded')); assert.containsOnce(kanban, '.o_kanban_group'); assert.doesNotHaveClass(kanban.$('.o_kanban_group'), 'o_column_folded'); assert.ok(kanban.$('.o_kanban_record').length > 0, 'should contain sample records'); kanban.destroy(); }); QUnit.test('empty grouped kanban with sample data: delete a column', async function (assert) { assert.expect(5); this.data.partner.records = []; let groups = [{ product_id: [1, 'New'], product_id_count: 0, __domain: [], }]; const kanban = await createView({ arch: `
`, data: this.data, groupBy: ['product_id'], model: 'partner', View: KanbanView, async mockRPC(route, { method }) { let result = await this._super(...arguments); if (method === 'web_read_group') { // override read_group to return a single, empty group return { groups, length: groups.length, }; } return result; }, }); assert.hasClass(kanban, 'o_legacy_view_sample_data'); assert.containsOnce(kanban, '.o_kanban_group'); assert.ok(kanban.$('.o_kanban_record').length > 0, 'should contain sample records'); // Delete the first column groups = []; await testUtils.dom.click(kanban.el.querySelector('.o_kanban_config > a')); await testUtils.dom.click(kanban.el.querySelector('.dropdown-item.o_column_delete')); await testUtils.dom.click(document.querySelector('.modal .btn-primary')); assert.containsNone(kanban, '.o_kanban_group'); assert.containsOnce(kanban, '.o_column_quick_create .o_quick_create_unfolded'); kanban.destroy(); }); QUnit.test('empty grouped kanban with sample data: add a column and delete it right away', async function (assert) { assert.expect(9); const kanban = await createView({ arch: `
`, data: this.data, groupBy: ['product_id'], model: 'partner', View: KanbanView, async mockRPC(route, { method }) { const result = await this._super(...arguments); if (method === 'web_read_group') { result.groups = this.data.product.records.map(r => { return { product_id: [r.id, r.display_name], product_id_count: 0, __domain: ['product_id', '=', r.id], // This domain shouldn't be valid. }; }); result.length = result.groups.length; } return result; }, }); assert.hasClass(kanban, 'o_legacy_view_sample_data'); assert.containsN(kanban, '.o_kanban_group', 2); assert.ok(kanban.$('.o_kanban_record').length > 0, 'should contain sample records'); // add a new column await testUtils.dom.click(kanban.el.querySelector('.o_kanban_add_column')); await testUtils.fields.editInput(kanban.el.querySelector('.o_kanban_header input'), "Yoohoo"); await testUtils.dom.click(kanban.el.querySelector('.btn.o_kanban_add')); assert.hasClass(kanban, 'o_legacy_view_sample_data'); assert.containsN(kanban, '.o_kanban_group', 3); assert.ok(kanban.$('.o_kanban_record').length > 0, 'should contain sample records'); // delete the column we just created const newColumn = kanban.el.querySelectorAll('.o_kanban_group')[2]; await testUtils.dom.click(newColumn.querySelector('.o_kanban_config > a')); await testUtils.dom.click(newColumn.querySelector('.dropdown-item.o_column_delete')); await testUtils.dom.click(document.querySelector('.modal .btn-primary')); assert.hasClass(kanban, 'o_legacy_view_sample_data'); assert.containsN(kanban, '.o_kanban_group', 2); assert.ok(kanban.$('.o_kanban_record').length > 0, 'should contain sample records'); kanban.destroy(); }); QUnit.test('bounce create button when no data and click on empty area', async function (assert) { assert.expect(2); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, viewOptions: { action: { help: "click to add a partner", } }, }); await testUtils.dom.click(kanban.$('.o_legacy_kanban_view')); assert.doesNotHaveClass(kanban.$('.o-kanban-button-new'), 'o_catch_attention'); await kanban.reload({ domain: [['id', '<', 0]] }); await testUtils.dom.click(kanban.$('.o_legacy_kanban_view')); assert.hasClass(kanban.$('.o-kanban-button-new'), 'o_catch_attention'); kanban.destroy(); }); QUnit.test('buttons with modifiers', async function (assert) { assert.expect(2); this.data.partner.records[1].bar = false; // so that test is more complete var kanban = await createView({ View: KanbanView, model: "partner", data: this.data, arch: '' + '' + '' + '' + '
' + '
' + '
', }); assert.containsOnce(kanban, ".o_btn_test_1", "kanban should have one buttons of type 1"); assert.containsN(kanban, ".o_btn_test_2", 3, "kanban should have three buttons of type 2"); kanban.destroy(); }); QUnit.test('button executes action and reloads', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: "partner", data: this.data, arch: '' + '
' + '' + '
' + '
', mockRPC: function (route) { assert.step(route); return this._super.apply(this, arguments); }, }); assert.ok(kanban.$('button[data-name="a1"]').length, "kanban should have at least one button a1"); var count = 0; testUtils.mock.intercept(kanban, 'execute_action', function (event) { count++; event.data.on_closed(); }); testUtils.dom.click($('button[data-name="a1"]').first()); await new Promise(r => setTimeout(r)); assert.strictEqual(count, 1, "should have triggered a execute action"); testUtils.dom.click($('button[data-name="a1"]').first()); await new Promise(r => setTimeout(r)); assert.strictEqual(count, 1, "double-click on kanban actions should be debounced"); assert.verifySteps([ '/web/dataset/search_read', '/web/dataset/call_kw/partner/read' ], 'a read should be done after the call button to reload the record'); kanban.destroy(); }); QUnit.test('button executes action and check domain', async function (assert) { assert.expect(2); var data = this.data; data.partner.fields.active = {string: "Active", type: "boolean", default: true}; for (var k in this.data.partner.records) { data.partner.records[k].active = true; } var kanban = await createView({ View: KanbanView, model: "partner", data: data, arch: '' + '
' + '' + '' + '
' + '
', }); testUtils.mock.intercept(kanban, 'execute_action', function (event) { data.partner.records[0].active = false; event.data.on_closed(); }); assert.strictEqual(kanban.$('.o_kanban_record:contains(yop)').length, 1, "should display 'yop' record"); await testUtils.dom.click(kanban.$('.o_kanban_record:contains(yop) button[data-name="toggle_active"]')); assert.strictEqual(kanban.$('.o_kanban_record:contains(yop)').length, 0, "should remove 'yop' record from the view"); kanban.destroy(); }); QUnit.test('button executes action with domain field not in view', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: "partner", data: this.data, domain: [['bar', '=', true]], arch: '' + '
' + '' + '
' + '
', }); testUtils.mock.intercept(kanban, 'execute_action', function (event) { event.data.on_closed(); }); try { await testUtils.dom.click(kanban.$('.o_kanban_record:contains(yop) button[data-name="toggle_action"]')); assert.strictEqual(true, true, 'Everything went fine'); } catch (_e) { assert.strictEqual(true, false, 'Error triggered at action execution'); } kanban.destroy(); }); QUnit.test('action/type attributes on kanban arch, type="object"', async function (assert) { assert.expect(6) var kanban = await createView({ View: KanbanView, model: "partner", data: this.data, arch: '' + '
' + '

some value

' + '
' + '
', mockRPC: function (route) { assert.step(route); return this._super.apply(this, arguments); }, }); var count = 0; testUtils.mock.intercept(kanban, 'execute_action', function (event) { count++; assert.strictEqual(event.data.action_data.type, "object"); assert.strictEqual(event.data.action_data.name, "a1"); event.data.on_closed(); }); testUtils.dom.click(kanban.$('p').first()); await new Promise(r => setTimeout(r)); assert.strictEqual(count, 1, "should have triggered a execute action"); assert.verifySteps([ '/web/dataset/search_read', '/web/dataset/call_kw/partner/read' ], 'a read should be done after the call button to reload the record'); kanban.destroy(); }); QUnit.test('action/type attributes on kanban arch, type="action"', async function (assert) { assert.expect(6); var kanban = await createView({ View: KanbanView, model: "partner", data: this.data, arch: '' + '
' + '

some value

' + '
' + '
', mockRPC: function (route) { assert.step(route); return this._super.apply(this, arguments); }, }); var count = 0; testUtils.mock.intercept(kanban, 'execute_action', function (event) { count++; assert.strictEqual(event.data.action_data.type, "action"); assert.strictEqual(event.data.action_data.name, "a1"); event.data.on_closed(); }); testUtils.dom.click(kanban.$('p').first()); await new Promise(r => setTimeout(r)); assert.strictEqual(count, 1, "should have triggered a execute action"); assert.verifySteps([ '/web/dataset/search_read', '/web/dataset/call_kw/partner/read' ], 'a read should be done after the call button to reload the record'); kanban.destroy(); }); QUnit.test('rendering date and datetime', async function (assert) { assert.expect(2); this.data.partner.records[0].date = "2017-01-25"; this.data.partner.records[1].datetime= "2016-12-12 10:55:05"; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '
' + '' + '' + '
' + '
' + '
', }); // FIXME: this test is locale dependant. we need to do it right. assert.strictEqual(kanban.$('div.o_kanban_record:contains(Wed Jan 25)').length, 1, "should have formatted the date"); assert.strictEqual(kanban.$('div.o_kanban_record:contains(Mon Dec 12)').length, 1, "should have formatted the datetime"); kanban.destroy(); }); QUnit.test('evaluate conditions on relational fields', async function (assert) { assert.expect(3); this.data.partner.records[0].product_id = false; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '
' + '' + '' + '
' + '
' + '
', }); assert.strictEqual($('.o_kanban_record:not(.o_kanban_ghost)').length, 4, "there should be 4 records"); assert.strictEqual($('.o_kanban_record:not(.o_kanban_ghost) .btn_a').length, 1, "only 1 of them should have the 'Action' button"); assert.strictEqual($('.o_kanban_record:not(.o_kanban_ghost) .btn_b').length, 2, "only 2 of them should have the 'Action' button"); kanban.destroy(); }); QUnit.test('resequence columns in grouped by m2o', async function (assert) { assert.expect(6); this.data.product.fields.sequence = {string: "Sequence", type: "integer"}; var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); assert.hasClass(kanban.$('.o_legacy_kanban_view'),'ui-sortable', "columns should be sortable"); assert.containsN(kanban, '.o_kanban_group', 2, "should have two columns"); assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 3, "first column should be id 3 before resequencing"); assert.deepEqual(kanban.exportState().resIds, envIDs); // there is a 100ms delay on the d&d feature (jquery sortable) for // kanban columns, making it hard to test. So we rather bypass the d&d // for this test, and directly call the event handler envIDs = [2, 4, 1, 3]; // the columns will be inverted kanban._onResequenceColumn({data: {ids: [5, 3]}}); await nextTick(); // wait for resequencing before re-rendering await kanban.update({}, {reload: false}); // re-render without reloading assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 5, "first column should be id 5 after resequencing"); assert.deepEqual(kanban.exportState().resIds, envIDs); kanban.destroy(); }); QUnit.test('properly evaluate more complex domains', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '' + '' + '
' + '' + '' + '
' + '
' + '
' + '
', }); assert.containsOnce(kanban, 'button.oe_kanban_action_button', "only one button should be visible"); kanban.destroy(); }); QUnit.test('edit the kanban color with the colorpicker', async function (assert) { assert.expect(5); var writeOnColor; this.data.category.records[0].color = 12; var kanban = await createView({ View: KanbanView, model: 'category', data: this.data, arch: '' + '' + '' + '' + '
' + '' + '' + '
' + '
' + '
' + '
', mockRPC: function (route, args) { if (args.method === 'write' && 'color' in args.args[1]) { writeOnColor = true; } return this._super.apply(this, arguments); }, }); var $firstRecord = kanban.$('.o_kanban_record:first()'); assert.containsNone(kanban, '.o_kanban_record.oe_kanban_color_12', "no record should have the color 12"); assert.strictEqual($firstRecord.find('.oe_kanban_colorpicker').length, 1, "there should be a color picker"); assert.strictEqual($firstRecord.find('.oe_kanban_colorpicker').children().length, 12, "the color picker should have 12 children (the colors)"); // Set a color testUtils.kanban.toggleRecordDropdown($firstRecord); await testUtils.dom.click($firstRecord.find('.oe_kanban_colorpicker a.oe_kanban_color_9')); assert.ok(writeOnColor, "should write on the color field"); $firstRecord = kanban.$('.o_kanban_record:first()'); // First record is reloaded here assert.ok($firstRecord.is('.oe_kanban_color_9'), "the first record should have the color 9"); kanban.destroy(); }); QUnit.test('load more records in column', async function (assert) { assert.expect(13); var envIDs = [1, 2, 4]; // the ids that should be in the environment during this test var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
' + '
', groupBy: ['bar'], viewOptions: { limit: 2, }, mockRPC: function (route, args) { if (route === '/web/dataset/search_read') { assert.step(args.limit + ' - ' + args.offset); } return this._super.apply(this, arguments); }, }); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 2, "there should be 2 records in the column"); assert.deepEqual(kanban.exportState().resIds, envIDs); // load more envIDs = [1, 2, 3, 4]; // id 3 will be loaded await testUtils.dom.click(kanban.$('.o_kanban_group:eq(1)').find('.o_kanban_load_more')); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 3, "there should now be 3 records in the column"); assert.verifySteps(['2 - undefined', '2 - undefined', '2 - 2'], "the records should be correctly fetched"); assert.deepEqual(kanban.exportState().resIds, envIDs); // reload await kanban.reload(); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 3, "there should still be 3 records in the column after reload"); assert.deepEqual(kanban.exportState().resIds, envIDs); assert.verifySteps(['4 - undefined', '2 - undefined']); kanban.destroy(); }); QUnit.test('load more records in column with x2many', async function (assert) { assert.expect(10); this.data.partner.records[0].category_ids = [7]; this.data.partner.records[1].category_ids = []; this.data.partner.records[2].category_ids = [6]; this.data.partner.records[3].category_ids = []; // record [2] will be loaded after var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '' + '' + '
' + '
' + '
', groupBy: ['bar'], viewOptions: { limit: 2, }, mockRPC: function (route, args) { if (args.model === 'category' && args.method === 'read') { assert.step(String(args.args[0])); } if (route === '/web/dataset/search_read') { if (args.limit) { assert.strictEqual(args.limit, 2, "the limit should be correctly set"); } if (args.offset) { assert.strictEqual(args.offset, 2, "the offset should be correctly set at load more"); } } return this._super.apply(this, arguments); }, }); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 2, "there should be 2 records in the column"); assert.verifySteps(['7'], "only the appearing category should be fetched"); // load more await testUtils.dom.click(kanban.$('.o_kanban_group:eq(1)').find('.o_kanban_load_more')); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 3, "there should now be 3 records in the column"); assert.verifySteps(['6'], "the other categories should not be fetched"); kanban.destroy(); }); QUnit.test('update buttons after column creation', async function (assert) { assert.expect(2); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '
', groupBy: ['product_id'], }); assert.isNotVisible(kanban.$buttons.find('.o-kanban-button-new'), "Create button should be hidden"); await testUtils.dom.click(kanban.$('.o_column_quick_create')); kanban.$('.o_column_quick_create input').val('new column'); await testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); assert.isVisible(kanban.$buttons.find('.o-kanban-button-new'), "Create button should now be visible"); kanban.destroy(); }); QUnit.test('group_by_tooltip option when grouping on a many2one', async function (assert) { assert.expect(12); delete this.data.partner.records[3].product_id; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '
' + '
', mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/product/read') { assert.strictEqual(args.args[0].length, 2, "read on two groups"); assert.deepEqual(args.args[1], ['display_name', 'name'], "should read on specified fields on the group by relation"); } return this._super.apply(this, arguments); }, }); assert.hasClass(kanban.$('.o_legacy_kanban_view'),'o_kanban_grouped', "should have classname 'o_kanban_grouped'"); assert.containsN(kanban, '.o_kanban_group', 2, "should have " + 2 + " columns"); // simulate an update coming from the searchview, with another groupby given await kanban.update({groupBy: ['product_id']}); assert.containsN(kanban, '.o_kanban_group', 3, "should have " + 3 + " columns"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1, "column should contain 1 record(s)"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 2, "column should contain 2 record(s)"); assert.strictEqual(kanban.$('.o_kanban_group:nth-child(3) .o_kanban_record').length, 1, "column should contain 1 record(s)"); assert.ok(kanban.$('.o_kanban_group:first span.o_column_title:contains(None)').length, "first column should have a default title for when no value is provided"); assert.ok(!kanban.$('.o_kanban_group:first .o_kanban_header_title').data('original-title'), "tooltip of first column should not defined, since group_by_tooltip title and the many2one field has no value"); assert.ok(kanban.$('.o_kanban_group:eq(1) span.o_column_title:contains(hello)').length, "second column should have a title with a value from the many2one"); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_header_title').data('bs-original-title'), "
Kikou
hello
", "second column should have a tooltip with the group_by_tooltip title and many2one field value"); kanban.destroy(); }); QUnit.test('move a record then put it again in the same column', async function (assert) { assert.expect(6); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['product_id'], }); await testUtils.dom.click(kanban.$('.o_column_quick_create')); kanban.$('.o_column_quick_create input').val('column1'); await testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); await testUtils.dom.click(kanban.$('.o_column_quick_create')); kanban.$('.o_column_quick_create input').val('column2'); await testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); await testUtils.dom.click(kanban.$('.o_kanban_group:eq(1) .o_kanban_quick_add i')); var $quickCreate = kanban.$('.o_kanban_group:eq(1) .o_kanban_quick_create'); await testUtils.fields.editInput($quickCreate.find('input'), 'new partner'); await testUtils.dom.click($quickCreate.find('button.o_kanban_add')); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 0, "column should contain 0 record"); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 1, "column should contain 1 records"); var $record = kanban.$('.o_kanban_group:eq(1) .o_kanban_record:eq(0)'); var $group = kanban.$('.o_kanban_group:eq(0)'); await testUtils.dom.dragAndDrop($record, $group); await nextTick(); // wait for resequencing after drag and drop assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 1, "column should contain 1 records"); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 0, "column should contain 0 records"); $record = kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)'); $group = kanban.$('.o_kanban_group:eq(1)'); await testUtils.dom.dragAndDrop($record, $group); await nextTick(); // wait for resequencing after drag and drop assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 0, "column should contain 0 records"); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 1, "column should contain 1 records"); kanban.destroy(); }); QUnit.test('resequence a record twice', async function (assert) { assert.expect(10); this.data.partner.records = []; var nbResequence = 0; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route) { if (route === '/web/dataset/resequence') { nbResequence++; return Promise.resolve(); } return this._super.apply(this, arguments); }, }); await testUtils.dom.click(kanban.$('.o_column_quick_create')); kanban.$('.o_column_quick_create input').val('column1'); await testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); await testUtils.dom.click(kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_add i')); var $quickCreate = kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_create'); await testUtils.fields.editInput($quickCreate.find('input'), 'record1'); await testUtils.dom.click($quickCreate.find('button.o_kanban_add')); await testUtils.dom.click(kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_add i')); $quickCreate = kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_create'); await testUtils.fields.editInput($quickCreate.find('input'), 'record2'); await testUtils.dom.click($quickCreate.find('button.o_kanban_add')); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 2, "column should contain 2 records"); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)').text(), "record2", "records should be correctly ordered"); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(1)').text(), "record1", "records should be correctly ordered"); var $record1 = kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(1)'); var $record2 = kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)'); await testUtils.dom.dragAndDrop($record1, $record2, {position: 'top'}); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 2, "column should contain 2 records"); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)').text(), "record1", "records should be correctly ordered"); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(1)').text(), "record2", "records should be correctly ordered"); await testUtils.dom.dragAndDrop($record2, $record1, {position: 'top'}); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 2, "column should contain 2 records"); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)').text(), "record2", "records should be correctly ordered"); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(1)').text(), "record1", "records should be correctly ordered"); assert.strictEqual(nbResequence, 2, "should have resequenced twice"); kanban.destroy(); }); QUnit.test('basic support for widgets', async function (assert) { // This test could be removed as soon as we drop the support of legacy widgets (see test // below, which is a duplicate of this one, but with an Owl Component instead). assert.expect(1); var MyWidget = Widget.extend({ init: function (parent, dataPoint) { this.data = dataPoint.data; }, start: function () { this.$el.text(JSON.stringify(this.data)); }, }); widgetRegistry.add('test', MyWidget); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '
' + '' + '' + '' + '
' + '
', }); assert.strictEqual(kanban.$('.o_widget:eq(2)').text(), '{"foo":"gnap","id":3}', "widget should have been instantiated"); kanban.destroy(); delete widgetRegistry.map.test; }); QUnit.test('basic support for widgets (being Owl Components)', async function (assert) { assert.expect(1); class MyComponent extends LegacyComponent { get value() { return JSON.stringify(this.props.record.data); } } MyComponent.template = xml`
`; widgetRegistryOwl.add('test', MyComponent); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, }); assert.strictEqual(kanban.$('.o_widget:eq(2)').text(), '{"foo":"gnap","id":3}'); kanban.destroy(); delete widgetRegistryOwl.map.test; }); QUnit.test('subwidgets with on_attach_callback when changing record color', async function (assert) { assert.expect(3); var counter = 0; var MyTestWidget = AbstractField.extend({ on_attach_callback: function () { counter++; }, }); fieldRegistry.add('test_widget', MyTestWidget); var kanban = await createView({ View: KanbanView, model: 'category', data: this.data, arch: '' + '' + '' + '' + '
' + '' + '' + '
' + '
' + '
' + '
', }); // counter should be 2 as there are 2 records assert.strictEqual(counter, 2, "on_attach_callback should have been called twice"); // set a color to kanban record var $firstRecord = kanban.$('.o_kanban_record:first()'); testUtils.kanban.toggleRecordDropdown($firstRecord); await testUtils.dom.click($firstRecord.find('.oe_kanban_colorpicker a.oe_kanban_color_9')); // first record has replaced its $el with a new one $firstRecord = kanban.$('.o_kanban_record:first()'); assert.hasClass($firstRecord, 'oe_kanban_color_9'); assert.strictEqual(counter, 3, "on_attach_callback method should be called 3 times"); delete fieldRegistry.map.test_widget; kanban.destroy(); }); QUnit.test('column progressbars properly work', async function (assert) { assert.expect(2); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['bar'], }); assert.containsN(kanban, '.o_legacy_kanban_counter', this.data.product.records.length, "kanban counters should have been created"); assert.strictEqual(parseInt(kanban.$('.o_kanban_counter_side').last().text()), 36, "counter should display the sum of int_field values"); kanban.destroy(); }); QUnit.test('column progressbars: "false" bar is clickable', async function (assert) { assert.expect(8); this.data.partner.records.push({id: 5, bar: true, foo: false, product_id: 5, state: "ghi"}); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['bar'], }); assert.containsN(kanban, '.o_kanban_group', 2); assert.strictEqual(kanban.$('.o_legacy_kanban_counter:last .o_kanban_counter_side').text(), "4"); assert.containsN(kanban, '.o_kanban_counter_progress:last .progress-bar', 4); assert.containsOnce(kanban, '.o_kanban_counter_progress:last .progress-bar[data-filter="__false"]', "should have false kanban color"); assert.hasClass(kanban.$('.o_kanban_counter_progress:last .progress-bar[data-filter="__false"]'), 'bg-200'); await testUtils.dom.click(kanban.$('.o_kanban_counter_progress:last .progress-bar[data-filter="__false"]')); assert.hasClass(kanban.$('.o_kanban_counter_progress:last .progress-bar[data-filter="__false"]'), 'progress-bar-animated'); assert.hasClass(kanban.$('.o_kanban_group:last'), 'o_kanban_group_show_200'); assert.strictEqual(kanban.$('.o_legacy_kanban_counter:last .o_kanban_counter_side').text(), "1"); kanban.destroy(); }); QUnit.test('column progressbars: "false" bar with sum_field', async function (assert) { assert.expect(4); this.data.partner.records.push({id: 5, bar: true, foo: false, int_field: 15, product_id: 5, state: "ghi"}); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['bar'], }); assert.containsN(kanban, '.o_kanban_group', 2); assert.strictEqual(kanban.$('.o_legacy_kanban_counter:last .o_kanban_counter_side').text(), "51"); await testUtils.dom.click(kanban.$('.o_kanban_counter_progress:last .progress-bar[data-filter="__false"]')); assert.hasClass(kanban.$('.o_kanban_counter_progress:last .progress-bar[data-filter="__false"]'), 'progress-bar-animated'); assert.strictEqual(kanban.$('.o_legacy_kanban_counter:last .o_kanban_counter_side').text(), "15"); kanban.destroy(); }); QUnit.test('column progressbars should not crash in non grouped views', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '' + '
' + '' + '
' + '
' + '
', mockRPC: function (route, args) { assert.step(route); return this._super(route, args); }, }); assert.strictEqual(kanban.$('.o_kanban_record').text(), 'namenamenamename', "should have renderer 4 records"); assert.verifySteps(['/web/dataset/search_read'], "no read on progress bar data is done"); kanban.destroy(); }); QUnit.test('column progressbars: creating a new column should create a new progressbar', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); var nbProgressBars = kanban.$('.o_legacy_kanban_counter').length; // Create a new column: this should create an empty progressbar var $columnQuickCreate = kanban.$('.o_column_quick_create'); await testUtils.dom.click($columnQuickCreate.find('.o_quick_create_folded')); $columnQuickCreate.find('input').val('test'); await testUtils.dom.click($columnQuickCreate.find('.btn-primary')); assert.containsN(kanban, '.o_legacy_kanban_counter', nbProgressBars + 1, "a new column with a new column progressbar should have been created"); kanban.destroy(); }); QUnit.test('column progressbars on quick create properly update counter', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['bar'], }); var initialCount = parseInt(kanban.$('.o_kanban_counter_side:first').text()); await testUtils.dom.click(kanban.$('.o_kanban_quick_add:first')); await testUtils.fields.editInput(kanban.$('.o_kanban_quick_create input'), 'Test'); await testUtils.dom.click(kanban.$('.o_kanban_add')); var lastCount = parseInt(kanban.$('.o_kanban_counter_side:first').text()); await nextTick(); // await update await nextTick(); // await read assert.strictEqual(lastCount, initialCount + 1, "kanban counters should have updated on quick create"); kanban.destroy(); }); QUnit.test('column progressbars are working with load more', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, domain: [['bar', '=', true]], arch: '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['bar'], }); // we have 1 record shown, load 2 more and check it worked await testUtils.dom.click(kanban.$('.o_kanban_group').find('.o_kanban_load_more')); await testUtils.dom.click(kanban.$('.o_kanban_group').find('.o_kanban_load_more')); var shownIDs = _.map(kanban.$('.o_kanban_record'), function(record) { return parseInt(record.innerText); }); assert.deepEqual(shownIDs, [1, 2, 3], "intended records are loaded"); kanban.destroy(); }); QUnit.test('column progressbars with an active filter are working with load more', async function (assert) { assert.expect(2); this.data.partner.records.push( { id: 5, bar: true, foo: "blork" }, { id: 6, bar: true, foo: "blork" }, { id: 7, bar: true, foo: "blork" } ); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, domain: [['bar', '=', true]], arch: `
`, groupBy: ['bar'], }); await testUtils.dom.click(kanban.el.querySelector('.o_kanban_counter_progress .progress-bar[data-filter="blork"]')); // we should have 1 record shown assert.deepEqual( [...kanban.el.querySelectorAll('.o_kanban_record')].map(el => parseInt(el.innerText)), [5] ); // load 2 more and check it worked await testUtils.dom.click(kanban.el.querySelector('.o_kanban_group .o_kanban_load_more')); await testUtils.dom.click(kanban.el.querySelector('.o_kanban_group .o_kanban_load_more')); assert.deepEqual( [...kanban.el.querySelectorAll('.o_kanban_record')].map(el => parseInt(el.innerText)), [5, 6, 7] ); kanban.destroy(); }); QUnit.test('column progressbars on archiving records update counter', async function (assert) { assert.expect(4); // add active field on partner model and make all records active this.data.partner.fields.active = {string: 'Active', type: 'char', default: true}; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['bar'], mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/action_archive') { var partnerIDS = args.args[0]; var records = this.data.partner.records; _.each(partnerIDS, function(partnerID) { _.find(records, function (record) { return record.id === partnerID; }).active = false; }); this.data.partner.records[0].active; return Promise.resolve(); } return this._super.apply(this, arguments); }, }); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_side').text(), "36", "counter should contain the correct value"); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_progress > .progress-bar:first').data('bsOriginalTitle'), "1 yop", "the counter progressbars should be correctly displayed"); // archive all records of the second columns testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:eq(1)')); await testUtils.dom.click(kanban.$('.o_column_archive_records:visible')); await testUtils.dom.click($('.modal-footer button:first')); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_side').text(), "0", "counter should contain the correct value"); assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_progress > .progress-bar:first').data('bsOriginalTitle'), "0 yop", "the counter progressbars should have been correctly updated"); kanban.destroy(); }); QUnit.test('kanban with progressbars: correctly update env when archiving records', async function (assert) { assert.expect(2); // add active field on partner model and make all records active this.data.partner.fields.active = {string: 'Active', type: 'char', default: true}; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['bar'], mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/action_archive') { var partnerIDS = args.args[0]; var records = this.data.partner.records _.each(partnerIDS, function(partnerID) { _.find(records, function (record) { return record.id === partnerID; }).active = false; }) this.data.partner.records[0].active; return Promise.resolve(); } return this._super.apply(this, arguments); }, }); assert.deepEqual(kanban.exportState().resIds, [1, 2, 3, 4]); // archive all records of the first column testUtils.kanban.toggleGroupSettings(kanban.$('.o_kanban_group:first')); await testUtils.dom.click(kanban.$('.o_column_archive_records:visible')); await testUtils.dom.click($('.modal-footer button:first')); assert.deepEqual(kanban.exportState().resIds, [1, 2, 3]); kanban.destroy(); }); QUnit.test('RPCs when (re)loading kanban view progressbars', async function (assert) { assert.expect(9); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['bar'], mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); await kanban.reload(); assert.verifySteps([ // initial load 'web_read_group', 'read_progress_bar', '/web/dataset/search_read', '/web/dataset/search_read', // reload 'web_read_group', 'read_progress_bar', '/web/dataset/search_read', '/web/dataset/search_read', ]); kanban.destroy(); }); QUnit.test('RPCs when (de)activating kanban view progressbar filters', async function (assert) { assert.expect(8); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['bar'], mockRPC(route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); // Activate "yop" on second column await testUtils.dom.click(kanban.el.querySelector('.o_kanban_group:nth-child(2) .progress-bar[data-filter="yop"]')); // Activate "gnap" on second column await testUtils.dom.click(kanban.el.querySelector('.o_kanban_group:nth-child(2) .progress-bar[data-filter="gnap"]')); // Deactivate "gnap" on second column await testUtils.dom.click(kanban.el.querySelector('.o_kanban_group:nth-child(2) .progress-bar[data-filter="gnap"]')); assert.verifySteps([ // initial load 'web_read_group', 'read_progress_bar', '/web/dataset/search_read', '/web/dataset/search_read', // activate filter '/web/dataset/search_read', // activate another filter (switching) '/web/dataset/search_read', // deactivate active filter '/web/dataset/search_read', ]); kanban.destroy(); }); QUnit.test('drag & drop records grouped by m2o with progressbar', async function (assert) { assert.expect(4); this.data.partner.records[0].product_id = false; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['product_id'], mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { return Promise.resolve(true); } return this._super(route, args); }, }); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "1", "counter should contain the correct value"); await testUtils.dom.dragAndDrop(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)'), kanban.$('.o_kanban_group:eq(1)')); await nextTick(); // wait for update resulting from drag and drop assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "0", "counter should contain the correct value"); await testUtils.dom.dragAndDrop(kanban.$('.o_kanban_group:eq(1) .o_kanban_record:eq(2)'), kanban.$('.o_kanban_group:eq(0)')); await nextTick(); // wait for update resulting from drag and drop assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "1", "counter should contain the correct value"); await testUtils.dom.dragAndDrop(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)'), kanban.$('.o_kanban_group:eq(1)')); await nextTick(); // wait for update resulting from drag and drop assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "0", "counter should contain the correct value"); kanban.destroy(); }); QUnit.test('progress bar subgroup count recompute', async function (assert) { assert.expect(2); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['bar'], }); let secondCounter = kanban.el.querySelector('.o_kanban_group:nth-child(2) .o_kanban_counter_side'); assert.strictEqual(parseInt(secondCounter.innerText), 3, "Initial count should be Three"); await testUtils.dom.click(kanban.el.querySelector('.o_kanban_group:nth-child(2) .bg-success')); secondCounter = kanban.el.querySelector('.o_kanban_group:nth-child(2) .o_kanban_counter_side'); assert.strictEqual(parseInt(secondCounter.innerText), 1, "kanban counters should vary according to what subgroup is selected"); kanban.destroy(); }); QUnit.test('progress bar recompute after drag&drop to and from other column', async function (assert) { assert.expect(4); const view = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['bar'], }); assert.deepEqual( [...view.el.querySelectorAll('.progress-bar')].map(el => el.getAttribute('data-bs-original-title')), ['0 yop', '0 gnap', '1 blip', '0 __false', '1 yop', '1 gnap', '1 blip', '0 __false'] ); assert.deepEqual( [...view.el.querySelectorAll('.o_kanban_counter_side')].map(el => parseInt(el.innerText)), [1, 3] ); // Drag the last kanban record to the first column await testUtils.dom.dragAndDrop( [...view.el.querySelectorAll('.o_kanban_record')].pop(), [...view.el.querySelectorAll('.o_kanban_group')].shift() ); assert.deepEqual( [...view.el.querySelectorAll('.progress-bar')].map(el => el.getAttribute('data-bs-original-title')), ['0 yop', '1 gnap', '1 blip', '0 __false', '1 yop', '0 gnap', '1 blip', '0 __false'] ); assert.deepEqual( [...view.el.querySelectorAll('.o_kanban_counter_side')].map(el => parseInt(el.innerText)), [2, 2] ); view.destroy(); }); QUnit.test('load more should load correct records after drag&drop event', async function (assert) { assert.expect(3); // Add a sequence number and initialize this.data.partner.fields = Object.assign(this.data.partner.fields, { sequence: { type: 'integer' } }); this.data.partner.records.forEach((el, i) => { el.sequence = i; }); const view = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, groupBy: ['bar'], favoriteFilters: [ { domain: '[]', is_default: true, sort: '["sequence asc"]', }, ], }); assert.deepEqual( [...view.el.querySelectorAll('.o_kanban_group:nth-child(1) .o_kanban_record span')] .map(el => parseInt(el.innerText))[0], 4, "first column's first record must be id 4" ); assert.deepEqual( [...view.el.querySelectorAll('.o_kanban_group:nth-child(2) .o_kanban_record span')] .map(el => parseInt(el.innerText)), [1], "second column's records should be only the id 1" ); // Drag the first kanban record on top of the last await testUtils.dom.dragAndDrop( [...view.el.querySelectorAll('.o_kanban_record')].shift(), [...view.el.querySelectorAll('.o_kanban_record')].pop(), { position: 'top' } ); // load more twice to load all records of second column await testUtils.dom.click(view.el.querySelector('.o_kanban_group:nth-child(2) .o_kanban_load_more')); await testUtils.dom.click(view.el.querySelector('.o_kanban_group:nth-child(2) .o_kanban_load_more')); // Check records of the second column assert.deepEqual( [...view.el.querySelectorAll('.o_kanban_group:nth-child(2) .o_kanban_record span')].map(el => parseInt(el.innerText)), [4, 1, 2, 3] ); view.destroy(); }); QUnit.test('column progressbars on quick create with quick_create_view are updated', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '
' + '' + '
' + '
' + '
', archs: { 'partner,some_view_ref,form': '
' + '' + '', }, groupBy: ['bar'], }); var initialCount = parseInt(kanban.$('.o_kanban_counter_side:first').text()); await testUtils.kanban.clickCreate(kanban); // fill the quick create and validate var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); await testUtils.fields.editInput($quickCreate.find('.o_field_widget[name=int_field]'), '44'); await testUtils.dom.click($quickCreate.find('button.o_kanban_add')); var lastCount = parseInt(kanban.$('.o_kanban_counter_side:first').text()); assert.strictEqual(lastCount, initialCount + 44, "kanban counters should have been updated on quick create"); kanban.destroy(); }); QUnit.test('column progressbars and active filter on quick create with quick_create_view are updated', async function (assert) { assert.expect(2); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, archs: { 'partner,some_view_ref,form': `
`, }, groupBy: ['bar'], }); await testUtils.dom.click(kanban.el.querySelector('.o_kanban_group:nth-child(1) .progress-bar[data-filter="blip"]')); const initialCount = parseInt(kanban.el.querySelector('.o_kanban_group:nth-child(1) .o_kanban_counter_side').innerText); assert.strictEqual(initialCount, -4, "Initial count should be -4"); // open the quick create await testUtils.kanban.clickCreate(kanban); // fill it with a record that satisfies the activated filter let quickCreate = kanban.el.querySelector('.o_kanban_group:nth-child(1) .o_kanban_quick_create'); await testUtils.fields.editInput(quickCreate.querySelector('.o_field_widget[name="int_field"]'), '44'); await testUtils.fields.editInput(quickCreate.querySelector('.o_field_widget[name="foo"]'), 'blip'); await testUtils.dom.click(quickCreate.querySelector('button.o_kanban_add')); // fill it again with another record that DOES NOT satisfies the activated filter quickCreate = kanban.el.querySelector('.o_kanban_group:nth-child(1) .o_kanban_quick_create'); await testUtils.fields.editInput(quickCreate.querySelector('.o_field_widget[name="int_field"]'), '1000'); await testUtils.fields.editInput(quickCreate.querySelector('.o_field_widget[name="foo"]'), 'yop'); await testUtils.dom.click(quickCreate.querySelector('button.o_kanban_add')); // check counter const lastCount = parseInt(kanban.el.querySelector('.o_kanban_group:nth-child(1) .o_kanban_counter_side').innerText); assert.strictEqual(lastCount, initialCount + 44, "kanban counters should have been updated on quick create, respecting the activated filter"); kanban.destroy(); }); QUnit.test('keep adding quickcreate in first column after a record from this column was moved', async function (assert) { assert.expect(2); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
' + '
', groupBy: ['foo'], mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { return Promise.resolve(true); } return this._super(route, args); }, }); var $quickCreateGroup; var $groups; await _quickCreateAndTest(); await testUtils.dom.dragAndDrop($groups.first().find('.o_kanban_record:first'), $groups.eq(1)); await _quickCreateAndTest(); kanban.destroy(); async function _quickCreateAndTest() { await testUtils.kanban.clickCreate(kanban); $quickCreateGroup = kanban.$('.o_kanban_quick_create').closest('.o_kanban_group'); $groups = kanban.$('.o_kanban_group'); assert.strictEqual($quickCreateGroup[0], $groups[0], "quick create should have been added in the first column"); } }); QUnit.test('test displaying image (URL, image field not set)', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '' + '
' + '
', }); // since the field image is not set, kanban_image will generate an URL var imageOnRecord = kanban.$('img[data-src*="/web/image"][data-src*="&id=1"]'); assert.strictEqual(imageOnRecord.length, 1, "partner with image display image by url"); kanban.destroy(); }); QUnit.test('test displaying image (__last_update field)', async function (assert) { // the presence of __last_update field ensures that the image is reloaded when necessary assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, mockRPC(route, args) { if (route === '/web/dataset/search_read') { assert.deepEqual(args.fields, ['id', '__last_update']); } return this._super(...arguments); }, }); kanban.destroy(); }); QUnit.test('test displaying image (binary & placeholder)', async function (assert) { assert.expect(2); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '
' + '
', }); var images = kanban.el.querySelectorAll('img'); var placeholders = []; for (var [index, img] of images.entries()) { if (img.dataset.src.indexOf(this.data.partner.records[index].image) === -1) { // Then we display a placeholder placeholders.push(img); } } assert.strictEqual(placeholders.length, this.data.partner.records.length - 1, "partner with no image should display the placeholder"); assert.strictEqual(images[0].dataset.src, "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA==", "The first partners non-placeholder image should be set"); kanban.destroy(); }); QUnit.test('test displaying image (for another record)', async function (assert) { assert.expect(2); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '
' + '
', }); // the field image is set, but we request the image for a specific id // -> for the record matching the ID, the base64 should be returned // -> for all the other records, the image should be displayed by url var imageOnRecord = kanban.$('img[data-src*="/web/image"][data-src*="&id=1"]'); assert.strictEqual(imageOnRecord.length, this.data.partner.records.length - 1, "display image by url when requested for another record"); assert.strictEqual(kanban.el.querySelector("img").dataset.src, "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA==", "display image as value when requested for the record itself"); kanban.destroy(); }); QUnit.test("test displaying image from m2o field (m2o field not set)", async function (assert) { assert.expect(2); this.data.foo_partner = { fields: { name: {string: "Foo Name", type: "char"}, partner_id: {string: "Partner", type: "many2one", relation: "partner"}, }, records: [ {id: 1, name: 'foo_with_partner_image', partner_id: 1}, {id: 2, name: 'foo_no_partner'}, ] }; const kanban = await createView({ View: KanbanView, model: "foo_partner", data: this.data, arch: `
`, }); assert.containsOnce(kanban, 'img[data-src*="/web/image"][data-src$="&id=1"]', "image url should contain id of set partner_id"); assert.containsOnce(kanban, 'img[data-src*="/web/image"][data-src$="&id="]', "image url should contain an empty id if partner_id is not set"); kanban.destroy(); }); QUnit.test('check if the view destroys all widgets and instances', async function (assert) { assert.expect(2); var instanceNumber = 0; testUtils.mock.patch(mixins.ParentedMixin, { init: function () { instanceNumber++; return this._super.apply(this, arguments); }, destroy: function () { if (!this.isDestroyed()) { instanceNumber--; } return this._super.apply(this, arguments); } }); var params = { View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '
' + '
', }; var kanban = await createView(params); assert.ok(instanceNumber > 0); kanban.destroy(); assert.strictEqual(instanceNumber, 0); testUtils.mock.unpatch(mixins.ParentedMixin); }); QUnit.test('grouped kanban becomes ungrouped when clearing domain then clearing groupby', async function (assert) { // in this test, we simulate that clearing the domain is slow, so that // clearing the groupby does not corrupt the data handled while // reloading the kanban view. assert.expect(4); var prom = makeTestPromise(); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', domain: [['foo', '=', 'norecord']], groupBy: ['bar'], mockRPC: function (route, args) { var result = this._super(route, args); if (args.method === 'web_read_group') { var isFirstUpdate = _.isEmpty(args.kwargs.domain) && args.kwargs.groupby && args.kwargs.groupby[0] === 'bar'; if (isFirstUpdate) { return prom.then(function () { return result; }); } } return result; }, }); assert.hasClass(kanban.$('.o_legacy_kanban_view'),'o_kanban_grouped', "the kanban view should be grouped"); assert.doesNotHaveClass(kanban.$('.o_legacy_kanban_view'), 'o_kanban_ungrouped', "the kanban view should not be ungrouped"); kanban.update({domain: []}); // 1st update on kanban view kanban.update({groupBy: []}); // 2n update on kanban view prom.resolve(); // simulate slow 1st update of kanban view await nextTick(); assert.doesNotHaveClass(kanban.$('.o_legacy_kanban_view'), 'o_kanban_grouped', "the kanban view should not longer be grouped"); assert.hasClass(kanban.$('.o_legacy_kanban_view'),'o_kanban_ungrouped', "the kanban view should have become ungrouped"); kanban.destroy(); }); QUnit.test('quick_create on grouped kanban without column', async function (assert) { assert.expect(1); this.data.partner.records = []; var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, // force group_create to false, otherwise the CREATE button in control panel is hidden arch: '' + '
' + '' + '
' + '
', groupBy: ['product_id'], intercepts: { switch_view: function (event) { assert.ok(true, "switch_view was called instead of quick_create"); }, }, }); await testUtils.kanban.clickCreate(kanban); kanban.destroy(); }); QUnit.test('keyboard navigation on kanban basic rendering', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '
' + '' + '' + '
' + '
', }); var $fisrtCard = kanban.$('.o_kanban_record:first'); var $secondCard = kanban.$('.o_kanban_record:eq(1)'); $fisrtCard.focus(); assert.strictEqual(document.activeElement, $fisrtCard[0], "the kanban cards are focussable"); $fisrtCard.trigger($.Event('keydown', { which: $.ui.keyCode.RIGHT, keyCode: $.ui.keyCode.RIGHT, })); assert.strictEqual(document.activeElement, $secondCard[0], "the second card should be focussed"); $secondCard.trigger($.Event('keydown', { which: $.ui.keyCode.LEFT, keyCode: $.ui.keyCode.LEFT, })); assert.strictEqual(document.activeElement, $fisrtCard[0], "the first card should be focussed"); kanban.destroy(); }); QUnit.test('keyboard navigation on kanban grouped rendering', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '
', groupBy: ['bar'], }); var $firstColumnFisrtCard = kanban.$('.o_kanban_record:first'); var $secondColumnFirstCard = kanban.$('.o_kanban_group:eq(1) .o_kanban_record:first'); var $secondColumnSecondCard = kanban.$('.o_kanban_group:eq(1) .o_kanban_record:eq(1)'); $firstColumnFisrtCard.focus(); //RIGHT should select the next column $firstColumnFisrtCard.trigger($.Event('keydown', { which: $.ui.keyCode.RIGHT, keyCode: $.ui.keyCode.RIGHT, })); assert.strictEqual(document.activeElement, $secondColumnFirstCard[0], "RIGHT should select the first card of the next column"); //DOWN should move up one card $secondColumnFirstCard.trigger($.Event('keydown', { which: $.ui.keyCode.DOWN, keyCode: $.ui.keyCode.DOWN, })); assert.strictEqual(document.activeElement, $secondColumnSecondCard[0], "DOWN should select the second card of the current column"); //LEFT should go back to the first column $secondColumnSecondCard.trigger($.Event('keydown', { which: $.ui.keyCode.LEFT, keyCode: $.ui.keyCode.LEFT, })); assert.strictEqual(document.activeElement, $firstColumnFisrtCard[0], "LEFT should select the first card of the first column"); kanban.destroy(); }); QUnit.test('keyboard navigation on kanban grouped rendering with empty columns', async function (assert) { assert.expect(2); var data = this.data; data.partner.records[1].state = "abc"; var kanban = await createView({ View: KanbanView, model: 'partner', data: data, arch: '' + '' + '' + '
' + '
', groupBy: ['state'], mockRPC: function (route, args) { if (args.method === 'web_read_group') { // override read_group to return empty groups, as this is // the case for several models (e.g. project.task grouped // by stage_id) return this._super.apply(this, arguments).then(function (result) { // add 2 empty columns in the middle result.groups.splice(1, 0, {state_count: 0, state: 'def', __domain: [["state", "=", "def"]]}); result.groups.splice(1, 0, {state_count: 0, state: 'def', __domain: [["state", "=", "def"]]}); // add 1 empty column in the beginning and the end result.groups.unshift({state_count: 0, state: 'def', __domain: [["state", "=", "def"]]}); result.groups.push({state_count: 0, state: 'def', __domain: [["state", "=", "def"]]}); return result; }); } return this._super.apply(this, arguments); }, }); /** * DEF columns are empty * * | DEF | ABC | DEF | DEF | GHI | DEF * |-----|------|-----|-----|------|----- * | | yop | | | gnap | * | | blip | | | blip | */ var $yop = kanban.$('.o_kanban_record:first'); var $gnap = kanban.$('.o_kanban_group:eq(4) .o_kanban_record:first'); $yop.focus(); //RIGHT should select the next column that has a card $yop.trigger($.Event('keydown', { which: $.ui.keyCode.RIGHT, keyCode: $.ui.keyCode.RIGHT, })); assert.strictEqual(document.activeElement, $gnap[0], "RIGHT should select the first card of the next column that has a card"); //LEFT should go back to the first column that has a card $gnap.trigger($.Event('keydown', { which: $.ui.keyCode.LEFT, keyCode: $.ui.keyCode.LEFT, })); assert.strictEqual(document.activeElement, $yop[0], "LEFT should select the first card of the first column that has a card"); kanban.destroy(); }); QUnit.test('keyboard navigation on kanban when the focus is on a link that ' + 'has an action and the kanban has no oe_kanban_global_... class', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '', }); testUtils.mock.intercept(kanban, 'switch_view', function (event) { assert.deepEqual(event.data, { view_type: 'form', res_id: 1, mode: 'edit', model: 'partner', }, 'When selecting focusing a card and hitting ENTER, the first link or button is clicked'); }); kanban.$('.o_kanban_record').first().focus().trigger($.Event('keydown', { keyCode: $.ui.keyCode.ENTER, which: $.ui.keyCode.ENTER, })); await testUtils.nextTick(); kanban.destroy(); }); QUnit.test('asynchronous rendering of a field widget (ungrouped)', async function (assert) { assert.expect(4); var fooFieldProm = makeTestPromise(); var FieldChar = fieldRegistry.get('char'); fieldRegistry.add('asyncwidget', FieldChar.extend({ willStart: function () { return fooFieldProm; }, start: function () { this.$el.html('LOADED'); }, })); const viewCreatedPromise = testUtils.createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '
' + '
', }); assert.strictEqual($('.o_kanban_record').length, 0, "kanban view is not ready yet"); fooFieldProm.resolve(); const kanbanController = await viewCreatedPromise; assert.strictEqual($('.o_kanban_record').text(), "LOADEDLOADEDLOADEDLOADED"); // reload with a domain fooFieldProm = makeTestPromise(); kanbanController.reload({domain: [['id', '=', 1]]}); await nextTick(); assert.strictEqual($('.o_kanban_record').text(), "LOADEDLOADEDLOADEDLOADED"); fooFieldProm.resolve(); await nextTick(); assert.strictEqual($('.o_kanban_record').text(), "LOADED"); kanbanController.destroy(); delete fieldRegistry.map.asyncWidget; }); QUnit.test('asynchronous rendering of a field widget (grouped)', async function (assert) { assert.expect(4); var fooFieldProm = makeTestPromise(); var FieldChar = fieldRegistry.get('char'); fieldRegistry.add('asyncwidget', FieldChar.extend({ willStart: function () { return fooFieldProm; }, start: function () { this.$el.html('LOADED'); }, })); const viewCreatedPromise = testUtils.createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '
' + '
', groupBy: ['foo'], }); assert.strictEqual($('.o_kanban_record').length, 0, "kanban view is not ready yet"); fooFieldProm.resolve(); const kanbanController = await viewCreatedPromise; assert.strictEqual($('.o_kanban_record').text(), "LOADEDLOADEDLOADEDLOADED"); // reload with a domain fooFieldProm = makeTestPromise(); kanbanController.reload({domain: [['id', '=', 1]]}); await nextTick(); assert.strictEqual($('.o_kanban_record').text(), "LOADEDLOADEDLOADEDLOADED"); fooFieldProm.resolve(); await nextTick(); assert.strictEqual($('.o_kanban_record').text(), "LOADED"); kanbanController.destroy(); delete fieldRegistry.map.asyncWidget; }); QUnit.test('asynchronous rendering of a field widget with display attr', async function (assert) { assert.expect(3); var fooFieldDef = makeTestPromise(); var FieldChar = fieldRegistry.get('char'); fieldRegistry.add('asyncwidget', FieldChar.extend({ willStart: function () { return fooFieldDef; }, start: function () { this.$el.html('LOADED'); }, })); const viewCreatedPromise = testUtils.createAsyncView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '
' + '
', }); assert.containsNone(document.body, '.o_kanban_record'); fooFieldDef.resolve(); const kanbanController = await viewCreatedPromise; assert.strictEqual(kanbanController.$('.o_kanban_record').text(), "LOADEDLOADEDLOADEDLOADED"); assert.hasClass(kanbanController.$('.o_kanban_record:first .o_field_char'), 'float-end'); kanbanController.destroy(); delete fieldRegistry.map.asyncWidget; }); QUnit.test('asynchronous rendering of a widget', async function (assert) { assert.expect(2); var widgetDef = makeTestPromise(); widgetRegistry.add('asyncwidget', Widget.extend({ willStart: function () { return widgetDef; }, start: function () { this.$el.html('LOADED'); }, })); const viewCreatedPromise = testUtils.createAsyncView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '
' + '
', }); assert.containsNone(document.body, '.o_kanban_record'); widgetDef.resolve(); const kanbanController = await viewCreatedPromise; assert.strictEqual(kanbanController.$('.o_kanban_record .o_widget').text(), "LOADEDLOADEDLOADEDLOADED"); kanbanController.destroy(); delete widgetRegistry.map.asyncWidget; }); QUnit.test('update kanban with asynchronous field widget', async function (assert) { assert.expect(3); var fooFieldDef = makeTestPromise(); var FieldChar = fieldRegistry.get('char'); fieldRegistry.add('asyncwidget', FieldChar.extend({ willStart: function () { return fooFieldDef; }, start: function () { this.$el.html('LOADED'); }, })); var kanban = await testUtils.createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '
' + '
', domain: [['id', '=', '0']], // no record matches this domain }); assert.containsNone(kanban, '.o_kanban_record:not(.o_kanban_ghost)'); kanban.update({domain: []}); // this rendering will be async assert.containsNone(kanban, '.o_kanban_record:not(.o_kanban_ghost)'); fooFieldDef.resolve(); await nextTick(); assert.strictEqual(kanban.$('.o_kanban_record').text(), "LOADEDLOADEDLOADEDLOADED"); kanban.destroy(); delete widgetRegistry.map.asyncWidget; }); QUnit.test('set cover image', async function (assert) { assert.expect(7); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '' + '
'+ ''+ '
'+ '
' + '
' + '
' + '
', mockRPC: function (route, args) { if (args.model === 'partner' && args.method === 'write') { assert.step(String(args.args[0][0])); return this._super(route, args); } return this._super(route, args); }, intercepts: { switch_view: function (event) { assert.deepEqual(_.pick(event.data, 'mode', 'model', 'res_id', 'view_type'), { mode: 'readonly', model: 'partner', res_id: 1, view_type: 'form', }, "should trigger an event to open the clicked record in a form view"); }, }, }); var $firstRecord = kanban.$('.o_kanban_record:first'); testUtils.kanban.toggleRecordDropdown($firstRecord); await testUtils.dom.click($firstRecord.find('[data-type=set_cover]')); assert.containsNone($firstRecord, 'img', "Initially there is no image."); await testUtils.dom.click($('.modal').find("img[data-id='1']")); await testUtils.modal.clickButton('Select'); assert.containsOnce(kanban, 'img[data-src*="/web/image/1"]'); var $secondRecord = kanban.$('.o_kanban_record:nth(1)'); testUtils.kanban.toggleRecordDropdown($secondRecord); await testUtils.dom.click($secondRecord.find('[data-type=set_cover]')); $('.modal').find("img[data-id='2']").dblclick(); await testUtils.nextTick(); assert.containsOnce(kanban, 'img[data-src*="/web/image/2"]'); await testUtils.dom.click(kanban.$('.o_kanban_record:first .o_attachment_image')); assert.verifySteps(["1", "2"], "should writes on both kanban records"); kanban.destroy(); }); QUnit.test('ungrouped kanban with handle field', async function (assert) { assert.expect(4); var envIDs = [1, 2, 3, 4]; // the ids that should be in the environment during this test var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '' + '
' + '' + '
' + '
', mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { assert.deepEqual(args.ids, envIDs, "should write the sequence in correct order"); return Promise.resolve(true); } return this._super(route, args); }, }); assert.hasClass(kanban.$('.o_legacy_kanban_view'), 'ui-sortable'); assert.strictEqual(kanban.$('.o_kanban_record:not(.o_kanban_ghost)').text(), 'yopblipgnapblip'); var $record = kanban.$('.o_legacy_kanban_view .o_kanban_record:first'); var $to = kanban.$('.o_legacy_kanban_view .o_kanban_record:nth-child(4)'); envIDs = [2, 3, 4, 1]; // first record of moved after last one await testUtils.dom.dragAndDrop($record, $to, {position: "bottom"}); assert.strictEqual(kanban.$('.o_kanban_record:not(.o_kanban_ghost)').text(), 'blipgnapblipyop'); kanban.destroy(); }); QUnit.test('ungrouped kanban without handle field', async function (assert) { assert.expect(3); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '' + '
' + '
', mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { assert.ok(false, "should not trigger a resequencing"); } return this._super(route, args); }, }); assert.doesNotHaveClass(kanban.$('.o_legacy_kanban_view'), 'ui-sortable'); assert.strictEqual(kanban.$('.o_kanban_record:not(.o_kanban_ghost)').text(), 'yopblipgnapblip'); var $draggedRecord = kanban.$('.o_legacy_kanban_view .o_kanban_record:first'); var $to = kanban.$('.o_legacy_kanban_view .o_kanban_record:nth-child(4)'); await testUtils.dom.dragAndDrop($draggedRecord, $to, {position: "bottom"}); assert.strictEqual(kanban.$('.o_kanban_record:not(.o_kanban_ghost)').text(), 'yopblipgnapblip'); kanban.destroy(); }); QUnit.test('click on image field in kanban with oe_kanban_global_click', async function (assert) { assert.expect(2); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '' + '
' + '
' + '
', mockRPC: function (route) { if (route.startsWith('data:image')) { return Promise.resolve(); } return this._super.apply(this, arguments); }, intercepts: { switch_view: function (event) { assert.deepEqual(_.pick(event.data, 'mode', 'model', 'res_id', 'view_type'), { mode: 'readonly', model: 'partner', res_id: 1, view_type: 'form', }, "should trigger an event to open the clicked record in a form view"); }, }, }); assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 4); await testUtils.dom.click(kanban.$('.o_field_image').first()); kanban.destroy(); }); QUnit.test('kanban view with boolean field', async function (assert) { assert.expect(2); const kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, }); assert.containsN(kanban, '.o_kanban_record:contains(True)', 3); assert.containsOnce(kanban, '.o_kanban_record:contains(False)'); kanban.destroy(); }); QUnit.test('kanban view with boolean widget', async function (assert) { assert.expect(1); const kanban = await testUtils.createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, }); assert.containsOnce(kanban.el.querySelector('.o_kanban_record'), 'div.form-check.o_field_boolean'); kanban.destroy(); }); QUnit.test('kanban view with monetary and currency fields without widget', async function (assert) { assert.expect(1); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: `
`, session: { currencies: _.indexBy(this.data.currency.records, 'id'), }, }); const kanbanRecords = kanban.el.querySelectorAll('.o_kanban_record:not(.o_kanban_ghost)'); assert.deepEqual([...kanbanRecords].map(r => r.innerText), ['$ 1750.00', '$ 1500.00', '2000.00 €', '$ 2222.00']); kanban.destroy(); }); QUnit.test("quick create: keyboard navigation to buttons", async function (assert) { assert.expect(2); const kanban = await createView({ arch: `
`, data: this.data, groupBy: ["bar"], model: "partner", View: KanbanView, }); // Open quick create await testUtils.kanban.clickCreate(kanban); assert.containsOnce(kanban, ".o_kanban_group:first .o_kanban_quick_create"); const $displayName = kanban.$(".o_kanban_quick_create .o_field_widget[name=display_name]"); // Fill in mandatory field await testUtils.fields.editInput($displayName, "aaa"); // Tab -> goes to first primary button await testUtils.dom.triggerEvent($displayName, "keydown", { keyCode: $.ui.keyCode.TAB, which: $.ui.keyCode.TAB, }); assert.hasClass(document.activeElement, "btn btn-primary o_kanban_add"); kanban.destroy(); }); QUnit.test('kanban with isHtmlEmpty method', async function (assert) { assert.expect(3); this.data.product.fields.description = {string: 'Description', type: 'html'}; this.data.product.records.push( {id: 11, display_name: "product 11", description: "hello"}, {id: 12, display_name: "product 12", description: "


"}, ); var kanban = await createView({ View: KanbanView, model: 'product', data: this.data, arch: `
`, domain: [['id', 'in', [11, 12]]], }); assert.containsOnce(kanban, '.o_kanban_record:first div.test', "the container is displayed if description have actual content"); assert.strictEqual(kanban.$('.o_kanban_record:first div.test span.text-info').html().trim(), 'hello', "the inner html content is rendered properly"); assert.containsNone(kanban, '.o_kanban_record:last div.test', "the container is not displayed if description just have formatting tags and no actual content"); kanban.destroy(); }); QUnit.test("progressbar filter state is kept unchanged when domain is updated (records still in group)", async function (assert) { assert.expect(16); const kanban = await createView({ View: KanbanView, model: "partner", data: this.data, arch: `
`, }); // Check that we have 2 columns and check their progressbar's state assert.containsN(kanban.el, ".o_kanban_group", 2); assert.containsNone(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_column_title")].map(el => el.innerText), ["No", "Yes"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["0 yop", "1 blip", "0 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["1 yop", "1 blip", "1 __false"], ); // Apply an active filter await testUtils.dom.click(kanban.el.querySelector(".o_kanban_group:nth-child(2) .progress-bar[data-filter=yop]")); assert.containsOnce(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.strictEqual(kanban.el.querySelector(".o_kanban_group.o_kanban_group_show .o_column_title").innerText, "Yes"); // Add searchdomain to something restricting progressbars' values (records still in filtered group) await kanban.update({ domain: [["foo", "=", "yop"]] }); // Check that we have now 1 column only and check its progressbar's state assert.containsOnce(kanban.el, ".o_kanban_group"); assert.containsOnce(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.strictEqual(kanban.el.querySelector(".o_column_title").innerText, "Yes"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["1 yop", "0 blip", "0 __false"], ); // Undo searchdomain await kanban.update({ domain: [] }); // Check that we have 2 columns back and check their progressbar's state assert.containsN(kanban.el, ".o_kanban_group", 2); assert.containsOnce(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_column_title")].map(el => el.innerText), ["No", "Yes"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["0 yop", "1 blip", "0 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["1 yop", "1 blip", "1 __false"], ); kanban.destroy(); }); QUnit.test("progressbar filter state is kept unchanged when domain is updated (emptying group)", async function (assert) { assert.expect(25); const kanban = await createView({ View: KanbanView, model: "partner", data: this.data, arch: `
`, }); // Check that we have 2 columns, check their progressbar's state and check records assert.containsN(kanban.el, ".o_kanban_group", 2); assert.containsNone(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_column_title")].map(el => el.innerText), ["No", "Yes"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["0 yop", "1 blip", "0 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_kanban_record")].map(el => el.innerText), ["4 blip"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["1 yop", "1 blip", "1 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map(el => el.innerText), ["1 yop", "2 blip", "3 gnap"], ); // Apply an active filter await testUtils.dom.click(kanban.el.querySelector(".o_kanban_group:nth-child(2) .progress-bar[data-filter=yop]")); assert.containsOnce(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.strictEqual(kanban.el.querySelector(".o_kanban_group.o_kanban_group_show .o_column_title").innerText, "Yes"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group.o_kanban_group_show .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["1 yop", "1 blip", "1 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group.o_kanban_group_show .o_kanban_record")].map(el => el.innerText), ["1 yop"], ); // Add searchdomain to something restricting progressbars' values + emptying the filtered group await kanban.update({ domain: [["foo", "=", "blip"]] }); // Check that we still have 2 columns, check their progressbar's state and check records assert.containsN(kanban.el, ".o_kanban_group", 2); assert.containsNone(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_column_title")].map(el => el.innerText), ["No", "Yes"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["0 yop", "1 blip", "0 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_kanban_record")].map(el => el.innerText), ["4 blip"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["0 yop", "1 blip", "0 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map(el => el.innerText), ["2 blip"], ); // Undo searchdomain await kanban.update({ domain: [] }); // Check that we still have 2 columns and check their progressbar's state assert.containsN(kanban.el, ".o_kanban_group", 2); assert.containsNone(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_column_title")].map(el => el.innerText), ["No", "Yes"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["0 yop", "1 blip", "0 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_kanban_record")].map(el => el.innerText), ["4 blip"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["1 yop", "1 blip", "1 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map(el => el.innerText), ["1 yop", "2 blip", "3 gnap"], ); kanban.destroy(); }); QUnit.test("filtered column keeps consistent counters when dropping in a non-matching record", async function (assert) { assert.expect(19); const kanban = await createView({ View: KanbanView, model: "partner", data: this.data, arch: `
`, }); // Check that we have 2 columns, check their progressbar's state, and check records assert.containsN(kanban.el, ".o_kanban_group", 2); assert.containsNone(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_column_title")].map(el => el.innerText), ["No", "Yes"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["0 yop", "1 blip", "0 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_kanban_record")].map(el => el.innerText), ["4 blip"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["1 yop", "1 blip", "1 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map(el => el.innerText), ["1 yop", "2 blip", "3 gnap"], ); // Apply an active filter await testUtils.dom.click(kanban.el.querySelector(".o_kanban_group:nth-child(2) .progress-bar[data-filter=yop]")); assert.hasClass(kanban.el.querySelector(".o_kanban_group:nth-child(2)"), "o_kanban_group_show"); assert.containsOnce(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.strictEqual(kanban.el.querySelector(".o_kanban_group.o_kanban_group_show .o_column_title").innerText, "Yes"); assert.containsOnce(kanban.el, ".o_kanban_group.o_kanban_group_show .o_kanban_record"); assert.strictEqual(kanban.el.querySelector(".o_kanban_group.o_kanban_group_show .o_kanban_record").innerText, "1 yop"); // Drop in the non-matching record from first column await testUtils.dom.dragAndDrop( kanban.el.querySelector(".o_kanban_group:nth-child(1) .o_kanban_record"), kanban.el.querySelector(".o_kanban_group.o_kanban_group_show") ); // Check that we have 2 columns, check their progressbar's state, and check records assert.containsN(kanban.el, ".o_kanban_group", 2); assert.containsOnce(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_column_title")].map(el => el.innerText), ["No", "Yes"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["0 yop", "0 blip", "0 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_kanban_record")].map(el => el.innerText), [], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["1 yop", "2 blip", "1 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map(el => el.innerText), ["1 yop", "4 blip"], ); kanban.destroy(); }); QUnit.test("filtered column is reloaded when dragging out its last record", async function (assert) { assert.expect(33); const kanban = await createView({ View: KanbanView, model: "partner", data: this.data, arch: `
`, mockRPC: function (route, args) { assert.step(args.method || route); return this._super(route, args); }, }); // Check that we have 2 columns, check their progressbar's state, and check records assert.containsN(kanban.el, ".o_kanban_group", 2); assert.containsNone(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_column_title")].map(el => el.innerText), ["No", "Yes"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["0 yop", "1 blip", "0 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_kanban_record")].map(el => el.innerText), ["4 blip"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["1 yop", "1 blip", "1 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map(el => el.innerText), ["1 yop", "2 blip", "3 gnap"], ); assert.verifySteps([ "web_read_group", "read_progress_bar", "/web/dataset/search_read", "/web/dataset/search_read", ]); // Apply an active filter await testUtils.dom.click(kanban.el.querySelector(".o_kanban_group:nth-child(2) .progress-bar[data-filter=yop]")); assert.hasClass(kanban.el.querySelector(".o_kanban_group:nth-child(2)"), "o_kanban_group_show"); assert.containsOnce(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.strictEqual(kanban.el.querySelector(".o_kanban_group.o_kanban_group_show .o_column_title").innerText, "Yes"); assert.containsOnce(kanban.el, ".o_kanban_group.o_kanban_group_show .o_kanban_record"); assert.strictEqual(kanban.el.querySelector(".o_kanban_group.o_kanban_group_show .o_kanban_record").innerText, "1 yop"); assert.verifySteps(["/web/dataset/search_read"]); // Drag out its only record onto the first column await testUtils.dom.dragAndDrop( kanban.el.querySelector(".o_kanban_group.o_kanban_group_show .o_kanban_record"), kanban.el.querySelector(".o_kanban_group:nth-child(1)") ); // Check that we have 2 columns, check their progressbar's state, and check records assert.containsN(kanban.el, ".o_kanban_group", 2); assert.containsNone(kanban.el, ".o_kanban_group.o_kanban_group_show"); assert.deepEqual( [...kanban.el.querySelectorAll(".o_column_title")].map(el => el.innerText), ["No", "Yes"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["1 yop", "1 blip", "0 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(1) .o_kanban_record")].map(el => el.innerText), ["4 blip", "1 yop"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_legacy_kanban_counter .progress-bar")].map(el => el.dataset.bsOriginalTitle), ["0 yop", "1 blip", "1 __false"], ); assert.deepEqual( [...kanban.el.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map(el => el.innerText), ["2 blip", "3 gnap"], ); assert.verifySteps([ "write", "read", "web_read_group", "read_progress_bar", "/web/dataset/search_read", "/web/dataset/resequence", ]); kanban.destroy(); }); QUnit.test("kanban widget supports options parameters", async function (assert) { assert.expect(2); widgetRegistry.add('widget_test_option', Widget.extend({ init(parent, state, options) { this._super(...arguments); this.title = options.attrs.title; }, start() { this.$el.append($("
", {text: this.title, class: 'o-test-widget-option'})); }, })); const kanban = await createView({ arch: `
`, data: this.data, model: "partner", View: KanbanView, }); assert.containsN(kanban, '.o-test-widget-option', 4); assert.strictEqual(kanban.$('.o-test-widget-option')[0].textContent, 'Widget with Option'); kanban.destroy(); delete widgetRegistry.map.optionwidget; }); QUnit.test('column quick create - title and placeholder', async function (assert) { assert.expect(2); var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, arch: '' + '' + '
' + '' + '
' + '
' + '
', groupBy: ['product_id'], }); const productFieldName = this.data.partner.fields.product_id.string; assert.strictEqual(kanban.el.querySelector('.o_column_quick_create .o_quick_create_folded').innerText, productFieldName); assert.strictEqual(kanban.el.querySelector('.o_column_quick_create .o_quick_create_unfolded .input-group .o_input').getAttribute('placeholder'), productFieldName + '...'); }); }); });