`,
});
expect("div.test").toHaveCount(4);
});
test("kanban records are clickable by default", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
selectRecord: (resId) => {
expect(resId).toBe(1, { message: "should trigger an event to open the form view" });
},
});
await contains(".o_kanban_record").click();
});
test("kanban records with global_click='0'", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
selectRecord: (resId) => {
expect.step("select record");
},
});
await contains(".o_kanban_record").click();
expect.verifySteps([]);
});
test("float fields are formatted properly without using a widget", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_kanban_record:first").toHaveText("0.40000\n0.400");
});
test("field with widget and attributes in kanban", async () => {
const myField = {
component: class MyField extends Component {
static template = xml``;
static props = ["*"];
setup() {
if (this.props.record.resId === 1) {
expect(this.props.attrs).toEqual({
name: "int_field",
widget: "my_field",
str: "some string",
bool: "true",
num: "4.5",
field_id: "int_field_0",
});
}
}
},
extractProps: ({ attrs }) => ({ attrs }),
};
fieldRegistry.add("my_field", myField);
after(() => fieldRegistry.remove("my_field"));
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
});
test("kanban with integer field with human_readable option", async () => {
Partner._records[0].int_field = 5 * 1000 * 1000;
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(queryAllTexts(".o_kanban_record:not(.o_kanban_ghost)")).toEqual(["5M", "9", "17", "-4"]);
expect(".o_field_widget").toHaveCount(0);
});
test.tags("desktop");
test("Hide tooltip when user click inside a kanban headers item", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_kanban_renderer").toHaveClass("o_kanban_grouped");
expect(".o_column_title").toHaveCount(2);
expect(".o-tooltip").toHaveCount(0);
await hover(".o_kanban_group:first-child .o_kanban_header_title .o_column_title");
await runAllTimers();
expect(".o-tooltip").toHaveCount(1);
await contains(
".o_kanban_group:first-child .o_kanban_header_title .o_kanban_quick_add"
).click();
expect(".o-tooltip").toHaveCount(0);
await hover(".o_kanban_group:first-child .o_kanban_header_title .o_column_title");
await runAllTimers();
expect(".o-tooltip").toHaveCount(1);
await contains(".o_kanban_group:first-child .o_kanban_header_title .fa-gear", {
visible: false,
}).click();
expect(".o-tooltip").toHaveCount(0);
});
test.tags("desktop");
test("basic grouped rendering", async () => {
expect.assertions(16);
patchWithCleanup(KanbanRenderer.prototype, {
setup() {
super.setup(...arguments);
onRendered(() => {
expect.step("rendered");
});
},
});
onRpc("web_read_group", ({ kwargs }) => {
// the lazy option is important, so the server can fill in the empty groups
expect(kwargs.lazy).toBe(true, { message: "should use lazy read_group" });
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_view").toHaveClass("o_kanban_test");
expect(".o_kanban_renderer").toHaveClass("o_kanban_grouped");
expect(".o_control_panel_main_buttons button.o-kanban-button-new").toHaveCount(1);
expect(".o_kanban_group").toHaveCount(2);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(3);
expect.verifySteps(["rendered"]);
await toggleKanbanColumnActions(0);
// check available actions in kanban header's config dropdown
expect(".o-dropdown--menu .o_kanban_toggle_fold").toHaveCount(1);
expect(".o_kanban_header:first-child .o_kanban_config .o_column_edit").toHaveCount(0);
expect(".o_kanban_header:first-child .o_kanban_config .o_column_delete").toHaveCount(0);
expect(".o_kanban_header:first-child .o_kanban_config .o_column_archive_records").toHaveCount(
0
);
expect(".o_kanban_header:first-child .o_kanban_config .o_column_unarchive_records").toHaveCount(
0
);
// focuses the search bar and closes the dropdown
await click(".o_searchview input");
// 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();
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(3);
expect.verifySteps(["rendered"]);
});
test("basic grouped rendering with no record", async () => {
Partner._records = [];
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_grouped").toHaveCount(1);
expect(".o_view_nocontent").toHaveCount(1);
expect(".o_control_panel_main_buttons button.o-kanban-button-new").toHaveCount(1, {
message:
"There should be a 'New' button even though there is no column when groupby is not a many2one",
});
});
test("grouped rendering with active field (archivable by default)", async () => {
// add active field on partner model and make all records active
Partner._fields.active = fields.Boolean({ default: true });
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
const clickColumnAction = await toggleKanbanColumnActions(1);
// check archive/restore all actions in kanban header's config dropdown
expect(".o_column_archive_records").toHaveCount(1, { root: getKanbanColumnDropdownMenu(0) });
expect(".o_column_unarchive_records").toHaveCount(1, { root: getKanbanColumnDropdownMenu(0) });
expect(".o_kanban_group").toHaveCount(2);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(1);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(1) })).toHaveCount(3);
await clickColumnAction("Archive All");
expect(".o_dialog").toHaveCount(1);
await contains(".o_dialog footer .btn-primary").click();
expect(".o_kanban_group").toHaveCount(2);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(1);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(1) })).toHaveCount(0);
});
test("grouped rendering with active field (archivable true)", async () => {
// add active field on partner model and make all records active
Partner._fields.active = fields.Boolean({ default: true });
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
const clickColumnAction = await toggleKanbanColumnActions(0);
// check archive/restore all actions in kanban header's config dropdown
expect(".o_column_archive_records").toHaveCount(1, { root: getKanbanColumnDropdownMenu(0) });
expect(".o_column_unarchive_records").toHaveCount(1, { root: getKanbanColumnDropdownMenu(0) });
expect(".o_kanban_group").toHaveCount(2);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(1);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(1) })).toHaveCount(3);
await clickColumnAction("Archive All");
expect(".o_dialog").toHaveCount(1);
await contains(".o_dialog footer .btn-primary").click();
expect(".o_kanban_group").toHaveCount(2);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(0);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(1) })).toHaveCount(3);
});
test.tags("desktop");
test("empty group when grouped by date", async () => {
Partner._records[0].date = "2017-01-08";
Partner._records[1].date = "2017-02-09";
Partner._records[2].date = "2017-02-08";
Partner._records[3].date = "2017-02-10";
await mountView({
type: "kanban",
resModel: "partner",
arch: ``,
groupBy: ["date:month"],
});
expect(queryAllTexts(".o_kanban_header")).toEqual(["January 2017\n(1)", "February 2017\n(3)"]);
Partner._records.shift(); // remove only record of the first group
await press("Enter"); // reload
await animationFrame();
expect(queryAllTexts(".o_kanban_header")).toEqual(["January 2017\n(0)", "February 2017\n(3)"]);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(0);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(1) })).toHaveCount(3);
});
test("grouped rendering with active field (archivable false)", async () => {
// add active field on partner model and make all records active
Partner._fields.active = fields.Boolean({ default: true });
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
await toggleKanbanColumnActions(0);
// check archive/restore all actions in kanban header's config dropdown
expect(".o_column_archive_records").toHaveCount(0, { root: getKanbanColumnDropdownMenu(0) });
expect(".o_column_unarchive_records").toHaveCount(0, { root: getKanbanColumnDropdownMenu(0) });
});
test.tags("desktop");
test("m2m grouped rendering with active field (archivable true)", async () => {
// add active field on partner model and make all records active
Partner._fields.active = fields.Boolean({ default: true });
// more many2many data
Partner._records[0].category_ids = [6, 7];
Partner._records[3].foo = "blork";
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["category_ids"],
});
expect(".o_kanban_group").toHaveCount(3);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(1) })).toHaveCount(2);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(2) })).toHaveCount(2);
expect(queryAllTexts(".o_kanban_group")).toEqual([
"None\n(1)",
"gold\n(2)\nyop\nblip",
"silver\n(2)\nyop\ngnap",
]);
await click(getKanbanColumn(0));
await animationFrame();
await toggleKanbanColumnActions(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.
expect(".o_column_archive_records").toHaveCount(0, { root: getKanbanColumnDropdownMenu(0) });
expect(".o_column_unarchive_records").toHaveCount(0, { root: getKanbanColumnDropdownMenu(0) });
});
test("kanban grouped by date field", async () => {
Partner._records[0].date = "2007-06-10";
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["date"],
});
expect(queryAllTexts(".o_column_title")).toEqual(["None\n(3)", "June 2007\n(1)"]);
});
test("context can be used in kanban template", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
context: { some_key: 1 },
domain: [["id", "=", 1]],
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
expect(".o_kanban_record span:contains(yop)").toHaveCount(1);
});
test("kanban with sub-template", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(queryAllTexts(".o_kanban_record:not(.o_kanban_ghost)")).toEqual([
"yop",
"blip",
"gnap",
"blip",
]);
});
test("kanban with t-set outside card", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(queryAllTexts(".o_kanban_record:not(.o_kanban_ghost)")).toEqual(["10", "9", "17", "-4"]);
});
test("kanban with t-if/t-else on field", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
Negative value`,
});
expect(queryAllTexts(".o_kanban_record:not(.o_kanban_ghost)")).toEqual([
"10",
"9",
"17",
"Negative value",
]);
});
test("kanban with t-if/t-else on field with widget", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
Negative value`,
});
expect(queryAllTexts(".o_kanban_record:not(.o_kanban_ghost)")).toEqual([
"10",
"9",
"17",
"Negative value",
]);
});
test("field with widget and dynamic attributes in kanban", async () => {
const myField = {
component: class MyField extends Component {
static template = xml``;
static props = ["*"];
},
extractProps: ({ attrs }) => {
expect.step(
`${attrs["dyn-bool"]}/${attrs["interp-str"]}/${attrs["interp-str2"]}/${attrs["interp-str3"]}`
);
},
};
fieldRegistry.add("my_field", myField);
after(() => fieldRegistry.remove("my_field"));
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect.verifySteps([
"false/hello yop/hello yop !/hello yop }}",
"true/hello blip/hello blip !/hello blip }}",
"true/hello gnap/hello gnap !/hello gnap }}",
"true/hello blip/hello blip !/hello blip }}",
]);
});
test("view button and string interpolated attribute in kanban", async () => {
patchWithCleanup(ViewButton.prototype, {
setup() {
super.setup();
expect.step(`[${this.props.clickParams["name"]}] className: '${this.props.className}'`);
},
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect.verifySteps([
"[one] className: 'hola oe_kanban_action'",
"[two] className: 'hola oe_kanban_action hello'",
"[sri] className: 'hola oe_kanban_action yop'",
"[foa] className: 'hola oe_kanban_action yop olleh'",
"[fye] className: 'hola oe_kanban_action hello yop'",
"[one] className: 'hola oe_kanban_action'",
"[two] className: 'hola oe_kanban_action hello'",
"[sri] className: 'hola oe_kanban_action blip'",
"[foa] className: 'hola oe_kanban_action blip olleh'",
"[fye] className: 'hola oe_kanban_action hello blip'",
"[one] className: 'hola oe_kanban_action'",
"[two] className: 'hola oe_kanban_action hello'",
"[sri] className: 'hola oe_kanban_action gnap'",
"[foa] className: 'hola oe_kanban_action gnap olleh'",
"[fye] className: 'hola oe_kanban_action hello gnap'",
"[one] className: 'hola oe_kanban_action'",
"[two] className: 'hola oe_kanban_action hello'",
"[sri] className: 'hola oe_kanban_action blip'",
"[foa] className: 'hola oe_kanban_action blip olleh'",
"[fye] className: 'hola oe_kanban_action hello blip'",
]);
});
test("pager should be hidden in grouped mode", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_pager").toHaveCount(0);
});
test("there should be no limit on the number of fetched groups", async () => {
patchWithCleanup(RelationalModel, { DEFAULT_GROUP_LIMIT: 1 });
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group").toHaveCount(2);
});
test("pager, ungrouped, with default limit", async () => {
expect.assertions(2);
onRpc("web_search_read", ({ kwargs }) => {
expect(kwargs.limit).toBe(40);
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_pager").toHaveCount(1);
});
test.tags("desktop");
test("pager, ungrouped, with default limit on desktop", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(getPagerValue()).toEqual([1, 4]);
});
test("pager, ungrouped, with limit given in options", async () => {
expect.assertions(1);
onRpc("web_search_read", ({ kwargs }) => {
expect(kwargs.limit).toBe(2);
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
limit: 2,
});
});
test.tags("desktop");
test("pager, ungrouped, with limit given in options on desktop", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
limit: 2,
});
expect(getPagerValue()).toEqual([1, 2]);
expect(getPagerLimit()).toBe(4);
});
test("pager, ungrouped, with limit set on arch and given in options", async () => {
expect.assertions(1);
onRpc("web_search_read", ({ kwargs }) => {
expect(kwargs.limit).toBe(3);
});
// the limit given in the arch should take the priority over the one given in options
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
limit: 2,
});
});
test.tags("desktop");
test("pager, ungrouped, with limit set on arch and given in options on desktop", async () => {
// the limit given in the arch should take the priority over the one given in options
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
limit: 2,
});
expect(getPagerValue()).toEqual([1, 3]);
expect(getPagerLimit()).toBe(4);
});
test.tags("desktop");
test("pager, ungrouped, with count limit reached", async () => {
patchWithCleanup(RelationalModel, { DEFAULT_COUNT_LIMIT: 3 });
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect(".o_pager_value").toHaveText("1-2");
expect(".o_pager_limit").toHaveText("3+");
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_search_read",
]);
await contains(".o_pager_limit").click();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect(".o_pager_value").toHaveText("1-2");
expect(".o_pager_limit").toHaveText("4");
expect.verifySteps(["search_count"]);
});
test("pager, ungrouped, with count limit reached, click next", async () => {
patchWithCleanup(RelationalModel, { DEFAULT_COUNT_LIMIT: 3 });
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_search_read",
]);
await contains(".o_pager_next").click();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect.verifySteps(["web_search_read"]);
});
test.tags("desktop");
test("pager, ungrouped, with count limit reached, click next on desktop", async () => {
patchWithCleanup(RelationalModel, { DEFAULT_COUNT_LIMIT: 3 });
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_pager_value").toHaveText("1-2");
expect(".o_pager_limit").toHaveText("3+");
await contains(".o_pager_next").click();
expect(".o_pager_value").toHaveText("3-4");
expect(".o_pager_limit").toHaveText("4");
});
test("pager, ungrouped, with count limit reached, click next (2)", async () => {
patchWithCleanup(RelationalModel, { DEFAULT_COUNT_LIMIT: 3 });
Partner._records.push({ id: 5, foo: "xxx" });
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_search_read",
]);
await contains(".o_pager_next").click();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect.verifySteps(["web_search_read"]);
await contains(".o_pager_next").click();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
expect.verifySteps(["web_search_read"]);
});
test.tags("desktop");
test("pager, ungrouped, with count limit reached, click next (2) on desktop", async () => {
patchWithCleanup(RelationalModel, { DEFAULT_COUNT_LIMIT: 3 });
Partner._records.push({ id: 5, foo: "xxx" });
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_pager_value").toHaveText("1-2");
expect(".o_pager_limit").toHaveText("3+");
await contains(".o_pager_next").click();
expect(".o_pager_value").toHaveText("3-4");
expect(".o_pager_limit").toHaveText("4+");
await contains(".o_pager_next").click();
expect(".o_pager_value").toHaveText("5-5");
expect(".o_pager_limit").toHaveText("5");
});
test("pager, ungrouped, with count limit reached, click previous", async () => {
patchWithCleanup(RelationalModel, { DEFAULT_COUNT_LIMIT: 3 });
Partner._records.push({ id: 5, foo: "xxx" });
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_search_read",
]);
await contains(".o_pager_previous").click();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
expect.verifySteps(["search_count", "web_search_read"]);
});
test.tags("desktop");
test("pager, ungrouped, with count limit reached, click previous on desktop", async () => {
patchWithCleanup(RelationalModel, { DEFAULT_COUNT_LIMIT: 3 });
Partner._records.push({ id: 5, foo: "xxx" });
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_pager_value").toHaveText("1-2");
expect(".o_pager_limit").toHaveText("3+");
await contains(".o_pager_previous").click();
expect(".o_pager_value").toHaveText("5-5");
expect(".o_pager_limit").toHaveText("5");
});
test.tags("desktop");
test("pager, ungrouped, with count limit reached, edit pager", async () => {
patchWithCleanup(RelationalModel, { DEFAULT_COUNT_LIMIT: 3 });
Partner._records.push({ id: 5, foo: "xxx" });
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect(".o_pager_value").toHaveText("1-2");
expect(".o_pager_limit").toHaveText("3+");
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_search_read",
]);
await contains("span.o_pager_value").click();
await contains("input.o_pager_value").edit("2-4");
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(3);
expect(".o_pager_value").toHaveText("2-4");
expect(".o_pager_limit").toHaveText("4+");
expect.verifySteps(["web_search_read"]);
await contains("span.o_pager_value").click();
await contains("input.o_pager_value").edit("2-14");
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(4);
expect(".o_pager_value").toHaveText("2-5");
expect(".o_pager_limit").toHaveText("5");
expect.verifySteps(["web_search_read"]);
});
test.tags("desktop");
test("count_limit attrs set in arch", async () => {
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect(".o_pager_value").toHaveText("1-2");
expect(".o_pager_limit").toHaveText("3+");
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_search_read",
]);
await contains(".o_pager_limit").click();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect(".o_pager_value").toHaveText("1-2");
expect(".o_pager_limit").toHaveText("4");
expect.verifySteps(["search_count"]);
});
test.tags("desktop");
test("pager, ungrouped, deleting all records from last page", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
Delete`,
});
expect(getPagerValue()).toEqual([1, 3]);
expect(getPagerLimit()).toBe(4);
// move to next page
await pagerNext();
expect(getPagerValue()).toEqual([4, 4]);
// delete a record
await contains(".o_kanban_record a").click();
expect(".o_dialog").toHaveCount(1);
await contains(".o_dialog footer .btn-primary").click();
expect(getPagerValue()).toEqual([1, 3]);
expect(getPagerLimit()).toBe(3);
});
test.tags("desktop");
test("pager, update calls onUpdatedPager", async () => {
class TestKanbanController extends KanbanController {
setup() {
super.setup();
onWillRender(() => {
expect.step("render");
});
}
async onUpdatedPager() {
expect.step("onUpdatedPager");
}
}
viewRegistry.add("test_kanban_view", {
...kanbanView,
Controller: TestKanbanController,
});
after(() => viewRegistry.remove("test_kanban_view"));
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
limit: 3,
});
expect(getPagerValue()).toEqual([1, 3]);
expect(getPagerLimit()).toBe(4);
expect.step("next page");
await contains(".o_pager_next").click();
expect(getPagerValue()).toEqual([4, 4]);
expect.verifySteps(["render", "next page", "render", "onUpdatedPager"]);
});
test("click on a button type='delete' to delete a record in a column", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
Delete`,
groupBy: ["product_id"],
});
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(2);
expect(queryAll(".o_kanban_load_more", { root: getKanbanColumn(0) })).toHaveCount(0);
await click(queryFirst(".o_kanban_record .o_delete", { root: getKanbanColumn(0) }));
await animationFrame();
expect(".modal").toHaveCount(1);
await contains(".modal .btn-primary").click();
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(1);
expect(queryAll(".o_kanban_load_more", { root: getKanbanColumn(0) })).toHaveCount(0);
});
test("click on a button type='archive' to archive a record in a column", async () => {
onRpc("action_archive", ({ args }) => {
expect.step(`archive:${args[0]}`);
return true;
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
archive`,
groupBy: ["product_id"],
});
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(2);
await contains(".o_kanban_record .o_archive").click();
expect(".modal").toHaveCount(1);
expect.verifySteps([]);
await contains(".modal .btn-primary").click();
expect.verifySteps(["archive:1"]);
});
test("click on a button type='unarchive' to unarchive a record in a column", async () => {
onRpc("action_unarchive", ({ args }) => {
expect.step(`unarchive:${args[0]}`);
return true;
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
unarchive`,
groupBy: ["product_id"],
});
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(2);
await contains(".o_kanban_record .o_unarchive").click();
expect.verifySteps(["unarchive:1"]);
});
test.tags("desktop");
test("kanban with an action id as on_create attrs", async () => {
mockService("action", {
doAction(action, options) {
// simplified flow in this test: simulate a target new action which
// creates a record and closes itself
expect.step(`doAction ${action}`);
Partner._records.push({ id: 299, foo: "new" });
options.onClose();
},
});
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(4);
await createKanbanRecord();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(5);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_search_read",
"doAction some.action",
"web_search_read",
]);
});
test.tags("desktop");
test("grouped kanban with quick_create attrs set to false", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
createRecord: () => expect.step("create record"),
});
expect(".o_kanban_group").toHaveCount(2);
expect(".o_kanban_quick_add").toHaveCount(0);
await createKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(0);
expect.verifySteps(["create record"]);
});
test.tags("desktop");
test("create in grouped on m2o", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group.o_group_draggable").toHaveCount(2);
expect(".o_control_panel_main_buttons button.o-kanban-button-new").toHaveCount(1);
expect(".o_column_quick_create").toHaveCount(1);
await createKanbanRecord();
expect(".o_kanban_group:first-child > .o_kanban_quick_create").toHaveCount(1);
expect(queryAllTexts(".o_column_title")).toEqual(["hello\n(2)", "xmo\n(2)"]);
});
test("create in grouped on char", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["foo"],
});
expect(".o_kanban_group.o_group_draggable").toHaveCount(0);
expect(".o_kanban_group").toHaveCount(3);
expect(queryAllTexts(".o_column_title")).toEqual(["blip\n(2)", "gnap\n(1)", "yop\n(1)"]);
expect(".o_kanban_group:first-child > .o_kanban_quick_create").toHaveCount(0);
});
test("prevent deletion when grouped by many2many field", async () => {
Partner._records[0].category_ids = [6, 7];
Partner._records[3].category_ids = [7];
await mountView({
type: "kanban",
resModel: "partner",
arch: `
delete`,
searchViewArch: `
`,
groupBy: ["category_ids"],
});
expect(".thisisdeletable").toHaveCount(0, { message: "records should not be deletable" });
await toggleSearchBarMenu();
await toggleMenuItem("GroupBy Foo");
expect(".thisisdeletable").toHaveCount(4, { message: "records should be deletable" });
});
test.tags("desktop");
test("kanban grouped by many2one: false column is folded by default", async () => {
Partner._records[0].product_id = false;
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group").toHaveCount(3);
expect(".o_column_folded").toHaveCount(1);
expect(queryAllTexts(".o_kanban_header")).toEqual(["None\n(1)", "hello\n(1)", "xmo\n(2)"]);
await contains(".o_kanban_header").click();
expect(".o_column_folded").toHaveCount(0);
expect(queryAllTexts(".o_kanban_header")).toEqual(["None\n(1)", "hello\n(1)", "xmo\n(2)"]);
// reload -> None column should remain open
await click(".o_searchview_input");
await press("Enter");
await animationFrame();
expect(".o_column_folded").toHaveCount(0);
expect(queryAllTexts(".o_kanban_header")).toEqual(["None\n(1)", "hello\n(1)", "xmo\n(2)"]);
});
test.tags("desktop");
test("quick created records in grouped kanban are on displayed top", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group").toHaveCount(2);
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(2);
await createKanbanRecord();
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:first .o_kanban_quick_create").toHaveCount(1);
await edit("new record");
await validateKanbanRecord();
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(3);
expect(".o_kanban_group:first .o_kanban_quick_create").toHaveCount(1);
// the new record must be the first record of the column
expect(queryAllTexts(" .o_kanban_group:first .o_kanban_record")).toEqual([
"new record",
"yop",
"gnap",
]);
await click(".o_kanban_quick_create input"); // FIXME: should not be necessary
await edit("another record");
await validateKanbanRecord();
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(4);
expect(".o_kanban_group:first .o_kanban_quick_create").toHaveCount(1);
expect(queryAllTexts(".o_kanban_group:first .o_kanban_record")).toEqual([
"another record",
"new record",
"yop",
"gnap",
]);
});
test.tags("desktop");
test("quick create record without quick_create_view", async () => {
stepAllNetworkCalls();
onRpc("name_create", ({ args }) => {
expect(args[0]).toBe("new partner");
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1);
// click on 'Create' -> should open the quick create in the first column
await createKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_quick_create").toHaveCount(1);
expect(".o_kanban_quick_create .o_form_view.o_xxs_form_view").toHaveCount(1);
expect(".o_kanban_quick_create input").toHaveCount(1);
expect(
".o_kanban_quick_create .o_field_widget.o_required_modifier input[placeholder=Title]"
).toHaveCount(1);
// fill the quick create and validate
await editKanbanRecordQuickCreateInput("display_name", "new partner");
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"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
"web_read", // read the created record
"onchange", // reopen the quick create automatically
]);
});
test.tags("desktop");
test("quick create record with quick_create_view", async () => {
Partner._views["form,some_view_ref"] = `
`;
stepAllNetworkCalls();
onRpc("web_save", ({ args }) => {
expect(args[1]).toEqual({
foo: "new partner",
int_field: 4,
state: "def",
});
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_control_panel").toHaveCount(1);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1);
// click on 'Create' -> should open the quick create in the first column
await createKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_quick_create").toHaveCount(1);
expect(".o_kanban_quick_create .o_form_view.o_xxs_form_view").toHaveCount(1);
expect(".o_control_panel").toHaveCount(1, {
message: "should not have instantiated another control panel",
});
expect(".o_kanban_quick_create input").toHaveCount(2);
expect(".o_kanban_quick_create .o_field_widget").toHaveCount(3);
// fill the quick create and validate
await editKanbanRecordQuickCreateInput("foo", "new partner");
await editKanbanRecordQuickCreateInput("int_field", "4");
await click(".o_kanban_quick_create .o_field_widget[name=state] .o_priority_star:first-child");
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"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
"web_save", // should perform a web_save to create the record
"web_read", // read the created record
"onchange", // new quick create
]);
});
test.tags("desktop");
test("quick create record flickering", async () => {
let def;
Partner._views["form,some_view_ref"] = `
`;
onRpc("web_save", ({ args }) => {
expect(args[1]).toEqual({
foo: "new partner",
int_field: 4,
state: "def",
});
});
onRpc("onchange", () => def);
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
// click on 'Create' -> should open the quick create in the first column
await createKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1);
expect(".o_kanban_group:first-child .o_kanban_quick_create").toHaveCount(1);
expect(".o_kanban_quick_create .o_form_view.o_xxs_form_view").toHaveCount(1);
expect(".o_kanban_quick_create input").toHaveCount(2);
expect(".o_kanban_quick_create .o_field_widget").toHaveCount(3);
// fill the quick create and validate
await editKanbanRecordQuickCreateInput("foo", "new partner");
await editKanbanRecordQuickCreateInput("int_field", "4");
await click(".o_kanban_quick_create .o_field_widget[name=state] .o_priority_star:first-child");
def = new Deferred();
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:first-child .o_kanban_quick_create").toHaveCount(1);
def.resolve();
await animationFrame();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:first-child .o_kanban_quick_create").toHaveCount(1);
});
test.tags("desktop");
test("quick create record flickering (load more)", async () => {
let def;
Partner._views["form,some_view_ref"] = ``;
onRpc("read", () => def);
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
// click on 'Create' -> should open the quick create in the first column
await createKanbanRecord();
// fill the quick create and validate
await editKanbanRecordQuickCreateInput("foo", "new partner");
def = new Deferred();
await validateKanbanRecord();
expect(".o_kanban_load_more").toHaveCount(0);
def.resolve();
await animationFrame();
expect(".o_kanban_load_more").toHaveCount(0);
});
test.tags("desktop");
test("quick create record should focus default field", async function () {
Partner._views["form,some_view_ref"] = `
`;
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
await createKanbanRecord();
expect(".o_field_widget[name=int_field] input:first").toBeFocused();
});
test.tags("desktop");
test("quick create record should focus first field input", async function () {
Partner._views["form,some_view_ref"] = `
`;
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
await createKanbanRecord();
expect(".o_field_widget[name=foo] input:first").toBeFocused();
});
test.tags("desktop");
test("quick_create_view without quick_create option", async () => {
Partner._views["form,some_view_ref"] = `
`;
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
createRecord() {
expect.step("create record");
},
});
expect(".o_kanban_group").toHaveCount(2);
expect(".o_kanban_group .o_kanban_quick_add").toHaveCount(2);
// click on 'Create' in control panel -> should not open the quick create
await createKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(0);
expect.verifySteps(["create record"]);
// click "+" icon in first column -> should open the quick create
await contains(".o_kanban_quick_add").click();
await animationFrame();
expect(".o_kanban_group:first .o_kanban_quick_create").toHaveCount(1);
expect.verifySteps([]);
});
test.tags("desktop");
test("quick create record in grouped on m2o (no quick_create_view)", async () => {
expect.assertions(6);
stepAllNetworkCalls();
onRpc("name_create", ({ args, kwargs }) => {
expect(args[0]).toBe("new partner");
const { default_product_id, default_float_field } = kwargs.context;
expect(default_product_id).toBe(3);
expect(default_float_field).toBe(2.5);
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
context: { default_float_field: 2.5 },
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
// click on 'Create', fill the quick create and validate
await createKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "new partner");
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(3);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"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
"web_read", // read the created record
"onchange", // reopen the quick create automatically
]);
});
test.tags("desktop");
test("quick create record in grouped on m2o (with quick_create_view)", async () => {
expect.assertions(6);
Partner._views["form,some_view_ref"] = `
`;
stepAllNetworkCalls();
onRpc("web_save", ({ method, args, kwargs }) => {
expect(args[1]).toEqual({
foo: "new partner",
int_field: 4,
state: "def",
});
const { default_product_id, default_float_field } = kwargs.context;
expect(default_product_id).toBe(3);
expect(default_float_field).toBe(2.5);
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
context: { default_float_field: 2.5 },
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
// click on 'Create', fill the quick create and validate
await createKanbanRecord();
await editKanbanRecordQuickCreateInput("foo", "new partner");
await animationFrame();
await editKanbanRecordQuickCreateInput("int_field", 4);
await animationFrame();
await contains(
".o_kanban_quick_create .o_field_widget[name=state] .o_priority_star:first-child"
).click();
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(3);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"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
"web_save", // should perform a web_save to create the record
"web_read", // read the created record
"onchange", // reopen the quick create automatically
]);
});
test("quick create record in grouped on m2m (no quick_create_view)", async () => {
stepAllNetworkCalls();
onRpc("name_create", ({ args, kwargs }) => {
expect(args[0]).toBe("new partner");
expect(kwargs.context.default_category_ids).toEqual([6]);
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["category_ids"],
});
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(1);
// click on 'Create', fill the quick create and validate
await quickCreateKanbanRecord(1);
await editKanbanRecordQuickCreateInput("display_name", "new partner");
await animationFrame();
await validateKanbanRecord();
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(2);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"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
"web_read", // read the created record
"onchange", // reopen the quick create automatically
]);
});
test.tags("desktop");
test("quick create record in grouped on m2m in the None column", async () => {
stepAllNetworkCalls();
onRpc("name_create", ({ args, kwargs }) => {
expect(args[0]).toBe("new partner");
expect(kwargs.context.default_category_ids).toBe(false);
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["category_ids"],
});
await contains(".o_kanban_group:nth-child(1)").click();
expect(".o_kanban_group:nth-child(1) .o_kanban_record").toHaveCount(2);
// click on 'Create', fill the quick create and validate
await quickCreateKanbanRecord(0);
await editKanbanRecordQuickCreateInput("display_name", "new partner");
await animationFrame();
await validateKanbanRecord();
expect(".o_kanban_group:nth-child(1) .o_kanban_record").toHaveCount(3);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_read_group", // initial read_group
"web_search_read", // initial search_read (first column)
"web_search_read", // initial search_read (second column)
"web_search_read", // read records when unfolding 'None'
"onchange", // quick create
"name_create", // should perform a name_create to create the record
"web_read", // read the created record
"onchange", // reopen the quick create automatically
]);
});
test("quick create record in grouped on m2m (field not in template)", async () => {
Partner._views["form,some_view_ref"] = ``;
onRpc("web_save", ({ args, kwargs }) => {
expect(args[1]).toEqual({ foo: "new partner" });
expect(kwargs.context.default_category_ids).toEqual([6]);
return [{ id: 5 }];
});
onRpc("web_read", ({ args }) => {
if (args[0][0] === 5) {
return [{ id: 5, foo: "new partner", category_ids: [6] }];
}
});
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["category_ids"],
});
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(1);
// click on 'Create', fill the quick create and validate
await quickCreateKanbanRecord(1);
await editKanbanRecordQuickCreateInput("foo", "new partner");
await validateKanbanRecord();
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(2);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"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", // get form view
"onchange", // quick create
"web_save", // should perform a web_save to create the record
"web_read", // read the created record
"onchange", // reopen the quick create automatically
]);
});
test("quick create record in grouped on m2m (field in the form view)", async () => {
Partner._views["form,some_view_ref"] = `
`;
stepAllNetworkCalls();
onRpc("web_save", ({ method, args, kwargs }) => {
expect(args[1]).toEqual({
category_ids: [[4, 6]],
foo: "new partner",
});
expect(kwargs.context.default_category_ids).toEqual([6]);
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["category_ids"],
});
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(1);
// click on 'Create', fill the quick create and validate
await quickCreateKanbanRecord(1);
// verify that the quick create m2m field contains the column value
expect(".o_tag_badge_text").toHaveCount(1);
expect(".o_tag_badge_text").toHaveText("gold");
await editKanbanRecordQuickCreateInput("foo", "new partner");
await validateKanbanRecord();
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(2);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"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", // get form view
"onchange", // quick create
"web_save", // should perform a web_save to create the record
"web_read",
"onchange",
]);
});
test.tags("desktop");
test("quick create record validation: stays open when invalid", async () => {
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_read_group",
"web_search_read",
"web_search_read",
]);
await createKanbanRecord();
expect.verifySteps(["onchange"]);
// do not fill anything and validate
await validateKanbanRecord();
expect.verifySteps([]);
expect(".o_kanban_group:first-child .o_kanban_quick_create").toHaveCount(1);
expect("[name=display_name]").toHaveClass("o_field_invalid");
expect(".o_notification_manager .o_notification").toHaveCount(1);
expect(".o_notification").toHaveText("Invalid fields:\nDisplay Name");
});
test.tags("desktop");
test("quick create record with default values and onchanges", async () => {
Partner._fields.int_field = fields.Integer({ default: 4 });
Partner._fields.foo = fields.Char({
onChange: (obj) => {
if (obj.foo) {
obj.int_field = 8;
}
},
});
Partner._views["form,some_view_ref"] = `
`;
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
// click on 'Create' -> should open the quick create in the first column
await createKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_quick_create").toHaveCount(1);
expect(".o_field_widget[name=int_field] input").toHaveValue("4", {
message: "default value should be set",
});
// fill the 'foo' field -> should trigger the onchange
// await fieldInput("foo").edit("new partner");
await editKanbanRecordQuickCreateInput("foo", "new partner");
expect(".o_field_widget[name=int_field] input").toHaveValue("8", {
message: "onchange should have been triggered",
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"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
]);
});
test("quick create record with quick_create_view: modifiers", async () => {
Partner._views["form,some_view_ref"] = `
`;
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
// create a new record
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create .o_field_widget[name=foo]").toHaveClass("o_required_modifier");
expect(".o_kanban_quick_create .o_field_widget[name=int_field]").toHaveCount(0);
// fill 'foo' field
await editKanbanRecordQuickCreateInput("foo", "new partner");
await animationFrame();
expect(".o_kanban_quick_create .o_field_widget[name=int_field]").toHaveCount(1);
});
test("quick create record with onchange of field marked readonly", async () => {
Partner._fields.foo = fields.Char({
onChange: (obj) => {
obj.int_field = 8;
},
});
Partner._views["form,some_view_ref"] = `
`;
stepAllNetworkCalls();
onRpc("web_save", ({ method, args }) => {
expect(args[1].int_field).toBe(undefined, {
message: "readonly field shouldn't be sent in create",
});
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"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 quickCreateKanbanRecord();
expect.verifySteps(["get_views", "onchange"]);
// fill the 'foo' field -> should trigger the onchange
await editKanbanRecordQuickCreateInput("foo", "new partner");
expect.verifySteps(["onchange"]);
await validateKanbanRecord();
expect.verifySteps(["web_save", "web_read", "onchange"]);
});
test("quick create record and change state in grouped mode", async () => {
Partner._fields.kanban_state = fields.Selection({
selection: [
["normal", "Grey"],
["done", "Green"],
["blocked", "Red"],
],
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["foo"],
});
// Quick create kanban record
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "Test");
await validateKanbanRecord();
// Select state in kanban
await click(".o_status", { root: getKanbanRecord({ index: 0 }) });
await animationFrame();
await contains(".dropdown-item:nth-child(2)").click();
expect(".o_status:first").toHaveClass("o_status_green");
});
test("window resize should not change quick create form size", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create .o_form_view").toHaveClass("o_xxs_form_view");
await resize({ width: 800, height: 400 });
expect(".o_kanban_quick_create .o_form_view").toHaveClass("o_xxs_form_view");
});
test("quick create record: cancel and validate without using the buttons", async () => {
Partner._views["form,some_view_ref"] = ``;
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(4);
// click to add an element and cancel the quick creation by pressing ESC
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1);
await press("Escape");
await animationFrame();
expect(".o_kanban_quick_create").toHaveCount(0);
// click to add and element and click outside, should cancel the quick creation
await quickCreateKanbanRecord();
await contains(".o_kanban_group:first-child .o_kanban_record:last-of-type").click();
expect(".o_kanban_quick_create").toHaveCount(0);
// click to input and drag the mouse outside, should not cancel the quick creation
await quickCreateKanbanRecord();
await (
await drag(".o_kanban_quick_create input")
).drop(".o_kanban_group:first-child .o_kanban_record:last-of-type");
await animationFrame();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "the quick create should not have been destroyed after clicking outside",
});
// click to really add an element
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("foo", "new partner");
// clicking outside should no longer destroy the quick create as it is dirty
await contains(".o_kanban_group:first-child .o_kanban_record:last-of-type").click();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "the quick create should not have been destroyed",
});
// confirm by pressing ENTER
await press("Enter");
await animationFrame();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(5);
expect(getKanbanRecordTexts(0)).toEqual(["new partner", "blip"]);
});
test("quick create record: validate with ENTER", async () => {
Partner._views["form,some_view_ref"] = `
`;
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_record").toHaveCount(4);
// add an element and confirm by pressing ENTER
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("foo", "new partner");
await validateKanbanRecord();
expect(".o_kanban_record").toHaveCount(5);
expect(".o_kanban_quick_create .o_field_widget[name=foo] input").toHaveValue("");
});
test("quick create record: prevent multiple adds with ENTER", async () => {
Partner._views["form,some_view_ref"] = `
`;
const def = new Deferred();
onRpc("web_save", () => {
expect.step("web_save");
return def;
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_record").toHaveCount(4);
// add an element and press ENTER twice
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("foo", "new partner");
await press("Enter");
await animationFrame();
expect(".o_kanban_record").toHaveCount(4);
expect(".o_kanban_quick_create .o_field_widget[name=foo] input").toHaveValue("new partner");
expect(".o_kanban_quick_create").toHaveClass("o_disabled");
def.resolve();
await animationFrame();
expect(".o_kanban_record").toHaveCount(5);
expect(".o_kanban_quick_create .o_field_widget[name=foo] input").toHaveValue("");
expect(".o_kanban_quick_create").not.toHaveClass("o_disabled");
expect.verifySteps(["web_save"]);
});
test("quick create record: prevent multiple adds with Add clicked", async () => {
Partner._views["form,some_view_ref"] = `
`;
const def = new Deferred();
onRpc("web_save", () => {
expect.step("web_save");
return def;
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_record").toHaveCount(4);
// add an element and click 'Add' twice
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("foo", "new partner");
await validateKanbanRecord();
await validateKanbanRecord();
expect(".o_kanban_record").toHaveCount(4);
expect(".o_kanban_quick_create .o_field_widget[name=foo] input").toHaveValue("new partner");
expect(".o_kanban_quick_create").toHaveClass("o_disabled");
def.resolve();
await animationFrame();
expect(".o_kanban_record").toHaveCount(5);
expect(".o_kanban_quick_create .o_field_widget[name=foo] input").toHaveValue("");
expect(".o_kanban_quick_create").not.toHaveClass("o_disabled");
expect.verifySteps(["web_save"]);
});
test.tags("desktop");
test("save a quick create record and create a new one simultaneously", async () => {
const def = new Deferred();
onRpc("name_create", () => {
expect.step("name_create");
return def;
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_record").toHaveCount(4);
// Create and save a record
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "new partner");
await validateKanbanRecord();
expect(".o_kanban_record").toHaveCount(4);
expect(".o_kanban_quick_create [name=display_name] input").toHaveValue("new partner");
expect(".o_kanban_quick_create").toHaveClass("o_disabled");
// Create a new record during the save of the first one
await createKanbanRecord();
expect(".o_kanban_record").toHaveCount(4);
expect(".o_kanban_quick_create [name=display_name] input").toHaveValue("new partner");
expect(".o_kanban_quick_create").toHaveClass("o_disabled");
def.resolve();
await animationFrame();
expect(".o_kanban_record").toHaveCount(5);
expect(".o_kanban_quick_create [name=display_name] input").toHaveValue("");
expect(".o_kanban_quick_create").not.toHaveClass("o_disabled");
expect.verifySteps(["name_create"]);
});
test("quick create record: prevent multiple adds with ENTER, with onchange", async () => {
Partner._fields.foo = fields.Char({
onChange: (obj) => {
obj.int_field += obj.foo ? 3 : 0;
},
});
Partner._views["form,some_view_ref"] = `
`;
onRpc("onchange", () => {
expect.step("onchange");
if (shouldDelayOnchange) {
return def;
}
});
onRpc("web_save", ({ args }) => {
expect.step("web_save");
const values = args[1];
expect(values.foo).toBe("new partner");
expect(values.int_field).toBe(3);
});
let shouldDelayOnchange = false;
const def = new Deferred();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_record").toHaveCount(4, {
message: "should have 4 records at the beginning",
});
// add an element and press ENTER twice
await quickCreateKanbanRecord();
shouldDelayOnchange = true;
await editKanbanRecordQuickCreateInput("foo", "new partner");
await press("Enter");
await animationFrame();
await press("Enter");
await animationFrame();
expect(".o_kanban_record").toHaveCount(4, {
message: "should not have created the record yet",
});
expect(".o_kanban_quick_create .o_field_widget[name=foo] input").toHaveValue("new partner", {
message: "quick create should not be empty yet",
});
expect(".o_kanban_quick_create").toHaveClass("o_disabled");
def.resolve();
await animationFrame();
expect(".o_kanban_record").toHaveCount(5, { message: "should have created a new record" });
expect(".o_kanban_quick_create .o_field_widget[name=foo] input").toHaveValue("", {
message: "quick create should now be empty",
});
expect(".o_kanban_quick_create").not.toHaveClass("o_disabled");
expect.verifySteps([
"onchange", // default_get
"onchange", // new partner
"web_save",
"onchange", // default_get
]);
});
test("quick create record: click Add to create, with delayed onchange", async () => {
Partner._fields.foo = fields.Char({
onChange: (obj) => {
obj.int_field += obj.foo ? 3 : 0;
},
});
Partner._views["form,some_view_ref"] = `
`;
onRpc("onchange", () => {
expect.step("onchange");
if (shouldDelayOnchange) {
return def;
}
});
onRpc("web_save", ({ args }) => {
expect.step("web_save");
expect(args[1]).toEqual({
foo: "new partner",
int_field: 3,
});
});
let shouldDelayOnchange = false;
const def = new Deferred();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_record").toHaveCount(4, {
message: "should have 4 records at the beginning",
});
// add an element and click 'add'
await quickCreateKanbanRecord();
shouldDelayOnchange = true;
await editKanbanRecordQuickCreateInput("foo", "new partner");
await validateKanbanRecord();
expect(".o_kanban_record").toHaveCount(4, {
message: "should not have created the record yet",
});
expect(".o_kanban_quick_create .o_field_widget[name=foo] input").toHaveValue("new partner", {
message: "quick create should not be empty yet",
});
expect(".o_kanban_quick_create").toHaveClass("o_disabled");
def.resolve(); // the onchange returns
await animationFrame();
expect(".o_kanban_record").toHaveCount(5, { message: "should have created a new record" });
expect(".o_kanban_quick_create .o_field_widget[name=foo] input").toHaveValue("", {
message: "quick create should now be empty",
});
expect(".o_kanban_quick_create").not.toHaveClass("o_disabled");
expect.verifySteps([
"onchange", // default_get
"onchange", // new partner
"web_save",
"onchange", // default_get
]);
});
test.tags("desktop");
test("quick create when first column is folded", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_group:first-child").not.toHaveClass("o_column_folded");
// fold the first column
let clickColumnAction = await toggleKanbanColumnActions(0);
await clickColumnAction("Fold");
expect(".o_kanban_group:first-child").toHaveClass("o_column_folded");
// click on 'Create' to open the quick create in the first column
await createKanbanRecord();
expect(".o_kanban_group:first-child").not.toHaveClass("o_column_folded");
expect(".o_kanban_group:first-child .o_kanban_quick_create").toHaveCount(1);
// fold again the first column
clickColumnAction = await toggleKanbanColumnActions(0);
await clickColumnAction("Fold");
expect(".o_kanban_group:first-child").toHaveClass("o_column_folded");
expect(".o_kanban_quick_create").toHaveCount(0);
});
test("quick create record: cancel when not dirty", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1, {
message: "first column should contain one record",
});
// click to add an element
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "should have open the quick create widget",
});
// click again to add an element -> should have kept the quick create open
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "should have kept the quick create open",
});
// click outside: should remove the quick create
await contains(".o_kanban_group:first-child .o_kanban_record:last-of-type").click();
expect(".o_kanban_quick_create").toHaveCount(0, {
message: "the quick create should not have been destroyed",
});
// click to reopen the quick create
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "should have open the quick create widget",
});
// press ESC: should remove the quick create
await press("Escape");
await animationFrame();
expect(".o_kanban_quick_create").toHaveCount(0, {
message: "quick create widget should have been removed",
});
// click to reopen the quick create
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "should have open the quick create widget",
});
// click on 'Discard': should remove the quick create
await quickCreateKanbanRecord();
await discardKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(0, {
message: "the quick create should be destroyed when the user clicks outside",
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1, {
message: "first column should still contain one record",
});
// click to reopen the quick create
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "should have open the quick create widget",
});
// clicking on the quick create itself should keep it open
await contains(".o_kanban_quick_create").click();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "the quick create should not have been destroyed when clicked on itself",
});
});
test.tags("desktop");
test("quick create record: cancel when modal is opened", async () => {
Partner._views["form,some_view_ref"] = `
`;
Product._views.form = '';
await mountView({
type: "kanban",
resModel: "partner",
groupBy: ["bar"],
arch: `
`,
});
// click to add an element
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1);
await press("t");
await press("e");
await press("s");
await press("t");
await runAllTimers();
await click(".o_m2o_dropdown_option_create_edit"); // open create and edit dialog
await animationFrame();
// 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.
expect(".o_dialog").toHaveCount(1, { message: "modal should be opening after m2o focusout" });
expect(document.body).toHaveClass("modal-open");
await click(document.body);
await animationFrame();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "quick create should stay open while modal is opening",
});
});
test("quick create record: cancel when dirty", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1, {
message: "first column should contain one record",
});
// click to add an element and edit it
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "should have open the quick create widget",
});
await editKanbanRecordQuickCreateInput("display_name", "some value");
// click outside: should not remove the quick create
await contains(".o_kanban_group:first-child .o_kanban_record").click();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "the quick create should not have been destroyed",
});
// press ESC: should remove the quick create
await press("Escape");
await animationFrame();
expect(".o_kanban_quick_create").toHaveCount(0, {
message: "quick create widget should have been removed",
});
// click to reopen quick create and edit it
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "should have open the quick create widget",
});
await editKanbanRecordQuickCreateInput("display_name", "some value");
// click on 'Discard': should remove the quick create
await discardKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(0, {
message: "the quick create should be destroyed when the user discard quick creation",
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1, {
message: "first column should still contain one record",
});
});
test("quick create record and edit in grouped mode", async () => {
expect.assertions(4);
onRpc("web_read", ({ args }) => {
newRecordID = args[0][0];
});
let newRecordID;
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
selectRecord: (resId) => {
expect(resId).toBe(newRecordID);
},
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1, {
message: "first column should contain one record",
});
// click to add and edit a record
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "new partner");
await editKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2, {
message: "first column should now contain two records",
});
expect(queryAllTexts(".o_kanban_group:first-child .o_kanban_record")).toEqual([
"new partner",
"blip",
]);
});
test.tags("desktop");
test("quick create several records in a row", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1, {
message: "first column should contain one record",
});
// click to add an element, fill the input and press ENTER
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1, { message: "the quick create should be open" });
await editKanbanRecordQuickCreateInput("display_name", "new partner 1");
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2, {
message: "first column should now contain two records",
});
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "the quick create should still be open",
});
// create a second element in a row
await createKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "new partner 2");
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(3, {
message: "first column should now contain three records",
});
expect(".o_kanban_quick_create").toHaveCount(1, {
message: "the quick create should still be open",
});
});
test("quick create is disabled until record is created and read", async () => {
const def = new Deferred();
onRpc("web_read", () => def);
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1, {
message: "first column should contain one record",
});
// click to add a record, and add two in a row (first one will be delayed)
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1, { message: "the quick create should be open" });
await editKanbanRecordQuickCreateInput("display_name", "new partner 1");
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1, {
message: "first column should still contain one record",
});
expect(".o_kanban_quick_create.o_disabled").toHaveCount(1, {
message: "quick create should be disabled",
});
def.resolve();
await animationFrame();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2, {
message: "first column should now contain two records",
});
expect(".o_kanban_quick_create.o_disabled").toHaveCount(0, {
message: "quick create should be enabled",
});
});
test.tags("desktop");
test("quick create record fail in grouped by many2one", async () => {
Partner._views["form,false"] = `
`;
onRpc("name_create", () => {
throw makeServerError({ message: "This is a user error" });
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(2);
await createKanbanRecord();
expect(".o_kanban_group:first .o_kanban_quick_create").toHaveCount(1);
await editKanbanRecordQuickCreateInput("display_name", "test");
await validateKanbanRecord();
expect(".modal .o_form_view .o_form_editable").toHaveCount(1);
expect(".modal .o_field_many2one input:first").toHaveValue("hello");
// specify a name and save
await contains(".modal .o_field_widget[name=foo] input").edit("test");
await contains(".modal .o_form_button_save").click();
expect(".modal").toHaveCount(0);
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(3);
expect(".o_kanban_group .o_kanban_record:first").toHaveText("test");
expect(".o_kanban_quick_create:not(.o_disabled)").toHaveCount(1);
});
test("quick create record and click Edit, name_create fails", async () => {
Partner._views["kanban,false"] = `
`;
Partner._views["search,false"] = "";
Partner._views["list,false"] = '';
Partner._views["form,false"] = `
`;
onRpc("name_create", () => {
throw makeServerError({ message: "This is a user error" });
});
await mountWithCleanup(WebClient);
await getService("action").doAction({
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "kanban"],
[false, "form"],
],
context: {
group_by: ["product_id"],
},
});
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(2);
await quickCreateKanbanRecord(0);
expect(".o_kanban_group:first .o_kanban_quick_create").toHaveCount(1);
await editKanbanRecordQuickCreateInput("display_name", "test");
await editKanbanRecord();
expect(".modal .o_form_view .o_form_editable").toHaveCount(1);
expect(".modal .o_field_many2one input:first").toHaveValue("hello");
// specify a name and save
await contains(".modal .o_field_widget[name=foo] input").edit("test");
await contains(".modal .o_form_button_save").click();
expect(".modal").toHaveCount(0);
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(3);
expect(".o_kanban_group .o_kanban_record:first").toHaveText("test");
expect(".o_kanban_quick_create:not(.o_disabled)").toHaveCount(1);
});
test.tags("desktop");
test("quick create record is re-enabled after discard on failure", async () => {
Partner._views["form,false"] = `
`;
onRpc("name_create", () => {
throw makeServerError({ message: "This is a user error" });
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(2);
await createKanbanRecord();
expect(".o_kanban_group:first .o_kanban_quick_create").toHaveCount(1);
await editKanbanRecordQuickCreateInput("display_name", "test");
await validateKanbanRecord();
expect(".modal .o_form_view .o_form_editable").toHaveCount(1);
await contains(".modal .o_form_button_cancel").click();
expect(".modal .o_form_view .o_form_editable").toHaveCount(0);
expect(".o_kanban_group:first .o_kanban_quick_create").toHaveCount(1);
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(2);
});
test("quick create record fails in grouped by char", async () => {
expect.assertions(7);
Partner._views["form,false"] = '';
onRpc("name_create", () => {
throw makeServerError({ message: "This is a user error" });
});
onRpc("web_save", ({ args, kwargs }) => {
expect(args[1]).toEqual({ foo: "blip" });
expect(kwargs.context).toEqual({
allowed_company_ids: [1],
default_foo: "blip",
default_name: "test",
lang: "en",
tz: "taht",
uid: 7,
});
});
await mountView({
type: "kanban",
resModel: "partner",
groupBy: ["foo"],
arch: `
`,
});
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(2);
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "test");
await validateKanbanRecord();
expect(".modal .o_form_view .o_form_editable").toHaveCount(1);
expect(".modal .o_field_widget[name=foo] input").toHaveValue("blip");
await contains(".modal .o_form_button_save").click();
expect(".modal .o_form_view .o_form_editable").toHaveCount(0);
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(3);
});
test("quick create record fails in grouped by selection", async () => {
expect.assertions(7);
Partner._views["form,false"] = '';
onRpc("name_create", () => {
throw makeServerError({ message: "This is a user error" });
});
onRpc("web_save", ({ args, kwargs }) => {
expect(args[1]).toEqual({ state: "abc" });
expect(kwargs.context).toEqual({
allowed_company_ids: [1],
default_state: "abc",
default_name: "test",
lang: "en",
tz: "taht",
uid: 7,
});
});
await mountView({
type: "kanban",
resModel: "partner",
groupBy: ["state"],
arch: `
`,
});
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(1);
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "test");
await validateKanbanRecord();
expect(".modal .o_form_view .o_form_editable").toHaveCount(1);
expect(".modal .o_field_widget[name=state] select:first").toHaveValue('"abc"');
await contains(".modal .o_form_button_save").click();
expect(".modal .o_form_view .o_form_editable").toHaveCount(0);
expect(".o_kanban_group:first .o_kanban_record").toHaveCount(2);
});
test.tags("desktop");
test("quick create record in empty grouped kanban", async () => {
onRpc("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,
product_id: [3, "xplone"],
},
{
__domain: [["product_id", "=", 5]],
product_id_count: 0,
product_id: [5, "xplan"],
},
],
length: 2,
};
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group").toHaveCount(2, { message: "there should be 2 columns" });
expect(".o_kanban_record").toHaveCount(0, { message: "both columns should be empty" });
await createKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_quick_create").toHaveCount(1, {
message: "should have opened the quick create in the first column",
});
});
test.tags("desktop");
test("quick create record in grouped on date(time) field", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
searchViewArch: `
`,
groupBy: ["date"],
createRecord: () => {
expect.step("createKanbanRecord");
},
});
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(0, {
message: "quick create should be disabled when grouped on a date field",
});
// clicking on CREATE in control panel should not open a quick create
await createKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(0, {
message: "should not have opened the quick create widget",
});
await toggleSearchBarMenu();
await toggleMenuItem("GroupBy Datetime");
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(0, {
message: "quick create should be disabled when grouped on a datetime field",
});
// clicking on CREATE in control panel should not open a quick create
await createKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(0, {
message: "should not have opened the quick create widget",
});
expect.verifySteps(["createKanbanRecord", "createKanbanRecord"]);
});
test("quick create record feature is properly enabled/disabled at reload", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
searchViewArch: `
`,
groupBy: ["foo"],
});
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(3, {
message: "quick create should be enabled when grouped on a char field",
});
await toggleSearchBarMenu();
await toggleMenuItem("GroupBy Date");
await toggleMenuItemOption("GroupBy Date", "Month");
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(0, {
message: "quick create should now be disabled (grouped on date field)",
});
await toggleMenuItemOption("GroupBy Date", "Month");
await toggleMenuItem("GroupBy Bar");
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(2, {
message: "quick create should be enabled again (grouped on boolean field)",
});
});
test("quick create record in grouped by char field", async () => {
expect.assertions(4);
onRpc("name_create", ({ kwargs }) => {
expect(kwargs.context.default_foo).toBe("blip");
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["foo"],
});
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(3);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "new record");
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(3);
});
test("quick create record in grouped by boolean field", async () => {
expect.assertions(4);
onRpc("name_create", ({ kwargs }) => {
expect(kwargs.context.default_bar).toBe(true);
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(2);
expect(".o_kanban_group:last-child .o_kanban_record").toHaveCount(3);
await quickCreateKanbanRecord(1);
await editKanbanRecordQuickCreateInput("display_name", "new record");
await validateKanbanRecord();
expect(".o_kanban_group:last-child .o_kanban_record").toHaveCount(4);
});
test("quick create record in grouped on selection field", async () => {
expect.assertions(4);
onRpc("name_create", ({ kwargs }) => {
expect(kwargs.context.default_state).toBe("abc");
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["state"],
});
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(3, {
message: "quick create should be enabled when grouped on a selection field",
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1, {
message: "first column (abc) should contain 1 record",
});
await quickCreateKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "new record");
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2, {
message: "first column (abc) should contain 2 records",
});
});
test("quick create record in grouped by char field (within quick_create_view)", async () => {
expect.assertions(6);
Partner._views["form,some_view_ref"] = ``;
onRpc("web_save", ({ args, kwargs }) => {
expect(args[1]).toEqual({ foo: "blip" });
expect(kwargs.context.default_foo).toBe("blip");
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["foo"],
});
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(3);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create input:first").toHaveValue("blip", {
message: "should have set the correct foo value by default",
});
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(3);
});
test("quick create record in grouped by boolean field (within quick_create_view)", async () => {
expect.assertions(6);
Partner._views["form,some_view_ref"] = ``;
onRpc("web_save", ({ args, kwargs }) => {
expect(args[1]).toEqual({ bar: true });
expect(kwargs.context.default_bar).toBe(true);
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(2, {
message: "quick create should be enabled when grouped on a boolean field",
});
expect(".o_kanban_group:last-child .o_kanban_record").toHaveCount(3);
await quickCreateKanbanRecord(1);
expect(".o_kanban_quick_create .o_field_boolean input").toBeChecked();
await contains(".o_kanban_quick_create .o_kanban_add").click();
await animationFrame();
expect(".o_kanban_group:last-child .o_kanban_record").toHaveCount(4);
});
test("quick create record in grouped by selection field (within quick_create_view)", async () => {
expect.assertions(6);
Partner._views["form,some_view_ref"] = ``;
onRpc("web_save", ({ args, kwargs }) => {
expect(args[1]).toEqual({ state: "abc" });
expect(kwargs.context.default_state).toBe("abc");
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["state"],
});
expect(".o_kanban_header .o_kanban_quick_add i").toHaveCount(3, {
message: "quick create should be enabled when grouped on a selection field",
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1, {
message: "first column (abc) should contain 1 record",
});
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create select:first").toHaveValue('"abc"', {
message: "should have set the correct state value by default",
});
await contains(".o_kanban_quick_create .o_kanban_add").click();
await animationFrame();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2, {
message: "first column (abc) should now contain 2 records",
});
});
test.tags("desktop");
test("quick create record while adding a new column", async () => {
const def = new Deferred();
onRpc("product", "name_create", () => def);
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group").toHaveCount(2);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
// add a new column
expect(".o_column_quick_create .o_quick_create_folded").toHaveCount(1);
await quickCreateKanbanColumn();
expect(".o_column_quick_create .o_quick_create_unfolded").toHaveCount(1);
await editKanbanColumnName("new column");
await validateKanbanColumn();
await animationFrame();
expect(".o_column_quick_create input:first").toHaveValue("");
expect(".o_kanban_group").toHaveCount(2);
// click to add a new record
await createKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1);
// unlock column creation
def.resolve();
await animationFrame();
expect(".o_kanban_group").toHaveCount(3);
expect(".o_kanban_quick_create").toHaveCount(1);
// quick create record in first column
await editKanbanRecordQuickCreateInput("display_name", "new record");
await validateKanbanRecord();
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(3);
});
test.tags("desktop");
test("close a column while quick creating a record", async () => {
Partner._views["form,some_view_ref"] = '';
let def;
onRpc("get_views", () => {
if (def) {
expect.step("get_views");
return def;
}
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
def = new Deferred();
expect.verifySteps([]);
expect(".o_kanban_group").toHaveCount(2);
expect(".o_column_folded").toHaveCount(0);
// click to quick create a new record in the first column (this operation is delayed)
await quickCreateKanbanRecord();
expect.verifySteps(["get_views"]);
expect(".o_form_view").toHaveCount(0);
// click to fold the first column
const clickColumnAction = await toggleKanbanColumnActions(0);
await clickColumnAction("Fold");
expect(".o_column_folded").toHaveCount(1);
def.resolve();
await animationFrame();
expect.verifySteps([]);
expect(".o_form_view").toHaveCount(0);
expect(".o_column_folded").toHaveCount(1);
await createKanbanRecord();
expect.verifySteps([]); // "get_views" should have already be done
expect(".o_form_view").toHaveCount(1);
expect(".o_column_folded").toHaveCount(0);
});
test("quick create record: open on a column while another column has already one", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
// Click on quick create in first column
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1);
expect(queryAll(".o_kanban_quick_create", { root: getKanbanColumn(0) })).toHaveCount(1);
// Click on quick create in second column
await quickCreateKanbanRecord(1);
expect(".o_kanban_quick_create").toHaveCount(1);
expect(queryAll(".o_kanban_quick_create", { root: getKanbanColumn(2) })).toHaveCount(1);
// Click on quick create in first column once again
await quickCreateKanbanRecord();
expect(".o_kanban_quick_create").toHaveCount(1);
expect(queryAll(".o_kanban_quick_create", { root: getKanbanColumn(0) })).toHaveCount(1);
});
test("many2many_tags in kanban views", async () => {
Partner._records[0].category_ids = [6, 7];
Partner._records[1].category_ids = [7, 8];
Category._records.push({
id: 8,
name: "hello",
color: 0,
});
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
selectRecord: (resId) => {
expect(resId).toBe(1, {
message: "should trigger an event to open the clicked record in a form view",
});
},
});
expect(
queryAll(".o_field_many2many_tags .o_tag", { root: getKanbanRecord({ index: 0 }) })
).toHaveCount(2, {
message: "first record should contain 2 tags",
});
expect(queryAll(".o_tag.o_tag_color_2", { root: getKanbanRecord({ index: 0 }) })).toHaveCount(
1,
{
message: "first tag should have color 2",
}
);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_search_read",
]);
// Checks that second records has only one tag as one should be hidden (color 0)
expect(".o_kanban_record:nth-child(2) .o_tag").toHaveCount(1, {
message: "there should be only one tag in second record",
});
expect(".o_kanban_record:nth-child(2) .o_tag:first").toHaveText("silver");
// Write on the record using the priority widget to trigger a re-render in readonly
await contains(".o_kanban_record:first-child .o_priority_star:first-child").click();
expect.verifySteps(["web_save"]);
expect(".o_kanban_record:first-child .o_field_many2many_tags .o_tag").toHaveCount(2, {
message: "first record should still contain only 2 tags",
});
const tags = queryAll(".o_kanban_record:first-child .o_tag");
expect(tags[0]).toHaveText("gold");
expect(tags[1]).toHaveText("silver");
// click on a tag (should trigger switch_view)
await contains(".o_kanban_record:first-child .o_tag:first-child").click();
});
test("priority field should not be editable when missing access rights", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
// Try to fill one star in the priority field of the first record
await contains(".o_kanban_record:first-child .o_priority_star:first-child").click();
expect(".o_kanban_record:first-child .o_priority .fa-star-o").toHaveCount(2, {
message: "first record should still contain 2 empty stars",
});
});
test("Do not open record when clicking on `a` with `href`", async () => {
expect.assertions(6);
Partner._records = [{ id: 1, foo: "yop" }];
mockService("action", {
async switchView() {
// when clicking on a record in kanban view,
// it switches to form view.
expect.step("switchView");
},
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
test link`,
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
expect(".o_kanban_record a").toHaveCount(1);
expect(".o_kanban_record a").toHaveAttribute("href", null, {
message: "link inside kanban record should have non-empty href",
});
// Prevent the browser default behaviour when clicking on anything.
// This includes clicking on a `` with `href`, so that it does not
// change the URL in the address bar.
// Note that we should not specify a click listener on 'a', otherwise
// it may influence the kanban record global click handler to not open
// the record.
const testLink = queryFirst(".o_kanban_record a");
testLink.addEventListener("click", (ev) => {
expect(ev.defaultPrevented).toBe(false, {
message: "should not prevented browser default behaviour beforehand",
});
expect(ev.target).toBe(testLink, {
message: "should have clicked on the test link in the kanban record",
});
ev.preventDefault();
});
await click(".o_kanban_record a");
expect.verifySteps([]);
});
test("Open record when clicking on widget field", async function (assert) {
expect.assertions(2);
Product._views["form,false"] = ``;
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
selectRecord: (resId) => {
expect(resId).toBe(1, { message: "should trigger an event to open the form view" });
},
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(4);
await click(".o_field_monetary[name=salary]");
});
test("o2m loaded in only one batch", async () => {
class Subtask extends models.Model {
_name = "subtask";
name = fields.Char();
_records = [
{ id: 1, name: "subtask #1" },
{ id: 2, name: "subtask #2" },
];
}
defineModels([Subtask]);
Partner._fields.subtask_ids = fields.One2many({ relation: "subtask" });
Partner._records[0].subtask_ids = [1];
Partner._records[1].subtask_ids = [2];
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
await validateSearch();
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_read_group",
"web_search_read",
"web_search_read",
"web_read_group",
"web_search_read",
"web_search_read",
]);
});
test.tags("desktop");
test("kanban with many2many, load and reload", async () => {
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
await press("Enter"); // reload
await animationFrame();
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_read_group",
"web_search_read",
"web_search_read",
"web_read_group",
"web_search_read",
"web_search_read",
]);
});
test.tags("desktop");
test("kanban with reference field", async () => {
Partner._fields.ref_product = fields.Reference({ selection: [["product", "Product"]] });
Partner._records[0].ref_product = "product,3";
Partner._records[1].ref_product = "product,5";
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
groupBy: ["product_id"],
arch: `
`,
});
await press("Enter"); // reload
await animationFrame();
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_read_group",
"web_search_read",
"web_search_read",
"web_read_group",
"web_search_read",
"web_search_read",
]);
expect(queryAllTexts(".o_kanban_record span")).toEqual(["hello", "", "xmo", ""]);
});
test.tags("desktop");
test("drag and drop a record with load more", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(queryAllTexts(".o_kanban_group:eq(0) .o_kanban_record")).toEqual(["4"]);
expect(queryAllTexts(".o_kanban_group:eq(1) .o_kanban_record")).toEqual(["1"]);
await contains(".o_kanban_group:eq(1) .o_kanban_record").dragAndDrop(".o_kanban_group:eq(0)");
expect(queryAllTexts(".o_kanban_group:eq(0) .o_kanban_record")).toEqual(["4", "1"]);
expect(queryAllTexts(".o_kanban_group:eq(1) .o_kanban_record")).toEqual(["2"]);
});
test.tags("desktop");
test("can drag and drop a record from one column to the next", async () => {
onRpc("/web/dataset/resequence", () => {
expect.step("resequence");
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
edit`,
groupBy: ["product_id"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(2);
expect(".thisiseditable").toHaveCount(4);
expect.verifySteps([]);
// first record of first column moved to the bottom of second column
await contains(".o_kanban_group:first-child .o_kanban_record").dragAndDrop(
".o_kanban_group:nth-child(2)"
);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(3);
expect(".thisiseditable").toHaveCount(4);
expect.verifySteps(["resequence"]);
});
test.tags("desktop");
test("user without permission cannot drag and drop a column thus sequence remains unchanged on drag and drop attempt", async () => {
expect.errors(1);
onRpc("/web/dataset/resequence", () => {
throw makeServerError({ message: "No Permission" }); // Simulate user without permission
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(queryAllTexts(".o_column_title")).toEqual(["hello\n(2)", "xmo\n(2)"]);
const groups = queryAll(".o_column_title");
await contains(groups[0]).dragAndDrop(groups[1]);
expect(queryAllTexts(".o_column_title")).toEqual(["hello\n(2)", "xmo\n(2)"]);
expect.verifyErrors(["No Permission"]);
});
test.tags("desktop");
test("user without permission cannot drag and drop a record thus sequence remains unchanged on drag and drop attempt", async () => {
expect.errors(1);
onRpc("partner", "web_save", () => {
throw makeServerError({ message: "No Permission" }); // Simulate user without permission
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_record:first").toHaveText("yop", {
message: "Checking the initial state of the view",
});
await contains(".o_kanban_record").dragAndDrop(".o_kanban_group:nth-child(2)");
expect(".o_kanban_record:first").toHaveText("yop", {
message: "Do not let the user d&d the record without permission",
});
await contains(".o_kanban_record").dragAndDrop(".o_kanban_record:nth-child(3)");
expect(".o_kanban_record:first").toHaveText("gnap", {
message: "Check that the record does not become static after d&d",
});
expect.verifyErrors(["No Permission"]);
});
test.tags("desktop");
test("drag and drop highlight on hover", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(2);
// first record of first column moved to the bottom of second column
const { drop, moveTo } = await contains(".o_kanban_group:first-child .o_kanban_record").drag();
await moveTo(".o_kanban_group:nth-child(2)");
expect(getKanbanColumn(1)).toHaveClass("o_kanban_hover");
await drop();
expect(".o_kanban_group:nth-child(2).o_kanban_hover").toHaveCount(0);
});
test("drag and drop outside of a column", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(2);
// first record of first column moved to the right of a column
await contains(".o_kanban_group:first-child .o_kanban_record").dragAndDrop(
".o_column_quick_create"
);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
});
test.tags("desktop");
test("drag and drop a record, grouped by selection", async () => {
expect.assertions(6);
onRpc("/web/dataset/resequence", () => {
expect.step("resequence");
return true;
});
onRpc("partner", "web_save", ({ args }) => {
expect(args[1]).toEqual({ state: "abc" });
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["state"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(1);
// first record of second column moved to the bottom of first column
await contains(".o_kanban_group:nth-child(2) .o_kanban_record").dragAndDrop(
".o_kanban_group:first-child"
);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(0);
expect.verifySteps(["resequence"]);
});
test.tags("desktop");
test("prevent drag and drop of record if grouped by readonly", async () => {
// Whether the kanban is grouped by state, foo, bar or product_id
// the user must not be able to drag and drop from one group to another,
// as state, foo bar, product_id are made readonly one way or another.
// state must not be draggable:
// state is not readonly in the model. state is passed in the arch specifying readonly="1".
// foo must not be draggable:
// foo is readonly in the model fields. foo is passed in the arch but without specifying readonly.
// bar must not be draggable:
// bar is readonly in the model fields. bar is not passed in the arch.
// product_id must not be draggable:
// product_id is readonly in the model fields. product_id is passed in the arch specifying readonly="0",
// but the readonly in the model takes over.
Partner._fields.foo = fields.Char({ readonly: true });
Partner._fields.bar = fields.Boolean({ readonly: true });
Partner._fields.product_id = fields.Many2one({ relation: "product", readonly: true });
onRpc("/web/dataset/resequence", () => true);
onRpc("partner", "write", () => {
expect.step("should not be called");
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_content").toHaveClass("o_view_sample_data");
expect(".o_view_nocontent").toHaveCount(1);
expect(".o_kanban_group:first .o_column_title").toHaveText("hello");
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(16);
expect(".o_kanban_record").toHaveText("hello");
});
test.tags("desktop");
test("bounce create button when no data and click on empty area", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
searchViewArch: `
`,
noContentHelp: "click to add a partner",
});
await contains(".o_kanban_view").click();
expect(".o-kanban-button-new").not.toHaveClass("o_catch_attention");
await toggleSearchBarMenu();
await toggleMenuItem("Match nothing");
await contains(".o_kanban_renderer").click();
expect(".o-kanban-button-new").toHaveClass("o_catch_attention");
});
test("buttons with modifiers", async () => {
Partner._records[1].bar = false; // so that test is more complete
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
});
expect(".o_btn_test_1").toHaveCount(1, { message: "kanban should have one buttons of type 1" });
expect(".o_btn_test_2").toHaveCount(3, {
message: "kanban should have three buttons of type 2",
});
});
test("support styling of anchor tags with action type", async function (assert) {
expect.assertions(3);
mockService("action", {
doActionButton(action) {
expect(action.name).toBe("42");
},
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["bar"],
});
expect(".o_kanban_group").toHaveCount(2);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(2);
expect(getKanbanProgressBars(0).map((pb) => pb.style.width)).toEqual(["50%", "50%"]);
expect(getKanbanProgressBars(1).map((pb) => pb.style.width)).toEqual(["50%", "50%"]);
expect(getKanbanCounters()).toEqual(["6", "26"]);
const clickColumnAction = await toggleKanbanColumnActions(1);
clickColumnAction("Fold");
await animationFrame();
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(2);
expect(getKanbanColumn(1)).toHaveClass("o_column_folded");
expect(queryText(getKanbanColumn(1))).toBe("Yes\n(2)");
await contains(".o_kanban_group:first-child .o_kanban_record").dragAndDrop(
".o_kanban_group:nth-child(2)"
);
expect(queryAll(".o_kanban_record", { root: getKanbanColumn(0) })).toHaveCount(1);
expect(queryText(getKanbanColumn(1))).toBe("Yes\n(3)");
expect(getKanbanProgressBars(0).map((pb) => pb.style.width)).toEqual(["100%"]);
expect(getKanbanCounters()).toEqual(["-4"]);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"read_progress_bar",
"web_read_group",
"web_search_read",
"web_search_read",
"web_save",
"read_progress_bar",
"web_read_group",
]);
});
test.tags("desktop");
test("quick create record in grouped kanban in a form view dialog", async () => {
Partner._fields.foo = fields.Char({ default: "ABC" });
Partner._views["form,false"] = ``;
onRpc("name_create", ({ method }) => {
throw makeServerError();
});
stepAllNetworkCalls();
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2, {
message: "first column should contain two records",
});
expect(queryAllTexts(".o_kanban_group:first-child .o_kanban_record")).toEqual(["yop", "gnap"]);
expect(".modal").toHaveCount(0);
// click on 'Create', fill the quick create and validate
await createKanbanRecord();
await editKanbanRecordQuickCreateInput("display_name", "new partner");
await validateKanbanRecord();
expect(".modal").toHaveCount(1);
await clickModalButton({ text: "Save & Close" });
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(3, {
message: "first column should contain three records",
});
expect(queryAllTexts(".o_kanban_group:first-child .o_kanban_record")).toEqual([
"ABC",
"yop",
"gnap",
]);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_read_group", // initial read_group
"web_search_read", // initial search_read (first column)
"web_search_read", // initial search_read (second column)
"onchange", // quick create
"name_create", // should perform a name_create to create the record
"get_views", // load views for form view dialog
"onchange", // load of a virtual record in form view dialog
"web_save", // save virtual record
"web_read", // read the created record to get foo value
"onchange", // reopen the quick create automatically
]);
});
test.tags("desktop");
test("no sample data when all groups are folded then one is unfolded", async () => {
onRpc("web_read_group", function ({ parent }) {
const result = parent();
for (const group of result.groups) {
group.__fold = true;
}
return result;
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_column_folded").toHaveCount(2);
await contains(".o_kanban_group").click();
expect(".o_column_folded").toHaveCount(1);
expect(".o_kanban_record").toHaveCount(2);
expect("o_view_sample_data").toHaveCount(0);
});
test.tags("desktop");
test("no content helper, all groups folded with (unloaded) records", async () => {
onRpc("web_read_group", function ({ parent }) {
const result = parent();
for (const group of result.groups) {
group.__fold = true;
}
return result;
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_column_folded").toHaveCount(2);
expect(queryAllTexts(".o_column_title")).toEqual(["hello\n(2)", "xmo\n(2)"]);
expect(".o_nocontent_help").toHaveCount(0);
});
test.tags("desktop");
test("Move multiple records in different columns simultaneously", async () => {
const def = new Deferred();
onRpc("read", () => def);
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["state"],
});
expect(getKanbanRecordTexts()).toEqual(["1", "2", "3", "4"]);
// Move 3 at end of 1st column
await contains(".o_kanban_group:last-of-type .o_kanban_record").dragAndDrop(
".o_kanban_group:first"
);
expect(getKanbanRecordTexts()).toEqual(["1", "3", "2", "4"]);
// Move 4 at end of 1st column
await contains(".o_kanban_group:last-of-type .o_kanban_record").dragAndDrop(
".o_kanban_group:first"
);
expect(getKanbanRecordTexts()).toEqual(["1", "3", "4", "2"]);
def.resolve();
await animationFrame();
expect(getKanbanRecordTexts()).toEqual(["1", "3", "4", "2"]);
});
test.tags("desktop");
test("drag & drop: content scrolls when reaching the edges", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["state"],
});
const width = 600;
const content = queryOne(".o_content");
content.setAttribute("style", `max-width:${width}px;overflow:auto;`);
expect(content.scrollLeft).toBe(0);
expect(content.getBoundingClientRect().width).toBe(600);
expect(".o_kanban_record.o_dragged").toHaveCount(0);
// Drag first record of first group to the right
let dragActions = await contains(".o_kanban_record").drag();
await dragActions.moveTo(".o_kanban_group:nth-child(3) .o_kanban_record:first");
expect(".o_kanban_record.o_dragged").toHaveCount(1);
// wait 30 frames, should be enough (default kanban speed is 20px per tick)
await advanceFrame(30);
// Should be at the end of the content
expect(content.scrollLeft + width).toBe(content.scrollWidth);
// Cancel drag: press "Escape"
await press("Escape");
await animationFrame();
expect(".o_kanban_record.o_dragged").toHaveCount(0);
// Drag first record of last group to the left
dragActions = await contains(".o_kanban_group:nth-child(3) .o_kanban_record").drag();
await dragActions.moveTo(".o_kanban_record:first");
expect(".o_kanban_record.o_dragged").toHaveCount(1);
await advanceFrame(30);
expect(content.scrollLeft).toBe(0);
// Cancel drag: click outside
await contains(".o_kanban_renderer").focus();
expect(".o_kanban_record.o_dragged").toHaveCount(0);
});
test("attribute default_order", async () => {
class CustomModel extends models.Model {
_name = "custom.model";
int = fields.Integer();
_records = [
{ id: 1, int: 1 },
{ id: 2, int: 3 },
{ id: 3, int: 2 },
];
}
defineModels([CustomModel]);
await mountView({
type: "kanban",
resModel: "custom.model",
arch: `
`,
groupBy: ["bar"],
limit: 1,
});
queryOne(".o_kanban_renderer").style.overflow = "scroll";
queryOne(".o_kanban_renderer").style.height = "500px";
const clickKanbanLoadMoreButton = queryFirst(".o_kanban_load_more button");
clickKanbanLoadMoreButton.scrollIntoView();
const previousScrollTop = queryOne(".o_kanban_renderer").scrollTop;
await contains(clickKanbanLoadMoreButton).click();
expect(previousScrollTop).not.toBe(0, { message: "Should not have the scrollTop value at 0" });
expect(queryOne(".o_kanban_renderer").scrollTop).toBe(previousScrollTop);
});
test("Kanban: no reset of the groupby when a non-empty column is deleted", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
searchViewArch: `
`,
});
// validate presence of the search arch info
await toggleSearchBarMenu();
expect(".o_group_by_menu span.o_menu_item").toHaveCount(1);
// select the groupby:category_ids filter
await contains(".o_group_by_menu span.o_menu_item").click();
// check the initial rendering
expect(".o_kanban_group").toHaveCount(3, { message: "should have three columns" });
// check availability of delete action in kanban header's config dropdown
await toggleKanbanColumnActions(2);
expect(queryAll(".o_column_delete", { root: getKanbanColumnDropdownMenu(2) })).toHaveCount(1, {
message: "should be able to delete the column",
});
// delete second column (first cancel the confirm request, then confirm)
let clickColumnAction = await toggleKanbanColumnActions(1);
await clickColumnAction("Delete");
await contains(".o_dialog footer .btn-secondary").click();
expect(queryText(".o_column_title", { root: getKanbanColumn(1) })).toBe("gold\n(1)");
clickColumnAction = await toggleKanbanColumnActions(1);
await clickColumnAction("Delete");
await contains(".o_dialog footer .btn-primary").click();
expect(".o_kanban_group").toHaveCount(2, { message: "should now have two columns" });
expect(queryText(".o_column_title", { root: getKanbanColumn(1) })).toBe("silver\n(1)");
expect(queryText(".o_column_title", { root: getKanbanColumn(0) })).toBe("None\n(3)");
});
test.tags("desktop");
test("searchbar filters are displayed directly", async () => {
let def;
onRpc("web_search_read", () => def);
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
searchViewArch: `
`,
});
expect(getFacetTexts()).toEqual([]);
// toggle a filter, and slow down the web_search_read rpc
def = new Deferred();
await toggleSearchBarMenu();
await toggleMenuItem("Some Filter");
expect(getFacetTexts()).toEqual(["Some Filter"]);
def.resolve();
await animationFrame();
expect(getFacetTexts()).toEqual(["Some Filter"]);
});
test("searchbar filters are displayed directly (with progressbar)", async () => {
let def;
onRpc("read_progress_bar", () => def);
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["int_field"],
searchViewArch: `
`,
});
expect(getFacetTexts()).toEqual([]);
// toggle a filter, and slow down the read_progress_bar rpc
def = new Deferred();
await toggleSearchBarMenu();
await toggleMenuItem("Some Filter");
expect(getFacetTexts()).toEqual(["Some Filter"]);
def.resolve();
await animationFrame();
expect(getFacetTexts()).toEqual(["Some Filter"]);
});
test.tags("desktop");
test("group by properties and drag and drop", async () => {
expect.assertions(7);
Partner._fields.properties = fields.Properties({
definition_record: "parent_id",
definition_record_field: "properties_definition",
});
Partner._fields.parent_id = fields.Many2one({ relation: "partner" });
Partner._fields.properties_definition = fields.PropertiesDefinition();
Partner._records[0].properties_definition = [
{
name: "my_char",
string: "My Char",
type: "char",
},
];
Partner._records[1].parent_id = 1;
Partner._records[1].properties = [
{
name: "my_char",
string: "My Char",
type: "char",
value: "aaa",
},
];
Partner._records[2].parent_id = 1;
Partner._records[2].properties = [
{
name: "my_char",
string: "My Char",
type: "char",
value: "bbb",
},
];
Partner._records[3].parent_id = 2;
onRpc("web_read_group", () => {
return {
groups: [
{
"properties.my_char": false,
__domain: [["properties.my_char", "=", false]],
"properties.my_char_count": 2,
},
{
"properties.my_char": "aaa",
__domain: [["properties.my_char", "=", "aaa"]],
"properties.my_char_count": 1,
},
{
"properties.my_char": "bbb",
__domain: [["properties.my_char", "=", "bbb"]],
"properties.my_char_count": 1,
},
],
length: 3,
};
});
onRpc("web_search_read", ({ kwargs }) => {
const value = kwargs.domain[0][2];
return {
length: 1,
records: [
{
id: value === "aaa" ? 2 : 3,
properties: [
{
name: "my_char",
string: "My Char",
type: "char",
value: value,
},
],
},
],
};
});
onRpc("/web/dataset/resequence", () => {
expect.step("resequence");
return true;
});
onRpc("web_save", ({ args }) => {
expect.step("web_save");
const expected = {
properties: [
{
name: "my_char",
string: "My Char",
type: "char",
value: "bbb",
},
],
};
expect(args[1]).toEqual(expected);
});
onRpc("get_property_definition", ({ args }) => {
expect.step("get_property_definition");
return {
name: "my_char",
type: "char",
};
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["properties.my_char"],
});
expect.verifySteps(["get_property_definition"]);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(1);
expect(".o_kanban_group:nth-child(3) .o_kanban_record").toHaveCount(1);
await contains(".o_kanban_group:nth-child(2) .o_kanban_record").dragAndDrop(
".o_kanban_group:nth-child(3)"
);
expect.verifySteps(["web_save", "resequence"]);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(0);
expect(".o_kanban_group:nth-child(3) .o_kanban_record").toHaveCount(2);
});
test("kanbans with basic and custom compiler, same arch", async () => {
// 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,
});
after(() => viewRegistry.remove("my_kanban"));
Partner._fields.one2many = fields.One2many({ relation: "partner" });
Partner._records[0].one2many = [1];
Partner._views["form,false"] = ``;
Partner._views["search,false"] = ``;
Partner._views["kanban,false"] = `
`;
await mountWithCleanup(WebClient);
await getService("action").doAction({
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "kanban"],
[false, "form"],
],
});
// main kanban, custom view
expect(".o_kanban_view").toHaveCount(1);
expect(".o_my_kanban_view").toHaveCount(1);
expect(".my_kanban_compiler").toHaveCount(4);
// switch to form
await contains(".o_kanban_record").click();
await animationFrame();
expect(".o_form_view").toHaveCount(1);
expect(".o_form_view .o_field_widget[name=one2many]").toHaveCount(1);
// x2many kanban, basic renderer
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
expect(".my_kanban_compiler").toHaveCount(0);
});
test("grouped on field with readonly expression depending on context", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
context: { abc: true },
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(2);
await contains(".o_kanban_group:first-child .o_kanban_record").dragAndDrop(
".o_kanban_group:nth-child(2)"
);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(2);
});
test.tags("desktop");
test("grouped on field with readonly expression depending on fields", async () => {
// Fields are not available in the current context as the drag and drop must be enabled globally
// for the view, it's not a per record thing.
// So if the readonly expression contains fields, it will resolve to readonly === false and
// the drag and drop will be enabled.
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(2);
await contains(".o_kanban_group:first-child .o_kanban_record").dragAndDrop(
".o_kanban_group:nth-child(2)"
);
expect(".o_kanban_group:first-child .o_kanban_record").toHaveCount(1);
expect(".o_kanban_group:nth-child(2) .o_kanban_record").toHaveCount(3);
});
test.tags("desktop");
test("quick create a column by pressing enter when input is focused", async () => {
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group").toHaveCount(2);
await quickCreateKanbanColumn();
// 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.
await press("N");
await press("e");
await press("w");
await press("Enter");
await animationFrame();
expect(".o_kanban_group").toHaveCount(3);
});
test("Correct values for progress bar with toggling filter and slow RPC", async () => {
let def;
onRpc("read_progress_bar", () => def);
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
searchViewArch: `
`,
});
expect(".o_kanban_record").toHaveCount(4);
// abc: 1, ghi: 1
expect(getKanbanProgressBars(1).map((pb) => pb.style.width)).toEqual(["50%", "50%"]);
// toggle a filter, and slow down the read_progress_bar rpc
def = new Deferred();
await toggleSearchBarMenu();
await toggleMenuItem("Some Filter");
// abc: 1, ghi: 1
expect(getKanbanProgressBars(1).map((pb) => pb.style.width)).toEqual(["50%", "50%"]);
def.resolve();
await animationFrame();
// After the call to read_progress_bar has resolved, the values should be updated correctly
expect(".o_kanban_record").toHaveCount(2);
// abc: 1
expect(getKanbanProgressBars(1).map((pb) => pb.style.width)).toEqual(["100%"]);
});
test.tags("desktop");
test("click on empty kanban must shake the NEW button", async () => {
onRpc("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,
product_id: [3, "xplone"],
},
{
__domain: [["product_id", "=", 5]],
product_id_count: 0,
product_id: [5, "xplan"],
},
],
length: 2,
};
});
await mountView({
type: "kanban",
resModel: "partner",
arch: `
`,
groupBy: ["product_id"],
});
expect(".o_kanban_group").toHaveCount(2, { message: "there should be 2 columns" });
expect(".o_kanban_record").toHaveCount(0, { message: "both columns should be empty" });
await click(".o_kanban_renderer");
expect("[data-bounce-button]").toHaveClass("o_catch_attention");
});