`,
resId: 1,
});
await contains(".o_field_x2many_list .o_field_x2many_list_row_add a").click();
expect(".o_dialog").toHaveCount(1);
});
test("do not send context in unity spec if field is invisible", async () => {
expect.assertions(1);
onRpc("web_read", ({ kwargs }) => {
expect(kwargs.specification).toEqual({
display_name: {},
p: {},
});
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
});
test("O2M List with pager, decoration and default_order: add and cancel adding", async () => {
// The decoration on the list implies that its condition will be evaluated
// against the data of the field (actual records *displayed*)
// If one data is wrongly formed, it will crash
// This test adds then cancels a record in a paged, ordered, and decorated list
// That implies prefetching of records for sorting
// and evaluation of the decoration against *visible records*
Partner._records[0].p = [2, 4];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_field_x2many_list .o_field_x2many_list_row_add a").click();
expect(".o_field_x2many_list .o_data_row").toHaveCount(2);
expect(queryOne(".o_selected_row")).toBe(queryOne(".o_field_x2many_list .o_data_row:eq(1)"), {
message: "The selected row should be the new one",
});
// Cancel Creation
await press("escape");
await animationFrame();
expect(".o_field_x2many_list .o_data_row").toHaveCount(1);
});
test.tags("desktop");
test("O2M with parented m2o and domain on parent.m2o", async () => {
expect.assertions(4);
// Records in an o2m can have a m2o pointing to themselves.
// In that case, a domain evaluation on that field followed by name_search
// shouldn't send virtual_ids to the server.
Turtle._fields.parent_id = fields.Many2one({
string: "Parent",
relation: "turtle",
});
Turtle._views = {
form: `
`,
});
await contains(".o_field_x2many_list_row_add a").click();
await clickFieldDropdown("parent_id");
await contains(".o_field_widget[name=parent_id] input").edit("ABC", { confirm: false });
await runAllTimers();
await clickFieldDropdownItem("parent_id", "Create and edit...");
await contains(".o_dialog:not(.o_inactive_modal) .modal-footer .o_form_button_save").click();
await contains(".o_dialog:not(.o_inactive_modal) .o_form_button_save_new").click();
expect(".o_data_row").toHaveCount(1);
await contains(".o_field_many2one input").click();
});
test.tags("desktop");
test('O2M with buttons with attr "special" in dialog close the dialog', async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_dialog").toHaveCount(1);
expect(".modal .btn").toHaveText("Cancel");
await contains(".modal .btn").click();
expect(".o_dialog").toHaveCount(0);
});
test.tags("desktop");
test("O2M modal buttons are disabled on click", async () => {
// Records in an o2m can have a m2o pointing to themselves.
// In that case, a domain evaluation on that field followed by name_search
// shouldn't send virtual_ids to the server
Turtle._fields.parent_id = fields.Many2one({
string: "Parent",
relation: "turtle",
});
Turtle._views = {
form: `
`,
resId: 1,
});
expect(".o_field_many2many_tags").toHaveText("first record");
await contains(".o_data_cell:eq(1)").click();
await contains(".o_selected_row .o_field_widget[name=turtle_foo] input").edit("hop", {
confirm: "blur",
});
expect(".o_field_many2many_tags").toHaveText("first record\nsecond record");
await clickSave();
});
test("onchange for embedded one2many in a one2many with a second page", async () => {
Turtle._fields.partner_ids = fields.One2many({ relation: "partner" });
Turtle._records[0].partner_ids = [1];
// we need a second page, so we set two records and only display one per page
Partner._records[0].turtles = [1, 2];
Partner._onChanges = {
turtles: function (obj) {
obj.turtles = [
[
1,
1,
{
partner_ids: [[4, 2]],
},
],
[
1,
2,
{
turtle_foo: "blip",
partner_ids: [[4, 1]],
},
],
];
},
};
onRpc("web_save", (args) => {
expect(args.args[1].turtles).toEqual([
[1, 1, { turtle_foo: "hop", partner_ids: [[4, 2]] }],
[
1,
2,
{
partner_ids: [[4, 1]],
turtle_foo: "blip",
},
],
]);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_data_cell:eq(1)").click();
await contains(".o_selected_row .o_field_widget[name=turtle_foo] input").edit("hop", {
confirm: "blur",
});
await clickSave();
});
test("onchange for embedded one2many in a one2many updated by server", async () => {
// here we test that after an onchange, the embedded one2many field has
// been updated by a new list of ids by the server response, to this new
// list should be correctly sent back at save time
expect.assertions(3);
Turtle._fields.partner_ids = fields.One2many({ relation: "partner" });
Partner._records[0].turtles = [2];
Turtle._records[1].partner_ids = [2];
Partner._onChanges = {
turtles: function (obj) {
obj.turtles = [
[
1,
2,
{
partner_ids: [[4, 4]],
},
],
];
},
};
onRpc("web_save", (args) => {
expect(args.args[1].turtles).toEqual(
[
[
1,
2,
{
partner_ids: [[4, 4]],
turtle_foo: "hop",
},
],
],
{
message: "The right values should be written",
}
);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["yop", "blip", "kawa"]);
// Drag and drop the second line in first position
await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop("tbody tr");
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blip", "yop", "kawa"]);
expect(turtleOnchange).toBe(2, { message: "should trigger one onchange per line updated" });
expect(partnerOnchange).toBe(1, { message: "should trigger only one onchange on the parent" });
});
test("onchange for embedded one2many with handle widget using same sequence", async () => {
Turtle._records[0].turtle_int = 1;
Turtle._records[1].turtle_int = 1;
Turtle._records[2].turtle_int = 1;
Partner._records[0].turtles = [1, 2, 3];
let turtleOnchange = 0;
Turtle._onChanges = {
turtle_int: function () {
turtleOnchange++;
},
};
onRpc("write", (args) => {
expect(args.args[1].turtles).toEqual(
[
[1, 2, { turtle_int: 1 }],
[1, 1, { turtle_int: 2 }],
[1, 3, { turtle_int: 3 }],
],
{
message:
"should change all lines that have changed (the first one doesn't change because it has the same sequence)",
}
);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["yop", "blip", "kawa"]);
// Drag and drop the second line in first position
await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop("tbody tr");
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blip", "yop", "kawa"]);
expect(turtleOnchange).toBe(3, { message: "should update all lines" });
await clickSave();
});
test("onchange for embedded one2many with handle widget (more records)", async () => {
const ids = [];
for (let i = 10; i < 50; i++) {
const id = 10 + i;
ids.push(id);
Turtle._records.push({
id: id,
turtle_int: 0,
turtle_foo: "#" + id,
});
}
ids.push(1, 2, 3);
Partner._records[0].turtles = ids;
Partner._onChanges = {
turtles: function (obj) {},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains("div[name=turtles] .o_pager_next").click();
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["yop", "blip", "kawa"]);
await contains(".o_data_cell.o_list_char").click();
await contains('.o_list_renderer div[name="turtle_foo"] input').edit("blurp");
// Drag and drop the third line in second position
await contains("tbody tr:eq(2) .o_handle_cell").dragAndDrop("tbody tr:eq(1)");
// need to unselect row...
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blurp", "kawa", "blip"]);
await clickSave();
await contains('div[name="turtles"] .o_pager_next').click();
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blurp", "kawa", "blip"]);
});
test("onchange with modifiers for embedded one2many on the second page", async () => {
const ids = [];
for (let i = 10; i < 60; i++) {
const id = 10 + i;
ids.push(id);
Turtle._records.push({
id: id,
turtle_int: 0,
turtle_foo: "#" + id,
});
}
ids.push(1, 2, 3);
Partner._records[0].turtles = ids;
Partner._onChanges = {
turtles: function (obj) {},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
const getTurtleFooValues = () => {
return queryAllTexts(".o_data_cell.o_list_char").join("");
};
expect(getTurtleFooValues()).toBe("#20#21#22#23#24#25#26#27#28#29");
await contains(".o_data_cell.o_list_char").click();
await contains("div[name=turtle_foo] input").edit("blurp");
// click outside of the one2many to unselect the row
await contains(".o_form_view").click();
expect(getTurtleFooValues()).toBe("blurp#21#22#23#24#25#26#27#28#29");
// the domain fail if the widget does not use the already loaded data.
await contains(".o_form_button_cancel").click();
expect(".modal").toHaveCount(0);
expect(getTurtleFooValues()).toBe("#20#21#22#23#24#25#26#27#28#29");
// Drag and drop the third line in second position
await contains("tbody tr:eq(2) .o_handle_cell").dragAndDrop("tbody tr:eq(1)");
expect(getTurtleFooValues()).toBe("#20#30#31#32#33#34#35#36#37#38");
// Drag and drop the third line in second position
await contains("tbody tr:eq(2) .o_handle_cell").dragAndDrop("tbody tr:eq(1)");
expect(getTurtleFooValues()).toBe("#20#39#40#41#42#43#44#45#46#47");
await contains(".o_form_view").click();
expect(getTurtleFooValues()).toBe("#20#39#40#41#42#43#44#45#46#47");
await contains(".o_form_button_cancel").click();
expect(".modal").toHaveCount(0);
expect(getTurtleFooValues()).toBe("#20#21#22#23#24#25#26#27#28#29");
});
test("onchange followed by edition on the second page", async () => {
const ids = [];
for (let i = 1; i < 85; i++) {
const id = 10 + i;
ids.push(id);
Turtle._records.push({
id: id,
turtle_int: (id / 3) | 0,
turtle_foo: "#" + i,
});
}
ids.splice(41, 0, 1, 2, 3);
Partner._records[0].turtles = ids;
Partner._onChanges = {
turtles: function (obj) {},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_field_widget[name=turtles] .o_pager_next").click();
await contains(".o_field_one2many .o_list_renderer tbody tr td.o_handle_cell:eq(1)").click();
await contains('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"] input').edit(
"value 1"
);
await contains(".o_field_one2many .o_list_renderer tbody tr td.o_handle_cell:eq(2)").click();
await contains('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"] input').edit(
"value 2"
);
expect(".o_data_row").toHaveCount(40);
expect(".o_field_one2many .o_list_renderer .o_data_cell.o_list_char:eq(0)").toHaveText("#39", {
message: "should display '#39' at the first line",
});
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(40, {
message: "should display 39 records and the create line",
});
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row", {
message: "should display the create line in first position",
});
expect('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"]').toHaveText("", {
message: "should be an empty input",
});
expect(".o_field_one2many .o_list_renderer .o_data_cell.o_list_char:eq(1)").toHaveText("#39");
await contains(".o_data_row input").edit("value 3", { confirm: "blur" });
expect(".o_data_row:eq(0)").toHaveClass(["o_data_row", "o_row_draggable"]);
expect(".o_field_one2many .o_list_renderer .o_data_cell.o_list_char:eq(1)").toHaveText("#39");
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(40, {
message: "should display 39 records and the create line",
});
expect(".o_field_one2many .o_list_renderer .o_data_cell.o_list_char:eq(1)").toHaveText(
"value 3"
);
expect(".o_field_one2many .o_list_renderer .o_data_cell.o_list_char:eq(2)").toHaveText("#39");
});
test("onchange followed by edition on the second page (part 2)", async () => {
const ids = [];
for (let i = 1; i < 85; i++) {
const id = 10 + i;
ids.push(id);
Turtle._records.push({
id: id,
turtle_int: (id / 3) | 0,
turtle_foo: "#" + i,
});
}
ids.splice(41, 0, 1, 2, 3);
Partner._records[0].turtles = ids;
Partner._onChanges = {
turtles: function (obj) {},
};
// bottom order
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_field_widget[name=turtles] .o_pager_next").click();
await contains(".o_field_one2many .o_list_renderer tbody tr td.o_handle_cell:eq(1)").click();
await contains('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"] input').edit(
"value 1",
{ confirm: "blur" }
);
await contains(".o_field_one2many .o_list_renderer tbody tr td.o_handle_cell:eq(2)").click();
await contains('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"] input').edit(
"value 2",
{ confirm: "blur" }
);
expect(".o_data_row").toHaveCount(40, { message: "should display 40 records" });
expect(".o_field_one2many .o_list_renderer tbody .o_data_cell.o_list_char:eq(0)").toHaveText(
"#39",
{
message: "should display '#39' at the first line",
}
);
expect(".o_field_one2many .o_list_renderer tbody .o_data_cell.o_list_char:eq(39)").toHaveText(
"#77",
{ message: "should display '#77' at the last line" }
);
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(41, {
message: "should display 41 records and the create line",
});
expect(".o_field_one2many .o_list_renderer tbody .o_data_cell.o_list_char:eq(39)").toHaveText(
"#77",
{ message: "should display '#77' at the penultimate line" }
);
expect(".o_data_row:eq(40)").toHaveClass("o_selected_row", {
message: "should display the create line in first position",
});
await contains('.o_field_one2many .o_list_renderer tbody div[name="turtle_foo"] input').edit(
"value 3",
{ confirm: "blur" }
);
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(42, {
message: "should display 42 records and the create line",
});
expect(".o_field_one2many .o_list_renderer tbody .o_data_cell.o_list_char:eq(40)").toHaveText(
"value 3"
);
expect(".o_field_one2many .o_list_renderer tbody .o_data_cell.o_list_char:eq(41)").toHaveText(
""
);
expect(".o_data_row:eq(41)").toHaveClass("o_selected_row", {
message: "should display the create line in first position",
});
});
test("onchange returning a commands 4 for an x2many", async () => {
Partner._onChanges = {
foo(obj) {
obj.turtles = [
[4, 1],
[4, 3],
];
},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_data_row").toHaveCount(1);
expect(".o_data_row .o_field_widget[name=partner_ids]").toHaveText("second record\naaa");
// change the value of foo to trigger the onchange
await contains(".o_field_widget[name=foo] input").edit("some value");
expect(".o_data_row").toHaveCount(3, {
message: "there should be three records in the relation",
});
expect(".o_data_row .o_field_widget[name=partner_ids]:eq(0)").toHaveText("first record");
});
test("reference fields inside x2manys are fetched after an onchange", async () => {
expect.assertions(4);
Turtle._records[1].turtle_ref = "product,41";
Partner._onChanges = {
foo: function (obj) {
obj.turtles = [
[4, 1],
[4, 3],
];
},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_data_row").toHaveCount(1);
expect(queryAllTexts(".ref_field")).toEqual(["xpad"]);
// change the value of foo to trigger the onchange
await contains(".o_field_widget[name=foo] input").edit("some value");
expect(".o_data_row").toHaveCount(3);
expect(queryAllTexts(".ref_field")).toEqual(["xpad", "", "xphone"]);
});
test.tags("desktop");
test("onchange on one2many containing x2many in form view", async () => {
Partner._onChanges = {
foo: function (obj) {
obj.turtles = [[0, false, { turtle_foo: "new record" }]];
},
};
Partner._views = { list: '', search: "" };
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
expect(".o_data_row").toHaveCount(1, {
message: "the onchange should have created one record in the relation",
});
// open the created o2m record in a form view, and add a m2m subrecord
// in its relation
await contains(".o_data_row .o_data_cell").click();
expect(".modal").toHaveCount(1);
expect(".modal .o_data_row").toHaveCount(0);
// add a many2many subrecord
await contains(".modal .o_field_x2many_list_row_add a").click();
expect(".modal").toHaveCount(2, { message: "should have opened a second dialog" });
// select a many2many subrecord
await contains(".modal:eq(1) .o_list_view .o_data_cell").click();
expect(".modal").toHaveCount(1);
expect(".modal .o_data_row").toHaveCount(1);
expect(".modal .o_x2m_control_panel .o_pager").toHaveCount(0, {
message: "m2m pager should be hidden",
});
// click on 'Save & Close'
await contains(".modal-footer .btn-primary").click();
expect(".modal").toHaveCount(0, { message: "dialog should be closed" });
// reopen o2m record, and another m2m subrecord in its relation, but
// discard the changes
await contains(".o_data_row .o_data_cell").click();
expect(".modal").toHaveCount(1);
expect(".modal .o_data_row").toHaveCount(1);
// add another m2m subrecord
await contains(".modal .o_field_x2many_list_row_add a").click();
expect(".modal").toHaveCount(2, { message: "should have opened a second dialog" });
await contains(".modal:eq(1) .o_list_view .o_data_cell").click();
expect(".modal").toHaveCount(1, { message: "second dialog should be closed" });
expect(".modal .o_data_row").toHaveCount(2, {
message: "there should be two records in the one2many in the dialog",
});
// click on 'Discard'
await contains(".modal-footer .btn-secondary").click();
expect(".modal").toHaveCount(0, { message: "dialog should be closed" });
// reopen o2m record to check that second changes have properly been discarded
await contains(".o_data_row .o_data_cell").click();
expect(".modal").toHaveCount(1);
expect(".modal .o_data_row").toHaveCount(1);
});
test.tags("desktop");
test("onchange on one2many with x2many in list (no widget) and form view (list)", async () => {
expect.assertions(7);
Turtle._fields.turtle_foo = fields.Char({ default: "a default value" });
Partner._onChanges = {
foo: function (obj) {
obj.p = [[0, false, { turtles: [[0, false, { turtle_foo: "hello" }]] }]];
},
};
onRpc("partner", "onchange", ({ args }) => {
expect(args[3]).toEqual({
display_name: {},
foo: {},
p: {
fields: {
turtles: {
fields: {
turtle_foo: {},
},
},
},
limit: 40,
order: "",
},
});
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
expect(".o_data_row").toHaveCount(1, {
message: "the onchange should have created one record in the relation",
});
// open the created o2m record in a form view
await contains(".o_data_row .o_data_cell").click();
expect(".modal").toHaveCount(1);
expect(".modal .o_data_row").toHaveCount(1);
expect(".modal .o_data_row").toHaveText("hello");
// add a one2many subrecord and check if the default value is correctly applied
await contains(".modal .o_field_x2many_list_row_add a").click();
expect(".modal .o_data_row").toHaveCount(2);
expect(".modal .o_data_row .o_field_widget[name=turtle_foo] input").toHaveValue(
"a default value"
);
});
test("save an o2m dialog form view and discard main form view", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
`,
});
expect(".o_data_row").toHaveCount(1, {
message: "the onchange should have created one record in the relation",
});
// open the created o2m record in a form view
await contains(".o_data_row .o_data_cell").click();
expect(".modal").toHaveCount(1);
expect(".modal .o_data_row").toHaveCount(1);
expect(".modal .o_data_row").toHaveText("hello");
// add a one2many subrecord and check if the default value is correctly applied
await contains(".modal .o_field_x2many_list_row_add a").click();
expect(".modal .o_data_row").toHaveCount(2);
expect(".modal .o_data_row .o_field_widget[name=turtle_foo] input").toHaveValue(
"a default value"
);
});
test("embedded one2many with handle widget with minimum setValue calls", async () => {
Turtle._records[0].turtle_int = 6;
Turtle._records.push(
{
id: 4,
turtle_int: 20,
turtle_foo: "a1",
},
{
id: 5,
turtle_int: 9,
turtle_foo: "a2",
},
{
id: 6,
turtle_int: 2,
turtle_foo: "a3",
},
{
id: 7,
turtle_int: 11,
turtle_foo: "a4",
}
);
Partner._records[0].turtles = [1, 2, 3, 4, 5, 6, 7];
patchWithCleanup(Record.prototype, {
_update() {
if (this.resModel === "turtle") {
expect.step(`${this.resId}`);
}
return super._update(...arguments);
},
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(queryAllTexts(".o_data_row [name='turtle_foo']")).toEqual([
"a3",
"yop",
"blip",
"a2",
"a4",
"a1",
"kawa",
]);
const positions = [
[6, 0, ["3", "6", "1", "2", "5", "7", "4"]], // move the last to the first line
[5, 1, ["7", "6", "1", "2", "5"]], // move the penultimate to the second line
[2, 5, ["1", "2", "5", "6"]], // move the third to the penultimate line
];
for (const [sourceIndex, targetIndex, steps] of positions) {
await contains(`tbody tr:eq(${sourceIndex}) .o_handle_cell`).dragAndDrop(
`tbody tr:eq(${targetIndex})`
);
expect.verifySteps(steps);
}
expect(queryAllTexts(".o_data_row [name='turtle_foo']")).toEqual([
"kawa",
"a4",
"yop",
"blip",
"a2",
"a3",
"a1",
]);
});
test("embedded one2many (editable list) with handle widget", async () => {
Partner._records[0].p = [1, 2, 4];
onRpc("web_save", (args) => {
expect.step(args.method);
expect(args.args[1].p).toEqual([
[1, 2, { int_field: 0 }],
[1, 4, { int_field: 1 }],
]);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([
"My little Foo Value",
"blip",
"yop",
]);
expect.verifySteps([]);
// Drag and drop the second line in first position
await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop(".o_field_one2many tbody tr:eq(0)");
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([
"blip",
"My little Foo Value",
"yop",
]);
await contains(".o_data_cell.o_list_char").click();
expect(".o_field_widget[name=foo] input").toHaveValue("blip");
expect.verifySteps([]);
await clickSave();
expect.verifySteps(["web_save"]);
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([
"blip",
"My little Foo Value",
"yop",
]);
});
test("one2many list order with handle widget", async () => {
onRpc("web_read", (args) => {
expect.step(`web_read`);
expect(args.kwargs.specification.p.order).toBe("int_field ASC, id ASC");
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect.verifySteps(["web_read"]);
});
test("one2many field when using the pager", async () => {
const ids = [];
for (let i = 0; i < 45; i++) {
const id = 10 + i;
ids.push(id);
Partner._records.push({
id,
name: `relational record ${id}`,
});
}
Partner._records[0].p = ids.slice(0, 42);
Partner._records[1].p = ids.slice(42);
onRpc("web_read", (args) => {
expect.step(`unity read ${args.args[0]}`);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
resIds: [1, 2],
});
expect.verifySteps(["unity read 1"]);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(40);
// move to record 2, which has 3 related records (and shouldn't contain the
// related records of record 1 anymore)
await contains(".o_form_view .o_control_panel .o_pager_next").click();
expect.verifySteps(["unity read 2"]);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(3);
// move back to record 1, which should contain again its first 40 related
// records
await contains(".o_form_view .o_control_panel .o_pager_previous").click();
expect.verifySteps(["unity read 1"]);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(40);
// move to the second page of the o2m: 1 RPC should have been done to fetch
// the 2 subrecords of page 2, and those records should now be displayed
await contains(".o_x2m_control_panel .o_pager_next").click();
expect.verifySteps(["unity read 50,51"]);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
// move to record 2 again and check that everything is correctly updated
await contains(".o_form_view .o_control_panel .o_pager_next").click();
expect.verifySteps(["unity read 2"]);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(3);
// move back to record 1 and move to page 2 again: all data should have
// been correctly reloaded
await contains(".o_form_view .o_control_panel .o_pager_previous").click();
expect.verifySteps(["unity read 1"]);
await contains(".o_x2m_control_panel .o_pager_next").click();
expect.verifySteps(["unity read 50,51"]);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
});
test("edition of one2many field with pager", async () => {
const ids = [];
for (let i = 0; i < 45; i++) {
const id = 10 + i;
ids.push(id);
Partner._records.push({
id: id,
name: "relational record " + id,
});
}
Partner._records[0].p = ids;
Partner._views = { form: '
' };
let saveCount = 0;
let checkRead = false;
let readIDs;
onRpc("web_read", (args) => {
if (checkRead) {
readIDs = args.args[0];
checkRead = false;
}
});
onRpc("web_save", (args) => {
expect.step("web_save");
saveCount++;
const commands = args.args[1].p;
switch (saveCount) {
case 1:
expect(commands).toEqual([[0, commands[0][1], { name: "new record" }]]);
break;
case 2:
expect(commands).toEqual([[2, 10]]);
break;
case 3:
expect(commands).toEqual([
[0, commands[0][1], { name: "new record page 1" }],
[2, 11],
[2, 52],
[0, commands[3][1], { name: "new record page 2" }],
]);
break;
}
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(40);
// add a record on page one
checkRead = true;
await contains(".o-kanban-button-new").click();
await contains(".modal input").edit("new record");
await contains(".modal .modal-footer .btn-primary").click();
// checks
expect(readIDs).toBe(undefined, { message: "should not have read any record" });
expect(".o_kanban_record:not(.o_kanban_ghost):contains('new record')").toHaveCount(0);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(40);
// save
await clickSave();
// delete a record on page one
checkRead = true;
expect(".o_kanban_record:not(.o_kanban_ghost):eq(0)").toHaveText("relational record 10");
await contains(".delete_icon").click(); // should remove record!!!
// checks
expect(readIDs).toEqual([50], {
message: "should have read a record (to display 40 records on page 1)",
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(40);
// save
await clickSave();
// add and delete records in both pages
checkRead = true;
readIDs = undefined;
// add and delete a record in page 1
await contains(".o-kanban-button-new").click();
await contains(".modal input").edit("new record page 1");
await contains(".modal .modal-footer .btn-primary").click();
expect(".o_kanban_record:not(.o_kanban_ghost):eq(0)").toHaveText("relational record 11", {
message: "first record should be the one with id 11 (next checks rely on that)",
});
await contains(".delete_icon").click(); // should remove record!!!
expect(readIDs).toEqual([51], {
message: "should have read a record (to display 40 records on page 1)",
});
// add and delete a record in page 2
await contains(".o_x2m_control_panel .o_pager_next").click();
expect(".o_kanban_record:not(.o_kanban_ghost):eq(0)").toHaveText("relational record 52", {
message: "first record should be the one with id 52 (next checks rely on that)",
});
checkRead = true;
readIDs = undefined;
await contains(".delete_icon").click(); // should remove record!!!
await contains(".o-kanban-button-new").click();
await contains(".modal input").edit("new record page 2");
await contains(".modal .modal-footer .btn-primary").click();
expect(readIDs).toBe(undefined, { message: "should not have read any record" });
// checks
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(5);
expect(".o_kanban_record:not(.o_kanban_ghost):contains('new record page 1')").toHaveCount(1);
expect(".o_kanban_record:not(.o_kanban_ghost):contains('new record page 2')").toHaveCount(1);
// save
await clickSave();
expect.verifySteps(["web_save", "web_save", "web_save"]);
});
test.tags("desktop");
test("edition of one2many field with pager on desktop", async () => {
const ids = [];
for (let i = 0; i < 45; i++) {
const id = 10 + i;
ids.push(id);
Partner._records.push({
id: id,
name: "relational record " + id,
});
}
Partner._records[0].p = ids;
Partner._views = { form: '
' };
let saveCount = 0;
let checkRead = false;
onRpc("web_read", (args) => {
if (checkRead) {
checkRead = false;
}
});
onRpc("web_save", (args) => {
expect.step("web_save");
saveCount++;
const commands = args.args[1].p;
switch (saveCount) {
case 1:
expect(commands).toEqual([[0, commands[0][1], { name: "new record" }]]);
break;
case 2:
expect(commands).toEqual([[2, 10]]);
break;
case 3:
expect(commands).toEqual([
[0, commands[0][1], { name: "new record page 1" }],
[2, 11],
[2, 52],
[0, commands[3][1], { name: "new record page 2" }],
]);
break;
}
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_x2m_control_panel .o_pager_counter").toHaveText("1-40 / 45");
// add a record on page one
checkRead = true;
await contains(".o-kanban-button-new").click();
await contains(".modal input").edit("new record");
await contains(".modal .modal-footer .btn-primary").click();
// checks
expect(".o_x2m_control_panel .o_pager_counter").toHaveText("1-40 / 46");
// save
await clickSave();
// delete a record on page one
checkRead = true;
await contains(".delete_icon").click(); // should remove record!!!
// checks
expect(".o_x2m_control_panel .o_pager_counter").toHaveText("1-40 / 45");
// save
await clickSave();
// add and delete records in both pages
checkRead = true;
// add and delete a record in page 1
await contains(".o-kanban-button-new").click();
await contains(".modal input").edit("new record page 1");
await contains(".modal .modal-footer .btn-primary").click();
await contains(".delete_icon").click(); // should remove record!!!
// add and delete a record in page 2
await contains(".o_x2m_control_panel .o_pager_next").click();
checkRead = true;
await contains(".delete_icon").click(); // should remove record!!!
await contains(".o-kanban-button-new").click();
await contains(".modal input").edit("new record page 2");
await contains(".modal .modal-footer .btn-primary").click();
// checks
expect(".o_x2m_control_panel .o_pager_counter").toHaveText("41-45 / 45");
// save
await clickSave();
expect.verifySteps(["web_save", "web_save", "web_save"]);
});
test("When viewing one2many records in an embedded kanban, the delete button should say 'Delete' and not 'Remove'", async () => {
expect.assertions(1);
Turtle._views = {
form: `
`,
resId: 1,
});
// Opening the record to see the footer buttons
await contains(".o_kanban_record").click();
expect(".o_btn_remove").toHaveText("Delete");
});
test("open a record in a one2many kanban (mode 'readonly')", async () => {
Turtle._views = {
form: `
`,
resId: 1,
});
expect(".o_field_x2many_list_row_add").toHaveCount(0);
});
test("one2many list: cannot open record in editable list and form in readonly mode", async () => {
Partner._records[0].p = [2];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_data_cell[name='name']").toHaveCount(1);
await contains(".o_data_cell[name='name']").click();
expect(".modal-dialog").toHaveCount(0);
});
test("one2many list: cannot open record in editable=bottom and edit=false list", async () => {
Partner._records[0].p = [2];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// bar is true -> create and delete action are available
expect(".o_field_x2many_list_row_add").toHaveCount(1);
expect("td.o_list_record_remove button").toHaveCount(2);
// set bar to false -> create and delete action are no longer available
await contains('.o_field_widget[name="bar"] input').click();
expect(".o_field_x2many_list_row_add").toHaveCount(0);
expect("td.o_list_record_remove button").toHaveCount(0);
});
test("boolean field in a one2many must be directly editable", async () => {
Partner._records[0].p = [2, 4];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect("td.o_list_record_remove button").toHaveCount(3);
await contains("td.o_list_record_remove button").click();
expect("td.o_list_record_remove button").toHaveCount(2);
// save and check that the correct command has been generated
await clickSave();
// FIXME: it would be nice to test that the view is re-rendered correctly,
// but as the relational data isn't re-fetched, the rendering is ok even
// if the changes haven't been saved
});
test("one2many kanban: edition", async () => {
expect.assertions(17);
Partner._records[0].p = [2];
onRpc("web_save", (args) => {
const commands = args.args[1].p;
expect(commands).toEqual([
[
0,
commands[0][1],
{
color: "red",
name: "new subrecord 3",
foo: "My little Foo Value",
},
],
[2, 2],
]);
});
await mountView({
type: "form",
resModel: "partner",
// color will be in the kanban but not in the form
// foo will be in the form but not in the kanban
arch: `
`,
resId: 1,
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
expect(".o_kanban_record span:eq(0)").toHaveText("second record");
expect(".o_kanban_record span:eq(1)").toHaveText("Red");
expect(".delete_icon").toHaveCount(1);
expect(".o_field_one2many .o-kanban-button-new").toHaveCount(1);
expect(".o_field_one2many .o-kanban-button-new").toHaveClass("btn-secondary");
expect(".o_field_one2many .o-kanban-button-new").toHaveText("Add");
// edit existing subrecord
await contains(".o_kanban_record:eq(0)").click();
await contains(".modal .o_form_view .o_field_widget:eq(0) input").edit("new name");
await contains(".modal .modal-footer .btn-primary:eq(0)").click();
expect(".o_kanban_record span:first").toHaveText("new name");
// create a new subrecord
await contains(".o-kanban-button-new:eq(0)").click();
await contains(".modal .o_form_view .o_field_widget:eq(0) input").edit("new subrecord 1");
await contains(".modal .modal-footer .btn-primary:eq(0)").click();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
expect(".o_kanban_record:eq(1) span:eq(0)").toHaveText("new subrecord 1", {
message: 'value of newly created subrecord should be "new subrecord 1"',
});
// create two new subrecords
await contains(".o-kanban-button-new:eq(0)").click();
await contains(".modal .o_form_view .o_field_widget:eq(0) input").edit("new subrecord 2");
await contains(".modal .modal-footer .btn-primary:eq(1)").click();
await contains(".modal .o_form_view .o_field_widget:eq(0) input").edit("new subrecord 3");
await contains(".modal .modal-footer .btn-primary:eq(0)").click();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(4);
// delete subrecords
await contains(".o_kanban_record:eq(0)").click();
expect(".modal .modal-footer .o_btn_remove").toHaveCount(1);
await contains(".modal .modal-footer .o_btn_remove:eq(0)").click();
expect(".o_modal").toHaveCount(0, { message: "modal should have been closed" });
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(3);
await contains(".o_kanban_renderer .delete_icon:first():eq(0)").click();
await contains(".o_kanban_renderer .delete_icon:first():eq(0)").click();
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
expect(".o_kanban_record span:first").toHaveText("new subrecord 3", {
message: 'the remaining subrecord should be "new subrecord 3"',
});
// save and check that the correct command has been generated
await clickSave();
});
test("one2many kanban (editable): properly handle add-label node attribute", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// bar is initially true -> create and delete actions are available
expect(".o-kanban-button-new").toHaveCount(1, { message: '"Add" button should be available' });
await contains(".o_kanban_record:first").click();
expect(".modal .modal-footer .o_btn_remove").toHaveCount(1, {
message: "There should be a Remove Button inside modal",
});
await contains(".modal .o_form_button_cancel").click();
// set bar false -> create and delete actions are no longer available
await contains('.o_field_widget[name="bar"] input').click();
expect(".o-kanban-button-new").toHaveCount(0, {
message: '"Add" button should not be available as bar is False',
});
await contains(".o_kanban_record:first").click();
expect(".modal .modal-footer .o_btn_remove").toHaveCount(0, {
message: "There should not be a Remove Button as bar field is False",
});
});
test.tags("desktop");
test("editable one2many list, pager is updated on desktop", async () => {
Turtle._records.push({ id: 4, turtle_foo: "stephen hawking" });
Partner._records[0].turtles = [1, 2, 3, 4];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// add a record, add value to turtle_foo then click in form view to confirm it
await contains(".o_field_x2many_list_row_add a").click();
await contains('div[name="turtle_foo"] input').edit("nora");
await contains(getFixture()).click();
expect(".o_field_widget[name=turtles] .o_pager").toHaveText("1-4 / 5");
});
test("one2many list (non editable): edition", async () => {
expect.assertions(11);
let nbWrite = 0;
Partner._records[0].p = [2, 4];
onRpc("web_save", (args) => {
nbWrite++;
expect(args.args[1]).toEqual({
p: [
[1, 2, { name: "new name" }],
[2, 4],
],
});
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect("td.o_list_number").toHaveCount(2);
expect(".o_list_renderer tbody td:eq(0)").toHaveText("second record");
expect(".o_list_record_remove").toHaveCount(2);
expect(".o_field_x2many_list_row_add").toHaveCount(1);
// edit first record
await contains(".o_list_renderer .o_data_cell").click();
expect(".o_list_renderer .o_data_cell:eq(0)").toHaveClass("o_readonly_modifier");
await contains(".modal .o_form_editable input").edit("new name");
contains(".modal .modal-footer .btn-primary").click();
await animationFrame();
expect(".o_list_renderer tbody td:eq(0)").toHaveText("new name");
expect(nbWrite).toBe(0, { message: "should not have write anything in DB" });
// remove second record
contains(".o_list_record_remove:eq(1)").click();
await animationFrame();
expect("td.o_list_number").toHaveCount(1);
expect(".o_list_renderer tbody td:eq(0)").toHaveText("new name");
await clickSave(); // save the record
expect(nbWrite).toBe(1, { message: "should have write the changes in DB" });
});
test("one2many list (editable): edition, part 2", async () => {
expect.assertions(11);
onRpc("web_save", (args) => {
// Would be nice to assert this way, but we don't control the virtual ids index
// expect(args.args[1].p).toEqual([
// [0, "virtual_2", { foo: "gemuse" }],
// [0, "virtual_1", { foo: "kartoffel" }],
// ]);
expect(args.args[1].p[0][0]).toBe(0);
expect(args.args[1].p[1][0]).toBe(0);
expect(args.args[1].p[0][2]).toEqual({ foo: "gemuse" });
expect(args.args[1].p[1][2]).toEqual({ foo: "kartoffel" });
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// edit mode, then click on Add an item and enter a value
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_selected_row > td input").edit("kartoffel", { confirm: "false" });
expect("td .o_field_char input").toHaveValue("kartoffel");
// click again on Add an item
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
expect(".o_data_cell:eq(1)").toHaveText("kartoffel");
expect(".o_selected_row > td input").toHaveCount(1);
expect("tr.o_data_row").toHaveCount(2);
// enter another value and save
await contains(".o_selected_row > td input").edit("gemuse", { confirm: "false" });
await clickSave();
expect("tr.o_data_row").toHaveCount(2);
expect(queryAllTexts(".o_data_cell")).toEqual(["gemuse", "kartoffel"]);
});
test("one2many list (editable): edition, part 3", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// edit mode, then click on Add an item, enter value in turtle_foo and Add an item again
expect("tr.o_data_row").toHaveCount(1);
await contains(".o_field_x2many_list_row_add a").click();
await contains('div[name="turtle_foo"] input').edit("nora", { confirm: "false" });
await contains(".o_field_x2many_list_row_add a").click();
expect("tr.o_data_row").toHaveCount(3);
// cancel the edition
await contains(".o_form_button_cancel").click();
expect(".modal").toHaveCount(0);
expect("tr.o_data_row").toHaveCount(1);
});
test.tags("desktop");
test("one2many list (editable): edition, part 4", async () => {
let i = 0;
Turtle._onChanges = {
turtle_trululu: function (obj) {
if (i) {
obj.turtle_description = "Some Description";
}
i++;
},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 2,
});
// edit mode, then click on Add an item
expect("tr.o_data_row").toHaveCount(0);
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row textarea").toHaveValue("");
// add a value in the turtle_trululu field to trigger an onchange
await clickFieldDropdown("turtle_trululu");
await press("Enter");
await animationFrame();
expect(".o_data_row textarea").toHaveValue("Some Description");
});
test("one2many list (editable): edition, part 5", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// edit mode, then click on Add an item, enter value in turtle_foo and Add an item again
expect("tr.o_data_row").toHaveCount(1);
expect(".o_data_cell").toHaveText("blip");
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_field_widget[name=turtle_foo] input").edit("aaa", { confirm: "false" });
expect("tr.o_data_row").toHaveCount(2);
await contains(".o_list_record_remove:eq(1)").click();
expect("tr.o_data_row").toHaveCount(1);
// cancel the edition
await contains(".o_form_button_cancel").click();
expect("tr.o_data_row").toHaveCount(1);
expect(".o_data_cell").toHaveText("blip");
});
test("one2many list (editable): discarding required empty data", async () => {
Turtle._fields.turtle_foo = fields.Char({ required: true });
onRpc((args) => {
expect.step(args.method);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 2,
});
// edit mode, then click on Add an item, then click elsewhere
expect("tr.o_data_row").toHaveCount(0);
await contains(".o_field_x2many_list_row_add a").click();
await contains(getFixture()).click();
expect("tr.o_data_row").toHaveCount(0);
// click on Add an item again, then click on save
await contains(".o_field_x2many_list_row_add a").click();
await clickSave();
expect("tr.o_data_row").toHaveCount(0);
expect.verifySteps(["get_views", "web_read", "onchange", "onchange"]);
});
test.tags("desktop");
test("discard O2M field with close button", async () => {
Partner._records[0].p = [2];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// add a record, to reach the page size limit
await contains(".o_field_x2many_list_row_add a").click();
// the record currently being added should not count in the pager
expect(".o_field_widget[name=turtles] .o_pager").toHaveCount(0);
// enter value in turtle_foo field and click outside to unselect the row
await contains('.o_field_widget[name="turtle_foo"] input').edit("nora");
await contains(getFixture()).click();
expect(".o_selected_row").toHaveCount(0);
expect(".o_field_widget[name=turtles] .o_pager").toHaveCount(0);
await clickSave();
expect(".o_field_widget[name=turtles] .o_pager").toHaveCount(1);
});
test.tags("desktop");
test("editable one2many list, adding line when only one page on desktop", async () => {
Partner._records[0].turtles = [1, 2, 3];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// add a record, to reach the page size limit
await contains(".o_field_x2many_list_row_add a").click();
// enter value in turtle_foo field and click outside to unselect the row
await contains('.o_field_widget[name="turtle_foo"] input').edit("nora");
await contains(getFixture()).click();
await clickSave();
expect(".o_field_widget[name=turtles] .o_pager").toHaveText("1-3 / 4");
});
test("editable one2many list, adding line, then discarding", async () => {
Turtle._records.push({ id: 4, turtle_foo: "stephen hawking" });
Partner._records[0].turtles = [1, 2, 3, 4];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// add a (empty) record
await contains(".o_field_x2many_list_row_add a").click();
// go on next page. The new record is not valid and should be discarded
await contains(".o_field_widget[name=turtles] .o_pager_next").click();
expect("tr.o_data_row").toHaveCount(1);
});
test.tags("desktop");
test("editable one2many list, required field, pager and confirm discard on desktop", async () => {
Turtle._records.push({ id: 4, turtle_foo: "stephen hawking" });
Turtle._fields.turtle_foo = fields.Char({ required: true });
Partner._records[0].turtles = [1, 2, 3, 4];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// add a record with a dirty state, but not valid
await contains(".o_field_x2many_list_row_add a").click();
await contains('.o_field_widget[name="turtle_int"] input').edit(4321);
// try to go to next page. The new record is not valid, but dirty so we should
// stay on the current page, and the record should be marked as invalid
await contains(".o_field_widget[name=turtles] .o_pager_next").click();
expect(".o_field_widget[name=turtles] .o_pager").toHaveText("1-4 / 5");
expect(".o_field_widget[name=turtles] .o_pager").toHaveText("1-4 / 5");
expect(".o_field_widget[name=turtle_foo].o_field_invalid").toHaveCount(1);
});
test("save a record with not new, dirty and invalid subrecord", async () => {
Partner._records[0].p = [2];
Partner._records[1].name = ""; // invalid record
onRpc("write", () => {
throw new Error("Should not call write as record is invalid");
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// add 4 records (to have more records than the limit)
await contains(".o_field_x2many_list_row_add a").click();
await contains('.o_field_widget[name="turtle_foo"] input').edit("nora", { confirm: false });
await contains(".o_field_x2many_list_row_add a").click();
await contains('.o_field_widget[name="turtle_foo"] input').edit("nora", { confirm: false });
await contains(".o_field_x2many_list_row_add a").click();
await contains('.o_field_widget[name="turtle_foo"] input').edit("nora", { confirm: false });
await contains(".o_field_x2many_list_row_add a").click();
expect("tr.o_data_row").toHaveCount(5);
expect(".o_field_widget[name=turtles] .o_pager").toHaveCount(0);
// discard
await contains(".o_form_button_cancel").click();
expect(".modal").toHaveCount(0);
expect("tr.o_data_row").toHaveCount(1);
expect(".o_field_widget[name=turtles] .o_pager").toHaveCount(0);
});
test("unselecting a line with missing required data", async () => {
Turtle._fields.turtle_foo = fields.Char({ required: true });
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 2,
});
// edit mode, then click on Add an item, then click elsewhere
expect("tr.o_data_row").toHaveCount(0);
await contains(".o_field_x2many_list_row_add a").click();
expect("tr.o_data_row").toHaveCount(1);
// adding a value in the non required field, so it is dirty, but with
// a missing required field
await contains('.o_field_widget[name="turtle_int"] input').edit("12345");
// click elsewhere
await contains(getFixture()).click();
expect(".modal").toHaveCount(0);
// the line should still be selected
expect("tr.o_data_row.o_selected_row").toHaveCount(1);
// click discard
await contains(".o_form_button_cancel").click();
expect(".modal").toHaveCount(0);
expect("tr.o_data_row").toHaveCount(0);
});
test("pressing enter in a o2m with a required empty field", async () => {
Turtle._fields.turtle_foo = fields.Char({ required: true });
onRpc((args) => {
expect.step(args.method);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 2,
});
// edit mode, then click on Add an item, then press enter
await contains(".o_field_x2many_list_row_add a").click();
await press("Enter");
await animationFrame();
expect('div[name="turtle_foo"]').toHaveClass("o_field_invalid");
expect.verifySteps(["get_views", "web_read", "onchange"]);
});
test("pressing enter several times in a one2many", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 2,
});
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(1);
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
await contains("[name='turtle_foo'] input").edit("a", { confirm: false });
await press("Enter");
await animationFrame();
expect(".o_data_row").toHaveCount(2);
expect(".o_data_row:eq(1)").toHaveClass("o_selected_row");
await contains("[name='turtle_foo'] input").edit("a", { confirm: false });
await press("Enter");
await animationFrame();
expect(".o_data_row").toHaveCount(3);
expect(".o_data_row:eq(2)").toHaveClass("o_selected_row");
// this is a weird case, but there's no required fields, so the record is already valid, we can press Enter directly.
await press("Enter");
await animationFrame();
expect(".o_data_row").toHaveCount(4);
expect(".o_data_row:eq(3)").toHaveClass("o_selected_row");
});
test("creating a new line in an o2m with an handle field does not focus the handler", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// switch the first row in edition
await contains(".o_data_cell").click();
expect(".o_selected_row .o_field_widget:eq(0)").toHaveClass("o_readonly_modifier", {
message: "first record should have name in readonly mode",
});
// switch the second row in edition
await contains(".o_data_row:not(.o_selected_row) .o_data_cell").click();
expect(".o_selected_row .o_field_widget").not.toHaveClass("o_readonly_modifier", {
message: "second record should not have name in readonly mode",
});
});
test("pager of one2many field in new record", async () => {
Partner._records[0].p = [];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
expect(".o_x2m_control_panel .o_pager").toHaveCount(0, {
message: "o2m pager should be hidden",
});
// click to create a subrecord
await contains(".o_field_x2many_list_row_add a").click();
expect("tr.o_data_row").toHaveCount(1);
expect(".o_x2m_control_panel .o_pager").toHaveCount(0, {
message: "o2m pager should be hidden",
});
});
test.tags("desktop");
test("one2many list with a many2one", async () => {
expect.assertions(5);
let checkOnchange = false;
Partner._records[0].p = [2];
Partner._records[1].product_id = 37;
Partner._onChanges.p = () => {};
Partner._views.form = '
`,
resId: 1,
});
expect(".o_data_cell[data-tooltip='xphone']").toHaveCount(1);
expect(".o_data_cell[data-tooltip='xpad']").toHaveCount(0);
await contains(".o_field_x2many_list_row_add a").click();
checkOnchange = true;
await clickFieldDropdown("product_id");
await contains('div[name="product_id"] .o_input_dropdown li:eq(1)').click();
await contains(".modal .modal-footer button").click();
expect(".o_data_cell[data-tooltip='xphone']").toHaveCount(1);
expect(".o_data_cell[data-tooltip='xpad']").toHaveCount(1);
});
test.tags("desktop");
test("one2many list with inline form view", async () => {
expect.assertions(5);
Partner._records[0].p = [];
onRpc("web_save", (args) => {
expect(args.args[1].p).toEqual([
[
0,
args.args[1].p[0][1],
{
foo: "My little Foo Value",
int_field: 123,
product_id: 41,
},
],
]);
});
await mountView({
type: "form",
resModel: "partner",
// don't remove foo field in sub tree view, it is useful to make sure
// the foo fieldwidget does not crash because the foo field is not in the form view
arch: `
`,
resId: 1,
});
await contains(".o_field_x2many_list_row_add a").click();
// write in the many2one field, value = 37 (xphone)
await clickFieldDropdown("product_id");
await press("Enter");
await animationFrame();
// write in the integer field
await contains('.modal .modal-body div[name="int_field"] input').edit("123", {
confirm: false,
});
// save and close
await contains(".modal .o_form_button_save").click();
expect(".o_data_cell[data-tooltip='xphone']").toHaveCount(1);
// reopen the record in form view
await contains(".o_data_cell[data-tooltip='xphone']").click();
expect(".modal .modal-body input:eq(0)").toHaveValue("xphone");
await contains('.modal .modal-body div[name="int_field"] input').edit("456", {
confirm: false,
});
// discard
await contains(".modal .o_form_button_cancel").click();
// reopen the record in form view
await contains(".o_data_cell[data-tooltip='xphone']").click();
expect('.modal .modal-body div[name="int_field"] input').toHaveValue("123", {
message: "should display 123 (previous change has been discarded)",
});
// write in the many2one field, value = 41 (xpad)
await clickFieldDropdown("product_id");
await contains('div[name="product_id"] .o_input_dropdown li:eq(1)').click();
// save and close
await contains(".modal .o_form_button_save").click();
expect(".o_data_cell[data-tooltip='xpad']").toHaveCount(1);
// save the record
await clickSave();
});
test.tags("desktop");
test("one2many, edit record in dialog, save, re-edit, discard", async () => {
Partner._records[0].p = [2];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains("button.oe_kanban_action").click();
});
test("one2many without inline tree arch", async () => {
Partner._records[0].turtles = [2, 3];
Turtle._views = {
list: `
`,
};
await mountView({
type: "form",
resModel: "partner",
// should not call loadViews for the field with many2many_tags widget,
// nor for the invisible field
arch: `
`,
resId: 1,
});
expect(".o_data_row").toHaveCount(2);
expect(".o_data_row .o_list_many2one").toHaveText("xphone");
expect('.o_data_row td div[name="partner_ids"] .badge').toHaveCount(2);
// edit the m2m of first row
await contains(".o_list_renderer tbody td").click();
expect(queryAllTexts(".o_selected_row .o_field_many2many_tags .badge")).toEqual([
"second record",
"aaa",
]);
// remove a tag
await contains(".o_selected_row .o_field_many2many_tags .badge .o_delete:eq(1)").click();
expect(queryAllTexts(".o_selected_row .o_field_many2many_tags .badge")).toEqual([
"second record",
]);
// add a tag
await contains('div[name="partner_ids"] input').click();
await contains('div[name="partner_ids"] .o_input_dropdown li').click(); // xpad
expect(queryAllTexts(".o_selected_row .o_field_many2many_tags .badge")).toEqual([
"second record",
"first record",
]);
// edit the m2o of first row
await clickFieldDropdown("product_id");
await contains('div[name="product_id"] .o_input_dropdown li:eq(1)').click(); // xpad
expect(".o_selected_row .o_field_many2one input").toHaveValue("xpad");
// save (should correctly generate the commands)
await clickSave();
});
test("many2manytag in one2many, onchange, some modifiers, and more than one page", async () => {
Partner._records[0].turtles = [1, 2, 3];
Partner._onChanges.turtles = function () {};
onRpc((args) => {
expect.step(args.method);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_data_row").toHaveCount(2);
await contains(".o_list_record_remove").click();
expect(".o_data_row").toHaveCount(2);
await contains(".o_list_record_remove").click();
expect(".o_data_row").toHaveCount(1);
expect.verifySteps([
"get_views", // main form view
"web_read", // initial read on partner
"web_read", // after first delete, read on turtle (to fetch 3rd record)
"onchange", // after first delete, onchange on field turtles
"onchange", // onchange after second delete
]);
});
test.tags("desktop");
test("onchange many2many in one2many list editable", async () => {
Product._records.push({
id: 1,
name: "xenomorphe",
});
Turtle._onChanges = {
product_id: function (rec) {
if (rec.product_id === 41) {
rec.partner_ids = [[4, 1]];
} else if (rec.product_id === 37) {
rec.partner_ids = [[4, 2]];
}
},
};
let enableOnchange = false;
const partnerOnchange = function (rec) {
if (!enableOnchange) {
return;
}
rec.turtles = [
[
0,
0,
{
name: "new line",
product_id: [37, "xphone"],
partner_ids: [[4, 1]],
},
],
[
1,
rec.turtles[0][1],
{
product_id: [1, "xenomorphe"],
partner_ids: rec.turtles[0][2].partner_ids.length
? [
[3, 1],
[4, 2],
]
: [[4, 2]],
},
],
];
};
Partner._onChanges = {
int_field: partnerOnchange,
turtles: partnerOnchange,
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
// add new line (first, xpad)
await contains(".o_field_x2many_list_row_add a").click();
await contains('div[name="name"] input').edit("first", { confirm: false });
await clickFieldDropdown("product_id");
await contains('div[name="product_id"] .o_input_dropdown li:eq(1)').click(); // xpad
expect(".o_field_many2many_tags .o_tags_input").toHaveCount(1, {
message: "should display the line in editable mode",
});
expect(".o_field_many2one input").toHaveValue("xpad");
expect(".o_field_many2many_tags .o_tag_badge_text").toHaveText("first record");
expect(".o_data_cell .o_required_modifier input").toHaveValue("xpad");
await contains('div[name="int_field"] input').click();
expect(".o_field_many2many_tags input.o_input").toHaveCount(0, {
message: "should display the tag in readonly",
});
// enable the many2many onchange and generate it
enableOnchange = true;
await contains('div[name="int_field"] input').edit("10");
expect(queryAllTexts(".o_data_cell")).toEqual([
"first",
"xenomorphe",
"second record",
"new line",
"xphone",
"first record",
]);
// disable the many2many onchange
enableOnchange = false;
// remove and start over
await contains(".o_list_record_remove button").click();
await contains(".o_list_record_remove button").click();
// enable the many2many onchange
enableOnchange = true;
// add new line (first, xenomorphe)
await contains(".o_field_x2many_list_row_add a").click();
await contains('div[name="name"] input').edit("first", { confirm: false });
await clickFieldDropdown("product_id");
await contains('div[name="product_id"] .o_input_dropdown li:eq(2)').click(); // xenomorphe
expect(".o_field_many2many_tags .o_tags_input").toHaveCount(1, {
message: "should display the line in editable mode",
});
expect('div[name="product_id"] input').toHaveValue("xenomorphe");
expect(".o_field_many2many_tags .o_tag_badge_text:eq(0)").toHaveText("second record");
// put list in readonly mode
await contains('div[name="int_field"] input').click();
expect(queryAllTexts(".o_data_cell")).toEqual([
"first",
"xenomorphe",
"second record",
"new line",
"xphone",
"first record",
]);
expect(".o_field_many2many_tags input.o_input").toHaveCount(0, {
message: "should display the tag in readonly",
});
await contains('div[name="int_field"] input').edit("10");
expect(queryAllTexts(".o_data_cell")).toEqual([
"first",
"xenomorphe",
"second record",
"new line",
"xphone",
"first record",
]);
await clickSave();
expect(queryAllTexts(".o_data_cell")).toEqual([
"first",
"xenomorphe",
"second record",
"new line",
"xphone",
"first record",
]);
});
test.tags("desktop");
test("load view for x2many in one2many", async () => {
Turtle._records[1].product_id = 37;
Partner._records[0].turtles = [2, 3];
Partner._records[2].turtles = [1, 3];
Partner._views = {
list: `
`,
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_data_row").toHaveCount(2);
await contains(".o_data_row td").click();
expect('.modal div[name="partner_ids"] .o_list_renderer').toHaveCount(1);
});
test.tags("desktop");
test("one2many (who contains a one2many) with list view and without form view", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_data_row td").click();
expect('.modal div[name="turtle_foo"]').toHaveText("blip");
});
test.tags("desktop");
test("one2many with x2many in form view (but not in list view)", async () => {
expect.assertions(1);
// avoid error when saving the edited related record (because the
// related x2m field is unknown in the inline list view)
// also ensure that the changes are correctly saved
onRpc("web_save", (args) => {
expect(args.args[1].turtles).toEqual([
[
1,
2,
{
partner_ids: [[4, 1]],
},
],
]);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_data_row td").click(); // edit first record
await contains('div[name="partner_ids"] input').click();
await contains('div[name="partner_ids"] .o_input_dropdown li').click();
// add a many2many tag and save
await contains(".modal .o_field_many2many_tags input").edit("test", { confirm: false });
await contains(".modal .modal-footer .btn-primary").click(); // save
await clickSave();
});
test.tags("desktop");
test("many2many list in a one2many opened by a many2one", async () => {
expect.assertions(1);
Turtle._records[1].turtle_trululu = 2;
Partner._views = { form: '
`,
resId: 1,
});
await contains(".o_data_row td").click();
expect('.modal div[name="turtle_foo"]').toHaveText("blip");
});
test("open a record in a one2many list (mode 'readonly') with a notebook", async () => {
Turtle._views = {
form: `
`,
resId: 4,
});
expect(".o_field_widget .o_kanban_renderer").toHaveCount(1, {
message: "should have one inner kanban view for the one2many field",
});
expect(".o_field_widget .o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost)").toHaveCount(
0,
{ message: "should not have kanban records yet" }
);
// create a new kanban record
await contains(".o_field_widget .o-kanban-button-new").click();
// save & close the modal
expect(".modal-content .o_field_widget input").toHaveValue("My little Foo Value", {
message: "should already have the default value for field foo",
});
await contains(".modal .o_form_button_save").click();
expect(".o_field_widget .o_kanban_renderer").toHaveCount(1, {
message: "should have one inner kanban view for the one2many field",
});
expect(".o_field_widget .o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost)").toHaveCount(
1,
{ message: "should now have one kanban record" }
);
expect(
".o_field_widget .o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost) .o_test_id"
).toHaveText("", { message: "should not have a value for the id field" });
expect(
".o_field_widget .o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost) .o_test_foo"
).toHaveText("My little Foo Value", { message: "should have a value for the foo field" });
// save the view to force a create of the new record in the one2many
await clickSave();
expect(".o_field_widget .o_kanban_renderer").toHaveCount(1, {
message: "should have one inner kanban view for the one2many field",
});
expect(".o_field_widget .o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost)").toHaveCount(
1,
{ message: "should now have one kanban record" }
);
expect(
".o_field_widget .o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost) .o_test_id"
).toHaveText("5", { message: "should now have a value for the id field" });
expect(
".o_field_widget .o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost) .o_test_foo"
).toHaveText("My little Foo Value", { message: "should still have a value for the foo field" });
});
test("one2many field with virtual ids with kanban button", async () => {
expect.assertions(23);
Partner._records[0].p = [4];
onRpc("web_save", (args) => {
expect.step(args.method);
expect(args.args[1].p).toHaveLength(1);
const command = args.args[1].p[0];
expect(command[0]).toBe(0);
expect(command[2]).toEqual({
foo: "My little Foo Value",
});
});
mockService("action", {
doActionButton: (params) => {
const { name, resModel, resId } = params;
expect.step(`${name}_${resModel}_${resId}`);
params.onClose();
},
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// 1. Define all css selector
const oKanbanView = ".o_field_widget .o_kanban_renderer";
const oKanbanRecordActive = oKanbanView + " .o_kanban_record:not(.o_kanban_ghost)";
const oAllKanbanButton = oKanbanRecordActive + " button";
const btn1 = oKanbanRecordActive + ":eq(0) button";
const btn2 = oKanbanRecordActive + ":eq(1) button";
const btn1Warn = btn1 + '[name="button_warn"]';
const btn1Disabled = btn1 + '[name="button_disabled"]';
const btn2Warn = btn2 + '[name="button_warn"]';
const btn2Disabled = btn2 + '[name="button_disabled"]';
// check if we already have one kanban card
expect(oKanbanView).toHaveCount(1, {
message: "should have one inner kanban view for the one2many field",
});
expect(oKanbanRecordActive).toHaveCount(1, { message: "should have one kanban records yet" });
// we have 2 buttons
expect(oAllKanbanButton).toHaveCount(2);
// disabled ?
expect(oAllKanbanButton + "[disabled]").toHaveCount(0, {
message: "should not have button type object disabled",
});
// click on the button
await contains(btn1Disabled).click();
expect.verifySteps(["button_disabled_partner_4"]);
await contains(btn1Warn).click();
expect.verifySteps(["button_warn_partner_4"]);
// click on existing buttons
await contains(btn1Disabled).click();
expect.verifySteps(["button_disabled_partner_4"]);
await contains(btn1Warn).click();
expect.verifySteps(["button_warn_partner_4"]);
// create new kanban record
await contains(".o_field_widget .o-kanban-button-new").click();
// save & close the modal
expect(".modal-content .o_field_widget input").toHaveValue("My little Foo Value", {
message: "should already have the default value for field foo",
});
await contains(".modal .o_form_button_save").click();
// check new item
expect(oAllKanbanButton).toHaveCount(4);
expect(btn1).toHaveCount(2);
expect(btn2).toHaveCount(2);
expect(oAllKanbanButton + "[disabled]").toHaveCount(0, {
message: "should have 1 button type object disabled",
});
expect(btn2Disabled).toBeEnabled();
expect(btn2Warn).toBeEnabled();
expect(btn2Warn).toHaveAttribute("warn", "warn", {
message: "Should have a button type object with warn attr in area 2",
});
// click all buttons
await contains(btn1Disabled).click();
expect.verifySteps(["web_save", "button_disabled_partner_4"]);
await contains(btn1Warn).click();
await contains(btn2Disabled).click();
await contains(btn2Warn).click();
expect.verifySteps([
"button_warn_partner_4",
"button_disabled_partner_5",
"button_warn_partner_5",
]);
// save the form
expect(".o_form_saved").toHaveCount(1);
// click all buttons
await contains(btn1Disabled).click();
await contains(btn1Warn).click();
await contains(btn2Disabled).click();
await contains(btn2Warn).click();
// should have clicked once on every button
expect.verifySteps([
"button_disabled_partner_4",
"button_warn_partner_4",
"button_disabled_partner_5",
"button_warn_partner_5",
]);
});
test("focusing fields in one2many list", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_data_row").toHaveCount(1);
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(2);
expect("tr.o_data_row input").toHaveValue("default foo turtle", {
message: "second row should be the new value",
});
expect("tr.o_data_row:eq(1)").toHaveClass("o_selected_row");
await clickSave();
});
test("one2many list editable - should properly unselect the list field after shift+tab", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
`,
resId: 1,
});
await contains(".o_data_row td:first-child").click();
expect(".o_selected_row").toHaveCount(1);
const events = await press("Shift+Tab");
await animationFrame();
expect(".o_selected_row").toHaveCount(0, { message: "list should not be in edition" });
// We also check the event is not default prevented, to make sure that the
// event flows and selection goes to the previous field.
expect(events[0].defaultPrevented).toBe(false);
expect(events[1].defaultPrevented).toBe(false);
});
test("one2many list editable - should not allow tab navigation focus on the optional field toggler", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
`,
resId: 1,
});
expect('.o_field_widget[name="int_field"] input').toHaveValue("0");
intFieldVal = 1;
await contains(".o_field_x2many_list_row_add a").click();
expect('.o_field_widget[name="int_field"] input').toHaveValue("0");
expect.verifySteps(["get_views", "web_read", "onchange"]);
await contains('.o_field_widget[name="turtle_foo"] input').edit("some text", {
confirm: "blur",
});
expect.verifySteps(["onchange"]);
expect('.o_field_widget[name="int_field"] input').toHaveValue("1");
});
test.tags("desktop");
test("one2many list editable: trigger onchange when row is valid", async () => {
// should omit require fields that aren't in the view as they (obviously)
// have no value, when checking the validity of required fields
// shouldn't consider numerical fields with value 0 as unset
Turtle._fields.turtle_foo = fields.Char({ required: true });
Turtle._fields.turtle_bar = fields.Boolean({ required: true });
Turtle._fields.turtle_int = fields.Integer({ required: true, default: 0 }); // required int field (default 0)
Turtle._fields.partner_ids = fields.Many2many({ relation: "partner", required: true }); // required many2many
let intFieldVal = 0;
Partner._onChanges = {
turtles: function (obj) {
obj.int_field = intFieldVal;
},
};
Partner._records[0].int_field = 0;
Partner._records[0].turtles = [];
Turtle._views = {
list: `
`,
};
onRpc((args) => {
expect.step(args.method);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect('.o_field_widget[name="int_field"] input').toHaveValue("0", {
message: "int_field should start with value 0",
});
intFieldVal = 1;
// add a new row (which is invalid at first)
await contains(".o_field_x2many_list_row_add a").click();
expect('.o_field_widget[name="int_field"] input').toHaveValue("0", {
message: "int_field should still be 0 (no onchange should have been done yet)",
});
expect.verifySteps(["get_views", "web_read", "onchange"]);
// fill turtle_foo field
await contains('.o_field_widget[name="turtle_foo"] input').edit("some text", {
confirm: false,
});
expect('.o_field_widget[name="int_field"] input').toHaveValue("0", {
message: "int_field should still be 0 (no onchange should have been done yet)",
});
// no onchange should have been applied
expect.verifySteps([]);
// fill partner_ids field with a tag (all required fields will then be set)
await selectFieldDropdownItem("partner_ids", "first record");
expect('.o_field_widget[name="int_field"] input').toHaveValue("1", {
message: "int_field should now be 1 (the onchange should have been done",
});
expect.verifySteps(["name_search", "web_read", "onchange"]);
});
test("one2many list editable: 'required' modifiers is properly working", async () => {
Partner._onChanges = {
turtles: function (obj) {
obj.int_field = 44;
},
};
Partner._records[0].turtles = [];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect('.o_field_widget[name="int_field"] input').toHaveValue("10");
await contains(".o_field_x2many_list_row_add a").click();
expect('.o_field_widget[name="int_field"] input').toHaveValue("10");
// fill turtle_foo field
await contains('.o_field_widget[name="turtle_foo"] input').edit("some text");
expect('.o_field_widget[name="int_field"] input').toHaveValue("44");
});
test("one2many list editable: 'required' modifiers is properly working, part 2", async () => {
Partner._onChanges = {
turtles: function (obj) {
obj.int_field = 44;
},
};
Partner._records[0].turtles = [];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect('.o_field_widget[name="int_field"] input').toHaveValue("10");
await contains(".o_field_x2many_list_row_add a").click();
expect('.o_field_widget[name="int_field"] input').toHaveValue("10");
// fill turtle_int field
await contains('.o_field_widget[name="turtle_int"] input').edit("1");
expect('.o_field_widget[name="int_field"] input').toHaveValue("44");
});
test.tags("desktop");
test("one2many list editable: add new line before onchange returns", async () => {
// If the user adds a new row (with a required field with onchange), selects
// a value for that field, then adds another row before the onchange returns,
// the editable list must wait for the onchange to return before trying to
// unselect the first row, otherwise it will be detected as invalid.
Turtle._onChanges = {
turtle_trululu: function () {},
};
let def;
onRpc("onchange", () => def);
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
// add a first line but hold the onchange back
await contains(".o_field_x2many_list_row_add a").click();
def = new Deferred();
expect(".o_data_row").toHaveCount(1);
await clickFieldDropdown("turtle_trululu");
await press("Enter");
await animationFrame();
// try to add a second line and check that it is correctly waiting
// for the onchange to return
await contains(".o_field_x2many_list_row_add a").click();
expect(".modal").toHaveCount(0);
expect(".o_field_invalid").toHaveCount(0);
expect(".o_data_row").toHaveCount(1);
expect(".o_data_row").toHaveClass("o_selected_row");
// resolve the onchange promise
def.resolve();
await animationFrame();
expect(".o_data_row").toHaveCount(2);
expect(".o_data_row").not.toHaveClass("o_selected_row");
});
test("editable list: multiple clicks on Add an item do not create invalid rows", async () => {
Turtle._onChanges = {
turtle_trululu: function () {},
};
let def;
onRpc("onchange", () => def);
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
def = new Deferred();
// click twice to add a new line
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(0);
// resolve the onchange promise
def.resolve();
await animationFrame();
expect(".o_data_row").toHaveCount(1);
expect(".o_data_row").toHaveClass("o_selected_row");
});
test("editable list: value reset by an onchange", async () => {
// this test reproduces a subtle behavior that may occur in a form view:
// the user adds a record in a one2many field, and directly clicks on a
// datetime field of the form view which has an onchange, which totally
// erases the value from the one2many (command 2 + command 0). The handler
// that switches the edited row to readonly is then called after the
// new value of the one2many field is applied (the one returned by the
// onchange), so the row that must go to readonly doesn't exist anymore.
Partner._onChanges = {
datetime: function (obj) {
if (obj.turtles.length) {
obj.turtles = [
[2, obj.turtles[0][1]],
[0, 0, { name: "new" }],
];
}
},
};
let def;
onRpc("onchange", () => def);
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_data_row").toHaveCount(1);
// open the dialog
await contains(".o_data_row td").click();
expect(".modal .o_form_editable").toHaveCount(1);
expect(".modal .o_data_row").toHaveCount(1);
// open the o2m again, in the dialog
await contains(".modal .o_data_row td").click();
expect(".modal .o_form_editable").toHaveCount(2);
// edit the name and click save modal that is on top
await contains(".modal:eq(1) .o_field_widget[name=name] input").edit("new name", {
confirm: false,
});
await contains(".modal:eq(1) .modal-footer .btn-primary").click();
expect(".modal .o_form_editable").toHaveCount(1);
// click save on the other modal
await contains(".modal .modal-footer .btn-primary").click();
expect(".modal").toHaveCount(0);
// save the main record
await clickSave();
});
test("onchange and required fields with override in arch", async () => {
Partner._onChanges = {
turtles: function () {},
};
Turtle._fields.turtle_foo = fields.Char({ required: true });
Partner._records[0].turtles = [];
onRpc((args) => {
expect.step(args.method);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// triggers an onchange on partner, because the new record is valid
await contains(".o_field_x2many_list_row_add a").click();
expect.verifySteps(["get_views", "web_read", "onchange", "onchange"]);
});
test("onchange on a one2many containing a one2many", async () => {
// the purpose of this test is to ensure that the onchange specs are
// correctly and recursively computed
expect.assertions(1);
Partner._onChanges = {
p: function () {},
};
let checkOnchange = false;
onRpc("onchange", (args) => {
if (checkOnchange) {
expect(args.args[3]).toEqual({
display_name: {},
p: {
fields: {
name: {},
p: {
fields: {
name: {},
},
limit: 40,
order: "",
},
},
limit: 40,
order: "",
},
});
}
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
// edit the line and enter an invalid value for int_field
await contains(".o_data_row .o_data_cell:eq(1)").click();
await contains(".o_field_widget[name=int_field] input").edit("e", { confirm: false });
await contains(".o_form_view").click();
expect(".o_data_row.o_selected_row").toHaveCount(1, {
message: "line should not have been removed and should still be in edition",
});
expect(".modal").toHaveCount(0, { message: "a confirmation dialog should not be opened" });
expect(".o_field_widget[name=int_field]").toHaveClass("o_field_invalid");
});
test("one2many with invalid value and click on another row", async () => {
Partner._records[0].p = [2, 4];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_data_row:eq(0) .o_data_cell").click();
expect(".o_data_row.o_selected_row").toHaveCount(1);
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
expect(".o_data_row:eq(1)").not.toHaveClass("o_selected_row");
await contains(".o_data_row [name='int_field'] input").edit("abc", { confirm: false });
await contains(".o_data_row:eq(1) .o_data_cell").click();
// Stays on the invalid row
expect(".o_data_row.o_selected_row").toHaveCount(1);
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
expect(".o_data_row:eq(0) [name='int_field'] .o_field_invalid").toHaveCount(1);
expect(".o_data_row:eq(1)").not.toHaveClass("o_selected_row");
});
test("default value for nested one2manys (coming from onchange)", async () => {
expect.assertions(3);
Partner._onChanges.p = function (obj) {
obj.p = [
[5],
[0, 0, { turtles: [[5], [4, 1, false]] }], // link record 1 by default
];
};
onRpc("web_save", (args) => {
expect(args.args[1].p[0][0]).toBe(0, {
message: "should send a command 0 (CREATE) for p",
});
expect(args.args[1].p[0][2]).toEqual(
{ turtles: [[4, 1]] },
{ message: "should send the correct values" }
);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_data_row .o_data_cell").toHaveText("blip");
// click and edit value to 'foo', which will trigger onchange
await contains(".o_data_row .o_data_cell").click();
await contains(".o_field_widget[name=turtle_foo] input").edit("foo", { confirm: false });
await contains(".o_form_view").click();
expect(".o_data_row .o_data_cell").toHaveText("foo");
// click and edit value to 'pinky', which trigger a failed onchange
await contains(".o_data_row .o_data_cell").click();
await contains(".o_field_widget[name=turtle_foo] input").edit("pinky", { confirm: false });
await contains(".o_form_view").click();
expect(".o_data_row .o_data_cell").toHaveText("foo");
// we make sure here that when we save, the values are the current
// values displayed in the field.
await clickSave();
});
test("propagate context to sub views without default_* keys", async () => {
expect.assertions(4);
onRpc("onchange", (args) => {
expect(args.kwargs.context.flutter).toBe("shy", {
message: "view context key should be used for every rpcs",
});
if (args.model === "partner") {
expect(args.kwargs.context.default_flutter).toBe("why", {
message: "should have default_* values in context for form view RPCs",
});
} else if (args.model === "turtle") {
expect(args.kwargs.context.default_flutter).toBe(undefined, {
message: "should not have default_* values in context for subview RPCs",
});
}
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
context: {
flutter: "shy",
default_flutter: "why",
},
});
await contains(".o_field_x2many_list_row_add a").click();
await contains('[name="turtle_foo"] input').edit("pinky pie", { confirm: false });
await clickSave();
});
test("nested one2manys with no widget in list and as invisible list in form", async () => {
Partner._records[0].p = [1];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// starting condition
expect(queryAllTexts(".o_data_row")).toEqual(["blip", "yop", "kawa"]);
// click add a new line
// save the record
// check line is at the correct place
const inputText = "ninja";
await contains(".o_field_x2many_list_row_add a").click();
await contains('[name="turtle_foo"] input').edit(inputText, { confirm: false });
await clickSave();
expect(queryAllTexts(".o_data_row")).toEqual(["blip", "yop", "kawa", inputText]);
});
test("one2many with sequence field, override default_get, top when inline", async () => {
Partner._records[0].turtles = [3, 2, 1];
Turtle._fields.turtle_int = fields.Integer({ default: 10 });
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// starting condition
expect(queryAllTexts(".o_data_row")).toEqual(["blip", "yop", "kawa"]);
// click add a new line
// save the record
// check line is at the correct place
const inputText = "ninja";
await contains(".o_field_x2many_list_row_add a").click();
await contains('[name="turtle_foo"] input').edit(inputText, { confirm: false });
await clickSave();
expect(queryAllTexts(".o_data_row")).toEqual([inputText, "blip", "yop", "kawa"]);
});
test("one2many with sequence field, override default_get, bottom when popup", async () => {
Partner._records[0].turtles = [3, 2, 1];
Turtle._fields.turtle_int = fields.Integer({ default: 10 });
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// starting condition
expect(queryAllTexts(".o_data_row")).toEqual(["blip", "yop", "kawa"]);
// click add a new line
// save the record
// check line is at the correct place
const inputText = "ninja";
await contains(".o_field_x2many_list_row_add a").click();
await contains('.modal [name="turtle_foo"] input').edit(inputText, { confirm: false });
await contains(".modal .o_form_button_save").click();
expect(queryAllTexts(".o_data_row")).toEqual(["blip", "yop", "kawa", inputText]);
await clickSave();
expect(queryAllTexts(".o_data_row")).toEqual(["blip", "yop", "kawa", inputText]);
});
test("one2many with sequence field, override default_get, not last page", async () => {
Partner._records[0].turtles = [3, 2, 1];
Turtle._fields.turtle_int = fields.Integer({ default: 5 });
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// click add a new line
// check turtle_int for new is the current max of the page
await contains(".o_field_x2many_list_row_add a").click();
expect('.modal [name="turtle_int"] input').toHaveValue("9");
});
test("one2many with sequence field, override default_get, last page", async () => {
Partner._records[0].turtles = [3, 2, 1];
Turtle._fields.turtle_int = fields.Integer({ default: 10 });
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// click add a new line
// check turtle_int for new is the current max of the page +1
await contains(".o_field_x2many_list_row_add a").click();
expect('.modal [name="turtle_int"] input').toHaveValue("22");
});
test("one2many with sequence field and text field", async () => {
Turtle._fields.turtle_int = fields.Integer({ default: 10 });
Turtle._fields.product_id = fields.Many2one({
relation: "product",
required: true,
default: 37,
});
Turtle._fields.not_required_product_id = fields.Many2one({
string: "Product",
relation: "product",
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
// starting condition
expect(".o_data_cell").toHaveCount(0);
const inputText1 = "relax";
const inputText2 = "max";
await contains(".o_field_x2many_list_row_add a").click();
await contains('div[name="turtle_foo"] input').edit(inputText1, { confirm: false });
await contains(".o_field_x2many_list_row_add a").click();
await contains('div[name="turtle_foo"] input').edit(inputText2, { confirm: false });
await contains(".o_field_x2many_list_row_add a").click();
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([inputText1, inputText2, ""]);
expect(".ui-sortable-handle").toHaveCount(3);
await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop("tbody tr:eq(0)");
// empty line has been discarded on the drag and drop)
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([inputText2, inputText1]);
});
test("one2many with several pages, onchange and default order", async () => {
// This test reproduces a specific scenario where a one2many is displayed
// over several pages, and has a default order such that a record that
// would normally be on page 1 is actually on another page. Moreover,
// there is an onchange on that one2many which converts all commands 4
// (LINK_TO) into commands 1 (UPDATE), which is standard in the ORM.
// This test ensures that the record displayed on page 2 is never fully
// read.
Partner._records[0].turtles = [1, 2, 3];
Turtle._records[0].partner_ids = [1];
Partner._onChanges = {
turtles: function (obj) {
const res = obj.turtles.map((command) => {
if (command[0] === 1) {
// already an UPDATE command: do nothing
return command;
}
// convert LINK_TO commands to UPDATE commands
const id = command[1];
const record = Turtle._records.find((record) => record.id === id);
return [1, id, pick(record, "turtle_int", "turtle_foo", "partner_ids")];
});
obj.turtles = res;
},
};
onRpc((args) => {
const ids = args.method === "web_read" ? " [" + args.args[0] + "]" : "";
expect.step(args.method + ids);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(queryAllTexts(".o_data_cell.foo")).toEqual(["blip", "kawa"]);
// edit turtle_int field of first row
await contains(".o_data_cell").click();
await contains(".o_data_row .o_field_widget[name=turtle_int] input").edit(3, {
confirm: false,
});
await contains(".o_form_view").click();
expect(queryAllTexts(".o_data_cell.foo")).toEqual(["blip", "kawa"]);
expect.verifySteps([
"get_views",
"web_read [1]", // main record
"onchange",
// this test's purpose is to assert that this rpc isn't
// done, but yet it is. Actually, it wasn't before because mockOnChange
// returned [1] as command list, instead of [[6, false, [1]]], so basically
// this value was ignored. Now that mockOnChange properly works, the value
// is taken into account but the basicmodel doesn't care it concerns a
// record of the second page, and does the read. I don't think we
// introduced a regression here, this test was simply wrong...
]);
});
test("one2many with several pages, onchange return command update on unknown record (readonly field)", async () => {
Turtle._fields.turtle_int = fields.Integer({ readonly: true });
Partner._onChanges = {
foo: function (obj) {
obj.turtles = [[1, 3, { turtle_int: 57, turtle_foo: "yop" }]];
},
};
onRpc("web_save", ({ args }) => {
expect(args[0]).toEqual([1]);
// for unknownCommand, we should not send readonly fields
expect(args[1]).toEqual({
foo: "blip",
turtles: [[1, 3, { turtle_foo: "yop" }]],
});
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_field_widget[name=foo] input").edit("blip", { confirm: false });
await clickSave();
});
test("new record, with one2many with more default values than limit", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
context: { default_turtles: [1, 2, 3] },
});
expect(queryAllTexts(".o_data_row")).toEqual(["yop", "blip"]);
await clickSave();
expect(queryAllTexts(".o_data_row")).toEqual(["yop", "blip"]);
});
test("add a new line after limit is reached should behave nicely", async () => {
Partner._records[0].turtles = [1, 2, 3];
Partner._onChanges = {
turtles: function () {},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(4);
await contains('div[name="turtle_foo"] .o_input').edit("a", { confirm: false });
expect(".o_data_row").toHaveCount(4, {
message: "should still have 4 data rows (the limit is increased to 4)",
});
});
test.tags("desktop");
test("onchange in a one2many with non inline view on an existing record", async () => {
Partner._fields.sequence = fields.Integer({ string: "Sequence", type: "integer" });
Partner._records[0].sequence = 1;
Partner._records[1].sequence = 2;
Partner._onChanges = { sequence: function () {} };
PartnerType._fields.partner_ids = fields.One2many({
string: "Partner",
relation: "partner",
});
PartnerType._records[0].partner_ids = [1, 2];
Partner._views = {
list: `
`,
};
onRpc((args) => {
expect.step(args.method);
});
await mountView({
type: "form",
resModel: "partner.type",
arch: `
`,
resId: 12,
});
// swap 2 lines in the one2many
await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop("tbody tr");
expect.verifySteps(["get_views", "get_views", "web_read", "onchange", "onchange"]);
});
test.tags("desktop");
test("onchange in a one2many with non inline view on a new record", async () => {
Turtle._onChanges = {
name: function (obj) {
if (obj.name) {
obj.turtle_int = 44;
}
},
};
Turtle._views = {
list: `
`,
};
onRpc((args) => {
expect.step(args.method || args.route);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
// add a row and trigger the onchange
await contains(".o_field_x2many_list_row_add a").click();
await contains('.o_data_row div[name="name"] input').edit("a name", { confirm: "blur" });
expect(".o_field_cell[name=turtle_int]").toHaveText("44");
expect.verifySteps([
"get_views", // load main form
"get_views", // load sub list
"onchange", // main record
"onchange", // sub record
"onchange", // edition of name of sub record
]);
});
test.tags("desktop");
test('add a line, edit it and "Save & New"', async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
expect(".o_data_row").toHaveCount(0);
// add a new record
await contains(".o_field_x2many_list_row_add a").click();
await contains(".modal .o_field_widget input").edit("new record", { confirm: false });
await contains(".modal .o_form_button_save").click();
expect(queryAllTexts(".o_data_row .o_data_cell")).toEqual(["new record"]);
// reopen freshly added record and edit it
await contains(".o_data_row .o_data_cell").click();
await contains(".modal .o_field_widget input").edit("new record edited", { confirm: false });
// save it, and choose to directly create another record
await contains(".modal .modal-footer .btn-primary:eq(1)").click();
expect(".modal").toHaveCount(1);
expect(".modal .o_field_widget").toHaveText("");
await contains(".modal .o_field_widget input").edit("another new record", { confirm: false });
await contains(".modal .o_form_button_save").click();
expect(queryAllTexts(".o_data_row .o_data_cell")).toEqual([
"new record edited",
"another new record",
]);
});
test.tags("desktop");
test('add a line with a context depending on the parent record, created a second record with "Save & New"', async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
await contains('button[name="test_button"]').click();
expect.verifySteps(["test_button"]);
});
test("o2m add a line custom control create align with handle", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
// controls correctly added, at one column offset when handle is present
expect(".o_list_table tr:eq(1) td").toHaveCount(2);
expect(".o_list_table tr:eq(1) td:eq(0)").toHaveText("");
expect(".o_list_table tr:eq(1) td:eq(1)").toHaveText("Add a line");
});
test.tags("desktop");
test("one2many form view with action button", async () => {
// once the action button is clicked, the record is reloaded (via the
// onClose handler, executed because the python method does not return
// any action, or an ir.action.act_window_close) ; this test ensures that
// it reloads the fields of the opened view (i.e. the form in this case).
// See https://github.com/odoo/odoo/issues/24189
mockService("action", {
doActionButton(params) {
Partner._records[1].name = "new name";
Partner._records[1].timmy = [12];
params.onClose();
},
});
Partner._records[0].p = [2];
PartnerType._views = {
list: `
`,
};
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
`,
});
expect(".o_data_row").toHaveCount(1);
expect(".o_data_cell").toHaveText("second record");
// open one2many record in form view
await contains(".o_data_cell").click();
expect(".modal .o_form_view").toHaveCount(1);
expect(".modal .o_form_view .o_data_row").toHaveCount(0);
// click on the action button
await contains(".modal .o_form_editable button").click();
expect(".modal .o_data_row").toHaveCount(1);
expect(".modal .o_data_cell").toHaveText("gold");
// save the dialog
await contains(".modal .modal-footer .btn-primary").click();
expect(".o_data_cell").toHaveText("new name");
});
test.tags("desktop");
test("onchange affecting inline unopened list view", async () => {
let numUserOnchange = 0;
Users._onChanges = {
partner_ids: function (obj) {
numUserOnchange++;
},
};
await mountView({
type: "form",
resModel: "res.users",
arch: `
`,
resId: 17,
});
// add a turtle on second partner
await contains(".o_data_row:eq(1) .o_data_cell").click();
await contains(".modal .o_field_x2many_list_row_add a").click();
await contains(".modal .o_field_widget[name=name] input").edit("michelangelo", {
confirm: false,
});
await contains(".modal .btn-primary").click();
// open first partner so changes from previous action are applied
await contains(".o_data_row .o_data_cell").click();
await contains(".modal .btn-primary").click();
await clickSave();
expect(numUserOnchange).toBe(1, {
message: "there should 1 and only 1 onchange from closing the partner modal",
});
await contains(".o_data_row .o_data_cell").click();
expect(".modal .o_data_row").toHaveCount(1, { message: "only 1 turtle for first partner" });
expect(".modal .o_data_cell").toHaveText("donatello");
await contains(".modal .modal-footer .btn-primary").click(); // Close
await contains(".o_data_row:eq(1) .o_data_cell").click();
expect(".modal .o_data_row").toHaveCount(1, { message: "only 1 turtle for second partner" });
expect(".modal .o_data_cell").toHaveText("michelangelo");
await contains(".modal .o_form_button_cancel").click();
});
test("click on URL should not open the record", async () => {
Partner._records[0].turtles = [1];
// avoid to open a new tab or the mail app
const onClick = (ev) => {
expect.step("link clicked");
ev.preventDefault();
};
browser.addEventListener("click", onClick, { capture: true });
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_email_cell a").click();
expect(".modal").toHaveCount(0);
expect.verifySteps(["link clicked"]);
await contains(".o_url_cell a").click();
expect(".modal").toHaveCount(0);
expect.verifySteps(["link clicked"]);
});
test.tags("desktop");
test("create and edit on m2o in o2m, and press ESCAPE", async () => {
Partner._views = {
form: `
`,
});
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_selected_row").toHaveCount(1);
await clickFieldDropdown("turtle_trululu");
await contains("[name=turtle_trululu] input").edit("ABC", { confirm: false });
await runAllTimers();
await clickFieldDropdownItem("turtle_trululu", "Create and edit...");
expect(".modal .o_form_view").toHaveCount(1);
await press("Escape");
await animationFrame();
expect(".modal .o_form_view").toHaveCount(0);
expect(".o_selected_row").toHaveCount(1);
expect(".o_selected_row [name=turtle_trululu] input").toBeFocused();
});
test.tags("desktop");
test("one2many add a line should not crash if orderedResIDs is not set on desktop", async () => {
mockService("action", {
doActionButton(args) {
return Promise.reject();
},
});
Partner._records[0].turtles = [];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
await contains('button[name="post"]').click();
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row.o_selected_row").toHaveCount(1);
});
test.tags("mobile");
test("one2many add a line should not crash if orderedResIDs is not set on mobile", async () => {
mockService("action", {
doActionButton(args) {
return Promise.reject();
},
});
Partner._records[0].turtles = [];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
await contains('button[name="post"]').click();
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row.o_selected_row").toHaveCount(1);
});
test("one2many shortcut tab should not crash when there is no input widget", async () => {
// create a one2many view which has no input (only 1 textarea in this case)
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// add a row, fill it, then trigger the tab shortcut
await contains(".o_field_x2many_list_row_add a").click();
// This is not how it should happen but non trusted event listeners are called sooner than
// trusted ones so the update is called after the list's tab listener in which case the field is
// not dirty when we press tab, therefore we need to set it dirty through onChange before pressing tab
// so in practice we could only run the following line but it wont work since the tab keydown event is not trusted
// await contains("[name=turtle_foo] textarea").edit("ninja", { confirm: false });
await contains("[name=turtle_foo] textarea").edit("ninja", { confirm: "blur" });
await contains("[name=turtle_foo]:eq(2)").click();
expect("[name=turtle_foo] textarea").toBeFocused();
await press("tab");
await animationFrame();
expect(queryAllTexts(".o_field_text")).toEqual(["blip", "ninja", ""]);
expect(".o_field_text textarea").toHaveCount(1);
});
test("o2m add a line custom control create editable with 'tab'", async () => {
onRpc("onchange", ({ kwargs }) => {
expect.step("onchange");
expect(kwargs.context.default_turtle_foo).toBe("soft");
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_data_row .o_data_cell").click();
// This is not how it should happen but non trusted event listeners are called sooner than
// trusted ones so the update is called after the list's tab listener in which case the field is
// not dirty when we press tab, therefore we need to set it dirty through onChange before pressing tab
// so in practice we could only run the following line but it wont work since the tab keydown event is not trusted
// await contains("[name=turtle_foo] textarea").edit("Test", { confirm: false });
await contains("[name=turtle_foo] input").edit("Test", { confirm: "blur" });
await contains("[name=turtle_foo]").click();
expect(".o_data_row").toHaveCount(1);
await press("Tab");
await animationFrame();
expect(".o_data_row").toHaveCount(2);
expect.verifySteps(["onchange"]);
});
test("one2many with onchange, required field, shortcut enter", async () => {
Turtle._onChanges = {
turtle_foo: function () {},
};
let def;
onRpc("onchange", () => def);
onRpc((args) => {
expect.step(args.method);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
expect.verifySteps(["get_views", "onchange"]);
// add a new line
await contains(".o_field_x2many_list_row_add a").click();
expect.verifySteps(["onchange"]);
// we want to add a delay to simulate an onchange
def = new Deferred();
// write something in the field, edit will confirm with enter
await contains("[name=turtle_foo] input").edit("hello");
// check that nothing changed before the onchange finished
expect("[name=turtle_foo] input").toHaveValue("hello");
expect(".o_data_row").toHaveCount(1);
expect.verifySteps(["onchange"]);
// unlock onchange
def.resolve();
await animationFrame();
// check the current line is added with the correct content and a new line is editable
expect(".o_data_row").toHaveCount(2);
expect(".o_data_row:eq(0) [name=turtle_foo]").toHaveText("hello");
expect(".o_data_row:eq(1) [name=turtle_foo] input").toHaveValue("");
expect.verifySteps(["onchange"]);
});
test("edit a field with a slow onchange in one2many", async () => {
Turtle._onChanges = {
turtle_foo: function () {},
};
let def;
onRpc("onchange", () => def);
onRpc((args) => {
expect.step(args.method);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
expect.verifySteps(["get_views", "onchange"]);
// add a new line
await contains(".o_field_x2many_list_row_add a").click();
expect.verifySteps(["onchange"]);
// we want to add a delay to simulate an onchange
def = new Deferred();
// write something in the field
await contains("[name=turtle_foo] input").edit("hello", { confirm: false });
expect("[name=turtle_foo] input").toHaveValue("hello");
await contains(".o_form_view").click();
// check that nothing changed before the onchange finished
expect("[name=turtle_foo] input").toHaveValue("hello");
expect.verifySteps(["onchange"]);
// unlock onchange
def.resolve();
await animationFrame();
// check the current line is added with the correct content
expect(".o_data_row [name=turtle_foo]").toHaveText("hello");
});
test("no deadlock when leaving a one2many line with uncommitted changes", async () => {
// Before unselecting a o2m line, field widgets are asked to commit their changes (new values
// that they wouldn't have sent to the model yet). This test is added alongside a bug fix
// ensuring that we don't end up in a deadlock when a widget actually has some changes to
// commit at that moment.
onRpc((args) => {
expect.step(args.method);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_field_widget[name=turtles] input").edit("some foo value", {
confirm: false,
});
// click to add a second row to unselect the current one, then save
await contains(".o_field_x2many_list_row_add a").click();
await clickSave();
expect(".o_form_editable").toHaveCount(1);
expect(".o_data_row:eq(0)").toHaveText("some foo value");
expect.verifySteps([
"get_views", // main form view
"onchange", // main record
"onchange", // line 1
"onchange", // line 2
"web_save",
]);
});
test("one2many with extra field from server not in form", async () => {
onRpc("web_save", (args) => {
args.args[1].p[0][2].datetime = "2018-04-05 12:00:00";
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// Add a record in the list
await contains(".o_field_x2many_list_row_add a").click();
await contains(".modal div[name=name] input").edit("michelangelo", { confirm: false });
// Save the record in the modal (though it is still virtual)
await contains(".modal .btn-primary").click();
expect(".o_data_row").toHaveCount(1);
let cells = queryAll(".o_data_cell");
expect(cells[0]).toHaveText("");
expect(cells[1]).toHaveText("michelangelo");
// Save the whole thing
await clickSave();
// Redo asserts in RO mode after saving
expect(".o_data_row").toHaveCount(1);
cells = queryAll(".o_data_cell");
expect(cells[0]).toHaveText("04/05/2018 13:00:00");
expect(cells[1]).toHaveText("michelangelo");
});
test.tags("desktop");
test("one2many invisible depends on parent field", async () => {
Partner._records[0].p = [2];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect("th:not(.o_list_actions_header)").toHaveCount(2);
await selectFieldDropdownItem("product_id", "xphone");
expect("th:not(.o_list_actions_header)").toHaveCount(1, {
message: "should be 1 column when the product_id is set",
});
await contains(".o_field_many2one[name=product_id] input").clear({ confirm: "blur" });
expect("th:not(.o_list_actions_header)").toHaveCount(2, {
message: "should be 2 columns in the one2many when product_id is not set",
});
await contains(".o_field_boolean[name=bar] input").click();
expect("th:not(.o_list_actions_header)").toHaveCount(1, {
message: "should be 1 column after the value change",
});
});
test.tags("desktop");
test("column_invisible attrs on a button in a one2many list", async () => {
Partner._records[0].p = [2];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
expect(".o_data_row").toHaveCount(2);
expect(queryAllTexts('.o_field_widget[name="partner_ids"]')).toEqual([
"second record",
"second record\naaa",
]);
expect.verifySteps(["get_views", "onchange"]);
});
test("two one2many fields with same relation and _onChanges", async () => {
// this test simulates the presence of two one2many fields with _onChanges, such that
// changes to the first o2m are repercuted on the second one
Partner._fields.turtles2 = fields.One2many({
string: "Turtles 2",
type: "one2many",
relation: "turtle",
relation_field: "turtle_trululu",
});
Partner._onChanges = {
turtles: function (obj) {
// replicate changes on turtles2
if (obj.turtles.length) {
const command = obj.turtles2 && obj.turtles2[0];
if (command) {
// second onchange (with ABC): there's already a create command
obj.turtles2 = [[1, command[1], obj.turtles[0][2]]];
} else {
// first onchange (when adding the row): replicate the create command
obj.turtles2 = [[0, false, obj.turtles[0][2]]];
}
}
},
turtles2: () => {}, // simulate an onchange on turtles2 as well
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
// trigger first onchange by adding a line in turtles field (should add a line in turtles2)
await contains('.o_field_widget[name="turtles"] .o_field_x2many_list_row_add a').click();
await contains('.o_field_widget[name="turtles"] .o_field_widget[name="name"] input').edit(
"ABC",
{ confirm: "blur" }
);
expect('.o_field_widget[name="turtles"] .o_data_row').toHaveCount(1, {
message: "line of first o2m should have been created",
});
expect('.o_field_widget[name="turtles2"] .o_data_row').toHaveCount(1, {
message: "line of second o2m should have been created",
});
// add a line in turtles2
await contains('.o_field_widget[name="turtles2"] .o_field_x2many_list_row_add a').click();
await contains('.o_field_widget[name="turtles2"] .o_field_widget[name="name"] input').edit(
"DEF",
{ confirm: false }
);
expect('.o_field_widget[name="turtles"] .o_data_row').toHaveCount(1, {
message: "we should still have 1 line in turtles",
});
expect('.o_field_widget[name="turtles2"] .o_data_row').toHaveCount(2);
expect('.o_field_widget[name="turtles2"] .o_data_row:eq(1)').toHaveClass("o_selected_row");
await clickSave();
expect(queryAllTexts('.o_field_widget[name="turtles2"] .o_data_row')).toEqual(["ABC", "DEF"]);
});
test.tags("desktop");
test("one2many reset by onchange (of another field) while being edited", async () => {
// In this test, we have a many2one and a one2many. The many2one has an onchange that
// updates the value of the one2many. We set a new value to the many2one (name_create)
// such that the onchange is delayed. During the name_create, we click to add a new row
// to the one2many. After a while, we unlock the name_create, which triggers the onchange
// and resets the one2many. At the end, we want the row to be in edition.
const def = new Deferred();
Partner._onChanges = {
trululu: () => {},
};
onRpc("name_create", () => def);
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
// set a new value for trululu (will delay the onchange)
await contains(".o_field_widget[name=trululu] input").edit("new value", { confirm: false });
await runAllTimers();
await clickFieldDropdownItem("trululu", `Create "new value"`);
// add a row in p
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(0);
// resolve the name_create to trigger the onchange, and the reset of p
def.resolve();
await animationFrame();
expect(".o_data_row").toHaveCount(1);
expect(".o_data_row").toHaveClass("o_selected_row");
});
test("one2many with many2many_tags in list and list in form with a limit", async () => {
// This test encodes a limitation of the current model architecture:
// we have an nested x2manys, and the inner one is displayed as tags
// in the list, and as a list in the form. As both the list and the
// form will use the same Record datapoint, the config of their static
// list will be the same. We obviously don't want to see the limit
// applied on the tags (in the background) when opening the form. So
// the stategy is to keep the initial config, and to ignore the
// limit set on the list
Partner._records[0].p = [1];
Partner._records[0].turtles = [1, 2, 3];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_field_widget[name=p] .o_data_row").toHaveCount(1);
expect(".o_data_row .o_field_many2many_tags .badge").toHaveCount(3);
await contains(".o_data_cell").click();
expect(".modal .o_form_view").toHaveCount(1);
expect(".modal .o_field_widget[name=turtles] .o_data_row").toHaveCount(3);
expect(".modal .o_field_x2many_list .o_pager").not.toBeVisible();
});
test("one2many with many2many_tags in list and list in form, and onchange", async () => {
Partner._onChanges = {
bar: function (obj) {
obj.p = [[0, 0, { turtles: [[0, 0, { name: "new turtle" }]] }]];
},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
expect(".o_field_widget[name=p] .o_data_row").toHaveCount(1);
expect(".o_data_row .o_field_many2many_tags .badge").toHaveCount(1);
await contains(".o_data_row .o_data_cell").click();
expect(".modal .o_form_view").toHaveCount(1);
expect(".modal .o_field_widget[name=turtles] .o_data_row").toHaveCount(1);
expect(queryAllTexts(".modal .o_data_cell")).toEqual(["new turtle"]);
await contains(".modal .o_field_x2many_list_row_add a").click();
expect(".modal .o_field_widget[name=turtles] .o_data_row").toHaveCount(2);
expect(queryAllTexts(".modal .o_data_cell")).toEqual(["new turtle", ""]);
expect(".modal .o_field_widget[name=turtles] .o_data_row:eq(1)").toHaveClass("o_selected_row");
});
test("one2many with many2many_tags in list and list in form, and onchange (2)", async () => {
Partner._onChanges = {
bar: function (obj) {
obj.p = [
[
0,
0,
{
turtles: [
[
0,
0,
{
display_name: "new turtle",
},
],
],
},
],
];
},
};
Turtle._onChanges = {
turtle_foo: function (obj) {
obj.display_name = obj.turtle_foo;
},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_data_cell").click();
expect(".modal").toHaveCount(1);
expect(queryAllTexts(".modal [name='name']")).toEqual(["aaa", "first record"]);
await contains(".modal tr:eq(2) .o_handle_cell").dragAndDrop(".modal tr:eq(1)");
expect(queryAllTexts(".modal [name='name']")).toEqual(["first record", "aaa"]);
});
test("nested one2many, onchange, no command value", async () => {
// This test ensures that we always send all values to onchange rpcs for nested
// one2manys, even if some field hasn't changed. In this particular test case,
// a first onchange returns a value for the inner one2many, and a second onchange
// removes it, thus restoring the field to its initial empty value. From this point,
// the nested one2many value must still be sent to onchange rpcs (on the main record),
// as it might be used to compute other fields (so the fact that the nested o2m is empty
// must be explicit).
expect.assertions(1);
Turtle._fields.o2m = fields.One2many({
string: "o2m",
relation: "partner",
relation_field: "trululu",
});
Partner._onChanges.turtles = function (obj) {};
Turtle._onChanges.turtle_bar = function (obj) {};
let step = 1;
onRpc((args) => {
if (step === 3 && args.method === "onchange" && args.model === "partner") {
expect(args.args[1].turtles[0][2]).toEqual({
o2m: [],
turtle_bar: false,
});
}
if (args.model === "turtle") {
if (step === 2) {
return {
value: {
o2m: [[0, false, { name: "default" }]],
turtle_bar: true,
},
};
}
if (step === 3) {
const virtualId = args.args[1].o2m[0][1];
return {
value: { o2m: [[2, virtualId]] },
};
}
}
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
step = 2;
await contains(".o_field_x2many_list_row_add a").click();
step = 3;
await contains(".o_data_row .o_field_boolean input").click();
});
test("edition in list containing widget with decoration", async () => {
// We use here a badge widget and check its decoration is properly managed
// in this scenario (we need a widget with specific decoration handling)
Partner._records[0].p = [1, 2];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(queryAllTexts(".o_field_x2many_list .o_data_row .o_data_cell[name=id]")).toEqual([
"1",
"5",
"6",
"3",
]);
});
test("one2many from a model that has been sorted", async () => {
Partner._views = {
list: ``,
search: ``,
form: `
`,
};
Partner._records[0].turtles = [3, 2];
await mountWithCleanup(WebClient);
await getService("action").doAction({
id: 1,
name: "test",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
});
expect(".o_list_view").toHaveCount(1);
expect(queryAllTexts(".o_data_cell")).toEqual(["10", "9", "0"]);
await contains("th.o_column_sortable").click();
expect(queryAllTexts(".o_data_cell")).toEqual(["0", "9", "10"]);
await contains(".o_data_row:eq(2) .o_data_cell").click();
expect(".o_form_view").toHaveCount(1);
expect(queryAllTexts(".o_data_cell")).toEqual(["kawa", "blip"], {
message: "The o2m should not have been sorted.",
});
});
test("prevent the dialog in readonly x2many list view with option no_open True", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect('.o_data_row:contains("blip")').toHaveCount(1, {
message: "There should be one record in x2many list view",
});
await contains(".o_data_row .o_data_cell").click();
expect(".modal").toHaveCount(0, {
message: "There is should be no dialog open on click of readonly list row",
});
});
test("delete a record while adding another one in a multipage", async () => {
// in a one2many with at least 2 pages, add a new line. Delete the line above it.
// it should load the next line to display it on the page.
Partner._records[0].turtles = [2, 3];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// add a line (virtual record)
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_field_widget[name=turtle_foo] input").edit("pi", { confirm: false });
// delete the line above it
await contains(".o_list_record_remove").click();
// the next line should be displayed below the newly added one
expect(".o_data_row").toHaveCount(2);
expect(queryAllTexts(".o_data_cell")).toEqual(["pi", "", "kawa", ""], {
message: "should display the correct records on page 1",
});
});
test("one2many, onchange, edition and multipage...", async () => {
Partner._onChanges = {
turtles: function (obj) {
obj.turtles = [[5]].concat(obj.turtles);
},
};
Partner._records[0].turtles = [1, 2, 3];
onRpc((args) => {
expect.step(args.method + " " + args.model);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
// bar is false so there should be 1 column
expect(".o_list_renderer th:not(.o_list_actions_header)").toHaveCount(1);
expect(".o_list_renderer .o_data_row").toHaveCount(1);
// add a new o2m record
await contains(".o_field_x2many_list_row_add a").click();
triggerOnchange = true;
await contains(".o_field_one2many input").edit("New line", { confirm: false });
await contains(".o_form_view").click();
expect(".o_list_renderer th:not(.o_list_actions_header)").toHaveCount(2);
});
test.tags("desktop");
test("one2many column_invisible on view not inline", async () => {
Partner._records[0].p = [2];
Partner._views = {
list: `
`,
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect("th:not(.o_list_actions_header)").toHaveCount(2);
await selectFieldDropdownItem("product_id", "xphone");
expect("th:not(.o_list_actions_header)").toHaveCount(1, {
message: "should be 1 column when the product_id is set",
});
await contains(".o_field_many2one[name=product_id] input").clear({ confirm: "blur" });
expect("th:not(.o_list_actions_header)").toHaveCount(2, {
message: "should be 2 columns in the one2many when product_id is not set",
});
await contains(".o_field_boolean[name=bar] input").click();
expect("th:not(.o_list_actions_header)").toHaveCount(1, {
message: "should be 1 column after the value change",
});
});
test.tags("desktop");
test("one2many field in edit mode with optional fields and trash icon", async () => {
Partner._records[0].p = [2];
Partner._views = {
list: `
`,
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_field_one2many table .o_optional_columns_dropdown .dropdown-toggle").toHaveCount(1);
// should have 2 columns 1 for foo and 1 for trash icon, dropdown is displayed
// on trash icon cell, no separate cell created for trash icon and advanced field dropdown
expect(".o_field_one2many th").toHaveCount(2, {
message: "should be 2 th in the one2many edit mode",
});
expect(".o_field_one2many .o_data_row:first > td").toHaveCount(2, {
message: "should be 2 cells in the one2many in edit mode",
});
await contains(".o_optional_columns_dropdown .dropdown-toggle").click();
expect(".o-dropdown--menu .dropdown-item").toHaveCount(2, {
message: "dropdown have 2 advanced field foo with checked and bar with unchecked",
});
await contains(".o-dropdown--menu .dropdown-item:eq(1)").click();
expect(".o_field_one2many th").toHaveCount(3, {
message: "should be 3 th in the one2many after enabling bar column from advanced dropdown",
});
await contains(".o-dropdown--menu .dropdown-item").click();
expect(".o_field_one2many th").toHaveCount(2, {
message: "should be 2 th in the one2many after disabling foo column from advanced dropdown",
});
expect(".o-dropdown--menu .dropdown-item").toHaveCount(2, {
message: "dropdown is still open",
});
await contains(".o_field_x2many_list_row_add a").click();
expect(".o-dropdown--menu").toHaveCount(0, { message: "dropdown is closed" });
expect(".o_field_one2many tr.o_selected_row").toHaveCount(1);
await contains(".o_optional_columns_dropdown .dropdown-toggle").click();
await contains(".o-dropdown--menu .dropdown-item").click();
expect(".o_field_one2many tr.o_selected_row").toHaveCount(1);
expect(".o_field_one2many th").toHaveCount(3, {
message:
"should be 3 th in the one2many after re-enabling foo column from advanced dropdown",
});
// optional columns must be preserved after save
await clickSave();
expect(".o_field_one2many th").toHaveCount(3, {
message: "should have 3 th in the one2many after reloading whole form view",
});
});
test("x2many list sorted by many2one", async () => {
Partner._records[0].p = [1, 2, 4];
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
`,
});
expect(queryAllTexts(".o_data_row .o_list_number")).toEqual(["1", "2", "4"], {
message: "should have correct order initially",
});
await contains(".o_list_renderer thead th:eq(1)").click();
expect(queryAllTexts(".o_data_row .o_list_number")).toEqual(["4", "1", "2"], {
message: "should have correct order (ASC)",
});
await contains(".o_list_renderer thead th:eq(1)").click();
expect(queryAllTexts(".o_data_row .o_list_number")).toEqual(["2", "1", "4"], {
message: "should have correct order (DESC)",
});
});
test("one2many with extra field from server not in (inline) form", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
`,
});
// Add a record in the list
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_field_widget[name=name] input").edit("michelangelo", { confirm: false });
// Save the record in the modal (though it is still virtual)
await contains(".modal .modal-footer .btn-primary").click();
expect(".o_data_row").toHaveCount(1);
});
test("one2many with extra X2many field from server not in inline form", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
`,
});
// Add a first record in the list
await contains(".o_field_x2many_list_row_add a").click();
await contains(".modal .o_field_widget[name=name] input").edit("first", { confirm: false });
// Save & New
await contains(".modal .btn-primary:eq(1)").click();
await contains(".modal .o_field_widget[name=name] input").edit("second", { confirm: false });
// Save & Close
await contains(".modal .btn-primary").click();
expect(".o_data_row").toHaveCount(2);
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["first", "second"]);
});
test("when Navigating to a one2many with tabs, the button add a line receives the focus", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains("[name=qux] input").click();
expect("[name=qux] input").toBeFocused();
// next tabable element is notebook tab
await press("Tab");
// go inside one2many
await press("Tab");
await animationFrame();
expect(".o_field_x2many_list_row_add a").toBeFocused();
});
test("Navigate to a one2many with tab then tab again focus the next field", async () => {
Partner._records[0].turtles = [];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains("[name=qux] input").click();
expect("[name=qux] input").toBeFocused();
// next tabable element is notebook tab
await press("Tab");
// go inside one2many
await press("Tab");
await animationFrame();
expect(".o_field_x2many_list_row_add a").toBeFocused();
expect("[name=turtles] .o_selected_row").toHaveCount(0);
// trigger Tab event and check that the default behavior can happen.
expect(getNextFocusableElement()).toBe(queryOne("[name=foo] input"));
await press("Tab");
expect("[name=foo] input").toBeFocused();
});
test("when Navigating to a one2many with tabs, not filling any field and hitting tab, no line is added and the next field is focused", async () => {
Partner._records[0].turtles = [];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains("[name=qux] input").click();
expect("[name=qux] input").toBeFocused();
// next tabable element is notebook tab
await press("Tab");
// go inside one2many
await press("Tab");
await animationFrame();
expect(".o_field_x2many_list_row_add a").toBeFocused();
expect("[name=turtles] .o_selected_row").toHaveCount(0);
await contains(".o_field_x2many_list_row_add a").click();
expect("[name=turtle_foo] input").toBeFocused();
await press("Tab"); // go to turtle_description field
await animationFrame();
expect("[name=turtle_description] textarea").toBeFocused();
expect(getNextFocusableElement()).toBe(queryOne("[name=foo] input"));
// trigger Tab event and check that the default behavior can happen.
await press("Tab");
expect("[name=foo] input").toBeFocused();
});
test("when Navigating to a one2many with tabs, editing in a popup, the popup should receive the focus then give it back", async () => {
Partner._records[0].turtles = [];
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains("[name=qux] input").click();
expect("[name=qux] input").toBeFocused();
// next tabable element is notebook tab
await press("Tab");
// go inside one2many
await press("Tab");
await animationFrame();
expect(".o_field_x2many_list_row_add a").toBeFocused();
await contains(".o_field_x2many_list_row_add a").click();
expect(".modal [name=turtle_foo] input").toBeFocused();
await press("Escape");
await animationFrame();
expect(".modal").toHaveCount(0);
expect(".o_field_x2many_list_row_add a").toBeFocused();
});
test.tags("desktop");
test("when creating a new many2one on a x2many then discarding it immediately with ESCAPE, it should not crash", async () => {
Partner._records[0].turtles = [];
Partner._views = {
form: `
`,
resId: 1,
});
// add a new line
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_selected_row").toHaveCount(1);
await clickFieldDropdown("turtle_trululu");
await contains(".o_field_widget[name=turtle_trululu] input").edit("ABC", {
confirm: false,
});
await runAllTimers();
// Discard input value
press("Escape").then(() => {
// ... then discard record
press("Escape");
});
clickFieldDropdownItem("turtle_trululu", "Create and edit..."); // Open create modal
await animationFrame();
await animationFrame();
expect(".modal").toHaveCount(0);
expect(".o_selected_row").toHaveCount(0);
});
test.tags("desktop");
test("navigating through an editable list with custom controls", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
});
expect("[name=name] input").toBeFocused();
expect("[name=p] .o_selected_row").toHaveCount(0);
// press tab to navigate to the list
expect(getNextFocusableElement()).toBe(queryFirst(".o_field_x2many_list_row_add a"));
await press("Tab");
expect(".o_field_x2many_list_row_add a:eq(0)").toBeFocused();
// press right to focus the second control
await press("ArrowRight");
await animationFrame();
expect(".o_field_x2many_list_row_add a:eq(1)").toBeFocused();
// press left to come back to first control
await press("ArrowLeft");
await animationFrame();
expect(".o_field_x2many_list_row_add a:eq(0)").toBeFocused();
expect(getNextFocusableElement()).toBe(queryOne(".o_field_x2many_list_row_add a:eq(1)"));
await press("Tab");
expect(".o_field_x2many_list_row_add a:eq(1)").toBeFocused();
expect(getNextFocusableElement()).toBe(queryOne("[name=int_field] input"));
await press("Tab");
expect("[name=int_field] input").toBeFocused();
});
test("be able to press a key on the keyboard when focusing a column header without crashing", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_data_row .o_data_cell").click();
await contains(".o_list_renderer .o_column_sortable").click();
await press("a");
await animationFrame();
expect(".o_data_row").toHaveCount(1);
});
test("Navigate from an invalid but not dirty row", async () => {
Partner._records[0].p = [2, 4];
Partner._records[1].name = ""; // invalid record
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
`,
resId: 1,
});
await contains(".o_data_cell").click(); // edit the first row
expect(".o_data_row.o_selected_row").toHaveCount(1);
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
await press("Tab"); // navigate with "Tab" to the second row
await animationFrame();
expect(".o_data_row.o_selected_row").toHaveCount(1);
expect(".o_data_row:eq(1)").toHaveClass("o_selected_row");
expect(".o_invalid_cell").toHaveCount(0);
await contains(".o_data_cell").click(); // come back on first row
expect(".o_data_row.o_selected_row").toHaveCount(1);
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
expect(".o_invalid_cell").toHaveCount(0);
await press("Enter"); // try to navigate with "Enter" to the second row
await animationFrame();
expect(".o_data_row.o_selected_row").toHaveCount(1);
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
expect(".o_invalid_cell").toHaveCount(1);
});
test.tags("desktop");
test("Check onchange with two consecutive one2one", async () => {
Product._fields.product_partner_ids = fields.One2many({
string: "User",
relation: "partner",
});
Product._records[0].product_partner_ids = [1];
Product._records[1].product_partner_ids = [2];
Turtle._fields.product_ids = fields.One2many({
string: "Product",
relation: "product",
});
Turtle._fields.user_ids = fields.One2many({
string: "Product",
relation: "res.users",
});
Turtle._onChanges = {
turtle_trululu: function (record) {
record.product_ids = [[4, 37]];
record.user_ids = [
[4, 17],
[4, 19],
];
},
};
await mountView({
type: "form",
resModel: "turtle",
arch: `
`,
resId: 1,
});
await clickFieldDropdown("turtle_trululu");
await press("Enter");
await animationFrame();
expect(
queryAllTexts(
'.o_field_many2many_tags[name="product_partner_ids"] .badge.o_tag_color_0 > .o_tag_badge_text'
)
).toEqual(["first record"], {
message: "should have the correct value in the many2many tag widget",
});
expect(
queryAllTexts(
'.o_field_many2many_tags[name="partner_ids"] .badge.o_tag_color_0 > .o_tag_badge_text'
)
).toEqual(["first record", "second record"], {
message: "should have the correct values in the many2many tag widget",
});
});
test("does not crash when you parse a tree arch containing another tree arch", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_list_renderer").toHaveCount(1);
});
test("open a one2many record containing a one2many", async () => {
Partner._views = {
[["form", 1234]]: `
`,
resId: 1,
});
expect.verifySteps(["get_views partner", "web_read partner"]);
await contains(".o_boolean_toggle").click();
expect.verifySteps(["onchange partner"]);
});
test("Boolean toggle in x2many must not be editable if form is not editable", async () => {
Turtle._views = {
[["form", false]]: /* xml */ `
`,
resId: 1,
});
expect("[name='p']").toHaveCount(0);
expect.verifySteps(["get_views partner", "web_read partner"]);
await contains("[name='foo'] input").edit("plop", { confirm: false });
await clickSave();
expect.verifySteps(["web_save partner"]);
});
test("can't select a record in a one2many", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_data_row").click();
expect(".o_data_row_selected").toHaveCount(0);
});
test("save a record after creating and editing a new invalid record in a one2many", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_field_widget[name=int_field] input").edit("3", { confirm: false });
await clickSave();
expect(".o_data_row.o_selected_row").toHaveCount(1, {
message: "line should not have been removed and should still be in edition",
});
expect(".o_field_widget[name=name]").toHaveClass("o_field_invalid");
});
test("nested one2manys, multi page, onchange", async () => {
Partner._records[2].int_field = 5;
Partner._records[0].p = [2, 4]; // limit 1 -> record 4 will be on second page
Partner._records[1].turtles = [1];
Partner._records[2].turtles = [2];
Turtle._records[0].turtle_int = 1;
Turtle._records[1].turtle_int = 2;
Partner._onChanges.int_field = function (obj) {
expect.step("onchange");
obj.p = [[5]];
obj.p.push([1, 2, { turtles: [[5], [1, 1, { turtle_int: obj.int_field }]] }]);
obj.p.push([1, 4, { turtles: [[5], [1, 2, { turtle_int: obj.int_field }]] }]);
};
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_field_x2many_list_row_add a").click();
// This is not how it should happen but non trusted event listeners are called sooner than
// trusted ones so the update is called after the list's tab listener in which case the field is
// not dirty when we press tab, therefore we need to set it dirty through onChange before pressing tab
// so in practice we could only run the following line but it wont work since the tab keydown event is not trusted
// await contains(".o_field_widget[name='name'] input").edit("gold", { confirm: false });
await contains(".o_field_widget[name='name'] input").edit("gold", { confirm: "blur" });
await contains(".o_data_cell[name='name']").click(); // focus the input again
await press("Tab");
onchangeGetPromise.resolve();
await animationFrame();
expect(".o_data_row").toHaveCount(2);
});
test("add a row to an x2many and ask canBeRemoved twice", async () => {
// This test simulates that the view is asked twice to save its changes because the user
// is leaving. Before the corresponding fix, the changes in the x2many field weren't
// removed after the save, and as a consequence they were saved twice (i.e. the row was
// created twice).
const def = new Deferred();
Partner._views = {
list: ``,
search: ``,
form: `
`,
};
onRpc("web_save", (args) => {
expect.step("web_save");
expect(args.args[1]).toEqual({
p: [[0, args.args[1].p[0][1], { name: "a name" }]],
});
});
onRpc("web_search_read", () => {
return def;
});
const actions = [
{
id: 1,
name: "test",
res_model: "partner",
res_id: 1,
type: "ir.actions.act_window",
views: [[false, "form"]],
},
{
id: 2,
name: "another action",
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "list"]],
},
];
await mountWithCleanup(WebClient);
await getService("action").doAction(actions[0]);
expect(".o_form_view").toHaveCount(1);
// add a row in the x2many
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_field_widget[name=name] input").edit("a name", { confirm: false });
expect(".o_data_row").toHaveCount(1);
getService("action").doAction(actions[1]);
await animationFrame();
getService("action").doAction(actions[1]);
await animationFrame();
expect.verifySteps(["web_save"]);
def.resolve();
await animationFrame();
expect(".o_list_view").toHaveCount(1);
expect.verifySteps([]);
});
test("one2many: save a record before the onchange is complete in a form dialog", async () => {
Turtle._onChanges = {
name: function () {},
};
Turtle._views = {
form: `
`,
resId: 1,
});
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["yop", "blip", "kawa"]);
// drag the third record to top of the list
await contains("tbody tr:eq(2) .o_handle_cell").dragAndDrop("tbody tr");
await clickSave();
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["kawa", "yop", "blip"]);
expect.verifySteps(["get_views", "web_read", "web_save"]);
});
test("one2many causes an onchange on the parent which fails", async () => {
expect.errors(1);
Partner._onChanges = {
turtles: function () {},
};
onRpc("partner", "onchange", () => {
throw makeServerError();
});
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
await contains(".o_data_cell").click();
expect(".o_field_widget[name='turtle_foo'] input").toHaveValue("blip");
// onchange on parent record fails
await contains(".o_field_widget[name='turtle_foo'] input").edit("new value", {
confirm: "blur",
});
await animationFrame();
expect(".o_data_cell[name='turtle_foo']").toHaveText("blip");
expect(".o_error_dialog").toHaveCount(1);
});
test.tags("desktop");
test("one2many custom which can be edited in dialog or on the line", async () => {
const customState = reactive({ isEditable: false });
class CustomX2manyField extends X2ManyField {
setup() {
super.setup();
this.canOpenRecord = true;
this.customState = useState(customState);
}
get rendererProps() {
const props = super.rendererProps;
props.editable = this.customState.isEditable;
return props;
}
}
const customX2ManyField = {
...x2ManyField,
component: CustomX2manyField,
};
registry.category("fields").add("custom", customX2ManyField);
await mountView({
type: "form",
resModel: "partner",
arch: `
`,
resId: 1,
});
expect(".o_form_status_indicator_buttons.invisible").toHaveCount(1, {
message: "form view is not dirty",
});
await contains(".o_data_cell").click();
expect(".modal").toHaveCount(1);
customState.isEditable = true;
await contains(".modal .btn-close").click();
expect(".o_form_status_indicator_buttons.invisible").toHaveCount(1, {
message: "form view is not dirty",
});
await contains(".o_data_cell").click();
await contains("[name='turtle_foo'] input").edit("new value", { confirm: false });
expect(".o_form_status_indicator_buttons:not(.invisible)").toHaveCount(1, {
message: "form view is dirty",
});
});
test("x2many kanban with float field in form (non inline) but not in kanban", async () => {
// In this test, the form view contains an extra float field and isn't inline. When we open
// a record, we add the form fields to the list of activeFields, and we load the
// corresponding data (for that record only). Afterwards, we force a re-rendering of the
// x2many kanban to ensure that the other record can still be rendered. Before the fix coming
// with this test, it wasn't the case, because those records had extra activeFields, but no
// entry in data for those fields.
Partner._records[0].turtles = [2, 3];
Turtle._views = {
form: `
`,
resId: 1,
});
expect(".o_field_widget[name=turtles]").toHaveCount(1);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
// open the first record
await contains(".o_kanban_record").click();
expect(".o_dialog").toHaveCount(1);
expect(".o_dialog .o_field_widget[name=turtle_qux]").toHaveCount(1);
// close the dialog
await contains(".o_dialog .o_form_button_save").click();
expect(".o_dialog").toHaveCount(0);
// toggle bar to make the x2many invisible
await contains(".o_field_widget[name=bar] input").click();
expect(".o_field_widget[name=turtles]").toHaveCount(0);
// toggle bar again to make the x2many visible and force kanban cards to re-render
await contains(".o_field_widget[name=bar] input").click();
expect(".o_field_widget[name=turtles]").toHaveCount(1);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
});
test("onchange on x2many returning an update command with only readonly fields", async () => {
Partner._records[0].turtles = [2];
Turtle._fields.name = fields.Char({ readonly: true });
Partner._onChanges = {
bar: (obj) => {
obj.turtles = [[1, 2, { name: "onchange name" }]];
},
};
onRpc((args) => {
expect.step(args.method);
});
onRpc("web_save", (args) => {
expect(args.args[1]).toEqual({ bar: false }); // should not contain turtles
});
await mountView({
type: "form",
resModel: "partner",
arch: `