`,
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: ["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: `
' +
"",
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:
'' +
'' +
'
`,
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"] =
"
' +
"",
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: `
' +
"",
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": `
' +
"" +
"",
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:
'' +
'' +
'
' +
"" +
"",
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: `
' +
"" +
"",
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: `
`,
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: `
" +
"" +
"" +
"",
async mockRPC(route, { method, args }) {
if (method === "write") {
assert.step(`write-color-${args[1].color}`);
}
},
});
await toggleRecordDropdown(0);
assert.containsNone(
target,
".o_kanban_record.oe_kanban_color_12",
"no record should have the color 12"
);
assert.containsOnce(
target,
".o_kanban_record:first-child .oe_kanban_colorpicker",
"there should be a color picker"
);
assert.containsN(
target,
".o_kanban_record:first-child .oe_kanban_colorpicker > *",
12,
"the color picker should have 12 children (the colors)"
);
await click(target, ".oe_kanban_colorpicker a.oe_kanban_color_9");
assert.verifySteps(["write-color-9"], "should write on the color field");
assert.hasClass(getCard(0), "oe_kanban_color_9");
});
QUnit.test("load more records in column", async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
"" +
'' +
'
' +
"" +
"",
groupBy: ["bar"],
limit: 2,
async mockRPC(_route, { method, kwargs }) {
if (method === "web_search_read") {
assert.step(`${kwargs.limit} - ${kwargs.offset}`);
}
},
});
assert.containsN(
getColumn(1),
".o_kanban_record",
2,
"there should be 2 records in the column"
);
assert.deepEqual(getCardTexts(1), ["1", "2"]);
// load more
await loadMore(1);
assert.containsN(
getColumn(1),
".o_kanban_record",
3,
"there should now be 3 records in the column"
);
assert.verifySteps(["2 - 0", "2 - 0", "4 - 0"], "the records should be correctly fetched");
assert.deepEqual(getCardTexts(1), ["1", "2", "3"]);
// reload
await validateSearch(target);
assert.containsN(
getColumn(1),
".o_kanban_record",
3,
"there should still be 3 records in the column after reload"
);
assert.deepEqual(getCardTexts(1), ["1", "2", "3"]);
assert.verifySteps(["2 - 0", "4 - 0"]);
});
QUnit.test("load more records in column with x2many", async (assert) => {
serverData.models.partner.records[0].category_ids = [7];
serverData.models.partner.records[1].category_ids = [];
serverData.models.partner.records[2].category_ids = [6];
serverData.models.partner.records[3].category_ids = [];
// record [2] will be loaded after
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
`,
async mockRPC(route, args) {
if (route === "/web/dataset/call_kw/product/read") {
assert.strictEqual(args.args[0].length, 2, "read on two groups");
assert.deepEqual(
args.args[1],
["display_name", "name"],
"should read on specified fields on the group by relation"
);
}
},
});
assert.hasClass(
target.querySelector(".o_kanban_renderer"),
"o_kanban_grouped",
"should have classname 'o_kanban_grouped'"
);
assert.containsN(target, ".o_kanban_group", 2, "should have 2 columns");
// simulate an update coming from the searchview, with another groupby given
await reload(kanban, { groupBy: ["product_id"] });
assert.containsN(target, ".o_kanban_group", 3, "should have 3 columns");
assert.strictEqual(
target.querySelectorAll(".o_kanban_group:nth-child(1) .o_kanban_record").length,
1,
"column should contain 1 record(s)"
);
assert.strictEqual(
target.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record").length,
2,
"column should contain 2 record(s)"
);
assert.strictEqual(
target.querySelectorAll(".o_kanban_group:nth-child(3) .o_kanban_record").length,
1,
"column should contain 1 record(s)"
);
assert.strictEqual(
target.querySelector(".o_kanban_group:first-child span.o_column_title").textContent,
"None",
"first column should have a default title for when no value is provided"
);
assert.ok(
!target.querySelector(
".o_kanban_group:first-child .o_kanban_header_title .o_column_title"
).dataset.tooltipInfo,
"tooltip of first column should not defined, since group_by_tooltip title and the many2one field has no value"
);
assert.ok(
!target.querySelector(
".o_kanban_group:first-child .o_kanban_header_title .o_column_title"
).dataset.tooltipTemplate,
"tooltip of first column should not defined, since group_by_tooltip title and the many2one field has no value"
);
assert.strictEqual(
target.querySelector(".o_kanban_group:nth-child(2) span.o_column_title").textContent,
"hello",
"second column should have a title with a value from the many2one"
);
assert.strictEqual(
target.querySelector(
".o_kanban_group:nth-child(2) .o_kanban_header_title .o_column_title"
).dataset.tooltipInfo,
`{"entries":[{"title":"Kikou","value":"hello"}]}`,
"second column should have a tooltip with the group_by_tooltip title and many2one field value"
);
assert.strictEqual(
target.querySelector(
".o_kanban_group:nth-child(2) .o_kanban_header_title .o_column_title"
).dataset.tooltipTemplate,
"web.KanbanGroupTooltip",
"second column should have a tooltip with the group_by_tooltip title and many2one field value"
);
});
QUnit.test("asynchronous tooltips when grouped", async (assert) => {
assert.expect(10);
serviceRegistry.add("tooltip", tooltipService);
const prom = makeDeferred();
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
" +
"" +
"",
groupBy: ["bar"],
});
assert.deepEqual(getCardTexts(0), ["1"]);
await loadMore(0);
await loadMore(0);
assert.deepEqual(getCardTexts(0), ["1", "2", "3"], "intended records are loaded");
});
QUnit.test(
"column progressbars with an active filter are working with load more",
async (assert) => {
serverData.models.partner.records.push(
{ id: 5, bar: true, foo: "blork" },
{ id: 6, bar: true, foo: "blork" },
{ id: 7, bar: true, foo: "blork" }
);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
domain: [["bar", "=", true]],
arch: `
`,
groupBy: ["bar"],
});
await click(target, ".o_kanban_counter_progress .progress-bar.bg-success");
assert.deepEqual(getCardTexts(), ["5"], "we should have 1 record shown");
await loadMore(0);
await loadMore(0);
assert.deepEqual(getCardTexts(), ["5", "6", "7"]);
}
);
QUnit.test("column progressbars on archiving records update counter", 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) => props.confirm());
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
"" +
'' +
'' +
'' +
'' +
'' +
"
" +
'' +
"
" +
"" +
"",
groupBy: ["bar"],
});
assert.deepEqual(getCounters(), ["-4", "36"], "counter should contain the correct value");
assert.deepEqual(
getTooltips(1),
["1 yop", "1 gnap", "1 blip"],
"the counter progressbars should be correctly displayed"
);
// archive all records of the second columns
const clickColumnAction = await toggleColumnActions(1);
await clickColumnAction("Archive All");
assert.deepEqual(getCounters(), ["-4", "0"], "counter should contain the correct value");
assert.containsNone(
getColumn(1),
".progress-bar",
"the counter progressbars should have been correctly updated"
);
});
QUnit.test(
"kanban with progressbars: correctly update env when archiving records",
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) => props.confirm());
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
"" +
'' +
'' +
'' +
'' +
'' +
"
`,
groupBy: ["bar"],
});
assert.deepEqual(getCounters(), ["1", "3"]);
await click(target, ".o_kanban_group:nth-child(2) .bg-success");
assert.deepEqual(getCounters(), ["1", "1"]);
});
QUnit.test(
"progress bar recompute after drag&drop to and from other column",
async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
`,
groupBy: ["bar"],
});
assert.deepEqual(getTooltips(), ["1 blip", "1 yop", "1 gnap", "1 blip"]);
assert.deepEqual(getCounters(), ["1", "3"]);
// Drag the last kanban record to the first column
await dragAndDrop(
".o_kanban_group:last-child .o_kanban_record:nth-child(4)",
".o_kanban_group:first-child"
);
assert.deepEqual(getTooltips(), ["1 gnap", "1 blip", "1 yop", "1 blip"]);
assert.deepEqual(getCounters(), ["2", "2"]);
}
);
QUnit.test("load more should load correct records after drag&drop event", async (assert) => {
// Add a sequence number and initialize
serverData.models.partner.records.forEach((el, i) => (el.sequence = i));
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
`,
groupBy: ["bar"],
});
assert.deepEqual(getCardTexts(0), ["4"], "first column's first record must be id 4");
assert.deepEqual(getCardTexts(1), ["1"], "second column's records should be only the id 1");
// Drag the first kanban record on top of the last
await dragAndDrop(
".o_kanban_group:first-child .o_kanban_record",
".o_kanban_group:last-child .o_kanban_record"
);
// load more twice to load all records of second column
await loadMore(1);
await loadMore(1);
// Check records of the second column
assert.deepEqual(
getCardTexts(1),
["4", "1", "2", "3"],
"first column's first record must be id 4"
);
});
QUnit.test(
"column progressbars on quick create with quick_create_view are updated",
async (assert) => {
serverData.views[
"partner,some_view_ref,form"
] = ``;
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
'' +
'' +
'' +
'' +
"
" +
'' +
"
" +
"" +
"",
groupBy: ["bar"],
});
assert.deepEqual(getCounters(), ["-4", "36"]);
await createRecord();
await editQuickCreateInput("int_field", 44);
await validateRecord();
assert.deepEqual(
getCounters(),
["40", "36"],
"kanban counters should have been updated on quick create"
);
}
);
QUnit.test(
"column progressbars and active filter on quick create with quick_create_view are updated",
async (assert) => {
serverData.views["partner,some_view_ref,form"] = `
`;
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
`,
groupBy: ["bar"],
});
await click(getColumn(0), ".progress-bar.bg-danger");
assert.containsOnce(getColumn(0), ".o_kanban_record");
assert.containsOnce(getColumn(0), ".oe_kanban_card_danger");
assert.deepEqual(getCounters(), ["-4", "36"]);
// open the quick create
await createRecord();
// fill it with a record that satisfies the active filter
await editQuickCreateInput("int_field", 44);
await editQuickCreateInput("foo", "blip");
await validateRecord();
// fill it again with another record that DOES NOT satisfy the active filter
await editQuickCreateInput("int_field", 1000);
await editQuickCreateInput("foo", "yop");
await validateRecord();
assert.containsN(getColumn(0), ".o_kanban_record", 3);
assert.containsN(getColumn(0), ".oe_kanban_card_danger", 2);
assert.containsOnce(getColumn(0), ".oe_kanban_card_success");
assert.deepEqual(
getCounters(),
["40", "36"],
"kanban counters should have been updated on quick create, respecting the active filter"
);
}
);
QUnit.test(
"keep adding quickcreate in first column after a record from this column was moved",
async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
'' +
'' +
'' +
'
' +
"" +
"",
groupBy: ["foo"],
async mockRPC(route, args) {
if (route === "/web/dataset/resequence") {
return true;
}
},
});
await createRecord();
assert.strictEqual(
target.querySelector(".o_kanban_quick_create").closest(".o_kanban_group"),
target.querySelector(".o_kanban_group"),
"quick create should have been added in the first column"
);
await dragAndDrop(".o_kanban_record", ".o_kanban_group:nth-child(2)");
await createRecord();
assert.strictEqual(
target.querySelector(".o_kanban_quick_create").closest(".o_kanban_group"),
target.querySelector(".o_kanban_group"),
"quick create should have been added in the first column"
);
}
);
QUnit.test("test displaying image (URL, image field not set)", async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
"" +
'' +
'
' +
"" +
"
" +
"",
});
// since the field image is not set, kanban_image will generate an URL
const imageOnRecord = target.querySelectorAll(
'img[data-src*="/web/image"][data-src*="&id=1"]'
);
assert.strictEqual(imageOnRecord.length, 1, "partner with image display image by url");
});
QUnit.test("test displaying image (__last_update field)", async (assert) => {
// the presence of __last_update field ensures that the image is reloaded when necessary
assert.expect(2);
const rec = serverData.models.partner.records.find((r) => r.id === 1);
rec.__last_update = "2022-08-05 08:37:00";
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
" +
"",
});
const images = target.querySelectorAll("img");
const placeholders = [];
for (const [index, img] of images.entries()) {
if (img.dataset.src.indexOf(serverData.models.partner.records[index].image) === -1) {
// Then we display a placeholder
placeholders.push(img);
}
}
assert.strictEqual(
placeholders.length,
serverData.models.partner.records.length - 1,
"partner with no image should display the placeholder"
);
assert.strictEqual(
images[0].dataset.src,
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA==",
"The first partners non-placeholder image should be set"
);
});
QUnit.test("test displaying image (for another record)", async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
"" +
'' +
'' +
'
' +
"" +
"
" +
"",
});
// the field image is set, but we request the image for a specific id
// -> for the record matching the ID, the base64 should be returned
// -> for all the other records, the image should be displayed by url
const imageOnRecord = target.querySelectorAll(
'img[data-src*="/web/image"][data-src*="&id=1"]'
);
assert.strictEqual(
imageOnRecord.length,
serverData.models.partner.records.length - 1,
"display image by url when requested for another record"
);
assert.strictEqual(
target.querySelector("img").dataset.src,
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA==",
"display image as value when requested for the record itself"
);
});
QUnit.test("test displaying image from m2o field (m2o field not set)", async (assert) => {
serverData.models.foo_partner = {
fields: {
name: { string: "Foo Name", type: "char" },
partner_id: { string: "Partner", type: "many2one", relation: "partner" },
},
records: [
{ id: 1, name: "foo_with_partner_image", partner_id: 1 },
{ id: 2, name: "foo_no_partner" },
],
};
await makeView({
type: "kanban",
resModel: "foo_partner",
serverData,
arch: `
`,
});
assert.containsOnce(
target,
'img[data-src*="/web/image"][data-src$="&id=1&unique="]',
"image url should contain id of set partner_id"
);
assert.containsOnce(
target,
'img[data-src*="/web/image"][data-src$="&id=&unique="]',
"image url should contain an empty id if partner_id is not set"
);
});
QUnit.test(
"grouped kanban becomes ungrouped when clearing domain then clearing groupby",
async (assert) => {
// in this test, we simulate that clearing the domain is slow, so that
// clearing the groupby does not corrupt the data handled while
// reloading the kanban view.
const prom = makeDeferred();
const kanban = await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
"" +
'' +
'' +
'
' +
"",
domain: [["foo", "=", "norecord"]],
groupBy: ["bar"],
async mockRPC(route, args, performRpc) {
if (args.method === "web_read_group") {
const result = performRpc(route, args);
const isFirstUpdate =
args.kwargs.domain.length === 0 &&
args.kwargs.groupby &&
args.kwargs.groupby[0] === "bar";
if (isFirstUpdate) {
await prom;
}
return result;
}
},
});
assert.hasClass(
target.querySelector(".o_kanban_renderer"),
"o_kanban_grouped",
"the kanban view should be grouped"
);
assert.doesNotHaveClass(
target.querySelector(".o_kanban_renderer"),
"o_kanban_ungrouped",
"the kanban view should not be ungrouped"
);
reload(kanban, { domain: [] }); // 1st update on kanban view
reload(kanban, { groupBy: [] }); // 2nd update on kanban view
prom.resolve(); // simulate slow 1st update of kanban view
await nextTick();
assert.doesNotHaveClass(
target.querySelector(".o_kanban_renderer"),
"o_kanban_grouped",
"the kanban view should not longer be grouped"
);
assert.hasClass(
target.querySelector(".o_kanban_renderer"),
"o_kanban_ungrouped",
"the kanban view should have become ungrouped"
);
}
);
QUnit.test("quick_create on grouped kanban without column", async (assert) => {
serverData.models.partner.records = [];
await makeView({
type: "kanban",
resModel: "partner",
serverData,
// force group_create to false, otherwise the CREATE button in control panel is hidden
arch:
'' +
"
" +
"",
});
getCard(0).focus();
assert.strictEqual(document.activeElement, getCard(0), "the kanban cards are focussable");
triggerHotkey("ArrowRight");
assert.strictEqual(
document.activeElement,
getCard(1),
"the second card should be focussed"
);
triggerHotkey("ArrowLeft");
assert.strictEqual(document.activeElement, getCard(0), "the first card should be focussed");
});
QUnit.test(
"keyboard navigation on kanban basic rendering does not crash when the focus is inside a card",
async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
'' +
"
" +
"",
});
getCard(0).querySelector(".o-this-is-focussable").focus();
triggerHotkey("ArrowDown");
assert.strictEqual(
document.activeElement,
getCard(1),
"the second card should be focussed"
);
}
);
QUnit.test("keyboard navigation on kanban grouped rendering", async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
"" +
'' +
'' +
'
' +
"",
groupBy: ["bar"],
});
const cardsByColumn = [...target.querySelectorAll(".o_kanban_group")].map((c) => [
...c.querySelectorAll(".o_kanban_record"),
]);
const firstColumnFirstCard = cardsByColumn[0][0];
const secondColumnFirstCard = cardsByColumn[1][0];
const secondColumnSecondCard = cardsByColumn[1][1];
firstColumnFirstCard.focus();
// RIGHT should select the next column
triggerHotkey("ArrowRight");
await nextTick();
assert.strictEqual(
document.activeElement,
secondColumnFirstCard,
"RIGHT should select the first card of the next column"
);
// DOWN should move up one card
triggerHotkey("ArrowDown");
await nextTick();
assert.strictEqual(
document.activeElement,
secondColumnSecondCard,
"DOWN should select the second card of the current column"
);
// LEFT should go back to the first column
triggerHotkey("ArrowLeft");
await nextTick();
assert.strictEqual(
document.activeElement,
firstColumnFirstCard,
"LEFT should select the first card of the first column"
);
});
QUnit.test(
"keyboard navigation on kanban grouped rendering with empty columns",
async (assert) => {
serverData.models.partner.records[1].state = "abc";
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
"" +
'' +
'' +
'
' +
"",
groupBy: ["state"],
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(route, args);
// add 2 empty columns in the middle
result.groups.splice(1, 0, {
state_count: 0,
state: "md1",
__domain: [["state", "=", "md1"]],
});
result.groups.splice(1, 0, {
state_count: 0,
state: "md2",
__domain: [["state", "=", "md2"]],
});
// add 1 empty column in the beginning and the end
result.groups.unshift({
state_count: 0,
state: "beg",
__domain: [["state", "=", "beg"]],
});
result.groups.push({
state_count: 0,
state: "end",
__domain: [["state", "=", "end"]],
});
return result;
}
},
});
/**
* Added columns in mockRPC are empty
*
* | BEG | ABC | MD1 | MD2 | GHI | END
* |-----|------|-----|-----|------|-----
* | | yop | | | gnap |
* | | blip | | | blip |
*/
const cardsByColumn = [...target.querySelectorAll(".o_kanban_group")].map((c) => [
...c.querySelectorAll(".o_kanban_record"),
]);
const yop = cardsByColumn[1][0];
const gnap = cardsByColumn[4][0];
yop.focus();
// RIGHT should select the next column that has a card
triggerHotkey("ArrowRight");
await nextTick();
assert.strictEqual(
document.activeElement,
gnap,
"RIGHT should select the first card of the next column that has a card"
);
// LEFT should go back to the first column that has a card
triggerHotkey("ArrowLeft");
await nextTick();
assert.strictEqual(
document.activeElement,
yop,
"LEFT should select the first card of the first column that has a card"
);
}
);
QUnit.test(
"keyboard navigation on kanban when the focus is on a link that " +
"has an action and the kanban has no oe_kanban_global_... class",
async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch:
'' +
'
`,
domain: [["id", "in", [11, 12]]],
});
assert.containsOnce(
target,
".o_kanban_record:first-child div.test",
"the container is displayed if description have actual content"
);
assert.strictEqual(
target
.querySelector(".o_kanban_record:first-child div.test span.text-info")
.innerText.trim(),
"hello",
"the inner html content is rendered properly"
);
assert.containsNone(
target,
".o_kanban_record:last-child div.test",
"the container is not displayed if description just have formatting tags and no actual content"
);
});
QUnit.test(
"progressbar filter state is kept unchanged when domain is updated (records still in group)",
async (assert) => {
const kanban = await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
`,
});
// Check that we have 2 columns and check their progressbar's state
assert.containsN(target, ".o_kanban_group", 2);
assert.containsNone(target, ".o_kanban_group.o_kanban_group_show");
assert.deepEqual(
[...target.querySelectorAll(".o_column_title")].map((el) => el.innerText),
["No", "Yes"]
);
assert.deepEqual(getTooltips(0), ["1 blip"]);
assert.deepEqual(getTooltips(1), ["1 yop", "1 blip", "1 Other"]);
// Apply an active filter
await click(target, ".o_kanban_group:nth-child(2) .progress-bar.bg-success");
assert.containsOnce(target, ".o_kanban_group.o_kanban_group_show");
assert.strictEqual(
target.querySelector(".o_kanban_group.o_kanban_group_show .o_column_title")
.innerText,
"Yes"
);
// Add searchdomain to something restricting progressbars' values (records still in filtered group)
await reload(kanban, { domain: [["foo", "=", "yop"]] });
// Check that we have now 1 column only and check its progressbar's state
assert.containsOnce(target, ".o_kanban_group");
assert.containsOnce(target, ".o_kanban_group.o_kanban_group_show");
assert.strictEqual(target.querySelector(".o_column_title").innerText, "Yes");
assert.deepEqual(getTooltips(), ["1 yop"]);
// Undo searchdomain
await reload(kanban, { domain: [] });
// Check that we have 2 columns back and check their progressbar's state
assert.containsN(target, ".o_kanban_group", 2);
assert.containsOnce(target, ".o_kanban_group.o_kanban_group_show");
assert.deepEqual(
[...target.querySelectorAll(".o_column_title")].map((el) => el.innerText),
["No", "Yes"]
);
assert.deepEqual(getTooltips(0), ["1 blip"]);
assert.deepEqual(getTooltips(1), ["1 yop", "1 blip", "1 Other"]);
}
);
QUnit.test(
"progressbar filter state is kept unchanged when domain is updated (emptying group)",
async (assert) => {
const kanban = await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
`,
});
// Check that we have 2 columns, check their progressbar's state and check records
assert.containsN(target, ".o_kanban_group", 2);
assert.containsNone(target, ".o_kanban_group.o_kanban_group_show");
assert.deepEqual(
[...target.querySelectorAll(".o_column_title")].map((el) => el.innerText),
["No", "Yes"]
);
assert.deepEqual(getTooltips(0), ["1 blip"]);
assert.deepEqual(getCardTexts(0), ["4blip"]);
assert.deepEqual(getTooltips(1), ["1 yop", "1 blip", "1 Other"]);
assert.deepEqual(getCardTexts(1), ["1yop", "2blip", "3gnap"]);
// Apply an active filter
await click(target, ".o_kanban_group:nth-child(2) .progress-bar.bg-success");
assert.containsOnce(target, ".o_kanban_group.o_kanban_group_show");
assert.strictEqual(
target.querySelector(".o_kanban_group.o_kanban_group_show .o_column_title")
.innerText,
"Yes"
);
assert.deepEqual(getTooltips(1), ["1 yop", "1 blip", "1 Other"]);
assert.deepEqual(getCardTexts(1), ["1yop"]);
// Add searchdomain to something restricting progressbars' values + emptying the filtered group
await reload(kanban, { domain: [["foo", "=", "blip"]] });
// Check that we still have 2 columns, check their progressbar's state and check records
assert.containsN(target, ".o_kanban_group", 2);
assert.containsNone(target, ".o_kanban_group.o_kanban_group_show");
assert.deepEqual(
[...target.querySelectorAll(".o_column_title")].map((el) => el.innerText),
["No", "Yes"]
);
assert.deepEqual(getTooltips(0), ["1 blip"]);
assert.deepEqual(getCardTexts(0), ["4blip"]);
assert.deepEqual(getTooltips(1), ["1 blip"]);
assert.deepEqual(getCardTexts(1), ["2blip"]);
// Undo searchdomain
await reload(kanban, { domain: [] });
// Check that we still have 2 columns and check their progressbar's state
assert.containsN(target, ".o_kanban_group", 2);
assert.containsNone(target, ".o_kanban_group.o_kanban_group_show");
assert.deepEqual(
[...target.querySelectorAll(".o_column_title")].map((el) => el.innerText),
["No", "Yes"]
);
assert.deepEqual(getTooltips(0), ["1 blip"]);
assert.deepEqual(getCardTexts(0), ["4blip"]);
assert.deepEqual(getTooltips(1), ["1 yop", "1 blip", "1 Other"]);
assert.deepEqual(getCardTexts(1), ["1yop", "2blip", "3gnap"]);
}
);
QUnit.test(
"filtered column keeps consistent counters when dropping in a non-matching record",
async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
`,
});
// Check that we have 2 columns, check their progressbar's state, and check records
assert.containsN(target, ".o_kanban_group", 2);
assert.containsNone(target, ".o_kanban_group.o_kanban_group_show");
assert.deepEqual(
[...target.querySelectorAll(".o_column_title")].map((el) => el.innerText),
["No", "Yes"]
);
assert.deepEqual(getTooltips(0), ["1 blip"]);
assert.deepEqual(getCardTexts(0), ["4blip"]);
assert.deepEqual(getTooltips(1), ["1 yop", "1 blip", "1 Other"]);
assert.deepEqual(getCardTexts(1), ["1yop", "2blip", "3gnap"]);
// Apply an active filter
await click(target, ".o_kanban_group:nth-child(2) .progress-bar.bg-success");
assert.hasClass(
target.querySelector(".o_kanban_group:nth-child(2)"),
"o_kanban_group_show"
);
assert.containsOnce(target, ".o_kanban_group.o_kanban_group_show");
assert.strictEqual(
target.querySelector(".o_kanban_group.o_kanban_group_show .o_column_title")
.innerText,
"Yes"
);
assert.containsOnce(target, ".o_kanban_group.o_kanban_group_show .o_kanban_record");
assert.deepEqual(getCardTexts(1), ["1yop"]);
// Drop in the non-matching record from first column
await dragAndDrop(
".o_kanban_group:first-child .o_kanban_record",
".o_kanban_group.o_kanban_group_show"
);
// Check that we have 2 columns, check their progressbar's state, and check records
assert.containsN(target, ".o_kanban_group", 2);
assert.containsOnce(target, ".o_kanban_group.o_kanban_group_show");
assert.deepEqual(
[...target.querySelectorAll(".o_column_title")].map((el) => el.innerText),
["No", "Yes"]
);
assert.deepEqual(getTooltips(0), []);
assert.deepEqual(getCardTexts(0), []);
assert.deepEqual(getTooltips(1), ["1 yop", "2 blip", "1 Other"]);
assert.deepEqual(getCardTexts(1), ["1yop", "4blip"]);
}
);
QUnit.test("filtered column is reloaded when dragging out its last record", async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
`,
async mockRPC(route, args) {
assert.step(args.method || route);
},
});
// Check that we have 2 columns, check their progressbar's state, and check records
assert.containsN(target, ".o_kanban_group", 2);
assert.containsNone(target, ".o_kanban_group.o_kanban_group_show");
assert.deepEqual(
[...target.querySelectorAll(".o_column_title")].map((el) => el.innerText),
["No", "Yes"]
);
assert.deepEqual(getTooltips(0), ["1 blip"]);
assert.deepEqual(getCardTexts(0), ["4blip"]);
assert.deepEqual(getTooltips(1), ["1 yop", "1 blip", "1 Other"]);
assert.deepEqual(getCardTexts(1), ["1yop", "2blip", "3gnap"]);
assert.verifySteps([
"get_views",
"web_read_group",
"read_progress_bar",
"web_search_read",
"web_search_read",
]);
// Apply an active filter
await click(target, ".o_kanban_group:nth-child(2) .progress-bar.bg-success");
assert.hasClass(
target.querySelector(".o_kanban_group:nth-child(2)"),
"o_kanban_group_show"
);
assert.containsOnce(target, ".o_kanban_group.o_kanban_group_show");
assert.strictEqual(
target.querySelector(".o_kanban_group.o_kanban_group_show .o_column_title").innerText,
"Yes"
);
assert.containsOnce(target, ".o_kanban_group.o_kanban_group_show .o_kanban_record");
assert.deepEqual(getCardTexts(1), ["1yop"]);
assert.verifySteps(["web_search_read"]);
// Drag out its only record onto the first column
await dragAndDrop(
".o_kanban_group.o_kanban_group_show .o_kanban_record",
".o_kanban_group:first-child"
);
// Check that we have 2 columns, check their progressbar's state, and check records
assert.containsN(target, ".o_kanban_group", 2);
assert.containsNone(target, ".o_kanban_group.o_kanban_group_show");
assert.deepEqual(
[...target.querySelectorAll(".o_column_title")].map((el) => el.innerText),
["No", "Yes"]
);
assert.deepEqual(getTooltips(0), ["1 yop", "1 blip"]);
assert.deepEqual(getCardTexts(0), ["4blip", "1yop"]);
assert.deepEqual(getTooltips(1), ["1 blip", "1 Other"]);
assert.deepEqual(getCardTexts(1), ["2blip", "3gnap"]);
assert.verifySteps([
"write",
"read_progress_bar",
"read", // read happens is delayed by the ORM batcher
"web_search_read",
"/web/dataset/resequence",
"read",
]);
});
QUnit.test("kanban widget can extract props from attrs", async (assert) => {
class TestWidget extends Component {}
TestWidget.template = xml``;
TestWidget.extractProps = ({ attrs }) => {
return {
title: attrs.title,
};
};
viewWidgetRegistry.add("widget_test_option", TestWidget);
await makeView({
arch: `
`,
});
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 */ `
`,
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: ["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": `
`,
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);
}
);
});