/** @odoo-module **/ import { makeFakeDialogService } from "@web/../tests/helpers/mock_services"; import { click, clickSave, drag, dragAndDrop, editInput, getFixture, getNodesTextContent, makeDeferred, mouseEnter, nextTick, patchWithCleanup, selectDropdownItem, triggerEvent, triggerHotkey, } from "@web/../tests/helpers/utils"; import { getFacetTexts, getPagerLimit, getPagerValue, pagerNext, toggleFilterMenu, validateSearch, } from "@web/../tests/search/helpers"; import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { createWebClient, doAction } from "@web/../tests/webclient/helpers"; import { browser } from "@web/core/browser/browser"; import { dialogService } from "@web/core/dialog/dialog_service"; import { makeErrorFromResponse, RPCError } from "@web/core/network/rpc_service"; import { registry } from "@web/core/registry"; import { tooltipService } from "@web/core/tooltip/tooltip_service"; import { nbsp } from "@web/core/utils/strings"; import { getNextTabableElement } from "@web/core/utils/ui"; import { session } from "@web/session"; import { KanbanAnimatedNumber } from "@web/views/kanban/kanban_animated_number"; import { KanbanController } from "@web/views/kanban/kanban_controller"; import { KanbanCompiler } from "@web/views/kanban/kanban_compiler"; import { KanbanRenderer } from "@web/views/kanban/kanban_renderer"; import { KanbanRecord } from "@web/views/kanban/kanban_record"; import { kanbanView } from "@web/views/kanban/kanban_view"; import { DynamicRecordList } from "@web/views/relational_model"; import { ViewButton } from "@web/views/view_button/view_button"; import { Component, onWillRender, xml } from "@odoo/owl"; import { SampleServer } from "@web/views/sample_server"; import { KanbanDynamicGroupList } from "@web/views/kanban/kanban_model"; const serviceRegistry = registry.category("services"); const viewWidgetRegistry = registry.category("view_widgets"); const viewRegistry = registry.category("views"); // ---------------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------------- function patchDialog(addDialog) { serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true }); } // Kanban // WOWL remove this helper and use the control panel instead async function reload(kanban, params = {}) { kanban.env.searchModel.reload(params); kanban.env.searchModel.search(); await nextTick(); } function getCard(cardIndex = 0) { return target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")[cardIndex]; } function getColumn(groupIndex = 0, ignoreFolded = false) { let selector = ".o_kanban_group"; if (ignoreFolded) { selector += ":not(.o_column_folded)"; } return target.querySelectorAll(selector)[groupIndex]; } function getCardTexts(groupIndex) { const root = groupIndex >= 0 ? getColumn(groupIndex) : target; return [...root.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")] .map((card) => card.innerText.trim()) .filter(Boolean); } function getCounters() { return [...target.querySelectorAll(".o_kanban_counter_side")].map( (counter) => counter.innerText ); } function getProgressBars(columnIndex) { const column = getColumn(columnIndex); return [...column.querySelectorAll(".o_kanban_counter_progress .progress-bar")]; } function getTooltips(groupIndex) { const root = groupIndex >= 0 ? getColumn(groupIndex) : target; return [...root.querySelectorAll(".o_kanban_counter_progress .progress-bar")] .map((card) => card.dataset.tooltip) .filter(Boolean); } // Record async function createRecord() { await click(target, "button.o-kanban-button-new"); } async function quickCreateRecord(groupIndex) { await click(getColumn(groupIndex), ".o_kanban_quick_add"); } async function editQuickCreateInput(field, value) { await editInput(target, `.o_kanban_quick_create .o_field_widget[name=${field}] input`, value); } async function validateRecord() { await click(target, ".o_kanban_quick_create .o_kanban_add"); } async function editRecord() { await click(target, ".o_kanban_quick_create .o_kanban_edit"); } async function discardRecord() { await click(target, ".o_kanban_quick_create .o_kanban_cancel"); } async function toggleRecordDropdown(recordIndex) { const group = target.querySelectorAll(`.o_kanban_record`)[recordIndex]; await click(group, ".o_dropdown_kanban .dropdown-toggle"); } // Column async function createColumn() { await click(target, ".o_column_quick_create > .o_quick_create_folded"); } async function editColumnName(value) { await editInput(target, ".o_column_quick_create input", value); } async function validateColumn() { await click(target, ".o_column_quick_create .o_kanban_add"); } async function toggleColumnActions(columnIndex) { const group = getColumn(columnIndex); await click(group, ".o_kanban_config .dropdown-toggle"); const buttons = group.querySelectorAll(".o_kanban_config .dropdown-menu .dropdown-item"); return (buttonText) => { const re = new RegExp(`\\b${buttonText}\\b`, "i"); const button = [...buttons].find((b) => re.test(b.innerText)); return click(button); }; } async function loadMore(columnIndex) { await click(getColumn(columnIndex), ".o_kanban_load_more button"); } let serverData; let target; QUnit.module("Views", (hooks) => { hooks.beforeEach(() => { patchWithCleanup(KanbanAnimatedNumber, { enableAnimations: false }); serverData = { models: { 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", }, sequence: { type: "integer" }, 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" }, ], }, }, views: {}, }; target = getFixture(); setupViewRegistries(); }); QUnit.module("KanbanView"); QUnit.test("basic ungrouped rendering", async (assert) => { assert.expect(6); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, mockRPC(route, args) { if (args.method === "web_search_read") { assert.ok( args.kwargs.context.bin_size, "should not request direct binary payload" ); } }, }); assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_ungrouped"); assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_test"); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 4); assert.containsN(target, ".o_kanban_ghost", 6); assert.containsOnce(target, ".o_kanban_record:contains(gnap)"); }); QUnit.test("Hide tooltip when user click inside a kanban headers item", async (assert) => { patchWithCleanup(browser, { setTimeout: (fn) => fn(), }); serviceRegistry.add("tooltip", tooltipService); await makeView({ type: "kanban", resModel: "partner", serverData, arch: ` `, }); assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped"); assert.containsN(target, ".o_column_title", 2); await mouseEnter( target, ".o_kanban_group:first-child .o_kanban_header_title .o_column_title" ); assert.containsOnce(target, ".o-tooltip"); await click( target, ".o_kanban_group:first-child .o_kanban_header_title .o_kanban_quick_add" ); assert.containsNone(target, ".o-tooltip"); await mouseEnter( target, ".o_kanban_group:first-child .o_kanban_header_title .o_column_title" ); assert.containsOnce(target, ".o-tooltip"); await click(target, ".o_kanban_group:first-child .o_kanban_header_title .fa-gear"); await nextTick(); assert.containsNone(target, ".o-tooltip"); }); QUnit.test("generic tags are case insensitive", async function (assert) { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
Hello
`, }); assert.containsN(target, "div.test", 4); }); QUnit.test("display full is supported on fields", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); assert.containsOnce(target.querySelector(".o_kanban_record"), "span.o_text_block"); assert.strictEqual(target.querySelector("span.o_text_block").textContent, "yop"); }); QUnit.test("dropdown without toggler are correctly rendered", async (assert) => { serverData.models.partner.records = [serverData.models.partner.records[0]]; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); assert.containsOnce(target, ".o-dropdown.dropdown.o_dropdown_kanban"); await click(target, ".o_kanban_record .o-dropdown button.dropdown-toggle"); assert.containsOnce( target, ".o_kanban_record .o-dropdown .o-dropdown--menu > div > a.someItem" ); }); QUnit.test("basic grouped rendering", async (assert) => { assert.expect(14); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], async mockRPC(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"); } }, }); assert.hasClass( target.querySelector(".o_kanban_group"), "bg-100", "o_kanban_group should have a background" ); assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped"); assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_test"); assert.containsN(target, ".o_kanban_group", 2); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_record"); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 3); await toggleColumnActions(0); // check available actions in kanban header's config dropdown assert.containsOnce( target, ".o_kanban_header:first-child .o_kanban_config .o_kanban_toggle_fold" ); assert.containsNone(target, ".o_kanban_header:first-child .o_kanban_config .o_column_edit"); assert.containsNone( target, ".o_kanban_header:first-child .o_kanban_config .o_column_delete" ); assert.containsNone( target, ".o_kanban_header:first-child .o_kanban_config .o_column_archive_records" ); assert.containsNone( target, ".o_kanban_header:first-child .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 validateSearch(target); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 3); }); QUnit.test("basic grouped rendering with no record", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], }); assert.containsOnce(target, ".o_kanban_grouped"); assert.containsOnce(target, ".o_view_nocontent"); assert.containsOnce( target, ".o-kanban-button-new", "There should be a 'New' button even though there is no column when groupby is not a many2one" ); }); QUnit.test( "basic grouped rendering with active field (archivable by default)", async (assert) => { // add active field on partner model and make all records active serverData.models.partner.fields.active = { string: "Active", type: "char", default: true, }; patchDialog((_cls, props) => { assert.step("open-dialog"); props.confirm(); }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '' + '
' + "
", groupBy: ["bar"], }); const clickColumnAction = await toggleColumnActions(1); // check archive/restore all actions in kanban header's config dropdown assert.containsOnce( target, ".o_kanban_group:last-child .o_kanban_header .o_kanban_config .o_column_archive_records" ); assert.containsOnce( target, ".o_kanban_group:last-child .o_kanban_header .o_kanban_config .o_column_unarchive_records" ); assert.containsN(target, ".o_kanban_group", 2); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_record"); assert.containsN(target, ".o_kanban_group:last-child .o_kanban_record", 3); assert.verifySteps([]); await clickColumnAction("Archive All"); assert.containsN(target, ".o_kanban_group", 2); assert.containsOnce(getColumn(0), ".o_kanban_record"); assert.containsNone(getColumn(1), ".o_kanban_record"); assert.verifySteps(["open-dialog"]); } ); QUnit.test("empty group when grouped by date", async (assert) => { serverData.models.partner.records[0].date = "2017-01-08"; serverData.models.partner.records[1].date = "2017-02-09"; serverData.models.partner.records[2].date = "2017-02-08"; serverData.models.partner.records[3].date = "2017-02-10"; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["date:month"], }); serverData.models.partner.records.shift(); // remove only record of the first group await reload(kanban, { groupBy: ["date:month"] }); assert.containsN(target, ".o_kanban_group", 2); assert.containsNone(getColumn(0), ".o_kanban_record"); assert.containsN(getColumn(1), ".o_kanban_record", 3); }); QUnit.test( "Ensure float fields are formatted properly without using a widget", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); // Would display 0.40 if digits attr is not applied assert.strictEqual(target.querySelector(".o_kanban_record").innerText, "0.40000"); } ); QUnit.test( "basic grouped rendering with active field and archive enabled (archivable true)", async (assert) => { // add active field on partner model and make all records active serverData.models.partner.fields.active = { string: "Active", type: "char", default: true, }; patchDialog((_cls, props) => { assert.step("open-dialog"); props.confirm(); }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '' + '
' + "
", groupBy: ["bar"], }); const clickColumnAction = await toggleColumnActions(0); // check archive/restore all actions in kanban header's config dropdown assert.containsOnce( target, ".o_kanban_header:first-child .o_kanban_config .o_column_archive_records" ); assert.containsOnce( target, ".o_kanban_header:first-child .o_kanban_config .o_column_unarchive_records" ); assert.containsN(target, ".o_kanban_group", 2); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_record"); assert.containsN(target, ".o_kanban_group:last-child .o_kanban_record", 3); assert.verifySteps([]); await clickColumnAction("Archive All"); assert.containsN(target, ".o_kanban_group", 2); assert.containsNone(getColumn(0), ".o_kanban_record"); assert.containsN(getColumn(1), ".o_kanban_record", 3); assert.verifySteps(["open-dialog"]); } ); QUnit.test( "basic grouped rendering with active field and hidden archive buttons (archivable false)", async (assert) => { // add active field on partner model and make all records active serverData.models.partner.fields.active = { string: "Active", type: "char", default: true, }; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '' + '
' + "
", groupBy: ["bar"], }); await toggleColumnActions(0); // check archive/restore all actions in kanban header's config dropdown assert.containsNone( target, ".o_kanban_header:first-child .o_kanban_config .o_column_archive_records" ); assert.containsNone( target, ".o_kanban_header:first-child .o_kanban_config .o_column_unarchive_records" ); } ); QUnit.test( "m2m grouped rendering with active field and archive enabled (archivable true)", async (assert) => { // add active field on partner model and make all records active serverData.models.partner.fields.active = { string: "Active", type: "char", default: true, }; // more many2many data serverData.models.partner.records[0].category_ids = [6, 7]; serverData.models.partner.records[3].foo = "blork"; serverData.models.partner.records[3].category_ids = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["category_ids"], }); assert.containsN(target, ".o_kanban_group", 3); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 2); assert.deepEqual( [...target.querySelectorAll(".o_kanban_group")].map((el) => el.innerText.replace(/\s/g, " ") ), ["None (1)", "gold yop blip", "silver yop gnap"] ); await click(getColumn(0)); await toggleColumnActions(0); // 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( target, ".o_kanban_header .o_kanban_config .o_column_archive_records", "should not be able to archive all the records" ); assert.containsNone( target, ".o_kanban_header .o_kanban_config .o_column_unarchive_records", "should not be able to unarchive all the records" ); } ); QUnit.test("kanban grouped by date field", async (assert) => { serverData.models.partner.records[0].date = "2007-06-10"; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["date"], }); assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_column_title")), [ "None", "June 2007", ]); }); QUnit.test("context can be used in kanban template", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, context: { some_key: 1 }, domain: [["id", "=", 1]], }); assert.containsOnce(target, ".o_kanban_record:not(.o_kanban_ghost)"); assert.containsOnce( target, ".o_kanban_record span:contains(yop)", "condition in the kanban template should have been correctly evaluated" ); }); QUnit.test("user context can be used in kanban template", async (assert) => { const fakeUserService = { start() { return { context: { some_key: true } }; }, }; serviceRegistry.add("user", fakeUserService, { force: true }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, domain: [["id", "=", 1]], }); assert.containsOnce(target, ".o_kanban_record:not(.o_kanban_ghost)"); assert.containsOnce( target, ".o_kanban_record span:contains(yop)", "condition in the kanban template should have been correctly evaluated" ); }); QUnit.test("kanban with sub-template", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); assert.deepEqual( getNodesTextContent(target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")), ["yop", "blip", "gnap", "blip"] ); }); QUnit.test("kanban with t-set outside card", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); assert.deepEqual( getNodesTextContent(target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")), ["10", "9", "17", "-4"] ); }); QUnit.test("kanban with t-if/t-else on field", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
Negative value
`, }); assert.deepEqual( getNodesTextContent(target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")), ["10", "9", "17", "Negative value"] ); }); QUnit.test("kanban with t-if/t-else on field with widget", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
Negative value
`, }); assert.deepEqual( getNodesTextContent(target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")), ["10", "9", "17", "Negative value"] ); }); QUnit.test("field with widget and attributes in kanban", async (assert) => { assert.expect(1); class MyField extends Component { setup() { if (this.props.record.resId === 1) { assert.deepEqual(this.props.attrs, { name: "int_field", widget: "my_field", str: "some string", bool: "true", num: "4.5", options: {}, field_id: "int_field", }); } } } MyField.template = xml``; MyField.extractProps = ({ attrs }) => ({ attrs }); registry.category("fields").add("my_field", MyField); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); }); QUnit.test("field with widget and dynamic attributes in kanban", async (assert) => { class MyField extends Component {} MyField.template = xml``; MyField.extractProps = ({ attrs }) => { assert.step( `${attrs["dyn-bool"]}/${attrs["interp-str"]}/${attrs["interp-str2"]}/${attrs["interp-str3"]}` ); }; registry.category("fields").add("my_field", MyField); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); assert.verifySteps([ "false/hello yop/hello yop !/hello yop }}", "true/hello blip/hello blip !/hello blip }}", "true/hello gnap/hello gnap !/hello gnap }}", "true/hello blip/hello blip !/hello blip }}", ]); }); QUnit.test("view button and string interpolated attribute in kanban", async (assert) => { patchWithCleanup(ViewButton.prototype, { setup() { this._super(); assert.step( `[${this.props.clickParams["name"]}] className: '${this.props.className}'` ); }, }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); assert.verifySteps([ "[one] className: 'hola oe_kanban_action oe_kanban_action_a'", "[two] className: 'hola oe_kanban_action oe_kanban_action_a hello'", "[sri] className: 'hola oe_kanban_action oe_kanban_action_a yop'", "[foa] className: 'hola oe_kanban_action oe_kanban_action_a yop olleh'", "[fye] className: 'hola oe_kanban_action oe_kanban_action_a hello yop'", "[one] className: 'hola oe_kanban_action oe_kanban_action_a'", "[two] className: 'hola oe_kanban_action oe_kanban_action_a hello'", "[sri] className: 'hola oe_kanban_action oe_kanban_action_a blip'", "[foa] className: 'hola oe_kanban_action oe_kanban_action_a blip olleh'", "[fye] className: 'hola oe_kanban_action oe_kanban_action_a hello blip'", "[one] className: 'hola oe_kanban_action oe_kanban_action_a'", "[two] className: 'hola oe_kanban_action oe_kanban_action_a hello'", "[sri] className: 'hola oe_kanban_action oe_kanban_action_a gnap'", "[foa] className: 'hola oe_kanban_action oe_kanban_action_a gnap olleh'", "[fye] className: 'hola oe_kanban_action oe_kanban_action_a hello gnap'", "[one] className: 'hola oe_kanban_action oe_kanban_action_a'", "[two] className: 'hola oe_kanban_action oe_kanban_action_a hello'", "[sri] className: 'hola oe_kanban_action oe_kanban_action_a blip'", "[foa] className: 'hola oe_kanban_action oe_kanban_action_a blip olleh'", "[fye] className: 'hola oe_kanban_action oe_kanban_action_a hello blip'", ]); }); QUnit.test("kanban with kanban-tooltip template", async (assert) => { serviceRegistry.add("tooltip", tooltipService); let simulateTimeout; patchWithCleanup(browser, { setTimeout: (fn) => { simulateTimeout = fn; }, }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); assert.deepEqual( getNodesTextContent(target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")), ["yop", "blip", "gnap", "blip"] ); assert.containsNone(target, ".o_popover_container .o_popover"); target.querySelector(".o_kanban_record").dispatchEvent(new Event("mouseenter")); await nextTick(); assert.containsNone(target, ".o_popover_container .o_popover"); simulateTimeout(); await nextTick(); assert.containsOnce(target, ".o_popover_container .o_popover"); assert.strictEqual( target.querySelector(".o_popover_container .o_popover").innerText, "yop" ); target.querySelector(".o_kanban_record").dispatchEvent(new Event("mouseleave")); await nextTick(); assert.containsNone(target, ".o_popover_container .o_popover"); }); QUnit.test("pager should be hidden in grouped mode", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
", groupBy: ["bar"], }); assert.containsNone(target, ".o_pager"); }); QUnit.test("there should be no limit on the number of fetched groups", async (assert) => { patchWithCleanup(KanbanDynamicGroupList, { DEFAULT_LIMIT: 1 }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group", 2, "there should be 2 groups"); }); QUnit.test("pager, ungrouped, with default limit", async (assert) => { assert.expect(3); await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '
' + "
", async mockRPC(route, { method, kwargs }) { if (method === "web_search_read") { assert.strictEqual(kwargs.limit, 40, "default limit should be 40 in Kanban"); } }, }); assert.containsOnce(target, ".o_pager"); assert.deepEqual(getPagerValue(target), [1, 4]); }); QUnit.test("pager, ungrouped, with limit given in options", async (assert) => { assert.expect(3); await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '
' + "
", async mockRPC(route, { method, kwargs }) { if (method === "web_search_read") { assert.strictEqual(kwargs.limit, 2); } }, limit: 2, }); assert.deepEqual(getPagerValue(target), [1, 2]); assert.strictEqual(getPagerLimit(target), 4); }); QUnit.test("pager, ungrouped, with limit set on arch and given in options", async (assert) => { assert.expect(3); // the limit given in the arch should take the priority over the one given in options await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
", async mockRPC(route, { method, kwargs }) { if (method === "web_search_read") { assert.strictEqual(kwargs.limit, 3); } }, limit: 2, }); assert.deepEqual(getPagerValue(target), [1, 3]); assert.strictEqual(getPagerLimit(target), 4); }); QUnit.test("pager, ungrouped, with count limit reached", async (assert) => { patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, async mockRPC(route, { method }) { assert.step(method); }, }); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); assert.verifySteps(["get_views", "web_search_read"]); await click(target.querySelector(".o_pager_limit")); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4"); assert.verifySteps(["search_count"]); }); QUnit.test("pager, ungrouped, with count limit reached, click next", async (assert) => { patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, async mockRPC(route, { method }) { assert.step(method); }, }); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); assert.verifySteps(["get_views", "web_search_read"]); await click(target.querySelector(".o_pager_next")); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "3-4"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4"); assert.verifySteps(["web_search_read"]); }); QUnit.test("pager, ungrouped, with count limit reached, click next (2)", async (assert) => { patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); serverData.models.partner.records.push({ id: 5, foo: "xxx" }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, async mockRPC(route, { method }) { assert.step(method); }, }); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); assert.verifySteps(["get_views", "web_search_read"]); await click(target.querySelector(".o_pager_next")); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "3-4"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4+"); assert.verifySteps(["web_search_read"]); await click(target.querySelector(".o_pager_next")); assert.containsOnce(target, ".o_kanban_record:not(.o_kanban_ghost)"); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "5-5"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "5"); assert.verifySteps(["web_search_read"]); }); QUnit.test("pager, ungrouped, with count limit reached, click previous", async (assert) => { patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); serverData.models.partner.records.push({ id: 5, foo: "xxx" }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, async mockRPC(route, { method }) { assert.step(method); }, }); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); assert.verifySteps(["get_views", "web_search_read"]); await click(target.querySelector(".o_pager_previous")); assert.containsOnce(target, ".o_kanban_record:not(.o_kanban_ghost)"); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "5-5"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "5"); assert.verifySteps(["search_count", "web_search_read"]); }); QUnit.test("pager, ungrouped, with count limit reached, edit pager", async (assert) => { patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); serverData.models.partner.records.push({ id: 5, foo: "xxx" }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, async mockRPC(route, { method }) { assert.step(method); }, }); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); assert.verifySteps(["get_views", "web_search_read"]); await click(target, ".o_pager_value"); await editInput(target, "input.o_pager_value", "2-4"); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 3); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "2-4"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4+"); assert.verifySteps(["web_search_read"]); await click(target, ".o_pager_value"); await editInput(target, "input.o_pager_value", "2-14"); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 4); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "2-5"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "5"); assert.verifySteps(["web_search_read"]); }); QUnit.test("count_limit attrs set in arch", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, async mockRPC(route, { method }) { assert.step(method); }, }); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); assert.verifySteps(["get_views", "web_search_read"]); await click(target.querySelector(".o_pager_limit")); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4"); assert.verifySteps(["search_count"]); }); QUnit.test( "pager, ungrouped, deleting all records from last page should move to previous page", async (assert) => { patchDialog((_cls, props) => { assert.step("open-dialog"); props.confirm(); }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
Delete
`, }); assert.deepEqual(getPagerValue(target), [1, 3]); assert.strictEqual(getPagerLimit(target), 4); // move to next page await pagerNext(target); assert.deepEqual(getPagerValue(target), [4, 4]); // delete a record await click(target, ".o_kanban_record a"); assert.verifySteps(["open-dialog"]); assert.deepEqual(getPagerValue(target), [1, 3]); assert.strictEqual(getPagerLimit(target), 3); } ); QUnit.test("pager, update calls onUpdatedPager before the render", async (assert) => { assert.expect(8); class TestKanbanController extends KanbanController { setup() { super.setup(); onWillRender(() => { assert.step("render"); }); } async onUpdatedPager() { assert.step("onUpdatedPager"); } } viewRegistry.add("test_kanban_view", { ...kanbanView, Controller: TestKanbanController, }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, limit: 3, }); assert.deepEqual(getPagerValue(target), [1, 3]); assert.strictEqual(getPagerLimit(target), 4); assert.step("next page"); await click(target.querySelector(".o_pager_next")); assert.deepEqual(getPagerValue(target), [4, 4]); assert.verifySteps(["render", "next page", "onUpdatedPager", "render"]); }); QUnit.test("click on a button type='delete' to delete a record in a column", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
Delete
`, groupBy: ["product_id"], }); let column = getColumn(); assert.containsN(column, ".o_kanban_record", 2); assert.containsNone(column, ".o_kanban_load_more"); await click(column.querySelector(".o_kanban_record .o_delete")); assert.containsOnce(target, ".modal"); await click(target, ".modal .btn-primary"); column = getColumn(); assert.containsOnce(column, ".o_kanban_record"); assert.containsNone(column, ".o_kanban_load_more"); }); QUnit.test("kanban with an action id as on_create attrs", async (assert) => { const actionService = { start() { return { doAction: (action, options) => { // simplified flow in this test: simulate a target new action which // creates a record and closes itself assert.step(`doAction ${action}`); serverData.models.partner.records.push({ id: 299, foo: "new" }); options.onClose(); }, }; }, }; registry.category("services").add("action", actionService, { force: true }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, mockRPC(route, args) { assert.step(args.method); }, }); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 4); await createRecord(); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 5); assert.verifySteps([ "get_views", "web_search_read", "doAction some.action", "web_search_read", ]); }); QUnit.test("grouped kanban with quick_create attrs set to false", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], createRecord: () => assert.step("create record"), }); assert.containsN(target, ".o_kanban_group", 2); assert.containsNone(target, ".o_kanban_quick_add"); await click(target.querySelector(".o-kanban-button-new")); assert.containsNone(target, ".o_kanban_quick_create"); assert.verifySteps(["create record"]); }); QUnit.test("create in grouped on m2o", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group.o_group_draggable", 2); assert.containsOnce(target, ".btn-primary.o-kanban-button-new"); assert.containsOnce(target, ".o_column_quick_create"); await createRecord(); assert.containsOnce(target, ".o_kanban_group:first-child > .o_kanban_quick_create"); assert.strictEqual(target.querySelector(".o_column_title").innerText, "hello"); }); QUnit.test("create in grouped on char", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["foo"], }); assert.containsNone(target, ".o_kanban_group.o_group_draggable"); assert.containsN(target, ".o_kanban_group", 3); assert.strictEqual(target.querySelector(".o_column_title").innerText, "blip"); assert.containsNone(target, ".o_kanban_group:first-child > .o_kanban_quick_create"); }); QUnit.test("prevent deletion when grouped by many2many field", async (assert) => { serverData.models.partner.records[0].category_ids = [6, 7]; serverData.models.partner.records[3].category_ids = [7]; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
delete
`, groupBy: ["category_ids"], }); assert.containsNone(target, ".thisisdeletable", "records should not be deletable"); await reload(kanban, { groupBy: ["foo"] }); assert.containsN(target, ".thisisdeletable", 4, "records should be deletable"); }); QUnit.test("quick created records in grouped kanban are on displayed top", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group", 2); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 2); await createRecord(); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 2); assert.containsOnce(target.querySelector(".o_kanban_group"), ".o_kanban_quick_create"); await editInput(target, ".o_field_widget[name=display_name] input", "new record"); await click(target, ".o_kanban_add"); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 3); assert.containsOnce(target.querySelector(".o_kanban_group"), ".o_kanban_quick_create"); // the new record must be the first record of the column assert.strictEqual(target.querySelector(".o_kanban_record").innerText, "new record"); await editInput(target, ".o_field_widget[name=display_name] input", "another record"); await click(target, ".o_kanban_add"); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 4); assert.containsOnce(target.querySelector(".o_kanban_group"), ".o_kanban_quick_create"); // the new record must be the first record of the column assert.strictEqual(target.querySelector(".o_kanban_record").innerText, "another record"); assert.strictEqual(target.querySelectorAll(".o_kanban_record")[1].innerText, "new record"); }); QUnit.test("quick create record without quick_create_view", async (assert) => { assert.expect(16); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], async mockRPC(route, { args, method }) { assert.step(method || route); if (method === "name_create") { assert.strictEqual(args[0], "new partner"); } }, }); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_record"); // click on 'Create' -> should open the quick create in the first column await createRecord(); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_quick_create"); const quickCreate = target.querySelector( ".o_kanban_group:first-child .o_kanban_quick_create" ); assert.containsOnce(quickCreate, ".o_form_view.o_xxs_form_view"); assert.containsOnce(quickCreate, "input"); assert.containsOnce( quickCreate, ".o_field_widget.o_required_modifier input[placeholder=Title]" ); // fill the quick create and validate await editQuickCreateInput("display_name", "new partner"); await validateRecord(); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.verifySteps([ "get_views", "web_read_group", // initial read_group "web_search_read", // initial search_read (first column) "web_search_read", // initial search_read (second column) "onchange", // quick create "name_create", // should perform a name_create to create the record "onchange", // reopen the quick create automatically "read", // read the created record ]); }); QUnit.test("quick create record with quick_create_view", async (assert) => { assert.expect(20); serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + '' + ""; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], async mockRPC(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.containsOnce(target, ".o_control_panel", "should have one control panel"); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_record"); // click on 'Create' -> should open the quick create in the first column await createRecord(); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_quick_create"); const quickCreate = target.querySelector( ".o_kanban_group:first-child .o_kanban_quick_create" ); assert.containsOnce(quickCreate, ".o_form_view.o_xxs_form_view"); assert.containsOnce( target, ".o_control_panel", "should not have instantiated an extra control panel" ); assert.containsN(quickCreate, "input", 2); assert.containsN(quickCreate, ".o_field_widget", 3, "should have rendered three widgets"); // fill the quick create and validate await editQuickCreateInput("foo", "new partner"); await editQuickCreateInput("int_field", 4); await click(quickCreate, ".o_field_widget[name=state] .o_priority_star:first-child"); await validateRecord(); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.verifySteps([ "get_views", "web_read_group", // initial read_group "web_search_read", // initial search_read (first column) "web_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", "onchange", // new quick create "read", // read the created record ]); }); QUnit.test("quick create record flickering", async (assert) => { let def; serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + '' + ""; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], async mockRPC(route, args) { if (args.method === "create") { assert.deepEqual( args.args[0], { foo: "new partner", int_field: 4, state: "def", }, "should send the correct values" ); } if (args.method === "onchange") { await def; } }, }); // click on 'Create' -> should open the quick create in the first column await createRecord(); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 1); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_quick_create"); const quickCreate = target.querySelector( ".o_kanban_group:first-child .o_kanban_quick_create" ); assert.containsOnce(quickCreate, ".o_form_view.o_xxs_form_view"); assert.containsN(quickCreate, "input", 2); assert.containsN(quickCreate, ".o_field_widget", 3, "should have rendered three widgets"); // fill the quick create and validate await editQuickCreateInput("foo", "new partner"); await editQuickCreateInput("int_field", 4); await click(quickCreate, ".o_field_widget[name=state] .o_priority_star:first-child"); def = makeDeferred(); await validateRecord(); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 1); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_quick_create"); def.resolve(); await nextTick(); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_quick_create"); }); QUnit.test( "quick create record should focus default field [REQUIRE FOCUS]", async function (assert) { serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + '' + ""; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], }); await click(target, ".o-kanban-button-new"); assert.strictEqual( document.activeElement, target.querySelector(".o_field_widget[name=int_field] input") ); } ); QUnit.test( "quick create record should focus first field input [REQUIRE FOCUS]", async function (assert) { serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + '' + ""; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], }); await click(target, ".o-kanban-button-new"); assert.strictEqual( document.activeElement, target.querySelector(".o_field_widget[name=foo] input") ); } ); QUnit.test("quick_create_view without quick_create option", async (assert) => { serverData.views["partner,some_view_ref,form"] = `
`; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], createRecord() { assert.step("create record"); }, }); assert.containsN(target, ".o_kanban_group", 2); assert.containsN(target, ".o_kanban_group .o_kanban_quick_add", 2); // click on 'Create' in control panel -> should not open the quick create await createRecord(); assert.containsNone(target, ".o_kanban_quick_create"); assert.verifySteps(["create record"]); // click "+" icon in first column -> should open the quick create await click(target.querySelector(".o_kanban_quick_add")); assert.containsOnce(target.querySelector(".o_kanban_group"), ".o_kanban_quick_create"); assert.verifySteps([]); }); QUnit.test("quick create record in grouped on m2o (no quick_create_view)", async (assert) => { assert.expect(14); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC(route, { method, args, kwargs }) { assert.step(method || route); if (method === "name_create") { assert.strictEqual(args[0], "new partner"); const { default_product_id, default_qux } = kwargs.context; assert.strictEqual(default_product_id, 3); assert.strictEqual(default_qux, 2.5); } }, context: { default_qux: 2.5 }, }); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "first column should contain two records" ); // click on 'Create', fill the quick create and validate await createRecord(); await editQuickCreateInput("display_name", "new partner"); await validateRecord(); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 3, "first column should contain three records" ); assert.verifySteps([ "get_views", "web_read_group", // initial read_group "web_search_read", // initial search_read (first column) "web_search_read", // initial search_read (second column) "onchange", // quick create "name_create", // should perform a name_create to create the record "onchange", // reopen the quick create automatically "read", // read the created record ]); }); QUnit.test("quick create record in grouped on m2o (with quick_create_view)", async (assert) => { assert.expect(16); serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + '' + ""; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["product_id"], async mockRPC(route, { method, args, kwargs }) { assert.step(method || route); if (method === "create") { assert.deepEqual( args[0], { foo: "new partner", int_field: 4, state: "def", }, "should send the correct values" ); const { default_product_id, default_qux } = kwargs.context; assert.strictEqual(default_product_id, 3); assert.strictEqual(default_qux, 2.5); } }, context: { default_qux: 2.5 }, }); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); // click on 'Create', fill the quick create and validate await createRecord(); const quickCreate = target.querySelector( ".o_kanban_group:first-child .o_kanban_quick_create" ); await editQuickCreateInput("foo", "new partner"); await editQuickCreateInput("int_field", 4); await click(quickCreate, ".o_field_widget[name=state] .o_priority_star:first-child"); await validateRecord(); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 3); assert.verifySteps([ "get_views", "web_read_group", // initial read_group "web_search_read", // initial search_read (first column) "web_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", "onchange", // reopen the quick create automatically "read", // read the created record ]); }); QUnit.test("quick create record validation: stays open when invalid", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], async mockRPC(route, { method }) { assert.step(method || route); }, }); assert.verifySteps(["get_views", "web_read_group", "web_search_read", "web_search_read"]); await createRecord(); assert.verifySteps(["onchange"]); // do not fill anything and validate await validateRecord(); assert.verifySteps([]); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_quick_create"); assert.hasClass(target.querySelector("[name=display_name]"), "o_field_invalid"); assert.containsOnce(target, ".o_notification_manager .o_notification"); assert.equal( target.querySelector(".o_notification").textContent, "Invalid fields: Display Name" ); }); QUnit.test("quick create record with default values and onchanges", async (assert) => { serverData.models.partner.fields.int_field.default = 4; serverData.models.partner.onchanges = { foo(obj) { if (obj.foo) { obj.int_field = 8; } }, }; serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + ""; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], async mockRPC(route, { method }) { assert.step(method || route); }, }); // click on 'Create' -> should open the quick create in the first column await createRecord(); const quickCreate = target.querySelector( ".o_kanban_group:first-child .o_kanban_quick_create" ); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_quick_create"); assert.strictEqual( quickCreate.querySelector(".o_field_widget[name=int_field] input").value, "4", "default value should be set" ); // fill the 'foo' field -> should trigger the onchange await editQuickCreateInput("foo", "new partner"); assert.strictEqual( quickCreate.querySelector(".o_field_widget[name=int_field] input").value, "8", "onchange should have been triggered" ); assert.verifySteps([ "get_views", "web_read_group", // initial read_group "web_search_read", // initial search_read (first column) "web_search_read", // initial search_read (second column) "get_views", // form view in quick create "onchange", // quick create "onchange", // onchange due to 'foo' field change ]); }); QUnit.test("quick create record with quick_create_view: modifiers", async (assert) => { serverData.views["partner,some_view_ref,form"] = "
" + '' + '"; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], }); // create a new record await quickCreateRecord(); assert.hasClass( target.querySelector(".o_kanban_quick_create .o_field_widget[name=foo]"), "o_required_modifier", "foo field should be required" ); assert.containsNone( target, ".o_kanban_quick_create .o_field_widget[name=int_field]", "int_field should be invisible" ); // fill 'foo' field await editQuickCreateInput("foo", "new partner"); assert.containsOnce( target, ".o_kanban_quick_create .o_field_widget[name=int_field]", "int_field should now be visible" ); }); QUnit.test("quick create record with onchange of field marked readonly", async (assert) => { assert.expect(15); serverData.models.partner.onchanges = { foo(obj) { obj.int_field = 8; }, }; serverData.views["partner,some_view_ref,form"] = ` `; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], async mockRPC(route, { method, args }) { if (method === "create") { assert.notOk( "int_field" in args[0], "readonly field shouldn't be sent in create" ); } assert.step(method || route); }, }); assert.verifySteps([ "get_views", "web_read_group", // initial read_group "web_search_read", // initial search_read (first column) "web_search_read", // initial search_read (second column) ]); // click on 'Create' -> should open the quick create in the first column await quickCreateRecord(); assert.verifySteps(["get_views", "onchange"]); // fill the 'foo' field -> should trigger the onchange await editQuickCreateInput("foo", "new partner"); assert.verifySteps(["onchange"]); await validateRecord(); assert.verifySteps(["create", "onchange", "read"]); }); QUnit.test("quick create record and change state in grouped mode", async (assert) => { serverData.models.partner.fields.kanban_state = { string: "Kanban State", type: "selection", selection: [ ["normal", "Grey"], ["done", "Green"], ["blocked", "Red"], ], }; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["foo"], }); // Quick create kanban record await quickCreateRecord(); await editQuickCreateInput("display_name", "Test"); await validateRecord(); // Select state in kanban await click(getCard(0), ".o_status"); await click(getCard(0), ".o_field_state_selection .dropdown-item:first-child"); assert.hasClass( target.querySelector(".o_status"), "o_status_green", "Kanban state should be done (Green)" ); }); QUnit.test("window resize should not change quick create form size", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, groupBy: ["bar"], arch: `
`, }); await quickCreateRecord(); assert.hasClass( target.querySelector(".o_kanban_quick_create .o_form_view"), "o_xxs_form_view" ); await triggerEvent(window, "", "resize"); assert.hasClass( target.querySelector(".o_kanban_quick_create .o_form_view"), "o_xxs_form_view" ); }); QUnit.test( "quick create record: cancel and validate without using the buttons", async (assert) => { serverData.views["partner,some_view_ref,form"] = `
`; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], }); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 4); // click to add an element and cancel the quick creation by pressing ESC await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create"); await triggerEvent(target, ".o_kanban_quick_create input", "keydown", { key: "Escape", }); assert.containsNone( target, ".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 quickCreateRecord(); await click(target, ".o_kanban_group:first-child .o_kanban_record:last-of-type"); assert.containsNone( target, ".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 quickCreateRecord(); await triggerEvent(target, ".o_kanban_quick_create input", "mousedown"); await click(target, ".o_kanban_group:first-child .o_kanban_record:last-of-type"); assert.containsOnce( target, ".o_kanban_quick_create", "the quick create should not have been destroyed after clicking outside" ); // click to really add an element await quickCreateRecord(); await editQuickCreateInput("foo", "new partner"); // clicking outside should no longer destroy the quick create as it is dirty await click(target, ".o_kanban_group:first-child .o_kanban_record:last-of-type"); assert.containsOnce( target, ".o_kanban_quick_create", "the quick create should not have been destroyed" ); // confirm by pressing ENTER await triggerEvent(target, ".o_kanban_quick_create input", "keydown", { key: "Enter", }); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 5); assert.deepEqual(getCardTexts(0), ["new partner", "blip"]); } ); QUnit.test("quick create record: validate with ENTER", async (assert) => { serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + ""; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], }); assert.containsN(target, ".o_kanban_record", 4, "should have 4 records at the beginning"); // add an element and confirm by pressing ENTER await quickCreateRecord(); await editQuickCreateInput("foo", "new partner"); await validateRecord(); // triggers a navigation event, leading to the 'commitChanges' and record creation assert.containsN(target, ".o_kanban_record", 5, "should have created a new record"); assert.strictEqual( target.querySelector(".o_kanban_quick_create .o_field_widget[name=foo] input").value, "", "quick create should now be empty" ); }); QUnit.test("quick create record: prevent multiple adds with ENTER", async (assert) => { serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + ""; const prom = makeDeferred(); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], async mockRPC(route, args) { if (args.method === "create") { assert.step("create"); await prom; } }, }); assert.containsN(target, ".o_kanban_record", 4, "should have 4 records at the beginning"); // add an element and press ENTER twice await quickCreateRecord(); await editQuickCreateInput("foo", "new partner"); await triggerEvent( target, ".o_kanban_quick_create .o_field_widget[name=foo] input", "keydown", { key: "Enter", } ); assert.containsN(target, ".o_kanban_record", 4, "should not have created the record yet"); assert.strictEqual( target.querySelector(".o_kanban_quick_create .o_field_widget[name=foo] input").value, "new partner", "quick create should not be empty yet" ); assert.hasClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be disabled" ); prom.resolve(); await nextTick(); assert.containsN(target, ".o_kanban_record", 5, "should have created a new record"); assert.strictEqual( target.querySelector(".o_kanban_quick_create .o_field_widget[name=foo] input").value, "", "quick create should now be empty" ); assert.doesNotHaveClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be enabled" ); assert.verifySteps(["create"]); }); QUnit.test("quick create record: prevent multiple adds with Add clicked", async (assert) => { serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + ""; const prom = makeDeferred(); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], async mockRPC(route, { method }) { if (method === "create") { assert.step("create"); await prom; } }, }); assert.containsN(target, ".o_kanban_record", 4, "should have 4 records at the beginning"); // add an element and click 'Add' twice await quickCreateRecord(); await editQuickCreateInput("foo", "new partner"); await validateRecord(); await validateRecord(); assert.containsN(target, ".o_kanban_record", 4, "should not have created the record yet"); assert.strictEqual( target.querySelector(".o_kanban_quick_create .o_field_widget[name=foo] input").value, "new partner", "quick create should not be empty yet" ); assert.hasClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be disabled" ); prom.resolve(); await nextTick(); assert.containsN(target, ".o_kanban_record", 5, "should have created a new record"); assert.strictEqual( target.querySelector(".o_kanban_quick_create .o_field_widget[name=foo] input").value, "", "quick create should now be empty" ); assert.doesNotHaveClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be enabled" ); assert.verifySteps(["create"]); }); QUnit.test( "save a quick create record and create a new record at the same time", async (assert) => { const prom = makeDeferred(); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], async mockRPC(route, { method }) { if (method === "name_create") { assert.step("name_create"); await prom; } }, }); assert.containsN( target, ".o_kanban_record", 4, "should have 4 records at the beginning" ); // Create and save a record await quickCreateRecord(); await editQuickCreateInput("display_name", "new partner"); await validateRecord(); assert.containsN( target, ".o_kanban_record", 4, "should not have created the record yet" ); assert.strictEqual( target.querySelector(".o_kanban_quick_create [name=display_name] input").value, "new partner", "quick create should not be empty yet" ); assert.hasClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be disabled" ); // Create a new record during the save of the first one await createRecord(); assert.containsN( target, ".o_kanban_record", 4, "should not have created the record yet" ); assert.strictEqual( target.querySelector(".o_kanban_quick_create [name=display_name] input").value, "new partner", "quick create should not be empty yet" ); assert.hasClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be disabled" ); prom.resolve(); await nextTick(); assert.containsN(target, ".o_kanban_record", 5, "should have created a new record"); assert.strictEqual( target.querySelector( ".o_kanban_quick_create .o_field_widget[name=display_name] input" ).value, "", "quick create should now be empty" ); assert.doesNotHaveClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be enabled" ); assert.verifySteps(["name_create"]); } ); QUnit.test( "quick create record: prevent multiple adds with ENTER, with onchange", async (assert) => { assert.expect(14); serverData.models.partner.onchanges = { foo(obj) { obj.int_field += obj.foo ? 3 : 0; }, }; serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + ""; let shouldDelayOnchange = false; const prom = makeDeferred(); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], async mockRPC(route, { method, args }) { switch (method) { case "onchange": { assert.step(method); if (shouldDelayOnchange) { await prom; } break; } case "create": { assert.step(method); assert.strictEqual(args[0].foo, "new partner"); assert.strictEqual(args[0].int_field, 3); break; } } }, }); assert.containsN( target, ".o_kanban_record", 4, "should have 4 records at the beginning" ); // add an element and press ENTER twice await quickCreateRecord(); shouldDelayOnchange = true; await editQuickCreateInput("foo", "new partner"); await triggerEvent( target, ".o_kanban_quick_create .o_field_widget[name=foo] input", "keydown", { key: "Enter", } ); assert.containsN( target, ".o_kanban_record", 4, "should not have created the record yet" ); assert.strictEqual( target.querySelector(".o_kanban_quick_create .o_field_widget[name=foo] input") .value, "new partner", "quick create should not be empty yet" ); assert.hasClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be disabled" ); prom.resolve(); await nextTick(); assert.containsN(target, ".o_kanban_record", 5, "should have created a new record"); assert.strictEqual( target.querySelector(".o_kanban_quick_create .o_field_widget[name=foo] input") .value, "", "quick create should now be empty" ); assert.doesNotHaveClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be enabled" ); assert.verifySteps([ "onchange", // default_get "onchange", // new partner "create", "onchange", // default_get ]); } ); QUnit.test( "quick create record: click Add to create, with delayed onchange", async (assert) => { assert.expect(13); serverData.models.partner.onchanges = { foo(obj) { obj.int_field += obj.foo ? 3 : 0; }, }; serverData.views["partner,some_view_ref,form"] = "
" + '' + '' + ""; let shouldDelayOnchange = false; const prom = makeDeferred(); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], async mockRPC(route, args) { if (args.method === "onchange") { assert.step("onchange"); if (shouldDelayOnchange) { await prom; } } if (args.method === "create") { assert.step("create"); assert.deepEqual(_.pick(args.args[0], "foo", "int_field"), { foo: "new partner", int_field: 3, }); } }, }); assert.containsN( target, ".o_kanban_record", 4, "should have 4 records at the beginning" ); // add an element and click 'add' await quickCreateRecord(); shouldDelayOnchange = true; await editQuickCreateInput("foo", "new partner"); await validateRecord(); assert.containsN( target, ".o_kanban_record", 4, "should not have created the record yet" ); assert.strictEqual( target.querySelector(".o_kanban_quick_create .o_field_widget[name=foo] input") .value, "new partner", "quick create should not be empty yet" ); assert.hasClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be disabled" ); prom.resolve(); // the onchange returns await nextTick(); assert.containsN(target, ".o_kanban_record", 5, "should have created a new record"); assert.strictEqual( target.querySelector(".o_kanban_quick_create .o_field_widget[name=foo] input") .value, "", "quick create should now be empty" ); assert.doesNotHaveClass( target.querySelector(".o_kanban_quick_create"), "o_disabled", "quick create should be enabled" ); assert.verifySteps([ "onchange", // default_get "onchange", // new partner "create", "onchange", // default_get ]); } ); QUnit.test("quick create when first column is folded", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], }); assert.doesNotHaveClass( target.querySelector(".o_kanban_group:first-child"), "o_column_folded", "first column should not be folded" ); // fold the first column let clickColumnAction = await toggleColumnActions(0); await clickColumnAction("Fold"); assert.hasClass( target.querySelector(".o_kanban_group:first-child"), "o_column_folded", "first column should be folded" ); // click on 'Create' to open the quick create in the first column await createRecord(); assert.doesNotHaveClass( target.querySelector(".o_kanban_group:first-child"), "o_column_folded", "first column should no longer be folded" ); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_quick_create", "should have added a quick create element in first column" ); // fold again the first column clickColumnAction = await toggleColumnActions(0); await clickColumnAction("Fold"); assert.hasClass( target.querySelector(".o_kanban_group:first-child"), "o_column_folded", "first column should be folded" ); assert.containsNone( target, ".o_kanban_quick_create", "there should be no more quick create" ); }); QUnit.test("quick create record: cancel when not dirty", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
", groupBy: ["bar"], }); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "first column should contain one record" ); // click to add an element await quickCreateRecord(); assert.containsOnce( target, ".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 quickCreateRecord(); assert.containsOnce( target, ".o_kanban_quick_create", "should have kept the quick create open" ); // click outside: should remove the quick create await click(target, ".o_kanban_group:first-child .o_kanban_record:last-of-type"); assert.containsNone( target, ".o_kanban_quick_create", "the quick create should not have been destroyed" ); // click to reopen the quick create await quickCreateRecord(); assert.containsOnce( target, ".o_kanban_quick_create", "should have open the quick create widget" ); // press ESC: should remove the quick create await triggerEvent(target, ".o_kanban_quick_create input", "keydown", { key: "Escape" }); assert.containsNone( target, ".o_kanban_quick_create", "quick create widget should have been removed" ); // click to reopen the quick create await quickCreateRecord(); assert.containsOnce( target, ".o_kanban_quick_create", "should have open the quick create widget" ); // click on 'Discard': should remove the quick create await quickCreateRecord(); await discardRecord(); assert.containsNone( target, ".o_kanban_quick_create", "the quick create should be destroyed when the user clicks outside" ); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "first column should still contain one record" ); // click to reopen the quick create await quickCreateRecord(); assert.containsOnce( target, ".o_kanban_quick_create", "should have open the quick create widget" ); // clicking on the quick create itself should keep it open await click(target, ".o_kanban_quick_create"); assert.containsOnce( target, ".o_kanban_quick_create", "the quick create should not have been destroyed when clicked on itself" ); }); QUnit.test("quick create record: cancel when modal is opened", async (assert) => { serverData.views["partner,some_view_ref,form"] = '
'; serverData.views["product,false,form"] = '
'; // patch setTimeout s.t. the autocomplete dropdown opens directly patchWithCleanup(browser, { setTimeout: (fn) => fn(), }); await makeView({ type: "kanban", resModel: "partner", serverData, groupBy: ["bar"], arch: `
`, }); // click to add an element await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create"); await editInput(target, ".o_kanban_quick_create input", "test"); await triggerEvent(target, ".o_kanban_quick_create input", "input"); await click(target, ".o_m2o_dropdown_option_create_edit"); // 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. assert.hasClass(document.body, "modal-open", "modal should be opening after m2o focusout"); await click(document.body); assert.containsOnce( target, ".o_kanban_quick_create", "quick create should stay open while modal is opening" ); }); QUnit.test("quick create record: cancel when dirty", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
", groupBy: ["bar"], }); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "first column should contain one record" ); // click to add an element and edit it await quickCreateRecord(); assert.containsOnce( target, ".o_kanban_quick_create", "should have open the quick create widget" ); await editQuickCreateInput("display_name", "some value"); // click outside: should not remove the quick create await click(target, ".o_kanban_group:first-child .o_kanban_record"); assert.containsOnce( target, ".o_kanban_quick_create", "the quick create should not have been destroyed" ); // press ESC: should remove the quick create await triggerEvent(target, ".o_kanban_quick_create input", "keydown", { key: "Escape" }); assert.containsNone( target, ".o_kanban_quick_create", "quick create widget should have been removed" ); // click to reopen quick create and edit it await quickCreateRecord(); assert.containsOnce( target, ".o_kanban_quick_create", "should have open the quick create widget" ); await editQuickCreateInput("display_name", "some value"); // click on 'Discard': should remove the quick create await discardRecord(); assert.containsNone( target, ".o_kanban_quick_create", "the quick create should be destroyed when the user discard quick creation" ); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "first column should still contain one record" ); }); QUnit.test("quick create record and edit in grouped mode", async (assert) => { assert.expect(5); let newRecordID; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", async mockRPC(route, { args, method }) { if (method === "read") { newRecordID = args[0][0]; } }, groupBy: ["bar"], selectRecord: (resId) => { assert.strictEqual(resId, newRecordID); }, }); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "first column should contain one record" ); // click to add and edit a record await quickCreateRecord(); await editQuickCreateInput("display_name", "new partner"); await editRecord(); assert.strictEqual( serverData.models.partner.records.length, 5, "should have created a partner" ); assert.strictEqual( _.last(serverData.models.partner.records).name, "new partner", "should have correct name" ); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "first column should now contain two records" ); }); QUnit.test("quick create several records in a row", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], }); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "first column should contain one record" ); // click to add an element, fill the input and press ENTER await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create", "the quick create should be open"); await editQuickCreateInput("display_name", "new partner 1"); await validateRecord(); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "first column should now contain two records" ); assert.containsOnce( target, ".o_kanban_quick_create", "the quick create should still be open" ); // create a second element in a row await createRecord(); await editQuickCreateInput("display_name", "new partner 2"); await validateRecord(); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 3, "first column should now contain three records" ); assert.containsOnce( target, ".o_kanban_quick_create", "the quick create should still be open" ); }); QUnit.test("quick create is disabled until record is created and read", async (assert) => { const prom = makeDeferred(); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["bar"], async mockRPC(route, { method }) { if (method === "read") { await prom; } }, }); assert.containsOnce( target, ".o_kanban_group:first-child .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 quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create", "the quick create should be open"); await editQuickCreateInput("display_name", "new partner 1"); await validateRecord(); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "first column should still contain one record" ); assert.containsOnce( target, ".o_kanban_quick_create.o_disabled", "quick create should be disabled" ); prom.resolve(); await nextTick(); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "first column should now contain two records" ); assert.containsNone( target, ".o_kanban_quick_create.o_disabled", "quick create should be enabled" ); }); QUnit.test("quick create record fail in grouped by many2one", async (assert) => { serverData.views["partner,false,form"] = `
`; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], async mockRPC(route, args) { if (args.method === "name_create") { throw makeErrorFromResponse({ code: 200, message: "Odoo Server Error", data: { name: "odoo.exceptions.UserError", debug: "traceback", arguments: ["This is a user error"], context: {}, }, }); } }, }); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 2); await createRecord(); assert.containsOnce(target.querySelector(".o_kanban_group"), ".o_kanban_quick_create"); await editQuickCreateInput("display_name", "test"); await validateRecord(); assert.containsOnce(target, ".modal .o_form_view .o_form_editable"); assert.strictEqual(target.querySelector(".modal .o_field_many2one input").value, "hello"); // specify a name and save await editInput(target, ".modal .o_field_widget[name=foo] input", "test"); await click(target, ".modal .o_form_button_save"); assert.containsNone(target, ".modal"); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 3); const firstRecord = target.querySelector(".o_kanban_group .o_kanban_record"); assert.strictEqual(firstRecord.innerText, "test"); assert.containsOnce(target, ".o_kanban_quick_create:not(.o_disabled)"); }); QUnit.test("quick create record and click Edit, name_create fails", async (assert) => { Object.assign(serverData, { views: { "partner,false,kanban": `
`, "partner,false,search": "", "partner,false,list": '', "partner,false,form": `
`, }, }); const webClient = await createWebClient({ serverData, async mockRPC(route, args) { if (args.method === "name_create") { throw makeErrorFromResponse({ code: 200, message: "Odoo Server Error", data: { name: "odoo.exceptions.UserError", debug: "traceback", arguments: ["This is a user error"], context: {}, }, }); } }, }); await doAction(webClient, { res_model: "partner", type: "ir.actions.act_window", views: [ [false, "kanban"], [false, "form"], ], context: { group_by: ["product_id"], }, }); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 2); await quickCreateRecord(0); assert.containsOnce(target.querySelector(".o_kanban_group"), ".o_kanban_quick_create"); await editQuickCreateInput("display_name", "test"); await editRecord(); assert.containsOnce(target, ".modal .o_form_view .o_form_editable"); assert.strictEqual(target.querySelector(".modal .o_field_many2one input").value, "hello"); // specify a name and save await editInput(target, ".modal .o_field_widget[name=foo] input", "test"); await click(target, ".modal .o_form_button_save"); assert.containsNone(target, ".modal"); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 3); const firstRecord = target.querySelector(".o_kanban_group .o_kanban_record"); assert.strictEqual(firstRecord.innerText, "test"); assert.containsOnce(target, ".o_kanban_quick_create:not(.o_disabled)"); }); QUnit.test("quick create record is re-enabled after discard on failure", async (assert) => { serverData.views["partner,false,form"] = `
`; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], async mockRPC(route, args) { if (args.method === "name_create") { throw makeErrorFromResponse({ code: 200, message: "Odoo Server Error", data: { name: "odoo.exceptions.UserError", debug: "traceback", arguments: ["This is a user error"], context: {}, }, }); } }, }); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 2); await createRecord(); assert.containsOnce(target.querySelector(".o_kanban_group"), ".o_kanban_quick_create"); await editQuickCreateInput("display_name", "test"); await validateRecord(); assert.containsOnce(target, ".modal .o_form_view .o_form_editable"); await click(target.querySelector(".modal .o_form_button_cancel")); assert.containsNone(target, ".modal .o_form_view .o_form_editable"); assert.containsOnce(target.querySelector(".o_kanban_group"), ".o_kanban_quick_create"); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 2); }); QUnit.test("quick create record fails in grouped by char", async (assert) => { assert.expect(7); serverData.views["partner,false,form"] = '
'; await makeView({ type: "kanban", resModel: "partner", serverData, groupBy: ["foo"], arch: `
`, async mockRPC(route, args) { if (args.method === "name_create") { throw makeErrorFromResponse({ code: 200, message: "Odoo Server Error", data: { name: "odoo.exceptions.UserError", debug: "traceback", arguments: ["This is a user error"], context: {}, }, }); } if (args.method === "create") { assert.deepEqual(args.args[0], { foo: "blip" }); assert.deepEqual(args.kwargs.context, { default_foo: "blip", default_name: "test", lang: "en", tz: "taht", uid: 7, }); } }, }); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 2); await quickCreateRecord(); await editQuickCreateInput("display_name", "test"); await validateRecord(); assert.containsOnce(target, ".modal .o_form_view .o_form_editable"); assert.strictEqual( target.querySelector(".modal .o_field_widget[name=foo] input").value, "blip" ); await click(target, ".modal .o_form_button_save"); assert.containsNone(target, ".modal .o_form_view .o_form_editable"); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 3); }); QUnit.test("quick create record fails in grouped by selection", async (assert) => { assert.expect(7); serverData.views["partner,false,form"] = '
'; await makeView({ type: "kanban", resModel: "partner", serverData, groupBy: ["state"], arch: `
`, async mockRPC(route, args) { if (args.method === "name_create") { throw makeErrorFromResponse({ code: 200, message: "Odoo Server Error", data: { name: "odoo.exceptions.UserError", debug: "traceback", arguments: ["This is a user error"], context: {}, }, }); } if (args.method === "create") { assert.deepEqual(args.args[0], { state: "abc" }); assert.deepEqual(args.kwargs.context, { default_state: "abc", default_name: "test", lang: "en", tz: "taht", uid: 7, }); } }, }); assert.containsOnce(target.querySelector(".o_kanban_group"), ".o_kanban_record"); await quickCreateRecord(); await editQuickCreateInput("display_name", "test"); await validateRecord(); assert.containsOnce(target, ".modal .o_form_view .o_form_editable"); assert.strictEqual( target.querySelector(".modal .o_field_widget[name=state] select").value, '"abc"' ); await click(target, ".modal .o_form_button_save"); assert.containsNone(target, ".modal .o_form_view .o_form_editable"); assert.containsN(target.querySelector(".o_kanban_group"), ".o_kanban_record", 2); }); QUnit.test("quick create record in empty grouped kanban", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC(route, { method }) { 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) return { groups: [ { __domain: [["product_id", "=", 3]], product_id_count: 0 }, { __domain: [["product_id", "=", 5]], product_id_count: 0 }, ], length: 2, }; } }, }); assert.containsN(target, ".o_kanban_group", 2, "there should be 2 columns"); assert.containsNone(target, ".o_kanban_record", "both columns should be empty"); await createRecord(); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_quick_create", "should have opened the quick create in the first column" ); }); QUnit.test("quick create record in grouped on date(time) field", async (assert) => { const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["date"], createRecord: () => { assert.step("createRecord"); }, }); assert.containsNone( target, ".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 createRecord(); assert.containsNone( target, ".o_kanban_quick_create", "should not have opened the quick create widget" ); await reload(kanban, { groupBy: ["datetime"] }); assert.containsNone( target, ".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 createRecord(); assert.containsNone( target, ".o_kanban_quick_create", "should not have opened the quick create widget" ); assert.verifySteps(["createRecord", "createRecord"]); }); QUnit.test( "quick create record if grouped on date(time) field with attribute allow_group_range_value: true", async (assert) => { serverData.models.partner.records[0].date = "2017-01-08"; serverData.models.partner.records[1].date = "2017-01-09"; serverData.models.partner.records[2].date = "2017-01-08"; serverData.models.partner.records[3].date = "2017-01-10"; serverData.models.partner.records[0].datetime = "2017-01-08 10:55:05"; serverData.models.partner.records[1].datetime = "2017-01-09 11:31:10"; serverData.models.partner.records[2].datetime = "2017-01-08 09:20:25"; serverData.models.partner.records[3].datetime = "2017-01-10 08:05:51"; serverData.views["partner,quick_form,form"] = "
" + '' + '' + ""; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '' + '
' + "
" + "
", groupBy: ["date"], }); assert.containsOnce( target, ".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 createRecord(); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_quick_create", "should have opened the quick create in the first column" ); assert.strictEqual( target.querySelector( ".o_kanban_group:first-child .o_kanban_quick_create .o_field_widget[name=date] .o_datepicker input" ).value, "01/31/2017" ); await reload(kanban, { groupBy: ["datetime"] }); assert.containsOnce( target, ".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 createRecord(); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_quick_create", "should have opened the quick create in the first column" ); assert.strictEqual( target.querySelector( ".o_kanban_group:first-child .o_kanban_quick_create .o_field_widget[name=datetime] .o_datepicker input" ).value, "01/31/2017 23:59:59" ); } ); QUnit.test( "quick create record feature is properly enabled/disabled at reload", async (assert) => { const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["foo"], }); assert.containsN( target, ".o_kanban_header .o_kanban_quick_add i", 3, "quick create should be enabled when grouped on a char field" ); await reload(kanban, { groupBy: ["date"] }); assert.containsNone( target, ".o_kanban_header .o_kanban_quick_add i", "quick create should now be disabled (grouped on date field)" ); await reload(kanban, { groupBy: ["bar"] }); assert.containsN( target, ".o_kanban_header .o_kanban_quick_add i", 2, "quick create should be enabled again (grouped on boolean field)" ); } ); QUnit.test("quick create record in grouped by char field", async (assert) => { assert.expect(4); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["foo"], async mockRPC(route, { method, kwargs }) { if (method === "name_create") { assert.strictEqual(kwargs.context.default_foo, "blip"); } }, }); assert.containsN(target, ".o_kanban_header .o_kanban_quick_add i", 3); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); await quickCreateRecord(); await editQuickCreateInput("display_name", "new record"); await validateRecord(); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 3); }); QUnit.test("quick create record in grouped by boolean field", async (assert) => { assert.expect(4); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["bar"], async mockRPC(route, { method, kwargs }) { if (method === "name_create") { assert.strictEqual(kwargs.context.default_bar, true); } }, }); assert.containsN(target, ".o_kanban_header .o_kanban_quick_add i", 2); assert.containsN(target, ".o_kanban_group:last-child .o_kanban_record", 3); await quickCreateRecord(1); await editQuickCreateInput("display_name", "new record"); await validateRecord(); assert.containsN(target, ".o_kanban_group:last-child .o_kanban_record", 4); }); QUnit.test("quick create record in grouped on selection field", async (assert) => { assert.expect(4); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", async mockRPC(route, { method, kwargs }) { if (method === "name_create") { assert.strictEqual(kwargs.context.default_state, "abc"); } }, groupBy: ["state"], }); assert.containsN( target, ".o_kanban_header .o_kanban_quick_add i", 3, "quick create should be enabled when grouped on a selection field" ); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "first column (abc) should contain 1 record" ); await quickCreateRecord(); await editQuickCreateInput("display_name", "new record"); await validateRecord(); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "first column (abc) should contain 2 records" ); }); QUnit.test( "quick create record in grouped by char field (within quick_create_view)", async (assert) => { assert.expect(6); serverData.views["partner,some_view_ref,form"] = "
" + '' + ""; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["foo"], async mockRPC(route, { method, args, kwargs }) { if (method === "create") { assert.deepEqual(args[0], { foo: "blip" }); assert.strictEqual(kwargs.context.default_foo, "blip"); } }, }); assert.containsN(target, ".o_kanban_header .o_kanban_quick_add i", 3); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); await quickCreateRecord(); assert.strictEqual( target.querySelector(".o_kanban_quick_create input").value, "blip", "should have set the correct foo value by default" ); await validateRecord(); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 3); } ); QUnit.test( "quick create record in grouped by boolean field (within quick_create_view)", async (assert) => { assert.expect(6); serverData.views["partner,some_view_ref,form"] = "
" + '' + ""; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["bar"], async mockRPC(route, { method, args, kwargs }) { if (method === "create") { assert.deepEqual(args[0], { bar: true }); assert.strictEqual(kwargs.context.default_bar, true); } }, }); assert.containsN( target, ".o_kanban_header .o_kanban_quick_add i", 2, "quick create should be enabled when grouped on a boolean field" ); assert.containsN(target, ".o_kanban_group:last-child .o_kanban_record", 3); await quickCreateRecord(1); assert.ok( target.querySelector(".o_kanban_quick_create .o_field_boolean input").checked ); await validateRecord(); assert.containsN(target, ".o_kanban_group:last-child .o_kanban_record", 4); } ); QUnit.test( "quick create record in grouped by selection field (within quick_create_view)", async (assert) => { assert.expect(6); serverData.views["partner,some_view_ref,form"] = `
`; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["state"], async mockRPC(route, { method, args, kwargs }) { if (method === "create") { assert.deepEqual(args[0], { state: "abc" }); assert.strictEqual(kwargs.context.default_state, "abc"); } }, }); assert.containsN( target, ".o_kanban_header .o_kanban_quick_add i", 3, "quick create should be enabled when grouped on a selection field" ); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "first column (abc) should contain 1 record" ); await quickCreateRecord(); assert.strictEqual( target.querySelector(".o_kanban_quick_create select").value, '"abc"', "should have set the correct state value by default" ); await validateRecord(); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "first column (abc) should now contain 2 records" ); } ); QUnit.test("quick create record while adding a new column", async (assert) => { const prom = makeDeferred(); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC(route, { method, model }) { if (method === "name_create" && model === "product") { await prom; } }, }); assert.containsN(target, ".o_kanban_group", 2); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); // add a new column assert.containsOnce(target, ".o_column_quick_create"); assert.isNotVisible(target.querySelector(".o_column_quick_create input")); await createColumn(); assert.isVisible(target.querySelector(".o_column_quick_create input")); await editColumnName("new column"); await validateColumn(); await nextTick(); assert.strictEqual(target.querySelector(".o_column_quick_create input").value, ""); assert.containsN(target, ".o_kanban_group", 2); // click to add a new record await createRecord(); // should wait for the column to be created (and view to be re-rendered // before opening the quick create assert.containsNone(target, ".o_kanban_quick_create"); // unlock column creation prom.resolve(); await nextTick(); assert.containsN(target, ".o_kanban_group", 3); assert.containsOnce(target, ".o_kanban_quick_create"); // quick create record in first column await editQuickCreateInput("display_name", "new record"); await validateRecord(); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 3); }); QUnit.test("close a column while quick creating a record", async (assert) => { serverData.views["partner,some_view_ref,form"] = '
'; let prom; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], async mockRPC(_route, { method }) { if (prom && method === "get_views") { assert.step(method); await prom; } }, }); prom = makeDeferred(); assert.verifySteps([]); assert.containsN(target, ".o_kanban_group", 2); assert.containsNone(target, ".o_column_folded"); // click to quick create a new record in the first column (this operation is delayed) await quickCreateRecord(); assert.verifySteps(["get_views"]); assert.containsNone(target, ".o_form_view"); // click to fold the first column const clickColumnAction = await toggleColumnActions(0); await clickColumnAction("Fold"); assert.containsOnce(target, ".o_column_folded"); prom.resolve(); await nextTick(); assert.verifySteps([]); assert.containsNone(target, ".o_form_view"); assert.containsOnce(target, ".o_column_folded"); await createRecord(); assert.verifySteps([]); // "get_views" should have already be done assert.containsOnce(target, ".o_form_view"); assert.containsNone(target, ".o_column_folded"); }); QUnit.test( "quick create record: open on a column while another column has already one", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); // Click on quick create in first column await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce( target.querySelector(".o_kanban_group:first-child"), ".o_kanban_quick_create" ); // Click on quick create in second column await quickCreateRecord(1); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce( target.querySelector(".o_kanban_group:nth-child(2)"), ".o_kanban_quick_create" ); // Click on quick create in first column once again await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce( target.querySelector(".o_kanban_group:first-child"), ".o_kanban_quick_create" ); } ); QUnit.test("many2many_tags in kanban views", async (assert) => { serverData.models.partner.records[0].category_ids = [6, 7]; serverData.models.partner.records[1].category_ids = [7, 8]; serverData.models.category.records.push({ id: 8, name: "hello", color: 0, }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '
' + '' + '' + '' + "
" + "
" + "
", async mockRPC(route) { assert.step(route); }, selectRecord: (resId) => { assert.deepEqual( resId, 1, "should trigger an event to open the clicked record in a form view" ); }, }); assert.containsN( getCard(0), ".o_field_many2many_tags .o_tag", 2, "first record should contain 2 tags" ); assert.containsOnce(getCard(0), ".o_tag.o_tag_color_2", "first tag should have color 2"); assert.verifySteps( [ "/web/dataset/call_kw/partner/get_views", "/web/dataset/call_kw/partner/web_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.containsOnce( target, ".o_kanban_record:nth-child(2) .o_tag", "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 click(target, ".o_kanban_record:first-child .o_priority_star:first-child"); 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.containsN( target, ".o_kanban_record:first-child .o_field_many2many_tags .o_tag", 2, "first record should still contain only 2 tags" ); // click on a tag (should trigger switch_view) await click(target, ".o_kanban_record:first-child .o_tag:first-child"); }); QUnit.test("Do not open record when clicking on `a` with `href`", async (assert) => { serverData.models.partner.records = [{ id: 1, foo: "yop" }]; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: ` `, }); patchWithCleanup(kanban.env.services.action, { async switchView() { // when clicking on a record in kanban view, // it switches to form view. throw new Error("should not switch view"); }, }); assert.containsOnce(target, ".o_kanban_record:not(.o_kanban_ghost)"); assert.containsOnce(target, ".o_kanban_record a"); const testLink = target.querySelector(".o_kanban_record a"); assert.ok(testLink.href.length, "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. testLink.addEventListener("click", (ev) => { assert.notOk( ev.defaultPrevented, "should not prevented browser default behaviour beforehand" ); assert.strictEqual( ev.target, testLink, "should have clicked on the test link in the kanban record" ); ev.preventDefault(); }); await click(testLink); }); QUnit.test("Open record when clicking on widget field", async function (assert) { assert.expect(2); serverData.views[ "product,false,form" ] = `
`; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, selectRecord: (resId) => { assert.strictEqual(resId, 1, "should trigger an event to open the form view"); }, }); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 4); await click(target.querySelector(".oe_kanban_global_click .o_field_monetary[name=salary]")); }); QUnit.test("o2m loaded in only one batch", async (assert) => { serverData.models.subtask = { fields: { name: { string: "Name", type: "char" }, }, records: [ { id: 1, name: "subtask #1" }, { id: 2, name: "subtask #2" }, ], }; serverData.models.partner.fields.subtask_ids = { string: "Subtasks", type: "one2many", relation: "subtask", }; serverData.models.partner.records[0].subtask_ids = [1]; serverData.models.partner.records[1].subtask_ids = [2]; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + "
" + '' + "
" + "
" + "
", groupBy: ["product_id"], async mockRPC(route, args) { assert.step(args.method || route); }, }); await reload(kanban, { groupBy: ["product_id"] }); assert.verifySteps([ "get_views", "web_read_group", "web_search_read", "web_search_read", "read", "web_read_group", "web_search_read", "web_search_read", "read", ]); }); QUnit.test("m2m loaded in only one batch", async (assert) => { const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + "
" + '' + "
" + "
" + "
", groupBy: ["product_id"], async mockRPC(route, args) { assert.step(args.method || route); }, }); await reload(kanban, { groupBy: ["product_id"] }); assert.verifySteps([ "get_views", "web_read_group", "web_search_read", "web_search_read", "read", "web_read_group", "web_search_read", "web_search_read", "read", ]); }); QUnit.test("fetch reference in only one batch", async (assert) => { serverData.models.partner.records[0].ref_product = "product,3"; serverData.models.partner.records[1].ref_product = "product,5"; serverData.models.partner.fields.ref_product = { string: "Reference Field", type: "reference", }; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, groupBy: ["product_id"], arch: `
`, mockRPC(route, args) { assert.step(args.method || route); }, }); await reload(kanban, { groupBy: ["product_id"] }); assert.verifySteps([ "get_views", "web_read_group", "web_search_read", "web_search_read", "name_get", "web_read_group", "web_search_read", "web_search_read", "name_get", ]); }); QUnit.test("wait x2manys batch fetches to re-render", async (assert) => { let prom = Promise.resolve(); const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + "
" + '' + "
" + "
" + "
", groupBy: ["product_id"], async mockRPC(route, { method }) { if (method === "read") { await prom; } }, }); assert.containsN(target, ".o_tag", 2); assert.containsN(target, ".o_kanban_group", 2); prom = makeDeferred(); reload(kanban, { groupBy: ["state"] }); await nextTick(); assert.containsN(target, ".o_tag", 2); assert.containsN(target, ".o_kanban_group", 2); prom.resolve(); await nextTick(); assert.containsN(target, ".o_kanban_group", 3); assert.containsN(target, ".o_tag", 2, "Should display 2 tags after update"); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(2) .o_tag").innerText, "gold", "First category should be 'gold'" ); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(3) .o_tag").innerText, "silver", "Second category should be 'silver'" ); }); QUnit.test("can drag and drop a record from one column to the next", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + 'edit' + "
" + "
" + "
", groupBy: ["product_id"], async mockRPC(route) { if (route === "/web/dataset/resequence") { assert.step("resequence"); } }, }); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); assert.containsN(target, ".thisiseditable", 4); assert.verifySteps([]); // first record of first column moved to the bottom of second column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_record"); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 3); assert.containsN(target, ".thisiseditable", 4); assert.verifySteps(["resequence"]); }); QUnit.test("drag and drop highlight on hover", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); // first record of first column moved to the bottom of second column const drop = drag( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.hasClass(target.querySelector(".o_kanban_group:nth-child(2)"), "o_kanban_hover"); await drop(); assert.containsNone(target, ".o_kanban_group:nth-child(2).o_kanban_hover"); }); QUnit.test("drag and drop outside of a column", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); // first record of first column moved to the right of a column await dragAndDrop(".o_kanban_group:first-child .o_kanban_record", ".o_column_quick_create"); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); }); QUnit.test("drag and drop a record, grouped by selection", async (assert) => { assert.expect(7); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + "" + '' + '
' + "
" + "
" + "
", groupBy: ["state"], async mockRPC(route, args) { if (route === "/web/dataset/resequence") { assert.step("resequence"); return true; } if (args.model === "partner" && args.method === "write") { assert.deepEqual(args.args[1], { state: "abc" }); } }, }); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_record"); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); // first record of second column moved to the bottom of first column await dragAndDrop( ".o_kanban_group:nth-child(2) .o_kanban_record", ".o_kanban_group:first-child" ); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsNone(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); assert.verifySteps(["resequence"]); }); QUnit.test("prevent drag and drop of record if grouped by readonly", async (assert) => { // Whether the kanban is grouped by state, foo, bar or product_id // the user must not be able to drag and drop from one group to another, // as state, foo bar, product_id are made readonly one way or another. // state must not be draggable: // state is not readonly in the model. state is passed in the arch specifying readonly="1". // foo must not be draggable: // foo is readonly in the model fields. foo is passed in the arch but without specifying readonly. // bar must not be draggable: // bar is readonly in the model fields. bar is not passed in the arch. // product_id must not be draggable: // product_id is readonly in the model fields. product_id is passed in the arch specifying readonly="0", // but the readonly in the model takes over. serverData.models.partner.fields.foo.readonly = true; serverData.models.partner.fields.bar.readonly = true; serverData.models.partner.fields.product_id.readonly = true; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + "" + '
' + '' + '' + '' + "
" + "
" + "
", groupBy: ["state"], async mockRPC(route, args) { if (route === "/web/dataset/resequence") { return true; } if (args.model === "partner" && args.method === "write") { throw new Error("should not be draggable"); } }, }); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_record"); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 2); // first record of first column moved to the bottom of second column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); // should not be draggable assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_record"); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 2); await reload(kanban, { groupBy: ["foo"] }); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); assert.containsOnce(target, ".o_kanban_group:nth-child(3) .o_kanban_record"); // first record of first column moved to the bottom of second column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); // should not be draggable assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); assert.containsOnce(target, ".o_kanban_group:nth-child(3) .o_kanban_record"); assert.deepEqual(getCardTexts(0), ["blipDEF", "blipGHI"]); // second record of first column moved at first place await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record:last-of-type", ".o_kanban_group:first-child .o_kanban_record" ); // should still be able to resequence assert.deepEqual(getCardTexts(0), ["blipGHI", "blipDEF"]); await reload(kanban, { groupBy: ["bar"] }); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 1); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 3); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 0); assert.deepEqual(getCardTexts(0), ["blipGHI"]); // first record of first column moved to the bottom of second column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); // should not be draggable assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 1); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 3); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 0); assert.deepEqual(getCardTexts(0), ["blipGHI"]); await reload(kanban, { groupBy: ["product_id"] }); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 0); assert.deepEqual(getCardTexts(0), ["yopABC", "gnapGHI"]); // first record of first column moved to the bottom of second column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); // should not be draggable assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 0); assert.deepEqual(getCardTexts(0), ["yopABC", "gnapGHI"]); }); QUnit.test("prevent drag and drop if grouped by date/datetime field", async (assert) => { serverData.models.partner.records[0].date = "2017-01-08"; serverData.models.partner.records[1].date = "2017-01-09"; serverData.models.partner.records[2].date = "2017-02-08"; serverData.models.partner.records[3].date = "2017-02-10"; serverData.models.partner.records[0].datetime = "2017-01-08 10:55:05"; serverData.models.partner.records[1].datetime = "2017-01-09 11:31:10"; serverData.models.partner.records[2].datetime = "2017-02-08 09:20:25"; serverData.models.partner.records[3].datetime = "2017-02-10 08:05:51"; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '' + '
' + "
", groupBy: ["date:month"], }); assert.containsN(target, ".o_kanban_group", 2); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2, "1st column should contain 2 records of January month" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2, "2nd column should contain 2 records of February month" ); // drag&drop a record in another column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); // should not drag&drop record assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "Should remain same records in first column (2 records)" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2, "Should remain same records in 2nd column (2 record)" ); await reload(kanban, { groupBy: ["datetime:month"] }); assert.containsN(target, ".o_kanban_group", 2); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "1st column should contain 2 records of January month" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2, "2nd column should contain 2 records of February month" ); // drag&drop a record in another column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); // should not drag&drop record assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "Should remain same records in first column(2 records)" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2, "Should remain same records in 2nd column(2 record)" ); }); QUnit.test("prevent drag and drop if grouped by many2many field", async (assert) => { serverData.models.partner.records[0].category_ids = [6, 7]; serverData.models.partner.records[3].category_ids = [7]; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["category_ids"], }); assert.containsN(target, ".o_kanban_group", 2); assert.strictEqual( target.querySelector(".o_kanban_group:first-child .o_column_title").innerText, "gold", "first column should have correct title" ); assert.strictEqual( target.querySelector(".o_kanban_group:last-child .o_column_title").innerText, "silver", "second column should have correct title" ); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:last-child .o_kanban_record", 3); // drag&drop a record in another column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:last-child .o_kanban_record", 3); // Sanity check: groupby a non m2m field and check dragdrop is working await reload(kanban, { groupBy: ["state"] }); assert.containsN(target, ".o_kanban_group", 3); assert.deepEqual( [...target.querySelectorAll(".o_kanban_group .o_column_title")].map( (el) => el.innerText ), ["ABC", "DEF", "GHI"], "columns should have correct title" ); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "first column should have 1 record" ); assert.containsN( target, ".o_kanban_group:last-child .o_kanban_record", 2, "last column should have 2 records" ); await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:last-child" ); assert.containsNone( target, ".o_kanban_group:first-child .o_kanban_record", "first column should not contain records" ); assert.containsN( target, ".o_kanban_group:last-child .o_kanban_record", 3, "last column should contain 3 records" ); }); QUnit.test( "drag and drop record if grouped by date/time field with attribute allow_group_range_value: true", async (assert) => { assert.expect(16); serverData.models.partner.records[0].date = "2017-01-08"; serverData.models.partner.records[1].date = "2017-01-09"; serverData.models.partner.records[2].date = "2017-02-08"; serverData.models.partner.records[3].date = "2017-02-10"; serverData.models.partner.records[0].datetime = "2017-01-08 10:55:05"; serverData.models.partner.records[1].datetime = "2017-01-09 11:31:10"; serverData.models.partner.records[2].datetime = "2017-02-08 09:20:25"; serverData.models.partner.records[3].datetime = "2017-02-10 08:05:51"; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + "" + '' + '
' + "
" + "
" + "
", groupBy: ["date:month"], async mockRPC(route, { model, method, args }) { if (route === "/web/dataset/resequence") { assert.step("resequence"); return true; } if (model === "partner" && method === "write") { if ("date" in args[1]) { assert.deepEqual(args[1], { date: "2017-02-28" }); } else if ("datetime" in args[1]) { assert.deepEqual(args[1], { datetime: "2017-02-28 22:59:59" }); } } }, }); assert.containsN(target, ".o_kanban_group", 2); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "1st column should contain 2 records of January month" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2, "2nd column should contain 2 records of February month" ); await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "Should only have one record remaining" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 3, "Should now have 3 records" ); assert.verifySteps(["resequence"]); await reload(kanban, { groupBy: ["datetime:month"] }); assert.containsN(target, ".o_kanban_group", 2); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "1st column should contain 2 records of January month" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2, "2nd column should contain 2 records of February month" ); await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "Should only have one record remaining" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 3, "Should now have 3 records" ); assert.verifySteps(["resequence"]); } ); QUnit.test("Ensuring each progress bar has some space", async (assert) => { serverData.models.partner.records = [ { id: 1, foo: "blip", state: "def", }, { id: 2, foo: "blip", state: "abc", }, ]; for (let i = 0; i < 20; i++) { serverData.models.partner.records.push({ id: 3 + i, foo: "blip", state: "ghi", }); } await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["foo"], }); assert.deepEqual( getProgressBars(0).map((pb) => pb.style.width), ["5%", "5%", "90%"] ); }); QUnit.test("Move record in grouped by date, progress bars and sum field", async (assert) => { serverData.models.partner.records[0].date = "2017-01-08"; serverData.models.partner.records[1].date = "2017-01-09"; serverData.models.partner.records[2].date = "2017-02-08"; serverData.models.partner.records[3].date = "2017-02-10"; await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["date:month"], }); assert.containsN(target, ".o_kanban_group", 2); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "1st column should contain 2 records of January month" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2, "2nd column should contain 2 records of February month" ); assert.deepEqual( getProgressBars(0).map((pb) => pb.style.width), ["50%", "50%"] ); assert.deepEqual( getProgressBars(1).map((pb) => pb.style.width), ["50%", "50%"] ); assert.deepEqual(getCounters(), ["19", "13"]); await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.containsOnce( target, ".o_kanban_group:first-child .o_kanban_record", "Should only have one record remaining" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 3, "Should now have 3 records" ); assert.deepEqual( getProgressBars(0).map((pb) => pb.style.width), ["100%"] ); assert.deepEqual( getProgressBars(1).map((pb) => pb.style.width), ["33.3333%", "33.3333%", "33.3333%"] // abridged to e-4 ); assert.deepEqual(getCounters(), ["9", "23"]); }); QUnit.test( "completely prevent drag and drop if records_draggable set to false", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", groupBy: ["product_id"], }); // testing initial state assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); assert.deepEqual(getCardTexts(), ["yop", "gnap", "blip", "blip"]); assert.containsNone(target, ".o_record_draggable"); // attempt to drag&drop a record in another column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); // should not drag&drop record assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2, "First column should still contain 2 records" ); assert.containsN( target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2, "Second column should still contain 2 records" ); assert.deepEqual( getCardTexts(), ["yop", "gnap", "blip", "blip"], "Records should not have moved" ); // attempt to drag&drop a record in the same column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:first-child .o_kanban_record:last-of-type" ); assert.deepEqual( getCardTexts(), ["yop", "gnap", "blip", "blip"], "Records should not have moved" ); } ); QUnit.test("prevent drag and drop of record if onchange fails", async (assert) => { serverData.models.partner.onchanges = { product_id() {}, }; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + "" + '
' + '' + '' + "
" + "
" + "
", groupBy: ["product_id"], async mockRPC(route, { model, method }) { if (model === "partner" && method === "onchange") { throw {}; } }, }); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); // drag&drop a record in another column await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); // should not be dropped, card should reset back to first column assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); }); QUnit.test("kanban view with default_group_by", async (assert) => { assert.expect(7); serverData.models.partner.records[0].product_id = 1; serverData.models.product.records.push({ id: 1, display_name: "third product" }); let readGroupCount = 0; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
", async mockRPC(route, { kwargs }) { if (route === "/web/dataset/call_kw/partner/web_read_group") { readGroupCount++; switch (readGroupCount) { case 1: return assert.deepEqual(kwargs.groupby, ["bar"]); case 2: return assert.deepEqual(kwargs.groupby, ["product_id"]); case 3: return assert.deepEqual(kwargs.groupby, ["bar"]); } } }, }); assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped"); assert.containsN(target, ".o_kanban_group", 2); // simulate an update coming from the searchview, with another groupby given await reload(kanban, { groupBy: ["product_id"] }); assert.containsN(target, ".o_kanban_group", 3); // simulate an update coming from the searchview, removing the previously set groupby await reload(kanban, { groupBy: [] }); assert.containsN(target, ".o_kanban_group", 2); }); QUnit.test("kanban view not groupable", async (assert) => { patchWithCleanup(kanbanView, { searchMenuTypes: ["filter", "favorite"] }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, searchViewArch: ` `, async mockRPC(route, { method }) { if (method === "web_read_group") { throw new Error("Should not do a read_group RPC"); } }, context: { search_default_itsName: 1 }, }); assert.doesNotHaveClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped"); assert.containsNone(target, ".o_control_panel div.o_search_options div.o_group_by_menu"); assert.deepEqual(getFacetTexts(target), []); // validate presence of the search arch info await toggleFilterMenu(target); assert.containsOnce(target, ".o_filter_menu .o_menu_item"); }); QUnit.test("kanban view with create=False", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
", }); assert.containsNone(target, ".o-kanban-button-new"); }); QUnit.test("kanban view with create=False and groupby", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], }); assert.containsNone(target, ".o-kanban-button-new"); assert.containsN(target, ".o_kanban_group", 2); assert.containsNone(target, ".o_kanban_quick_add"); }); QUnit.test("clicking on a link triggers correct event", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '
' + "", selectRecord: (resId, { mode }) => { assert.equal(resId, 1); assert.equal(mode, "edit"); }, }); await click(getCard(0), "a"); }); QUnit.test("environment is updated when (un)folding groups", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); assert.deepEqual(getCardTexts(), ["1", "3", "2", "4"]); // fold the second group and check that the res_ids it contains are no // longer in the environment const clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Fold"); assert.deepEqual(getCardTexts(), ["1", "3"]); // re-open the second group and check that the res_ids it contains are // back in the environment await click(getColumn(1)); assert.deepEqual(getCardTexts(), ["1", "3", "2", "4"]); }); QUnit.test("create a column in grouped on m2o", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC(route, { method }) { if (method === "name_create" || route === "/web/dataset/resequence") { assert.step(method || route); } }, }); assert.containsN(target, ".o_kanban_group", 2); assert.containsOnce(target, ".o_column_quick_create", "should have a quick create column"); assert.containsNone( target, ".o_column_quick_create input", "the input should not be visible" ); await createColumn(); assert.containsOnce(target, ".o_column_quick_create input", "the input should be visible"); // discard the column creation and click it again await triggerEvent(target, ".o_column_quick_create input", "keydown", { key: "Escape", }); assert.containsNone( target, ".o_column_quick_create input", "the input should not be visible" ); await createColumn(); assert.containsOnce(target, ".o_column_quick_create input", "the input should be visible"); await editColumnName("new value"); await validateColumn(); assert.containsN(target, ".o_kanban_group", 3); assert.containsOnce( getColumn(2), "span:contains(new value)", "the last column should be the newly created one" ); assert.ok(getColumn(2).dataset.id, "the created column should have an associated id"); assert.doesNotHaveClass( getColumn(2), "o_column_folded", "the created column should not be folded" ); assert.verifySteps(["name_create", "/web/dataset/resequence"]); // fold and unfold the created column, and check that no RPCs are done (as there are no records) const clickColumnAction = await toggleColumnActions(2); await clickColumnAction("Fold"); assert.hasClass(getColumn(2), "o_column_folded", "the created column should now be folded"); await click(getColumn(2)); assert.doesNotHaveClass(getColumn(1), "o_column_folded"); assert.verifySteps([], "no rpc should have been done when folding/unfolding"); // quick create a record await createRecord(); assert.hasClass( getColumn(0).querySelector(":scope > div:nth-child(2)"), "o_kanban_quick_create", "clicking on create should open the quick_create in the first column" ); }); QUnit.test("auto fold group when reach the limit", async (assert) => { for (let i = 0; i < 12; i++) { serverData.models.product.records.push({ id: 8 + i, name: "column", }); serverData.models.partner.records.push({ id: 20 + i, foo: "dumb entry", product_id: 8 + i, }); } await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], async mockRPC(route, args, performRPC) { if (args.method === "web_read_group") { const result = await performRPC(route, args); result.groups[2].__fold = true; result.groups[8].__fold = true; return result; } if (args.method === "web_search_read") { assert.step(`web_search_read domain: ${args.kwargs.domain}`); } }, }); // we look if column are folded/unfolded according to what is expected assert.doesNotHaveClass(getColumn(1), "o_column_folded"); assert.doesNotHaveClass(getColumn(3), "o_column_folded"); assert.doesNotHaveClass(getColumn(9), "o_column_folded"); assert.hasClass(getColumn(2), "o_column_folded"); assert.hasClass(getColumn(8), "o_column_folded"); // we look if columns are actually folded after we reached the limit assert.hasClass(getColumn(12), "o_column_folded"); assert.hasClass(getColumn(13), "o_column_folded"); // we look if we have the right count of folded/unfolded column assert.containsN(target, ".o_kanban_group:not(.o_column_folded)", 10); assert.containsN(target, ".o_kanban_group.o_column_folded", 4); assert.verifySteps([ "web_search_read domain: product_id,=,3", "web_search_read domain: product_id,=,5", "web_search_read domain: product_id,=,9", "web_search_read domain: product_id,=,10", "web_search_read domain: product_id,=,11", "web_search_read domain: product_id,=,12", "web_search_read domain: product_id,=,13", "web_search_read domain: product_id,=,15", "web_search_read domain: product_id,=,16", "web_search_read domain: product_id,=,17", ]); }); QUnit.test("auto fold group when reach the limit (2)", async (assert) => { // this test is similar to the previous one, except that in this one, // read_group sets the __fold key on each group, even those that are // unfolded, which could make subtle differences in the code for (let i = 0; i < 12; i++) { serverData.models.product.records.push({ id: 8 + i, name: "column", }); serverData.models.partner.records.push({ id: 20 + i, foo: "dumb entry", product_id: 8 + i, }); } await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], async mockRPC(route, args, performRPC) { if (args.method === "web_read_group") { const result = await performRPC(route, args); for (let i = 0; i < result.groups.length; i++) { result.groups[i].__fold = i == 2 || i == 8; } return result; } if (args.method === "web_search_read") { assert.step(`web_search_read domain: ${args.kwargs.domain}`); } }, }); // we look if column are folded/unfolded according to what is expected assert.doesNotHaveClass(getColumn(1), "o_column_folded"); assert.doesNotHaveClass(getColumn(3), "o_column_folded"); assert.doesNotHaveClass(getColumn(9), "o_column_folded"); assert.hasClass(getColumn(2), "o_column_folded"); assert.hasClass(getColumn(8), "o_column_folded"); // we look if columns are actually folded after we reached the limit assert.hasClass(getColumn(12), "o_column_folded"); assert.hasClass(getColumn(13), "o_column_folded"); // we look if we have the right count of folded/unfolded column assert.containsN(target, ".o_kanban_group:not(.o_column_folded)", 10); assert.containsN(target, ".o_kanban_group.o_column_folded", 4); assert.verifySteps([ "web_search_read domain: product_id,=,3", "web_search_read domain: product_id,=,5", "web_search_read domain: product_id,=,9", "web_search_read domain: product_id,=,10", "web_search_read domain: product_id,=,11", "web_search_read domain: product_id,=,12", "web_search_read domain: product_id,=,13", "web_search_read domain: product_id,=,15", "web_search_read domain: product_id,=,16", "web_search_read domain: product_id,=,17", ]); }); QUnit.test( "hide and display help message (ESC) in kanban quick create [REQUIRE FOCUS]", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); await createColumn(); await nextTick(); // Wait for the autofocus to trigger after the update assert.containsOnce(target, ".o_discard_msg", "the ESC to discard message is visible"); // click outside the column (to lose focus) await click(getColumn(0), ".o_kanban_header"); assert.containsNone( target, ".o_discard_msg", "the ESC to discard message is no longer visible" ); } ); QUnit.test("delete a column in grouped on m2o", async (assert) => { assert.expect(38); let resequencedIDs = []; let dialogProps; patchDialog((_cls, props) => { assert.ok(true, "a confirm modal should be displayed"); dialogProps = props; }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["product_id"], async mockRPC(route, { ids, method }) { if (route === "/web/dataset/resequence") { resequencedIDs = ids; assert.strictEqual( ids.filter(isNaN).length, 0, "column resequenced should be existing records with IDs" ); } if (method) { assert.step(method); } }, }); // check the initial rendering assert.containsN(target, ".o_kanban_group", 2, "should have two columns"); assert.strictEqual( getColumn(0).querySelector(".o_column_title").innerText, "hello", 'first column should be [3, "hello"]' ); assert.strictEqual( getColumn(1).querySelector(".o_column_title").innerText, "xmo", 'second column should be [5, "xmo"]' ); assert.containsN( getColumn(1), ".o_kanban_record", 2, "second column should have two records" ); // check available actions in kanban header's config dropdown await toggleColumnActions(0); assert.containsOnce( getColumn(0), ".o_kanban_toggle_fold", "should be able to fold the column" ); assert.containsOnce(getColumn(0), ".o_column_edit", "should be able to edit the column"); assert.containsOnce( getColumn(0), ".o_column_delete", "should be able to delete the column" ); assert.containsNone( getColumn(0), ".o_column_archive_records", "should not be able to archive all the records" ); assert.containsNone( getColumn(0), ".o_column_unarchive_records", "should not be able to restore all the records" ); // delete second column (first cancel the confirm request, then confirm) let clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Delete"); dialogProps.cancel(); await nextTick(); assert.strictEqual( getColumn(1).querySelector(".o_column_title").innerText, "xmo", 'column [5, "xmo"] should still be there' ); dialogProps.confirm(); await nextTick(); clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Delete"); assert.strictEqual( getColumn(1).querySelector(".o_column_title").innerText, "hello", 'last column should now be [3, "hello"]' ); assert.containsN(target, ".o_kanban_group", 2, "should still have two columns"); assert.strictEqual( getColumn(0).querySelector(".o_column_title").innerText, "None (2)", "first column should have no id (Undefined column)" ); // check available actions on 'Undefined' column await click(getColumn(0)); await toggleColumnActions(0); assert.containsOnce( getColumn(0), ".o_kanban_toggle_fold", "should be able to fold the column" ); assert.containsNone(getColumn(0), ".o_column_edit", "should be able to edit the column"); assert.containsNone( getColumn(0), ".o_column_delete", "should be able to delete the column" ); assert.containsNone( getColumn(0), ".o_column_archive_records", "should not be able to archive all the records" ); assert.containsNone( getColumn(0), ".o_column_unarchive_records", "should not be able to restore all the records" ); assert.verifySteps([ "get_views", "web_read_group", "web_search_read", "web_search_read", "unlink", "web_read_group", "web_search_read", "web_search_read", ]); assert.containsN( target, ".o_kanban_group", 2, "the old groups should have been correctly deleted" ); // test column drag and drop having an 'Undefined' column await dragAndDrop( ".o_kanban_group:first-child .o_column_title", ".o_kanban_group:nth-child(2)" ); assert.deepEqual( resequencedIDs, [], "resequencing require at least 2 not Undefined columns" ); await createColumn(); await editColumnName("once third column"); await validateColumn(); assert.deepEqual(resequencedIDs, [3, 4], "creating a column should trigger a resequence"); await dragAndDrop( ".o_kanban_group:first-child .o_column_title", ".o_kanban_group:nth-child(3)" ); assert.deepEqual( resequencedIDs, [3, 4], "moving the Undefined column should not affect order of other columns" ); await dragAndDrop( ".o_kanban_group:nth-child(2) .o_column_title", ".o_kanban_group:nth-child(3)" ); assert.deepEqual(resequencedIDs, [4, 3], "moved column should be resequenced accordingly"); assert.verifySteps(["name_create"]); }); QUnit.test("create a column, delete it and create another one", async (assert) => { patchDialog((_cls, props) => props.confirm()); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group", 2); await createColumn(); await editColumnName("new column 1"); await validateColumn(); assert.containsN(target, ".o_kanban_group", 3); const clickColumnAction = await toggleColumnActions(2); await clickColumnAction("Delete"); assert.containsN(target, ".o_kanban_group", 2); await createColumn(); await editColumnName("new column 2"); await validateColumn(); assert.containsN(target, ".o_kanban_group", 3); assert.strictEqual( getColumn(2).querySelector("span").innerText, "new column 2", "the last column should be the newly created one" ); }); QUnit.test("edit a column in grouped on m2o", async (assert) => { serverData.views["product,false,form"] = '
'; let nbRPCs = 0; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC() { nbRPCs++; }, }); assert.strictEqual( getColumn(1).querySelector(".o_column_title").innerText, "xmo", 'title of the column should be "xmo"' ); // edit the title of column [5, 'xmo'] and close without saving let clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Edit"); assert.containsOnce( target, ".modal .o_form_editable", "a form view should be open in a modal" ); assert.strictEqual( target.querySelector(".modal .o_form_editable input").value, "xmo", 'the name should be "xmo"' ); await editInput(target, ".modal .o_form_editable input", "ged"); // change the value nbRPCs = 0; await click(target, ".modal-header .btn-close"); assert.containsNone(target, ".modal"); assert.strictEqual( getColumn(1).querySelector(".o_column_title").innerText, "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 clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Edit"); await editInput(target, ".modal .o_form_editable input", "ged"); // change the value nbRPCs = 0; await click(target, ".modal button.o_form_button_cancel"); assert.containsNone(target, ".modal"); assert.strictEqual( getColumn(1).querySelector(".o_column_title").innerText, "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 clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Edit"); await editInput(target, ".modal .o_form_editable input", "ged"); // change the value nbRPCs = 0; await click(target, ".modal .o_form_button_save"); // click on save assert.containsNone(target, ".modal", "the modal should be closed"); assert.strictEqual( getColumn(1).querySelector(".o_column_title").innerText, "ged", 'title of the column should be "ged"' ); assert.strictEqual(nbRPCs, 4, "should have done 1 write, 1 read_group and 2 search_read"); }); QUnit.test("edit a column propagates right context", async (assert) => { assert.expect(4); serverData.views["product,false,form"] = '
'; patchWithCleanup(session.user_context, { lang: "brol" }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC(_route, { method, model, kwargs }) { if (model === "partner" && method === "web_search_read") { assert.strictEqual( kwargs.context.lang, "brol", "lang is present in context for partner operations" ); } else if (model === "product") { assert.strictEqual( kwargs.context.lang, "brol", "lang is present in context for product operations" ); } }, }); const clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Edit"); }); QUnit.test("quick create column should be opened if there is no column", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], domain: [["foo", "=", "norecord"]], }); assert.containsNone(target, ".o_kanban_group"); assert.containsOnce(target, ".o_column_quick_create"); assert.containsOnce( target, ".o_column_quick_create input", "the quick create should be opened" ); }); QUnit.test( "quick create column should not be closed on window click if there is no column", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], domain: [["foo", "=", "norecord"]], }); assert.containsNone(target, ".o_kanban_group"); assert.containsOnce(target, ".o_column_quick_create"); assert.containsOnce( target, ".o_column_quick_create input", "the quick create should be opened" ); // click outside should not discard quick create column await click(target, ".o_kanban_example_background_container"); assert.containsOnce( target, ".o_column_quick_create input", "the quick create should still be opened" ); } ); QUnit.test("quick create several columns in a row", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group", 2, "should have two columns"); assert.containsOnce( target, ".o_column_quick_create", "should have a ColumnQuickCreate widget" ); assert.containsOnce( target, ".o_column_quick_create .o_quick_create_folded:visible", "the ColumnQuickCreate should be folded" ); assert.containsNone( target, ".o_column_quick_create .o_quick_create_unfolded:visible", "the ColumnQuickCreate should be folded" ); // add a new column await createColumn(); assert.containsNone( target, ".o_column_quick_create .o_quick_create_folded:visible", "the ColumnQuickCreate should be unfolded" ); assert.containsOnce( target, ".o_column_quick_create .o_quick_create_unfolded:visible", "the ColumnQuickCreate should be unfolded" ); await editColumnName("New Column 1"); await validateColumn(); assert.containsN(target, ".o_kanban_group", 3, "should now have three columns"); // add another column assert.containsNone( target, ".o_column_quick_create .o_quick_create_folded:visible", "the ColumnQuickCreate should still be unfolded" ); assert.containsOnce( target, ".o_column_quick_create .o_quick_create_unfolded:visible", "the ColumnQuickCreate should still be unfolded" ); await editColumnName("New Column 2"); await validateColumn(); assert.containsN(target, ".o_kanban_group", 4); }); QUnit.test("quick create column with enter", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); // add a new column await createColumn(); await editColumnName("New Column 1"); await triggerEvent(target, ".o_column_quick_create input", "keydown", { key: "Enter", }); assert.containsN(target, ".o_kanban_group", 3, "should now have three columns"); }); QUnit.test("quick create column and examples", async (assert) => { serviceRegistry.add("dialog", dialogService, { force: true }); registry.category("kanban_examples").add("test", { allowedGroupBys: ["product_id"], 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: `A fantastic description.`, }, ], }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); assert.containsOnce(target, ".o_column_quick_create", "should have quick create available"); // open the quick create await createColumn(); assert.containsOnce( target, ".o_column_quick_create .o_kanban_examples:visible", "should have a link to see examples" ); // click to see the examples await click(target, ".o_column_quick_create .o_kanban_examples"); assert.containsOnce( target, ".modal .o_kanban_examples_dialog", "should have open the examples dialog" ); assert.containsN( target, ".modal .o_notebook_headers li", 2, "should have two examples (in the menu)" ); assert.strictEqual( target.querySelector(".modal .o_notebook_headers").innerText, "A first example\nA second example", "example names should be correct" ); assert.containsOnce( target, ".modal .o_notebook_content .tab-pane", "should have only rendered one page" ); const firstPane = target.querySelector(".modal .o_notebook_content .tab-pane"); assert.containsN( firstPane, ".o_kanban_examples_group", 3, "there should be 3 stages in the first example" ); assert.strictEqual( [...firstPane.querySelectorAll("h6")].map((e) => e.textContent).join(""), "Column 1Column 2Column 3", "column titles should be correct" ); assert.strictEqual( firstPane.querySelector(".o_kanban_examples_description").innerHTML, "A weak description.", "An escaped description should be displayed" ); await click(target.querySelector(".nav-item:nth-child(2) .nav-link")); const secondPane = target.querySelector(".o_notebook_content"); assert.containsN( secondPane, ".o_kanban_examples_group", 2, "there should be 2 stages in the second example" ); assert.strictEqual( [...secondPane.querySelectorAll("h6")].map((e) => e.textContent).join(""), "Col 1Col 2", "column titles should be correct" ); assert.strictEqual( secondPane.querySelector(".o_kanban_examples_description").innerHTML, "A fantastic description.", "A formatted description should be displayed." ); }); QUnit.test("quick create column's apply button's display text", async (assert) => { serviceRegistry.add("dialog", dialogService, { force: true }); const applyExamplesText = "Use This For My Test"; registry.category("kanban_examples").add("test", { allowedGroupBys: ["product_id"], applyExamplesText: applyExamplesText, examples: [ { name: "A first example", columns: ["Column 1", "Column 2", "Column 3"], }, { name: "A second example", columns: ["Col 1", "Col 2"], }, ], }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); // open the quick create await createColumn(); // click to see the examples await click(target, ".o_column_quick_create .o_kanban_examples"); const $primaryActionButton = $(".modal footer.modal-footer button.btn-primary"); assert.strictEqual( $primaryActionButton.text(), applyExamplesText, "the primary button should display the value of applyExamplesText" ); }); QUnit.test( "quick create column and examples background with ghostColumns titles", async (assert) => { serverData.models.partner.records = []; registry.category("kanban_examples").add("test", { allowedGroupBys: ["product_id"], 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"], }, ], }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); assert.containsOnce( target, ".o_kanban_example_background", "should have ExamplesBackground when no data" ); assert.deepEqual( [...target.querySelectorAll(".o_kanban_examples_group h6")].map( (el) => el.innerText ), ["Ghost 1", "Ghost 2", "Ghost 3", "Ghost 4"], "ghost title should be correct" ); assert.containsOnce( target, ".o_column_quick_create", "should have a ColumnQuickCreate widget" ); assert.containsOnce( target, ".o_column_quick_create .o_kanban_examples:visible", "should not have a link to see examples as there is no examples registered" ); } ); QUnit.test( "quick create column and examples background without ghostColumns titles", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); assert.containsOnce( target, ".o_kanban_example_background", "should have ExamplesBackground when no data" ); assert.deepEqual( [...target.querySelectorAll(".o_kanban_examples_group h6")].map( (el) => el.innerText ), ["Column 1", "Column 2", "Column 3", "Column 4"], "ghost title should be correct" ); assert.containsOnce( target, ".o_column_quick_create", "should have a ColumnQuickCreate widget" ); assert.containsNone( target, ".o_column_quick_create .o_kanban_examples:visible", "should not have a link to see examples as there is no examples registered" ); } ); QUnit.test( "nocontent helper after adding a record (kanban with progressbar)", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], domain: [["foo", "=", "abcd"]], async mockRPC(route, args) { if (args.method === "web_read_group") { return { groups: [ { __domain: [["product_id", "=", 3]], product_id_count: 0, product_id: [3, "hello"], }, ], }; } }, noContentHelp: "No content helper", }); assert.containsOnce(target, ".o_view_nocontent", "the nocontent helper is displayed"); // add a record await quickCreateRecord(); await editQuickCreateInput("display_name", "twilight sparkle"); await validateRecord(); assert.containsNone( target, ".o_view_nocontent", "the nocontent helper is not displayed after quick create" ); // cancel quick create await discardRecord(); assert.containsNone( target, ".o_view_nocontent", "the nocontent helper is not displayed after cancelling the quick create" ); } ); QUnit.test( "if view was not grouped at start, it can be grouped and ungrouped", async (assert) => { const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '' + '
' + "
" + "
", }); assert.doesNotHaveClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped"); await reload(kanban, { groupBy: ["product_id"] }); assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped"); await reload(kanban, { groupBy: [] }); assert.doesNotHaveClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped"); } ); QUnit.test("no content helper when archive all records in kanban group", async (assert) => { // add active field on partner model to have archive option serverData.models.partner.fields.active = { string: "Active", type: "boolean", default: true, }; // remove last records to have only one column serverData.models.partner.records = serverData.models.partner.records.slice(0, 3); patchDialog((_cls, props) => { assert.step("open-dialog"); props.confirm(); }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, noContentHelp: '

click to add a partner

', groupBy: ["bar"], }); // check that the (unique) column contains 3 records assert.containsN(target, ".o_kanban_group:last-child .o_kanban_record", 3); // archive the records of the last column const clickColumnAction = await toggleColumnActions(0); await clickColumnAction("Archive All"); // check no content helper is exist assert.containsOnce(target, ".o_view_nocontent"); assert.verifySteps(["open-dialog"]); }); QUnit.test("no content helper when no data", async (assert) => { const records = serverData.models.partner.records; serverData.models.partner.records = []; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + "
" + '' + '' + "
" + "
", noContentHelp: '

click to add a partner

', }); assert.containsOnce(target, ".o_view_nocontent", "should display the no content helper"); assert.strictEqual( target.querySelector(".o_view_nocontent").innerText, '

click to add a partner

', "should have rendered no content helper from action" ); serverData.models.partner.records = records; await reload(kanban); assert.containsNone( target, ".o_view_nocontent", "should not display the no content helper" ); }); QUnit.test("no nocontent helper for grouped kanban with empty groups", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC(route, args, performRpc) { 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) const result = await performRpc(...arguments); for (const group of result.groups) { group[args.kwargs.groupby[0] + "_count"] = 0; } return result; } }, noContentHelp: "No content helper", }); assert.containsN(target, ".o_kanban_group", 2, "there should be two columns"); assert.containsNone(target, ".o_kanban_record", "there should be no records"); }); QUnit.test("no nocontent helper for grouped kanban with no records", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '
' + "
" + "
", groupBy: ["product_id"], noContentHelp: "No content helper", }); assert.containsNone(target, ".o_kanban_group", "there should be no columns"); assert.containsNone(target, ".o_kanban_record", "there should be no records"); assert.containsNone( target, ".o_view_nocontent", "there should be no nocontent helper (we are in 'column creation mode')" ); assert.containsOnce( target, ".o_column_quick_create", "there should be a column quick create" ); }); QUnit.test("no nocontent helper is shown when no longer creating column", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '
' + "
" + "
", groupBy: ["product_id"], noContentHelp: "No content helper", }); assert.containsNone( target, ".o_view_nocontent", "there should be no nocontent helper (we are in 'column creation mode')" ); // creating a new column await editColumnName("applejack"); await validateColumn(); assert.containsNone( target, ".o_view_nocontent", "there should be no nocontent helper (still in 'column creation mode')" ); // leaving column creation mode await triggerEvent(target, ".o_column_quick_create .o_input", "keydown", { key: "Escape", }); assert.containsOnce(target, ".o_view_nocontent", "there should be a nocontent helper"); }); QUnit.test("no nocontent helper is hidden when quick creating a column", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC(route, args) { if (args.method === "web_read_group") { return { groups: [ { __domain: [["product_id", "=", 3]], product_id_count: 0, product_id: [3, "hello"], }, ], length: 1, }; } }, noContentHelp: "No content helper", }); assert.containsOnce(target, ".o_view_nocontent", "there should be a nocontent helper"); await createColumn(); assert.containsNone( target, ".o_view_nocontent", "there should be no nocontent helper (we are in 'column creation mode')" ); }); QUnit.test("remove nocontent helper after adding a record", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC(route, args) { if (args.method === "web_read_group") { return { groups: [ { __domain: [["product_id", "=", 3]], product_id_count: 0, product_id: [3, "hello"], }, ], length: 1, }; } }, noContentHelp: "No content helper", }); assert.containsOnce(target, ".o_view_nocontent", "there should be a nocontent helper"); // add a record await quickCreateRecord(); await editQuickCreateInput("display_name", "twilight sparkle"); await validateRecord(); assert.containsNone( target, ".o_view_nocontent", "there should be no nocontent helper (there is now one record)" ); }); QUnit.test("remove nocontent helper when adding a record", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC(route, args) { if (args.method === "web_read_group") { return { groups: [ { __domain: [["product_id", "=", 3]], product_id_count: 0, product_id: [3, "hello"], }, ], length: 1, }; } }, noContentHelp: "No content helper", }); assert.containsOnce(target, ".o_view_nocontent", "there should be a nocontent helper"); // add a record await quickCreateRecord(); await editQuickCreateInput("display_name", "twilight sparkle"); assert.containsNone( target, ".o_view_nocontent", "there should be no nocontent helper (there is now one record)" ); }); QUnit.test( "nocontent helper is displayed again after canceling quick create", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '
' + "
" + "
", groupBy: ["product_id"], async mockRPC(route, args) { if (args.method === "web_read_group") { return { groups: [ { __domain: [["product_id", "=", 3]], product_id_count: 0, product_id: [3, "hello"], }, ], length: 1, }; } }, noContentHelp: "No content helper", }); // add a record await quickCreateRecord(); await click(target); assert.containsOnce( target, ".o_view_nocontent", "there should be again a nocontent helper" ); } ); QUnit.test( "nocontent helper for grouped kanban (on m2o field) with no records with no group_create", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: '' + '' + '
' + "
" + "
", groupBy: ["product_id"], noContentHelp: "No content helper", }); assert.containsNone(target, ".o_kanban_group", "there should be no columns"); assert.containsNone(target, ".o_kanban_record", "there should be no records"); assert.containsNone( target, ".o_view_nocontent", "there should not be a nocontent helper" ); assert.containsNone( target, ".o_column_quick_create", "there should not be a column quick create" ); } ); QUnit.test( "nocontent helper for grouped kanban (on date field) with no records with no group_create", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["date"], noContentHelp: "No content helper", }); assert.containsNone(target, ".o_kanban_group"); assert.containsNone(target, ".o_kanban_record"); assert.containsOnce(target, ".o_view_nocontent"); assert.containsNone(target, ".o_column_quick_create"); assert.containsNone(target, ".o_kanban_example_background"); } ); QUnit.test("empty grouped kanban with sample data and no columns", async (assert) => { serverData.models.partner.records = []; await makeView({ arch: `
`, serverData, groupBy: ["product_id"], resModel: "partner", type: "kanban", noContentHelp: "No content helper", }); assert.containsNone(target, ".o_view_nocontent"); assert.containsOnce(target, ".o_quick_create_unfolded"); assert.containsOnce(target, ".o_kanban_example_background_container"); }); QUnit.test( "empty kanban with sample data grouped by date range (fill temporal)", async (assert) => { serverData.models.partner.records = []; await makeView({ arch: `
`, serverData, groupBy: ["date:month"], resModel: "partner", type: "kanban", noContentHelp: "No content helper", mockRPC(route, args) { if (args.method === "web_read_group") { // Simulate fill temporal return { groups: [ { date_count: 0, state: false, "date:month": "December 2022", __range: { "date:month": { from: "2022-12-01", to: "2023-01-01", }, }, __domain: [ ["date", ">=", "2022-12-01"], ["date", "<", "2023-01-01"], ], }, ], length: 1, }; } }, }); assert.containsOnce(target, ".o_view_nocontent"); assert.strictEqual( target.querySelector(".o_kanban_group .o_column_title").textContent, "December 2022" ); assert.containsOnce(target, ".o_kanban_group"); assert.containsN(target, ".o_kanban_group .o_kanban_record", 16); } ); QUnit.test("empty grouped kanban with sample data and click quick create", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], async mockRPC(route, { kwargs, method }, performRpc) { const result = await performRpc(...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; }, noContentHelp: "No content helper", }); assert.containsN(target, ".o_kanban_group", 2, "there should be two columns"); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsOnce(target, ".o_view_nocontent"); assert.containsN( target, ".o_kanban_record", 16, "there should be 8 sample records by column" ); await quickCreateRecord(); assert.doesNotHaveClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsNone(target, ".o_kanban_record"); assert.containsNone(target, ".o_view_nocontent"); assert.containsOnce( target.querySelector(".o_kanban_group:first-child"), ".o_kanban_quick_create" ); await editQuickCreateInput("display_name", "twilight sparkle"); await validateRecord(); assert.doesNotHaveClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsOnce( target.querySelector(".o_kanban_group:first-child"), ".o_kanban_record" ); assert.containsNone(target, ".o_view_nocontent"); }); QUnit.test("empty grouped kanban with sample data and cancel quick create", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], async mockRPC(route, { kwargs, method }, performRpc) { const result = await performRpc(...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; }, noContentHelp: "No content helper", }); assert.containsN(target, ".o_kanban_group", 2, "there should be two columns"); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsOnce(target, ".o_view_nocontent"); assert.containsN( target, ".o_kanban_record", 16, "there should be 8 sample records by column" ); await quickCreateRecord(); assert.doesNotHaveClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsNone(target, ".o_kanban_record"); assert.containsNone(target, ".o_view_nocontent"); assert.containsOnce( target.querySelector(".o_kanban_group:first-child"), ".o_kanban_quick_create" ); await click(target.querySelector(".o_kanban_view")); assert.doesNotHaveClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsNone(target, ".o_kanban_quick_create"); assert.containsNone(target, ".o_kanban_record"); assert.containsOnce(target, ".o_view_nocontent"); }); QUnit.test("empty grouped kanban with sample data: keyboard navigation", async (assert) => { await makeView({ arch: `
`, serverData, groupBy: ["product_id"], resModel: "partner", type: "kanban", async mockRPC(route, { kwargs, method }, performRpc) { const result = await performRpc(...arguments); if (method === "web_read_group") { result.groups.forEach((g) => (g.product_id_count = 0)); } return result; }, }); await toggleColumnActions(0); assert.containsN(target, ".o_kanban_record", 16); assert.hasClass(document.activeElement, "o_searchview_input"); await triggerEvent(document.activeElement, null, "keydown", { key: "ArrowDown" }); assert.hasClass(document.activeElement, "o_searchview_input"); }); QUnit.test("empty kanban with sample data", async (assert) => { serverData.models.partner.records = []; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, noContentHelp: "No content helper", }); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsN( target, ".o_kanban_record:not(.o_kanban_ghost)", 10, "there should be 10 sample records" ); assert.containsOnce(target, ".o_view_nocontent"); await reload(kanban, { domain: [["id", "<", 0]] }); assert.doesNotHaveClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsNone(target, ".o_kanban_record:not(.o_kanban_ghost)"); assert.containsOnce(target, ".o_view_nocontent"); }); QUnit.test("empty grouped kanban with sample data and many2many_tags", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], async mockRPC(route, { kwargs, method }, performRpc) { assert.step(method || route); const result = await performRpc(...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(target, ".o_kanban_group", 2, "there should be 2 'real' columns"); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.ok( target.querySelectorAll(".o_kanban_record").length >= 1, "there should be sample records" ); assert.ok( target.querySelectorAll(".o_field_many2many_tags .o_tag").length >= 1, "there should be tags" ); assert.verifySteps(["get_views", "web_read_group"], "should not read the tags"); }); QUnit.test("sample data does not change after reload with sample data", async (assert) => { Object.assign(serverData, { views: { "partner,false,kanban": `
`, "partner,false,search": "", // list-view so that there is a view switcher, unused "partner,false,list": '', }, }); const webClient = await createWebClient({ serverData, async mockRPC(route, { kwargs, method }, performRpc) { const result = await performRpc(...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; }, }); await doAction(webClient, { res_model: "partner", type: "ir.actions.act_window", views: [ [false, "kanban"], [false, "list"], ], context: { group_by: ["product_id"], }, }); const columns = target.querySelectorAll(".o_kanban_group"); assert.ok(columns.length >= 1, "there should be at least 1 sample column"); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsN(target, ".o_kanban_record", 16); const kanbanText = target.querySelector(".o_kanban_view").innerText; await click(target.querySelector(".o_control_panel .o_switch_view.o_kanban")); assert.strictEqual( kanbanText, target.querySelector(".o_kanban_view").innerText, "the content should be the same after reloading the view" ); }); QUnit.test("non empty kanban with sample data", async (assert) => { const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, noContentHelp: "No content helper", }); assert.doesNotHaveClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 4); assert.containsNone(target, ".o_view_nocontent"); await reload(kanban, { domain: [["id", "<", 0]] }); assert.doesNotHaveClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsNone(target, ".o_kanban_record:not(.o_kanban_ghost)"); }); QUnit.test("empty grouped kanban with sample data: add a column", async (assert) => { await makeView({ arch: `
`, serverData, groupBy: ["product_id"], resModel: "partner", type: "kanban", async mockRPC(route, { method }, performRpc) { const result = await performRpc(...arguments); if (method === "web_read_group") { result.groups = serverData.models.product.records.map((r) => { return { product_id: [r.id, r.display_name], product_id_count: 0, __domain: [["product_id", "=", r.id]], }; }); result.length = result.groups.length; } return result; }, }); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsN(target, ".o_kanban_group", 2); assert.ok( target.querySelectorAll(".o_kanban_record").length > 0, "should contain sample records" ); await createColumn(); await editColumnName("Yoohoo"); await validateColumn(); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsN(target, ".o_kanban_group", 3); assert.ok( target.querySelectorAll(".o_kanban_record").length > 0, "should contain sample records" ); }); QUnit.test("empty grouped kanban with sample data: cannot fold a column", async (assert) => { // folding a column in grouped kanban with sample data is disabled, for the sake of simplicity await makeView({ arch: `
`, serverData, groupBy: ["product_id"], resModel: "partner", type: "kanban", async mockRPC(route, { kwargs, method }, performRpc) { const result = await performRpc(...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(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsOnce(target, ".o_kanban_group"); assert.ok( target.querySelectorAll(".o_kanban_record").length > 0, "should contain sample records" ); await toggleColumnActions(0); assert.hasClass(target.querySelector(".o_kanban_config .o_kanban_toggle_fold"), "disabled"); }); QUnit.skip("empty grouped kanban with sample data: fold/unfold a column", async (assert) => { // folding/unfolding of grouped kanban with sample data is currently disabled await makeView({ arch: `
`, serverData, groupBy: ["product_id"], resModel: "partner", type: "kanban", async mockRPC(route, { kwargs, method }, performRpc) { const result = await performRpc(...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(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsOnce(target, ".o_kanban_group"); assert.ok( target.querySelectorAll(".o_kanban_record").length > 0, "should contain sample records" ); // Fold the column const clickColumnAction = await toggleColumnActions(0); await clickColumnAction("Fold"); assert.containsOnce(target, ".o_kanban_group"); assert.hasClass(target.querySelector(".o_kanban_group"), "o_column_folded"); // Unfold the column await click(target, ".o_kanban_group.o_column_folded"); assert.containsOnce(target, ".o_kanban_group"); assert.doesNotHaveClass(target.querySelector(".o_kanban_group"), "o_column_folded"); assert.ok( target.querySelectorAll(".o_kanban_record").length > 0, "should contain sample records" ); }); QUnit.test("empty grouped kanban with sample data: delete a column", async (assert) => { serverData.models.partner.records = []; patchDialog((_cls, props) => props.confirm()); let groups = [ { product_id: [1, "New"], product_id_count: 0, __domain: [], }, ]; await makeView({ arch: `
`, serverData, groupBy: ["product_id"], resModel: "partner", type: "kanban", async mockRPC(route, { method }, performRpc) { const result = await performRpc(...arguments); if (method === "web_read_group") { // override read_group to return a single, empty group return { groups, length: groups.length, }; } return result; }, }); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsOnce(target, ".o_kanban_group"); assert.ok( target.querySelectorAll(".o_kanban_record").length > 0, "should contain sample records" ); // Delete the first column groups = []; const clickColumnAction = await toggleColumnActions(0); await clickColumnAction("Delete"); assert.containsNone(target, ".o_kanban_group"); assert.containsOnce(target, ".o_column_quick_create .o_quick_create_unfolded"); }); QUnit.test( "empty grouped kanban with sample data: add a column and delete it right away", async (assert) => { patchDialog((_cls, props) => props.confirm()); await makeView({ arch: `
`, serverData, groupBy: ["product_id"], resModel: "partner", type: "kanban", async mockRPC(route, { method }, performRpc) { const result = await performRpc(...arguments); if (method === "web_read_group") { result.groups = serverData.models.product.records.map((r) => { return { product_id: [r.id, r.display_name], product_id_count: 0, __domain: [["product_id", "=", r.id]], }; }); result.length = result.groups.length; } return result; }, }); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsN(target, ".o_kanban_group", 2); assert.ok( target.querySelectorAll(".o_kanban_record").length > 0, "should contain sample records" ); // add a new column await createColumn(); await editColumnName("Yoohoo"); await validateColumn(); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsN(target, ".o_kanban_group", 3); assert.ok( target.querySelectorAll(".o_kanban_record").length, "should contain sample records" ); // delete the column we just created const clickColumnAction = await toggleColumnActions(2); await clickColumnAction("Delete"); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsN(target, ".o_kanban_group", 2); assert.ok( target.querySelectorAll(".o_kanban_record").length, "should contain sample records" ); } ); QUnit.test("kanban with sample data: do an on_create action", async (assert) => { serverData.models.partner.records = []; serverData.views["partner,some_view_ref,form"] = `
`; await makeView({ arch: `
`, serverData, resModel: "partner", type: "kanban", mockRPC: async (route, args) => { if (route === "/web/action/load" && args.action_id === "myCreateAction") { return { type: "ir.actions.act_window", name: "Archive Action", res_model: "partner", view_mode: "form", target: "new", views: [[false, "form"]], }; } }, }); assert.hasClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsN( target, ".o_kanban_record:not(.o_kanban_ghost)", 10, "there should be 10 sample records" ); assert.containsOnce(target, ".o_view_nocontent"); await createRecord(target); assert.containsOnce(target, ".modal"); await click(target, ".modal .o_cp_buttons .o_form_button_save"); assert.doesNotHaveClass(target.querySelector(".o_content"), "o_view_sample_data"); assert.containsOnce(target, ".o_kanban_record:not(.o_kanban_ghost)"); assert.containsNone(target, ".o_view_nocontent"); }); QUnit.test("bounce create button when no data and click on empty area", async (assert) => { const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, noContentHelp: "click to add a partner", }); await click(target, ".o_kanban_view"); assert.doesNotHaveClass(target.querySelector(".o-kanban-button-new"), "o_catch_attention"); await reload(kanban, { domain: [["id", "<", 0]] }); await click(target, ".o_kanban_renderer"); assert.hasClass(target.querySelector(".o-kanban-button-new"), "o_catch_attention"); }); QUnit.test("buttons with modifiers", async (assert) => { serverData.models.partner.records[1].bar = false; // so that test is more complete await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '' + '
' + '
" + "
", }); assert.containsOnce(target, ".o_btn_test_1", "kanban should have one buttons of type 1"); assert.containsN(target, ".o_btn_test_2", 3, "kanban should have three buttons of type 2"); }); QUnit.test("support styling of anchor tags with action type", async function (assert) { assert.expect(3); const actionService = { start() { return { doActionButton: (action) => assert.strictEqual(action.name, "42"), }; }, }; registry.category("services").add("action", actionService, { force: true }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: ` `, }); await click(target.querySelector("a[type='action']")); assert.hasClass(target.querySelector("a[type='action']"), "btn-primary"); assert.strictEqual(target.querySelector("a[type='action']").style.marginLeft, "10px"); }); QUnit.test("button executes action and reloads", async (assert) => { const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, async mockRPC(route) { assert.step(route); }, }); assert.verifySteps([ "/web/dataset/call_kw/partner/get_views", "/web/dataset/call_kw/partner/web_search_read", ]); assert.ok( target.querySelectorAll("button.a1").length, "kanban should have at least one button a1" ); let count = 0; patchWithCleanup(kanban.env.services.action, { doActionButton({ onClose }) { count++; onClose(); }, }); click(target.querySelector("button.a1")); await click(target.querySelector("button.a1")); assert.strictEqual(count, 1, "should have triggered an execute action only once"); assert.verifySteps( ["/web/dataset/call_kw/partner/web_search_read"], "the records should be reloaded after executing a button action" ); }); QUnit.test("button executes action and check domain", async (assert) => { serverData.models.partner.fields.active = { string: "Active", type: "boolean", default: true, }; for (const k in serverData.models.partner.records) { serverData.models.partner.records[k].active = true; } const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '
' + '' + '' + '
" + "
", }); patchWithCleanup(kanban.env.services.action, { doActionButton({ onClose }) { serverData.models.partner.records[0].active = false; onClose(); }, }); assert.strictEqual( getCard(0).querySelector("span").textContent, "yop", "should display 'yop' record" ); await click(getCard(0), "button.toggle-active"); assert.notEqual( getCard(0).querySelector("span").textContent, "yop", "should remove 'yop' record from the view" ); }); QUnit.test("button executes action with domain field not in view", async (assert) => { const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, domain: [["bar", "=", true]], arch: "" + '
' + '' + '
" + "
", }); patchWithCleanup(kanban.env.services.action, { doActionButton({ onClose }) { onClose(); }, }); try { await click(target.querySelector('.o_kanban_record button[name="toggle_action"]')); assert.strictEqual(true, true, "Everything went fine"); } catch { assert.strictEqual(true, false, "Error triggered at action execution"); } }); QUnit.test("field tag with modifiers but no widget", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); assert.strictEqual(target.querySelector(".o_kanban_record").innerText, ""); assert.strictEqual(target.querySelectorAll(".o_kanban_record")[1].innerText, "blip"); }); QUnit.test("field tag with widget and class attributes", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); assert.containsN(target, ".o_field_widget.hi", 4); }); QUnit.test("rendering date and datetime (value)", async (assert) => { serverData.models.partner.records[0].date = "2017-01-25"; serverData.models.partner.records[1].datetime = "2016-12-12 10:55:05"; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '' + "
" + '' + '' + "
" + "
" + "
", }); assert.strictEqual(getCard(0).querySelector(".date").innerText, "01/25/2017"); assert.strictEqual(getCard(1).querySelector(".datetime").innerText, "12/12/2016 11:55:05"); }); QUnit.test("rendering date and datetime (raw value)", async (assert) => { serverData.models.partner.records[0].date = "2017-01-25"; serverData.models.partner.records[1].datetime = "2016-12-12 10:55:05"; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '' + "
" + '' + '' + "
" + "
" + "
", }); assert.equal(getCard(0).querySelector(".date").innerText, "2017-01-25T00:00:00.000+01:00"); assert.equal( getCard(1).querySelector(".datetime").innerText, "2016-12-12T11:55:05.000+01:00" ); }); QUnit.test("rendering many2one (value)", async (assert) => { serverData.models.partner.records[1].product_id = false; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + "
" + '' + "
" + "
" + "
", }); assert.deepEqual(getCardTexts(), ["hello", "hello", "xmo"]); }); QUnit.test("rendering many2one (raw value)", async (assert) => { serverData.models.partner.records[1].product_id = false; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + "
" + '' + "
" + "
" + "
", }); assert.deepEqual(getCardTexts(), ["3", "false", "3", "5"]); }); QUnit.test("evaluate conditions on relational fields", async (assert) => { serverData.models.partner.records[0].product_id = false; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '' + "
" + '' + '' + "
" + "
" + "
", }); assert.containsN( target, ".o_kanban_record:not(.o_kanban_ghost)", 4, "there should be 4 records" ); assert.containsOnce( target, ".o_kanban_record:not(.o_kanban_ghost) .btn_a", "only 1 of them should have the 'Action' button" ); assert.containsN( target, ".o_kanban_record:not(.o_kanban_ghost) .btn_b", 2, "only 2 of them should have the 'Action' button" ); }); QUnit.test("resequence columns in grouped by m2o", async (assert) => { serverData.models.product.fields.sequence = { type: "integer" }; await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '
' + "
" + "
", groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group", 2); assert.strictEqual(getColumn(0).querySelector(".o_column_title").innerText, "hello"); assert.deepEqual(getCardTexts(), ["1", "3", "2", "4"]); await dragAndDrop(".o_kanban_group:first-child", ".o_kanban_group:nth-child(2)"); // Drag & drop on column (not title) should not work assert.strictEqual(getColumn(0).querySelector(".o_column_title").innerText, "hello"); assert.deepEqual(getCardTexts(), ["1", "3", "2", "4"]); await dragAndDrop( ".o_kanban_group:first-child .o_column_title", ".o_kanban_group:nth-child(2)" ); assert.strictEqual(getColumn(0).querySelector(".o_column_title").innerText, "xmo"); assert.deepEqual(getCardTexts(), ["2", "4", "1", "3"]); }); QUnit.test( "resequence all when create(ing) a new record + partial resequencing", async (assert) => { let resequenceOffset; await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["product_id"], mockRPC(route, params) { if (route === "/web/dataset/resequence") { assert.step(JSON.stringify({ ids: params.ids, offset: params.offset })); resequenceOffset = params.offset || 0; return true; } if (params.method === "read") { // Important to simulate the server returning the new sequence. const [ids, fields] = params.args; return ids.map((id, index) => ({ id, [fields[0]]: resequenceOffset + index, })); } }, }); await createColumn(); await editColumnName("foo"); await validateColumn(); assert.verifySteps([JSON.stringify({ ids: [3, 5, 6] })]); await editColumnName("bar"); await validateColumn(); assert.verifySteps([JSON.stringify({ ids: [3, 5, 6, 7] })]); await editColumnName("baz"); await validateColumn(); assert.verifySteps([JSON.stringify({ ids: [3, 5, 6, 7, 8] })]); await editColumnName("boo"); await validateColumn(); assert.verifySteps([JSON.stringify({ ids: [3, 5, 6, 7, 8, 9] })]); // When rearranging, only resequence the affected records. In this example, // dragging column 2 to column 4 should only resequence [5, 6, 7] to [6, 7, 5] // with offset 1. await dragAndDrop( ".o_kanban_group:nth-child(2) .o_column_title", ".o_kanban_group:nth-child(4)" ); assert.verifySteps([JSON.stringify({ ids: [6, 7, 5], offset: 1 })]); } ); QUnit.test("prevent resequence columns if groups_draggable=false", async (assert) => { serverData.models.product.fields.sequence = { type: "integer" }; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group", 2); assert.strictEqual(getColumn(0).querySelector(".o_column_title").innerText, "hello"); assert.deepEqual(getCardTexts(), ["1", "3", "2", "4"]); await dragAndDrop(".o_kanban_group:first-child", ".o_kanban_group:nth-child(2)"); // Drag & drop on column (not title) should not work assert.strictEqual(getColumn(0).querySelector(".o_column_title").innerText, "hello"); assert.deepEqual(getCardTexts(), ["1", "3", "2", "4"]); await dragAndDrop( ".o_kanban_group:first-child .o_column_title", ".o_kanban_group:nth-child(2)" ); assert.strictEqual(getColumn(0).querySelector(".o_column_title").innerText, "hello"); assert.deepEqual(getCardTexts(), ["1", "3", "2", "4"]); }); QUnit.test("properly evaluate more complex domains", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: "" + '' + '' + '' + "" + '' + "
" + '' + "
`, }); assert.deepEqual(getCardTexts(), ["EDITDELETE", "EDITDELETE", "EDITDELETE", "EDITDELETE"]); }); QUnit.test("folded groups are kept when leaving and coming back", async (assert) => { serverData.views = { "partner,false,kanban": `
`, "partner,false,search": "", "partner,false,form": "
", }; const webClient = await createWebClient({ serverData }); await doAction(webClient, { name: "Partners", res_model: "partner", type: "ir.actions.act_window", views: [ [false, "kanban"], [false, "form"], ], context: { group_by: ["product_id"], }, }); assert.containsOnce(target, ".o_kanban_view"); assert.containsN(target, ".o_kanban_group", 2); assert.containsNone(target, ".o_column_folded"); assert.containsN(target, ".o_kanban_record", 4); // fold the first group const clickColumnAction = await toggleColumnActions(0); await clickColumnAction("Fold"); assert.containsOnce(target, ".o_column_folded"); assert.containsN(target, ".o_kanban_record", 2); // open a record and go back await click(target.querySelector(".o_kanban_record")); assert.containsOnce(target, ".o_form_view"); await click(target.querySelector(".breadcrumb-item a")); assert.containsOnce(target, ".o_column_folded"); assert.containsN(target, ".o_kanban_record", 2); }); QUnit.test("filter groups are kept when leaving and coming back", async (assert) => { serverData.models.partner.records[1].state = "abc"; serverData.views = { "partner,false,kanban": `
`, "partner,false,search": "", "partner,false,form": ` `, }; const webClient = await createWebClient({ serverData }); await doAction(webClient, { name: "Partners", res_model: "partner", type: "ir.actions.act_window", views: [ [false, "kanban"], [false, "form"], ], context: { group_by: ["bar"], }, }); // Filter on state "abc" => matches 2 records await click(getProgressBars(1)[0]); assert.deepEqual(getCardTexts(0), ["4"]); assert.deepEqual(getCardTexts(1), ["1", "2"]); // open a record await click(target.querySelectorAll(".o_kanban_record")[1]); assert.containsOnce(target, ".o_form_view"); // go back to kanban view await click(target.querySelector(".breadcrumb-item a")); assert.deepEqual(getCardTexts(0), ["4"]); assert.deepEqual(getCardTexts(1), ["1", "2"]); // open a record await click(target.querySelectorAll(".o_kanban_record")[1]); assert.containsOnce(target, ".o_form_view"); // select another state await click(target.querySelectorAll("input.o_radio_input")[1]); // go back to kanban view await click(target.querySelector(".breadcrumb-item a")); assert.deepEqual(getCardTexts(0), ["4"]); assert.deepEqual(getCardTexts(1), ["2"]); }); QUnit.test( "folded groups are kept when leaving and coming back (grouped by date)", async (assert) => { serverData.models.partner.fields.date.default = "2022-10-10"; serverData.models.partner.records[0].date = "2022-05-10"; serverData.views = { "partner,false,kanban": `
`, "partner,false,search": "", "partner,false,form": "
", }; const webClient = await createWebClient({ serverData }); await doAction(webClient, { name: "Partners", res_model: "partner", type: "ir.actions.act_window", views: [ [false, "kanban"], [false, "form"], ], context: { group_by: ["date"], }, }); assert.containsOnce(target, ".o_kanban_view"); assert.containsN(target, ".o_kanban_group", 2); assert.containsNone(target, ".o_column_folded"); assert.containsN(target, ".o_kanban_record", 4); // fold the second column const clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Fold"); assert.containsOnce(target, ".o_column_folded"); assert.containsOnce(target, ".o_kanban_record"); // open a record and go back await click(target.querySelector(".o_kanban_record")); assert.containsOnce(target, ".o_form_view"); await click(target.querySelector(".breadcrumb-item a")); assert.containsOnce(target, ".o_column_folded"); assert.containsOnce(target, ".o_kanban_record"); } ); QUnit.test("loaded records are kept when leaving and coming back", async (assert) => { serverData.views = { "partner,false,kanban": `
`, "partner,false,search": "", "partner,false,form": "", }; const webClient = await createWebClient({ serverData }); await doAction(webClient, { name: "Partners", res_model: "partner", type: "ir.actions.act_window", views: [ [false, "kanban"], [false, "form"], ], context: { group_by: ["product_id"], }, }); assert.containsOnce(target, ".o_kanban_view"); assert.containsN(target, ".o_kanban_group", 2); assert.containsN(target, ".o_kanban_record", 2); // load more records in second group await loadMore(1); assert.containsN(target, ".o_kanban_record", 3); // open a record and go back await click(target.querySelector(".o_kanban_record")); assert.containsOnce(target, ".o_form_view"); await click(target.querySelector(".breadcrumb-item a")); assert.containsN(target, ".o_kanban_record", 3); }); QUnit.test("basic rendering with 2 groupbys", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar", "product_id"], async mockRPC(route, args) { assert.step(args.method); }, }); assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped"); assert.containsN(target, ".o_kanban_group", 2); assert.containsOnce(target, ".o_kanban_group:first-child .o_kanban_record"); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 3); assert.verifySteps(["get_views", "web_read_group", "web_search_read", "web_search_read"]); }); QUnit.test("basic rendering with a date groupby with a granularity", async (assert) => { serverData.models.partner.records[0].date = "2022-06-23"; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["date:day"], async mockRPC(route, args) { if (args.method === "web_read_group") { assert.deepEqual(args.kwargs.fields, ["foo", "date"]); assert.deepEqual(args.kwargs.groupby, ["date:day"]); } assert.step(args.method); }, }); assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped"); assert.containsN(target, ".o_kanban_group", 2); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 3); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); assert.verifySteps(["get_views", "web_read_group", "web_search_read", "web_search_read"]); }); QUnit.test("quick create record and click outside (no dirty input)", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], createRecord: () => { assert.step("create record"); }, }); assert.containsNone(target, ".o_kanban_quick_create"); await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_quick_create"); await click(target, ".o_control_panel"); assert.containsNone(target, ".o_kanban_quick_create"); await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_quick_create"); await quickCreateRecord(1); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_quick_create"); await click(target, ".o_kanban_load_more button"); assert.containsNone(target, ".o_kanban_quick_create"); await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_quick_create"); assert.verifySteps([]); await click(target, ".o-kanban-button-new"); assert.verifySteps(["create record"]); assert.containsNone(target, ".o_kanban_quick_create"); }); QUnit.test("quick create record and click outside (with dirty input)", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], createRecord: () => { assert.step("create record"); }, }); assert.containsNone(target, ".o_kanban_quick_create"); await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_quick_create"); await editInput(target, ".o_kanban_quick_create [name=display_name] input", "ABC"); assert.strictEqual( target.querySelector(".o_kanban_quick_create [name=display_name] input").value, "ABC" ); await click(target, ".o_control_panel"); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_quick_create"); assert.strictEqual( target.querySelector(".o_kanban_quick_create [name=display_name] input").value, "ABC" ); await quickCreateRecord(1); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_quick_create"); assert.strictEqual( target.querySelector(".o_kanban_quick_create [name=display_name] input").value, "" ); await editInput(target, ".o_kanban_quick_create [name=display_name] input", "ABC"); assert.strictEqual( target.querySelector(".o_kanban_quick_create [name=display_name] input").value, "ABC" ); await click(target, ".o_kanban_load_more button"); assert.containsNone(target, ".o_kanban_quick_create"); await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_quick_create"); await editInput(target, ".o_kanban_quick_create [name=display_name] input", "ABC"); assert.strictEqual( target.querySelector(".o_kanban_quick_create [name=display_name] input").value, "ABC" ); assert.verifySteps([]); await click(target, ".o-kanban-button-new"); assert.verifySteps(["create record"]); assert.containsNone(target, ".o_kanban_quick_create"); }); QUnit.test("quick create record and click on 'Load more'", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], }); assert.containsNone(target, ".o_kanban_quick_create"); await quickCreateRecord(1); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_quick_create"); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); await click(target, ".o_kanban_load_more button"); await nextTick(); assert.containsNone(target, ".o_kanban_quick_create"); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 3); }); QUnit.test("classes on dropdown menu do not end on dropdown main div", async (assert) => { serverData.models.partner.records.splice(1, 3); // keep one record only await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, }); const dropdown = target.querySelector(".o_kanban_record .o-dropdown"); assert.isVisible(dropdown); assert.hasClass(dropdown, "o_dropdown_kanban"); assert.doesNotHaveClass(dropdown, "dropdown-menu"); assert.doesNotHaveClass(dropdown, "o_kanban_card_manage_pane"); await click(dropdown, "button.dropdown-toggle"); assert.containsOnce(dropdown, ".o_kanban_card_manage_pane.dropdown-menu"); }); QUnit.test("classes on toggler do not end on dropdown main div", async (assert) => { serverData.models.partner.records.splice(1, 3); // keep one record only await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ ` `, }); const dropdown = target.querySelector(".o_kanban_record .o-dropdown"); assert.isVisible(dropdown); assert.hasClass(dropdown, "o_dropdown_kanban"); assert.doesNotHaveClass(dropdown, "o_kanban_manage_toggle_button"); assert.doesNotHaveClass(dropdown, "o_left"); assert.containsOnce(dropdown, ".o_kanban_manage_toggle_button.o_left"); }); QUnit.test("dropdown is closed on item click", async (assert) => { serverData.models.partner.records.splice(1, 3); // keep one record only await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ ` `, }); assert.containsNone(target, ".dropdown-menu"); await click(target, ".o_kanban_renderer .dropdown-toggle"); assert.containsOnce(target, ".dropdown-menu"); await click(target, ".o_kanban_renderer .dropdown-menu .dropdown-item"); assert.containsNone(target, ".dropdown-menu"); }); QUnit.test( "classes on dropdown are on dropdown main div but not the other attributes", async (assert) => { serverData.models.partner.records.splice(1, 3); // keep one record only await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, }); const dropdown = target.querySelector(".o_kanban_record .o-dropdown"); assert.isVisible(dropdown); assert.strictEqual( dropdown.className, "o-dropdown dropdown o_dropdown_kanban o_kanban_manage_button_section my_class o-dropdown--no-caret" ); assert.notOk(dropdown.hasAttribute("placeholder")); } ); QUnit.test("declaring only the menu does not insert a dropdown", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, }); assert.containsNone(target, ".o_kanban_record .o-dropdown"); }); QUnit.test("support multiple dropdowns", async (assert) => { assert.expect(4); await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, }); assert.containsN(target, ".o_kanban_renderer .dropdown-true", 3); assert.containsN(target, ".o_kanban_renderer .dropdown", 4); await toggleRecordDropdown(2); assert.strictEqual(getCardTexts()[2], "gnap\nTOGGLER TRUE\nMENU TRUE"); await toggleRecordDropdown(3); assert.strictEqual(getCardTexts()[3], "blip\nTOGGLER FALSE\nMENU FALSE"); }); QUnit.test("can use JSON in kanban template", async (assert) => { serverData.models.partner.records = [{ id: 1, foo: '["g", "e", "d"]' }]; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, }); assert.containsOnce(target, ".o_kanban_record:not(.o_kanban_ghost)"); assert.containsN(target, ".o_kanban_record span", 3); assert.strictEqual(target.querySelector(".o_kanban_record").innerText, "ged"); }); QUnit.test("Dropdowns in subtemplates", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, }); assert.containsN(target, ".o_kanban_record .dropdown", 4); await toggleRecordDropdown(0); assert.strictEqual(getCardTexts()[0], "yop\nTOGGLER\nMENU"); }); QUnit.test( "Color '200' (gray) can be used twice (for false value and another value) in progress bar", async (assert) => { serverData.models.partner.records.push({ id: 5, bar: true }, { id: 6, bar: false }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], }); assert.containsN(target, ".o_kanban_group:nth-child(1) .progress-bar", 2); assert.deepEqual( [...target.querySelectorAll(".o_kanban_group:nth-child(1) .progress-bar")].map( (el) => el.dataset.tooltip ), ["1 blip", "1 Other"] ); assert.containsN(target, ".o_kanban_group:nth-child(2) .progress-bar", 4); assert.deepEqual( [...target.querySelectorAll(".o_kanban_group:nth-child(2) .progress-bar")].map( (el) => el.dataset.tooltip ), ["1 yop", "1 gnap", "1 blip", "1 Other"] ); assert.deepEqual(getCounters(), ["2", "4"]); await click(target.querySelector(".o_kanban_group:nth-child(2) .progress-bar")); assert.deepEqual(getCounters(), ["2", "1"]); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_record").innerText, "ABC" ); assert.containsNone(target, ".o_kanban_group:nth-child(2) .o_kanban_load_more"); await click( target.querySelector(".o_kanban_group:nth-child(2) .progress-bar:nth-child(2)") ); assert.deepEqual(getCounters(), ["2", "1"]); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_record").innerText, "GHI" ); assert.containsNone(target, ".o_kanban_group:nth-child(2) .o_kanban_load_more"); await click( target.querySelector(".o_kanban_group:nth-child(2) .progress-bar:nth-child(4)") ); assert.deepEqual(getCounters(), ["2", "1"]); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_record").innerText, "" ); assert.containsNone(target, ".o_kanban_group:nth-child(2) .o_kanban_load_more"); } ); QUnit.test("update field on which progress bars are computed", async (assert) => { serverData.models.partner.records.push({ id: 5, state: "abc", bar: true }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["bar"], }); // Initial state: 2 columns, the "Yes" column contains 2 records "abc", 1 "def" and 1 "ghi" assert.deepEqual(getCounters(), ["1", "4"]); assert.containsN(getColumn(1), ".o_kanban_record", 4); assert.containsN(getColumn(1), ".o_kanban_counter_progress .progress-bar", 3); assert.strictEqual(getProgressBars(1)[0].style.width, "50%"); // abc: 2 assert.strictEqual(getProgressBars(1)[1].style.width, "25%"); // def: 1 assert.strictEqual(getProgressBars(1)[2].style.width, "25%"); // ghi: 1 // Filter on state "abc" => matches 2 records await click(getProgressBars(1)[0]); assert.deepEqual(getCounters(), ["1", "2"]); assert.containsN(getColumn(1), ".o_kanban_record", 2); assert.containsN(getColumn(1), ".o_kanban_counter_progress .progress-bar", 3); assert.strictEqual(getProgressBars(1)[0].style.width, "50%"); // abc: 2 assert.strictEqual(getProgressBars(1)[1].style.width, "25%"); // def: 1 assert.strictEqual(getProgressBars(1)[2].style.width, "25%"); // ghi: 1 // Changes the state of the first record of the "Yes" column to "def" // The updated record should remain visible await click(getCard(2), ".o_status"); await click(getCard(2), ".o_field_state_selection .dropdown-item:first-child"); assert.deepEqual(getCounters(), ["1", "1"]); assert.containsN(getColumn(1), ".o_kanban_record", 2); assert.containsN(getColumn(1), ".o_kanban_counter_progress .progress-bar", 3); assert.strictEqual(getProgressBars(1)[0].style.width, "25%"); // abc: 1 assert.strictEqual(getProgressBars(1)[1].style.width, "50%"); // def: 2 assert.strictEqual(getProgressBars(1)[2].style.width, "25%"); // ghi: 1 // Filter on state "def" => matches 2 records (including the one we just changed) await click(getProgressBars(1)[1]); assert.deepEqual(getCounters(), ["1", "2"]); assert.containsN(getColumn(1), ".o_kanban_record", 2); assert.strictEqual(getProgressBars(1)[0].style.width, "25%"); // abc: 1 assert.strictEqual(getProgressBars(1)[1].style.width, "50%"); // def: 2 assert.strictEqual(getProgressBars(1)[2].style.width, "25%"); // ghi: 1 // Filter back on state "abc" => matches only 1 record await click(getProgressBars(1)[0]); assert.deepEqual(getCounters(), ["1", "1"]); assert.containsN(getColumn(1), ".o_kanban_record", 1); assert.strictEqual(getProgressBars(1)[0].style.width, "25%"); // abc: 1 assert.strictEqual(getProgressBars(1)[1].style.width, "50%"); // def: 2 assert.strictEqual(getProgressBars(1)[2].style.width, "25%"); // ghi: 1 }); QUnit.test("load more button shouldn't be visible when unfiltering column", async (assert) => { serverData.models.partner.records.push({ id: 5, state: "abc", bar: true }); let def; await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["bar"], mockRPC: async (route, args) => { const { method } = args; if (method === "web_search_read") { await def; } }, }); // Initial state: 2 columns, the "No" column contains 1 record, The "Yes" column contains 4 records assert.deepEqual(getCounters(), ["1", "4"]); // Filter on state "abc" => matches 2 records await click(getProgressBars(1)[0]); // Filtered state: 2 columns, the "No" column contains 1 record, The "Yes" column contains 2 records assert.deepEqual(getCounters(), ["1", "2"]); def = makeDeferred(); // UnFiltered the "Yes" column await click(getProgressBars(1)[0]); assert.containsNone( target, ".o_kanban_load_more", "The load more button should not be visible" ); def.resolve(); await nextTick(); //Return to initial state assert.deepEqual(getCounters(), ["1", "4"]); assert.containsNone( target, ".o_kanban_load_more", "The load more button should not be visible" ); }); QUnit.test("click on the progressBar of a new column", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], domain: [["id", ">", 0]], mockRPC: (route, args) => { const { method, kwargs } = args; if (args.method === "web_search_read") { assert.step(method); assert.deepEqual(kwargs.domain, [ "&", "&", ["id", ">", 0], ["product_id", "=", 6], "!", ["state", "in", ["abc", "def", "ghi"]], ]); } }, }); // Create a new column await editColumnName("new column"); await validateColumn(); // Crete a record in the new column await quickCreateRecord(); await editQuickCreateInput("display_name", "new product"); await validateRecord(); assert.containsOnce(target, ".o_kanban_record"); // Togggle the progressBar await click(getProgressBars(0)[0]); assert.containsOnce(target, ".o_kanban_record"); assert.verifySteps(["web_search_read"]); }); QUnit.test( "keep focus inside control panel when pressing arrowdown and no kanban card", async (assert) => { serverData.models.partner.records = []; await makeView({ type: "kanban", resModel: "partner", serverData, groupBy: ["product_id"], arch: /* xml */ `
`, }); // Check that there is a column quick create assert.containsOnce(target, ".o_column_quick_create"); await editColumnName("new col"); await validateColumn(); // Check that there is only one group and no kanban card assert.containsOnce(target, ".o_kanban_group"); assert.containsOnce(target, ".o_kanban_group.o_kanban_no_records"); assert.containsNone(target, ".o_kanban_record"); // Check that the focus is on the searchview input await quickCreateRecord(); assert.containsOnce(target, ".o_kanban_group.o_kanban_no_records"); assert.containsOnce(target, ".o_kanban_quick_create"); assert.containsNone(target, ".o_kanban_record"); // Somehow give the focus in the control panel, i.e. in the search view // Note that a simple click in the control panel should normally close the quick // create, so in order to give the focus in the search input, the user would // normally have to right-click on it then press escape. These are behaviors // handled through the browser, so we simply call focus directly here. target.querySelector(".o_searchview_input").focus(); // Make sure no async code will have a side effect on the focused element await nextTick(); assert.hasClass(document.activeElement, "o_searchview_input"); // Trigger the ArrowDown hotkey triggerHotkey("ArrowDown"); await nextTick(); assert.hasClass(document.activeElement, "o_searchview_input"); } ); QUnit.test("no leak of TransactionInProgress (grouped case)", async (assert) => { let def; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["state"], async mockRPC(route) { if (route === "/web/dataset/resequence") { assert.step("resequence"); await Promise.resolve(def); } }, }); def = makeDeferred(); assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(1) .o_kanban_record").innerText, "yop" ); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_record").innerText, "blip" ); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 2); assert.verifySteps([]); // move "yop" from first to second column await dragAndDrop( ".o_kanban_group:nth-child(1) .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.containsNone(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); assert.deepEqual( [...target.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map( (el) => el.innerText ), ["blip", "yop"] ); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 2); assert.verifySteps(["resequence"]); // try to move "yop" from second to third column await dragAndDrop( ".o_kanban_group:nth-child(2) .o_kanban_record:nth-child(3)", // move yop ".o_kanban_group:nth-child(3)" ); assert.containsNone(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); assert.deepEqual( [...target.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map( (el) => el.innerText ), ["blip", "yop"] ); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 2); assert.verifySteps([]); def.resolve(); await nextTick(); // try again to move "yop" from second to third column await dragAndDrop( ".o_kanban_group:nth-child(2) .o_kanban_record:nth-child(3)", // move yop ".o_kanban_group:nth-child(3)" ); assert.containsNone(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 3); assert.deepEqual( [...target.querySelectorAll(".o_kanban_group:nth-child(3) .o_kanban_record")].map( (el) => el.innerText ), ["gnap", "blip", "yop"] ); assert.verifySteps(["resequence"]); }); QUnit.test("no leak of TransactionInProgress (not grouped case)", async (assert) => { let def; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, async mockRPC(route) { if (route === "/web/dataset/resequence") { assert.step("resequence"); await Promise.resolve(def); } }, }); def = makeDeferred(); assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 4); assert.deepEqual( [...target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")].map( (el) => el.innerText ), ["blip", "blip", "yop", "gnap"] ); assert.verifySteps([]); // move second "blip" to third place await dragAndDrop(".o_kanban_record:nth-child(2)", ".o_kanban_record:nth-child(3)"); assert.deepEqual( [...target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")].map( (el) => el.innerText ), ["blip", "blip", "yop", "gnap"] ); assert.verifySteps(["resequence"]); // try again await dragAndDrop(".o_kanban_record:nth-child(2)", ".o_kanban_record:nth-child(3)"); -assert.verifySteps([]); def.resolve(); await nextTick(); assert.deepEqual( [...target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")].map( (el) => el.innerText ), ["blip", "yop", "blip", "gnap"] ); await dragAndDrop(".o_kanban_record:nth-child(3)", ".o_kanban_record:nth-child(4)"); assert.deepEqual( [...target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")].map( (el) => el.innerText ), ["blip", "yop", "gnap", "blip"] ); assert.verifySteps(["resequence"]); }); QUnit.test("dragged record cannot be saved", async (assert) => { let def; serverData.models.partner.records = [ { id: 1, bar: false, state: "abc" }, { id: 2, bar: true, state: "def", foo: "blip" }, ]; serverData.models.partner.onchanges = { bar() {}, }; const kanban = await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], async mockRPC(route, args) { if (args.method === "onchange") { assert.step("onchange"); await Promise.resolve(def); } }, }); patchWithCleanup(kanban.env.services.notification, { add(message, options) { assert.step("notification"); assert.strictEqual(options.title, "Invalid fields: "); assert.strictEqual(`${message}`, "
  • Foo
"); }, }); def = makeDeferred(); assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(1) .o_kanban_record").innerText, "ABC" ); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_record").innerText, "blipDEF" ); assert.verifySteps([]); // move "yop" from first to second column await dragAndDrop( ".o_kanban_group:nth-child(1) .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.containsNone(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); assert.deepEqual( [...target.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map( (el) => el.innerText ), ["blipDEF", "ABC"] ); assert.verifySteps(["onchange"]); def.resolve(); await nextTick(); assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(1) .o_kanban_record").innerText, "ABC" ); assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); assert.strictEqual( target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_record").innerText, "blipDEF" ); assert.verifySteps(["notification"]); }); QUnit.test("renders banner_route", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], async mockRPC(route) { if (route === "/mybody/isacage") { assert.step(route); return { html: `
myBanner
` }; } }, }); assert.verifySteps(["/mybody/isacage"]); assert.containsOnce(target, ".setmybodyfree"); }); QUnit.test("fieldDependencies support for fields", async (assert) => { class CustomField extends Component {} CustomField.fieldDependencies = { int_field: { type: "integer" }, }; CustomField.template = xml``; registry.category("fields").add("custom_field", CustomField); await makeView({ resModel: "partner", type: "kanban", arch: `
`, serverData, }); assert.strictEqual(target.querySelector("[name=foo] span").innerText, "10"); }); QUnit.test( "fieldDependencies support for fields: dependence on a relational field", async (assert) => { class CustomField extends Component {} CustomField.fieldDependencies = { product_id: { type: "many2one", relation: "product" }, }; CustomField.template = xml``; registry.category("fields").add("custom_field", CustomField); await makeView({ resModel: "partner", type: "kanban", arch: `
`, serverData, mockRPC: (route, args) => { assert.step(args.method); }, }); assert.strictEqual(target.querySelector("[name=foo] span").innerText, "hello"); assert.verifySteps(["get_views", "web_search_read"]); } ); QUnit.test("column quick create - title and placeholder", async function (assert) { assert.expect(2); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], }); const productFieldName = serverData.models.partner.fields.product_id.string; assert.strictEqual( target.querySelector(".o_column_quick_create .o_quick_create_folded").innerText, productFieldName ); await click(target, "button.o_kanban_add_column"); assert.strictEqual( target .querySelector( ".o_column_quick_create .o_quick_create_unfolded .input-group .o_input" ) .getAttribute("placeholder"), productFieldName + "..." ); }); QUnit.test("fold a column and drag record on it should not unfold it", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group", 2); assert.containsN(getColumn(0), ".o_kanban_record", 2); assert.containsN(getColumn(1), ".o_kanban_record", 2); const clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Fold"); assert.containsN(getColumn(0), ".o_kanban_record", 2); assert.hasClass(getColumn(1), "o_column_folded"); assert.strictEqual(getColumn(1).innerText, "xmo (2)"); await dragAndDrop(".o_kanban_group:first-child .o_kanban_record", ".o_column_folded"); assert.containsN(getColumn(0), ".o_kanban_record", 1); assert.hasClass(getColumn(1), "o_column_folded"); assert.strictEqual(getColumn(1).innerText, "xmo (3)"); }); QUnit.test("drag record on initially folded column should not unfold it", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["product_id"], async mockRPC(route, args, performRPC) { if (args.method === "web_read_group") { const result = await performRPC(route, args); result.groups[1].__fold = true; return result; } }, }); assert.containsN(getColumn(0), ".o_kanban_record", 2); assert.hasClass(getColumn(1), "o_column_folded"); assert.strictEqual(getColumn(1).innerText, "xmo (2)"); await dragAndDrop(".o_kanban_group:first-child .o_kanban_record", ".o_column_folded"); assert.containsN(getColumn(0), ".o_kanban_record", 1); assert.hasClass(getColumn(1), "o_column_folded"); assert.strictEqual(getColumn(1).innerText, "xmo (3)"); }); QUnit.test("drag record to folded column, with progressbars", async (assert) => { serverData.models.partner.records[0].bar = false; await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["bar"], }); assert.containsN(target, ".o_kanban_group", 2); assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2); assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); assert.deepEqual( getProgressBars(0).map((pb) => pb.style.width), ["50%", "50%"] ); assert.deepEqual( getProgressBars(1).map((pb) => pb.style.width), ["50%", "50%"] ); assert.deepEqual(getCounters(), ["6", "26"]); const clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Fold"); assert.containsN(getColumn(0), ".o_kanban_record", 2); assert.hasClass(getColumn(1), "o_column_folded"); assert.strictEqual(getColumn(1).innerText, "Yes (2)"); await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.containsOnce(getColumn(0), ".o_kanban_record"); assert.strictEqual(getColumn(1).innerText, "Yes (3)"); assert.deepEqual( getProgressBars(0).map((pb) => pb.style.width), ["100%"] ); assert.deepEqual(getCounters(), ["-4"]); }); QUnit.test("quick create record in grouped kanban in a form view dialog", async (assert) => { serverData.models.partner.fields.foo.default = "ABC"; serverData.views = { "partner,false,form": ` `, }; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], async mockRPC(route, { method }) { assert.step(method || route); if (method === "name_create") { throw new RPCError(); } }, }); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 2, "first column should contain two records" ); assert.deepEqual( [...target.querySelectorAll(".o_kanban_group:first-child .o_kanban_record")].map( (el) => el.innerText ), ["yop", "gnap"] ); assert.containsNone(target, ".modal"); // click on 'Create', fill the quick create and validate await createRecord(); await editQuickCreateInput("display_name", "new partner"); await validateRecord(); assert.containsOnce(target, ".modal"); await clickSave(target.querySelector(".modal")); assert.containsN( target, ".o_kanban_group:first-child .o_kanban_record", 3, "first column should contain three records" ); assert.deepEqual( [...target.querySelectorAll(".o_kanban_group:first-child .o_kanban_record")].map( (el) => el.innerText ), ["ABC", "yop", "gnap"] ); assert.verifySteps([ "get_views", "web_read_group", // initial read_group "web_search_read", // initial search_read (first column) "web_search_read", // initial search_read (second column) "onchange", // quick create "name_create", // should perform a name_create to create the record "get_views", // load views for form view dialog "onchange", // load of a virtual record in form view dialog "create", // save virtual record "read", // read the created record to get foo value "onchange", // reopen the quick create automatically ]); }); QUnit.test("Move new record with onchanges and different active fields", async (assert) => { serverData.models.partner.fields.foo.default = "abc"; serverData.models.partner.onchanges = { bar(obj) { obj.foo = [...obj.foo].reverse().join(""); }, }; serverData.views["partner,some_view_ref,form"] = /* xml */ `
`; await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["bar"], }); assert.deepEqual(getCardTexts(0), ["blip-4"]); assert.deepEqual(getCardTexts(1), ["yop10", "blip9", "gnap17"]); await quickCreateRecord(0); await editQuickCreateInput("int_field", "13"); await selectDropdownItem(target, "category_ids", "gold"); await validateRecord(); assert.deepEqual(getCardTexts(0), ["abc13", "blip-4"]); assert.deepEqual(getCardTexts(1), ["yop10", "blip9", "gnap17"]); await dragAndDrop(".o_kanban_record", ".o_kanban_group:nth-child(2)"); assert.deepEqual(getCardTexts(0), ["blip-4"]); assert.deepEqual(getCardTexts(1), ["yop10", "blip9", "gnap17", "cba13"]); }); QUnit.test("no sample data when all groups are folded then one is unfolded", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["product_id"], async mockRPC(route, args, performRPC) { if (args.method === "web_read_group") { const result = await performRPC(route, args); for (const group of result.groups) { group.__fold = true; } return result; } }, }); assert.containsN(target, ".o_column_folded", 2); const groupHandle = target.querySelector(".o_kanban_group"); await click(groupHandle); assert.containsOnce(target, ".o_column_folded"); assert.containsN(target, ".o_kanban_record", 2); assert.containsNone(target, "o_view_sample_data"); }); QUnit.test( "no content helper when all groups are folded but there are (unloaded) records", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["product_id"], async mockRPC(route, args, performRPC) { if (args.method === "web_read_group") { const result = await performRPC(route, args); for (const group of result.groups) { group.__fold = true; } return result; } }, }); assert.containsN(target, ".o_column_folded", 2); assert.strictEqual( getNodesTextContent(target.querySelectorAll("span.o_column_title")).join(" "), "hello (2) xmo (2)" ); assert.containsNone(target, ".o_nocontent_help"); } ); QUnit.test("Move multiple records in different columns simultaneously", async (assert) => { let def; await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["state"], async mockRPC(_route, { method }) { if (method === "read") { await def; } }, }); def = makeDeferred(); assert.deepEqual(getCardTexts(), ["1", "2", "3", "4"]); // Move 3 at end of 1st column await dragAndDrop(".o_kanban_group:last-of-type .o_kanban_record", ".o_kanban_group"); assert.deepEqual(getCardTexts(), ["1", "3", "2", "4"]); // Move 4 at end of 1st column await dragAndDrop(".o_kanban_group:last-of-type .o_kanban_record", ".o_kanban_group"); assert.deepEqual(getCardTexts(), ["1", "3", "4", "2"]); def.resolve(); await nextTick(); assert.deepEqual(getCardTexts(), ["1", "3", "4", "2"]); }); QUnit.test("group key in foreach cannot be a duplicate", async function (assert) { serverData.models.product.records = [ { id: 1, name: "Product with id 1", }, ]; serverData.models.partner.records = [ { id: 1, name: "Partner 1", product_id: 1, }, ]; await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["product_id"], async mockRPC(route, args, performRPC) { if (args.method === "web_read_group") { const result = await performRPC(route, args); result.groups = [ ...result.groups, { // Add an empty and valueless group, will result in foreach key group_key_0 __domain: [["product_id", "=", null]], __fold: false, }, { // Add an empty and valueless group, will result in foreach key group_key_1 __domain: [["product_id", "=", null]], __fold: false, }, ]; result.length = 2; return result; } }, }); assert.strictEqual(target.querySelectorAll(".o_kanban_group").length, 3); assert.strictEqual(target.querySelectorAll(".o_kanban_record").length, 1); }); QUnit.test("drag & drop: content scrolls when reaching the edges", async (assert) => { const nextAnimationFrame = async (timeDelta) => { timeStamp += timeDelta; animationFrameDef.resolve(); animationFrameDef = makeDeferred(); await Promise.resolve(); }; let animationFrameDef = makeDeferred(); let timeStamp = 0; patchWithCleanup(browser, { async requestAnimationFrame(handler) { await animationFrameDef; handler(timeStamp); }, performance: { now: () => timeStamp }, }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["state"], }); const content = target.querySelector(".o_content"); content.setAttribute("style", "max-width:600px;overflow:auto;"); assert.strictEqual(content.scrollLeft, 0); assert.strictEqual(content.getBoundingClientRect().width, 600); assert.containsNone(target, ".o_kanban_record.o_dragged"); // Drag first record of first group to the right await drag(".o_kanban_record", ".o_kanban_group:nth-child(3) .o_kanban_record"); assert.strictEqual(content.scrollLeft, 0); // next frame (normal time delta) await nextAnimationFrame(16); // Default kanban speed is 20px per tick assert.strictEqual(content.scrollLeft, 20); assert.containsOnce(target, ".o_kanban_record.o_dragged"); // next frame (time delta x20) await nextAnimationFrame(16 * 20); // Should be at the end of the content assert.strictEqual(content.clientWidth + content.scrollLeft, content.scrollWidth); // Cancel drag: press "Escape" triggerHotkey("Escape"); await nextTick(); assert.containsNone(target, ".o_kanban_record.o_dragged"); // Drag first record of last group to the left await drag(".o_kanban_group:nth-child(3) .o_kanban_record", ".o_kanban_record"); // next frame (normal time delta) await nextAnimationFrame(16); assert.containsOnce(target, ".o_kanban_record.o_dragged"); // next frame (time delta x20) await nextAnimationFrame(16 * 20); assert.strictEqual(content.scrollLeft, 0); // Cancel drag: click outside await triggerEvent(content, ".o_kanban_renderer", "mousedown"); assert.containsNone(target, ".o_kanban_record.o_dragged"); }); QUnit.test("draggable area contains overflowing visible elements", async (assert) => { const nextAnimationFrame = async (timeDelta) => { timeStamp += timeDelta; animationFrameDef.resolve(); animationFrameDef = makeDeferred(); await Promise.resolve(); }; let animationFrameDef = makeDeferred(); let timeStamp = 0; patchWithCleanup(browser, { async requestAnimationFrame(handler) { await animationFrameDef; handler(timeStamp); }, performance: { now: () => timeStamp }, }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: /* xml */ `
`, groupBy: ["state"], }); const controller = target.querySelector(".o_view_controller"); controller.setAttribute("style", "max-width:900px; min-width: 900px;"); const content = target.querySelector(".o_content"); content.setAttribute("style", "max-width:600px; min-width: 600px;"); const renderer = target.querySelector(".o_kanban_renderer"); renderer.setAttribute("style", "overflow: visible;"); for (const kanbanGroup of target.querySelectorAll(".o_kanban_group")) { kanbanGroup.setAttribute("style", "max-width: 300px; min-width: 300px; padding: 0;"); } assert.strictEqual(content.scrollLeft, 0); assert.strictEqual(controller.getBoundingClientRect().width, 900); assert.strictEqual(content.getBoundingClientRect().width, 600); assert.strictEqual(renderer.getBoundingClientRect().width, 600); assert.strictEqual(renderer.scrollWidth, 900); assert.containsNone(target, ".o_kanban_record.o_dragged"); // Drag first record of first group to the right await drag(".o_kanban_record", ".o_kanban_group:nth-child(3) .o_kanban_record"); assert.strictEqual(content.scrollLeft, 0); // Next frame (normal time delta) await nextAnimationFrame(16); // Verify that there is no scrolling assert.strictEqual(content.scrollLeft, 0); assert.containsOnce(target, ".o_kanban_record.o_dragged"); const dragged = target.querySelector(".o_kanban_record.o_dragged"); const sibling = target.querySelector(".o_kanban_group:nth-child(3) .o_kanban_record"); // Ensure that no rotation is applied on the element dragged.style.transform = "none"; // Verify that the dragged element is allowed to go inside the // overflowing part of the draggable container. assert.strictEqual( dragged.getBoundingClientRect().right, 900 + target.getBoundingClientRect().x ); assert.strictEqual( sibling.getBoundingClientRect().right, 900 + target.getBoundingClientRect().x ); // Cancel drag: press "Escape" triggerHotkey("Escape"); await nextTick(); assert.containsNone(target, ".o_kanban_record.o_dragged"); }); QUnit.test("attribute default_order", async function (assert) { serverData.models.custom_model = { fields: { int: { type: "integer", string: "Int" }, }, records: [ { id: 1, int: 1 }, { id: 2, int: 3 }, { id: 3, int: 2 }, ], }; await makeView({ type: "kanban", resModel: "custom_model", serverData, arch: `
`, }); assert.deepEqual( [...target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")].map( (el) => el.innerText ), ["1", "2", "3"] ); }); QUnit.test( "drag & drop records grouped by m2o with m2o displayed in records", async (assert) => { const prom = makeDeferred(); const readIds = [[2], [1, 3, 2]]; await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], mockRPC: async (route, args) => { assert.step(args.method || route); if (args.method === "read") { assert.deepEqual(args.args[0], readIds.shift()); await prom; } }, }); assert.verifySteps([ "get_views", "web_read_group", "web_search_read", "web_search_read", ]); assert.deepEqual( [...target.querySelectorAll(".o_kanban_record")].map((el) => el.innerText), ["hello", "hello", "xmo", "xmo"] ); await dragAndDrop( ".o_kanban_group:nth-child(2) .o_kanban_record", ".o_kanban_group:first-child" ); assert.deepEqual( [...target.querySelectorAll(".o_kanban_record")].map((el) => el.innerText), ["hello", "hello", "hello", "xmo"] ); prom.resolve(); await nextTick(); assert.verifySteps(["write", "read", "/web/dataset/resequence", "read"]); assert.deepEqual( [...target.querySelectorAll(".o_kanban_record")].map((el) => el.innerText), ["hello", "hello", "hello", "xmo"] ); } ); QUnit.test("rerenders only once after resequencing records", async (assert) => { // actually it's not once, but twice, because we must render directly after // the drag&drop s.t. the dropped record remains where it has been dropped, // and once again after the reload class MyField extends Component { setup() { this.renderCount = 0; owl.onWillRender(() => this.renderCount++); } } MyField.template = xml``; MyField.extractProps = ({ attrs }) => ({ attrs }); registry.category("fields").add("my_field", MyField); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], }); assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_kanban_record")), [ "yop1", "gnap1", "blip1", "blip1", ]); await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_kanban_record")), [ "gnap3", "blip3", "blip3", "yop1", // new instance ]); await dragAndDrop( ".o_kanban_group:first-child .o_kanban_record", ".o_kanban_group:nth-child(2)" ); assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_kanban_record")), [ "blip5", "blip5", "yop3", "gnap1", // new instance ]); }); QUnit.test("sample server: _mockWebReadGroup API", async (assert) => { serverData.models.partner.records = []; patchWithCleanup(SampleServer.prototype, { async _mockWebReadGroup() { const result = await this._super(...arguments); const { "date:month": dateValue } = result.groups[0]; assert.strictEqual(dateValue, "December 2022"); return result; }, }); await makeView({ arch: `
`, serverData, groupBy: ["date:month"], resModel: "partner", type: "kanban", noContentHelp: "No content helper", mockRPC(_, args) { if (args.method === "web_read_group") { return { groups: [ { date_count: 0, state: false, "date:month": "December 2022", __range: { "date:month": { from: "2022-12-01", to: "2023-01-01", }, }, __domain: [ ["date", ">=", "2022-12-01"], ["date", "<", "2023-01-01"], ], }, ], length: 1, }; } }, }); assert.containsOnce(target, ".o_kanban_view .o_view_sample_data"); assert.containsOnce(target, ".o_kanban_group"); assert.strictEqual( target.querySelector(".o_kanban_group .o_column_title").textContent, "December 2022" ); assert.containsN(target, ".o_kanban_group .o_kanban_record", 16); }); QUnit.test("Keep scrollTop when loading records with load more", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["bar"], limit: 1, }); target.querySelector(".o_kanban_renderer").style.overflow = "scroll"; target.querySelector(".o_kanban_renderer").style.height = "500px"; const loadMoreButton = target.querySelector(".o_kanban_load_more button"); loadMoreButton.scrollIntoView(); const previousScrollTop = target.querySelector(".o_kanban_renderer").scrollTop; await click(loadMoreButton); assert.strictEqual( previousScrollTop, target.querySelector(".o_kanban_renderer").scrollTop, "Should have the same scrollTop value" ); assert.notEqual(previousScrollTop, 0, "Should not have the scrollTop value at 0"); }); QUnit.test( "Kanban: no reset of the groupby when a non-empty column is deleted", async (assert) => { let dialogProps; patchDialog((_cls, props) => { dialogProps = props; }); await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, searchViewArch: ` `, }); await toggleFilterMenu(target); // select the groupby:category_ids filter await click(target.querySelector(".o_group_by_menu .dropdown-toggle")); await click(target.querySelector(".o_group_by_menu .o_menu_item")); // check the initial rendering assert.containsN(target, ".o_kanban_group", 3, "should have three columns"); // check availability of delete action in kanban header's config dropdown await toggleColumnActions(2); assert.containsOnce( getColumn(2), ".o_column_delete", "should be able to delete the column" ); // delete second column (first cancel the confirm request, then confirm) let clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Delete"); dialogProps.cancel(); await nextTick(); assert.strictEqual( getColumn(1).querySelector(".o_column_title").innerText, "gold", 'column [6, "gold"] should still be there' ); dialogProps.confirm(); await nextTick(); clickColumnAction = await toggleColumnActions(1); await clickColumnAction("Delete"); assert.strictEqual( getColumn(1).querySelector(".o_column_title").innerText, "silver", 'last column should now be [7, "silver"]' ); assert.containsN(target, ".o_kanban_group", 2, "should now have two columns"); assert.strictEqual( getColumn(0).querySelector(".o_column_title").innerText, "None (3)", "first column should have no id (Undefined column)" ); } ); QUnit.test("kanbans with basic and custom compiler, same arch", async (assert) => { // In this test, the exact same arch will be rendered by 2 different kanban renderers: // once with the basic one, and once with a custom renderer having a custom compiler. The // purpose of the test is to ensure that the template is compiled twice, once by each // compiler, even though the arch is the same. class MyKanbanCompiler extends KanbanCompiler { setup() { super.setup(); this.compilers.push({ selector: "div", fn: this.compileDiv }); } compileDiv(node, params) { const compiledNode = this.compileGenericNode(node, params); compiledNode.setAttribute("class", "my_kanban_compiler"); return compiledNode; } } class MyKanbanRecord extends KanbanRecord {} MyKanbanRecord.Compiler = MyKanbanCompiler; class MyKanbanRenderer extends KanbanRenderer {} MyKanbanRenderer.components = { ...KanbanRenderer.components, KanbanRecord: MyKanbanRecord, }; viewRegistry.add("my_kanban", { ...kanbanView, Renderer: MyKanbanRenderer, }); serverData.models.partner.fields.one2many = { type: "one2many", name: "o2m", relation: "partner", }; serverData.models.partner.records[0].one2many = [1]; serverData.views = { "partner,false,form": `
`, "partner,false,search": ``, "partner,false,kanban": `
`, }; const webClient = await createWebClient({ serverData }); await doAction(webClient, { res_model: "partner", type: "ir.actions.act_window", views: [ [false, "kanban"], [false, "form"], ], }); // main kanban, custom view assert.containsOnce(target, ".o_kanban_view"); assert.containsN(target, ".my_kanban_compiler", 4); // switch to form await click(target.querySelector(".o_kanban_record")); assert.containsOnce(target, ".o_form_view"); assert.containsOnce(target, ".o_form_view .o_field_widget[name=one2many]"); // x2many kanban, basic renderer assert.containsOnce(target, ".o_kanban_record:not(.o_kanban_ghost)"); assert.containsNone(target, ".my_kanban_compiler"); }); QUnit.test( "can quick create a column when pressing enter when input is focused", async (assert) => { await makeView({ type: "kanban", resModel: "partner", serverData, arch: `
`, groupBy: ["product_id"], }); assert.containsN(target, ".o_kanban_group", 2); await createColumn(); // We don't use the editInput helper as it would trigger a change event automatically. // We need to wait for the enter key to trigger the event. const input = target.querySelector(".o_column_quick_create input"); input.value = "New Column"; await triggerEvent(input, null, "input"); await triggerEvent(target, ".o_quick_create_unfolded input", "keydown", { key: "Enter", }); assert.containsN(target, ".o_kanban_group", 3); } ); });