Odoo18-Base/addons/web/static/tests/views/fields/many2one_field.test.js
2025-01-06 10:57:38 +07:00

3873 lines
118 KiB
JavaScript

import { describe, expect, getFixture, test } from "@odoo/hoot";
import { click, press, queryAll, queryAllTexts, queryOne, scroll } from "@odoo/hoot-dom";
import { Deferred, animationFrame, runAllTimers } from "@odoo/hoot-mock";
import { Component, xml } from "@odoo/owl";
import {
clickFieldDropdown,
clickFieldDropdownItem,
clickSave,
contains,
defineModels,
editSearch,
fields,
getDropdownMenu,
getFacetTexts,
getService,
makeServerError,
mockService,
models,
mountView,
mountViewInDialog,
mountWithCleanup,
onRpc,
selectFieldDropdownItem,
serverState,
toggleMenuItem,
toggleSearchBarMenu,
validateSearch,
} from "@web/../tests/web_test_helpers";
import { user } from "@web/core/user";
import { Record } from "@web/model/record";
import { Field } from "@web/views/fields/field";
import { WebClient } from "@web/webclient/webclient";
describe.current.tags("desktop");
class ResPartner extends models.Model {
name = fields.Char({ string: "Res Partner Name" });
_records = [
{
id: 1,
name: "res partner",
},
];
}
class Partner extends models.Model {
name = fields.Char({ string: "Displayed name" });
foo = fields.Char({ default: "My little Foo Value" });
bar = fields.Boolean({ default: true });
int_field = fields.Integer({ sortable: true });
p = fields.One2many({
string: "one2many field",
relation: "partner",
relation_field: "trululu",
});
turtles = fields.One2many({
string: "one2many turtle field",
relation: "turtle",
relation_field: "turtle_trululu",
});
trululu = fields.Many2one({ string: "Trululu", relation: "partner" });
res_trululu = fields.Many2one({ string: "Res Trululu", relation: "res.partner" });
timmy = fields.Many2many({ string: "pokemon", relation: "partner.type" });
product_id = fields.Many2one({ string: "Product", relation: "product" });
date = fields.Date({ string: "Some Date" });
datetime = fields.Datetime({ string: "Datetime Field" });
user_id = fields.Many2one({ string: "Users", relation: "res.users" });
_records = [
{
id: 1,
name: "first record",
bar: true,
foo: "yop",
int_field: 10,
p: [],
turtles: [2],
timmy: [],
trululu: 4,
res_trululu: 1,
user_id: 1,
},
{
id: 2,
name: "second record",
bar: true,
foo: "blip",
int_field: 9,
p: [],
timmy: [],
trululu: 1,
product_id: 37,
date: "2017-01-25",
datetime: "2016-12-12 10:55:05",
user_id: 1,
},
{
id: 4,
name: "aaa",
bar: false,
},
];
}
class Product extends models.Model {
name = fields.Char();
_records = [
{
id: 37,
name: "xphone",
},
{
id: 41,
name: "xpad",
},
];
}
class PartnerType extends models.Model {
name = fields.Char();
_records = [
{ id: 12, name: "gold" },
{ id: 14, name: "silver" },
];
}
class Turtle extends models.Model {
name = fields.Char({ string: "Displayed name" });
turtle_foo = fields.Char({ string: "Foo" });
turtle_bar = fields.Boolean({ string: "Bar", default: true });
turtle_trululu = fields.Many2one({
string: "Trululu",
relation: "partner",
});
product_id = fields.Many2one({
string: "Product",
relation: "product",
required: true,
});
partner_ids = fields.Many2many({ string: "Partner", relation: "partner" });
_records = [
{
id: 1,
name: "leonardo",
turtle_bar: true,
turtle_foo: "yop",
partner_ids: [],
},
{
id: 2,
name: "donatello",
turtle_bar: true,
turtle_foo: "blip",
partner_ids: [2, 4],
},
{
id: 3,
name: "raphael",
product_id: 37,
turtle_bar: false,
turtle_foo: "kawa",
partner_ids: [],
},
];
}
class Users extends models.Model {
_name = "res.users";
name = fields.Char();
partner_ids = fields.One2many({ relation: "partner", relation_field: "user_id" });
has_group() {
return true;
}
_records = [
{
id: 1,
name: "Aline",
partner_ids: [1, 2],
},
{
id: 2,
name: "Christine",
},
];
}
defineModels([ResPartner, Partner, Product, PartnerType, Turtle, Users]);
test("many2ones in form views", async () => {
expect.assertions(2);
mockService("action", {
doAction(params) {
expect(params.res_id).toBe(17);
},
loadState() {},
});
Partner._views = {
form: `
<form>
<field name="name" />
</form>`,
};
onRpc("get_formview_action", function ({ args }) {
expect(args[0]).toEqual([4]);
return {
res_id: 17,
type: "ir.actions.act_window",
target: "current",
res_model: "res.partner",
};
});
onRpc("get_formview_id", function ({ args }) {
expect(args[0]).toEqual([4]);
return false;
});
await mountViewInDialog({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<group>
<field name="trululu" string="custom label"/>
</group>
</sheet>
</form>`,
});
await contains(".o_external_button:enabled", { visible: false }).click();
expect(".o_dialog:not(.o_inactive_modal) .modal-title").toHaveText("Open: custom label");
// TODO: test that we can edit the record in the dialog, and that
// the value is correctly updated on close
});
test("editing a many2one, but not changing anything", async () => {
expect.assertions(1);
Partner._views = {
form: `
<form>
<field name="name" />
</form>`,
};
onRpc("get_formview_id", function ({ args }) {
expect(args[0]).toEqual([4]);
return false;
});
onRpc("web_save", () => {
throw new Error("should not call write");
});
await mountViewInDialog({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<field name="trululu"/>
</sheet>
</form>`,
});
// click on the external button (should do an RPC)
await contains(".o_external_button", { visible: false }).click();
// save and close modal
await contains(".modal:eq(1) .o_form_button_save").click();
// save form
await clickSave();
});
test("context in many2one and default get", async () => {
expect.assertions(2);
Partner._fields.int_field.default = 14;
Partner._fields.trululu.default = 2;
onRpc("onchange", ({ args }) => {
const context = args[3].trululu.context;
expect(context.blip).toBe(undefined);
expect(context.blop).toBe(3);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="int_field" />
<field name="trululu" context="{'blip': int_field, 'blop': 3}" />
</form>`,
});
});
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: {},
int_field: {},
trululu: {
fields: {},
},
});
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="int_field" />
<field name="trululu" invisible="1" context="{'blip': int_field, 'blop': 3}" />
</form>`,
});
});
test("editing a many2one (with form view opened with external button)", async () => {
expect.assertions(4);
Partner._views = {
form: `
<form>
<field name="foo" />
</form>`,
};
onRpc("get_formview_id", () => {
return false;
});
onRpc("web_save", () => {
expect.step("web_save");
});
onRpc("read", ({ args, model, kwargs }) => {
if (model === "partner" && args[0][0] === 4) {
expect.step(`read partner: ${args[1]}`);
expect(kwargs.context.blip).toBe(10);
expect(kwargs.context.blop).toBe(3);
}
});
await mountViewInDialog({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<field name="int_field" />
<field name="trululu" context="{'blip': int_field, 'blop': 3}"/>
</sheet>
</form>`,
});
// click on the external button (should do an RPC)
await contains(".o_external_button", { visible: false }).click();
await contains(".o_dialog:not(.o_inactive_modal) .o_field_widget[name='foo'] input").edit(
"brandon"
);
// save and close modal
await contains(".modal:eq(1) .o_form_button_save").click();
expect.verifySteps(["web_save", "read partner: display_name"]);
// save form
await clickSave();
expect.verifySteps([]);
});
test("many2ones in form views with show_address", async () => {
onRpc("web_read", ({ kwargs }) => {
if (kwargs.specification.trululu.context.show_address) {
return [
{
name: "",
trululu: { display_name: "aaa\nStreet\nCity ZIP" },
},
];
}
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<group>
<field name="trululu" context="{'show_address': 1}" />
</group>
</sheet>
</form>`,
});
expect("input.o_input").toHaveValue("aaa");
expect(".o_field_many2one_extra").toHaveInnerHTML(
"<span>Street</span><br><span>City ZIP</span>",
{ type: "html" }
);
expect("button.o_external_button").toHaveCount(1);
});
test("many2one show_address in edit", async () => {
const namegets = {
1: "first record\nFirst\nRecord",
2: "second record\nSecond\nRecord",
4: "aaa\nAAA\nRecord",
};
onRpc("web_read", async ({ kwargs, parent }) => {
if (kwargs.specification.trululu.context.show_address) {
const result = await parent();
result[0].trululu = {
id: result[0].trululu.id,
display_name: namegets[result[0].trululu.id],
};
return result;
}
});
onRpc("name_search", async ({ parent }) => {
const result = await parent();
return result.map(([id]) => [id, namegets[id]]);
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<group>
<field name="trululu" context="{'show_address': 1}" />
</group>
</sheet>
</form>`,
});
expect(".o_field_widget input").toHaveValue("aaa");
expect(".o_field_many2one_extra").toHaveInnerHTML("<span>AAA</span><br><span>Record</span>", {
type: "html",
});
await contains(".o_field_widget input").edit("first record", { confirm: false });
await runAllTimers();
await contains(".dropdown-menu li").click();
expect(".o_field_widget input").toHaveValue("first record");
expect(".o_field_many2one_extra").toHaveInnerHTML("<span>First</span><br><span>Record</span>", {
type: "html",
});
await contains(".o_field_widget input").edit("second record", { confirm: false });
await runAllTimers();
await contains(".dropdown-menu li").click();
expect(".o_field_widget input").toHaveValue("second record");
expect(".o_field_many2one_extra").toHaveInnerHTML(
"<span>Second</span><br><span>Record</span>",
{ type: "html" }
);
});
test("show_address works in a view embedded in a view of another type", async () => {
expect.assertions(2);
Turtle._records[1].turtle_trululu = 2;
Turtle._views = {
form: `
<form>
<field name="name" />
<field name="turtle_trululu" context="{'show_address': 1}" />
</form>`,
list: `
<list>
<field name="name" />
</list>`,
};
onRpc("turtle", "web_read", ({ kwargs }) => {
expect(kwargs.specification.turtle_trululu.context).toEqual({
show_address: 1,
});
return [
{
id: 1,
name: "donatello",
turtle_trululu: {
id: 2,
display_name: "second record\nrue morgue\nparis 75013",
},
},
];
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form edit="0">
<field name="name" />
<field name="turtles" />
</form>`,
});
// click the turtle field, opens a modal with the turtle form view
await contains(".o_data_row td.o_data_cell").click();
expect('[name="turtle_trululu"]').toHaveText("second record\nrue morgue\nparis 75013");
});
test("many2ones in form views with search more", async () => {
for (let i = 5; i < 11; i++) {
Partner._records.push({ id: i, name: `Partner ${i}` });
}
Partner._fields.datetime.searchable = true;
Partner._views = {
search: `
<search>
<filter name="filter" string="Filter" domain="[[0, '=', 1]]"/>
</search>`,
list: `
<list>
<field name="name" />
</list>`,
};
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<group>
<field name="trululu" />
</group>
</sheet>
</form>`,
});
await selectFieldDropdownItem("trululu", "Search More...");
expect("tr.o_data_row").toHaveCount(9);
expect(".o_field_widget[name=trululu] input").toHaveValue("aaa");
await toggleSearchBarMenu(".modal");
await toggleMenuItem("Filter");
expect("tr.o_data_row").toHaveCount(0);
});
test("many2ones: Open the selection dialog several times using the 'Search More...' button with a context containing 'search_default_...'", async () => {
for (let i = 5; i < 11; i++) {
Partner._records.push({ id: i, name: `Partner ${i}` });
}
Partner._fields.name.searchable = true;
Partner._views = {
search: `
<search>
<field name="name" />
</search>`,
list: `
<list>
<field name="name" />
</list>`,
};
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<group>
<field name="trululu" context="{ 'search_default_name': 'Partner 10'}"/>
</group>
</sheet>
</form>`,
});
await selectFieldDropdownItem("trululu", "Search More...");
expect(".modal .o_data_row").toHaveCount(1);
expect(getFacetTexts(".modal")).toEqual(["Displayed name\nPartner 10"]);
await contains(".modal .btn-close").click();
expect(".modal").toHaveCount(0);
await selectFieldDropdownItem("trululu", "Search More...");
expect(".modal .o_data_row").toHaveCount(1);
expect(getFacetTexts(".modal")).toEqual(["Displayed name\nPartner 10"]);
});
test("many2ones in list views: create in dialog keeps the input", async () => {
Partner._views = {
form: `
<form>
<field name="name" />
</form>`,
};
onRpc("web_save", ({ args }) => {
expect.step(`web_save: ${JSON.stringify(args)}`);
});
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="top">
<field name="trululu" />
</list>`,
});
await contains(".o_data_cell:eq(0)").click();
await contains(".o_field_widget[name=trululu] input").edit("yy", { confirm: false });
await runAllTimers();
await clickFieldDropdownItem("trululu", "Create and edit...");
await clickSave();
expect.verifySteps([`web_save: [[],{"name":"yy"}]`]);
expect(".o_field_widget[name=trululu] input").toHaveValue("yy");
await contains(getFixture()).click();
expect.verifySteps([`web_save: [[1],{"trululu":5}]`]);
expect(".o_data_cell[name=trululu]:eq(0)").toHaveText("yy");
});
test("many2ones in list views: create a new record with a context", async () => {
expect.assertions(6);
onRpc("res.users", "name_create", ({ kwargs }) => {
const { context } = kwargs;
expect.step("name_create");
expect(context.default_test).toBe(1);
expect(context.test).toBe(2);
expect(context).not.toInclude("default_yop");
expect(context.yop).toBe(4);
});
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="top">
<field name="user_id" context="{'default_test': 1, 'test':2 }" />
</list>`,
context: {
default_yop: 3,
yop: 4,
},
});
await contains(".o_data_cell:eq(0)").click();
await contains(".o_field_widget[name=user_id] input").edit("yy", { confirm: false });
await runAllTimers();
await clickFieldDropdownItem("user_id", 'Create "yy"');
expect(".o_external_button").toHaveCount(1);
expect.verifySteps(["name_create"]);
});
test("using a many2one widget must take into account the decorations", async () => {
await mountView({
type: "list",
resModel: "partner",
arch: `
<list>
<field name="user_id" decoration-danger="int_field > 9" widget="many2one"/>
<field name="int_field"/>
</list>`,
});
expect(".o_list_many2one a.text-danger").toHaveCount(1);
expect(".o_data_row").toHaveCount(3);
});
test("onchanges on many2ones trigger when editing record in form view", async () => {
expect.assertions(2);
Partner._onChanges = {
user_id: () => {},
};
Users._fields.other_field = fields.Char({ string: "Other Field" });
Users._views = {
form: `
<form>
<field name="other_field" />
</form>`,
};
onRpc("get_formview_id", () => false);
onRpc("onchange", ({ args }) => {
expect(args[1].user_id).toBe(1);
});
onRpc(({ method }) => {
expect.step(method);
});
await mountViewInDialog({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<group>
<field name="user_id"/>
</group>
</sheet>
</form>`,
});
// open the many2one in form view and change something
await contains(".o_external_button", { visible: false }).click();
await contains(
".o_dialog:not(.o_inactive_modal) .o_field_widget[name='other_field'] input"
).edit("wood");
// TODISCUSS ? Same record, don't change the display name (opti ?)
// save the modal and make sure an onchange is triggered
await contains(".modal:eq(1) .o_form_button_save").click();
expect.verifySteps([
"get_views",
"web_read",
"get_formview_id",
"get_views",
"web_read",
"web_save",
"read",
"onchange",
]);
});
test("edit many2one before onchange is finished should not reset the value", async () => {
Partner._onChanges = {
name: function (obj) {
obj.user_id = 19;
},
};
onRpc("onchange", () => {
expect.step("onchange");
return def;
});
const def = new Deferred();
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="name"/>
<field name="user_id"/>
</form>`,
});
await contains("[name='name'] input").edit("new name");
await contains("[name='user_id'] input").edit("Plop");
expect("[name='user_id'] input").toHaveValue("Plop");
def.resolve();
await animationFrame();
expect("[name='user_id'] input").toHaveValue("Plop");
expect.verifySteps(["onchange"]);
});
test("many2one doesn't trigger field_change when being emptied", async () => {
await mountView({
type: "list",
resModel: "partner",
arch: `
<list multi_edit="1">
<field name="trululu"/>
</list>`,
});
// Select two records
await contains(".o_data_row:eq(0) .o_list_record_selector input").click();
await contains(".o_data_row:eq(1) .o_list_record_selector input").click();
await contains(".o_data_row .o_data_cell").click();
await contains(".o_field_widget[name=trululu] input").clear({ confirm: false });
await runAllTimers();
expect(".modal").toHaveCount(0);
await contains(".o_field_widget[name=trululu] .ui-menu-item").click();
expect(".modal").toHaveCount(1);
});
test("..._view_ref keys are removed from many2one context on create and edit", async () => {
onRpc("get_views", ({ kwargs, method }) => {
expect.step(
JSON.stringify([method, kwargs.context.default_name, kwargs.context.form_view_ref])
);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="trululu"/>
</group>
</sheet>
</form>`,
context: {
form_view_ref: "test_form_view",
},
});
expect.verifySteps(['["get_views",null,"test_form_view"]']);
await contains(".o_field_widget[name=trululu] input").edit("ABC", { confirm: false });
await runAllTimers();
await contains(".o_field_widget[name=trululu] .o_m2o_dropdown_option_create_edit").click();
expect.verifySteps(['["get_views",null,null]']);
});
test("empty a many2one field in list view", async () => {
onRpc("web_save", ({ args }) => {
expect.step("web_save");
expect(args[1]).toEqual({ trululu: false });
});
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="top">
<field name="trululu"/>
</list>`,
});
await contains(".o_data_row .o_data_cell").click();
await contains(".o_field_widget[name=trululu] input").edit("");
expect(".o_data_row .o_field_widget[name=trululu] input").toHaveText("");
await contains(".o_list_view").click();
expect(".o_data_row:eq(0)").toHaveText("");
expect.verifySteps(["web_save"]);
});
test("focus tracking on a many2one in a list", async () => {
Partner._views = {
form: `
<form>
<field name="foo" />
</form>`,
};
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="top">
<field name="trululu"/>
</list>`,
});
// Select two records
await contains(".o_data_row:eq(0) .o_list_record_selector input").click();
await contains(".o_data_row:eq(1) .o_list_record_selector input").click();
await contains(".o_data_row .o_data_cell").click();
expect(".o_data_row .o_data_cell input").toBeFocused();
await contains(".o_field_widget[name=trululu] input").edit("ABC", { confirm: false });
await runAllTimers();
await contains(".o_field_widget[name=trululu] .o_m2o_dropdown_option_create_edit").click();
// At this point, if the focus is correctly registered by the m2o, there
// should be only one modal (the "Create" one) and none for saving changes.
expect(".modal").toHaveCount(1);
await contains(".o_form_button_cancel").click();
expect(".o_data_row .o_data_cell input").toBeFocused();
expect(".o_data_row .o_data_cell input").toHaveValue("");
});
test('many2one fields with option "no_open"', async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<group>
<field name="trululu" options="{'no_open': 1}" />
</group>
</sheet>
</form>`,
});
expect(".o_field_widget[name='trululu'] .o_external_button").toHaveCount(0);
});
test("empty many2one field", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="trululu" />
</group>
</sheet>
</form>`,
});
await contains(".o_field_many2one input").click();
expect(".dropdown-menu li.o_m2o_dropdown_option").toHaveCount(1);
expect(".dropdown-menu li.o_m2o_start_typing").toHaveCount(1);
await contains(".o_field_many2one[name='trululu'] input").edit("abc", { confirm: false });
await runAllTimers();
expect(".dropdown-menu li.o_m2o_dropdown_option").toHaveCount(2);
expect(".dropdown-menu li.o_m2o_start_typing").toHaveCount(0);
});
test("empty readonly many2one field", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `<form><field name="trululu" readonly="1"/></form>`,
});
expect("div.o_field_widget[name=trululu]").toHaveCount(1);
expect(".o_field_widget[name=trululu]").toHaveInnerHTML("");
});
test("empty many2one field with node options", async () => {
expect.assertions(2);
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="trululu" options="{'no_create_edit': 1}" />
<field name="product_id" options="{'no_create_edit': 1, 'no_quick_create': 1}" />
</group>
</sheet>
</form>`,
});
await contains(".o_field_many2one[name='trululu'] input").click();
expect(".o_field_many2one[name='trululu'] .dropdown-menu li.o_m2o_start_typing").toHaveCount(1);
await contains(".o_field_many2one[name='product_id'] input").click();
expect(".o_field_many2one[name='product_id'] .dropdown-menu li.o_m2o_start_typing").toHaveCount(
0
);
});
test("many2one with no_create_edit and no_quick_create options should show no records when no result match", async () => {
expect.assertions(2);
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="product_id" options="{'no_create_edit': 1, 'no_quick_create': 1}" />
</group>
</sheet>
</form>`,
});
await contains(".o_field_many2one[name='product_id'] input").click();
expect(".o_field_many2one[name='product_id'] .dropdown-menu li.o_m2o_no_result").toHaveCount(0);
await contains(".o_field_many2one[name='product_id'] input").edit("aze", { confirm: false });
await runAllTimers();
expect(".o_field_many2one[name='product_id'] .dropdown-menu li.o_m2o_no_result").toHaveCount(1);
});
test("many2one in edit mode", async () => {
expect.assertions(17);
// create 10 partners to have the 'Search More' option in the autocomplete dropdown
for (let i = 0; i < 10; i++) {
const id = 20 + i;
Partner._records.push({ id, name: `Partner ${id}` });
}
Partner._views = {
list: `
<list>
<field name="name" />
</list>`,
search: `
<search>
<field name="name" string="Name" />
</search>`,
};
onRpc("partner", "web_save", ({ args }) => {
expect(args[1].trululu).toBe(20);
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<group>
<field name="trululu" />
</group>
</sheet>
</form>`,
});
await clickFieldDropdown("trululu");
expect(".o_field_many2one[name='trululu'] .dropdown-menu").toBeVisible();
expect(
".o_field_many2one[name='trululu'] .dropdown-menu li:not(.o_m2o_dropdown_option)"
).toHaveCount(8);
expect(".o_field_many2one[name='trululu'] .dropdown-menu li.o_m2o_dropdown_option").toHaveCount(
1
);
expect(".o_field_many2one[name='trululu'] .dropdown-menu li.o_m2o_start_typing").toHaveCount(0);
await contains(".o_field_many2one[name='trululu'] input").click();
expect(".o_field_many2one[name='trululu'] .dropdown-menu").toHaveCount(0);
// change the value of the m2o with a suggestion of the dropdown
await selectFieldDropdownItem("trululu", "first record");
expect(".o_field_many2one[name='trululu'] .dropdown-menu").not.toBeVisible();
expect(".o_field_many2one input").toHaveValue("first record");
// change the value of the m2o with a record in the 'Search More' modal
await clickFieldDropdown("trululu");
// click on 'Search More' (mouseenter required by ui-autocomplete)
await contains(
".o_field_many2one[name='trululu'] .dropdown-menu .o_m2o_dropdown_option_search_more"
).click();
expect(".modal .o_list_view").toHaveCount(1);
expect(".modal .o_list_view .o_list_record_selector").toHaveCount(0);
expect(".modal .modal-footer .o_select_button").toHaveCount(0);
expect(queryAll(".modal tbody tr").length).toBeGreaterThan(10, {
message: "list should contain more than 10 records",
});
await contains(".modal .o_searchview_input").edit("P", { confirm: false });
await runAllTimers();
await press("Enter");
await animationFrame();
expect(".modal tbody tr").toHaveCount(10);
// choose a record
await contains(".modal .o_data_cell[data-tooltip='Partner 20']").click();
expect(".modal").toHaveCount(0);
expect(".o_field_many2one[name='trululu'] .dropdown-menu").not.toBeVisible();
expect(".o_field_many2one input").toHaveValue("Partner 20");
// save
await clickSave();
expect(".o_field_many2one input").toHaveValue("Partner 20");
});
test("many2one in non edit mode (with value)", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form edit="0">
<field name="res_trululu" />
<field name="trululu" />
</form>`,
});
expect("a.o_form_uri").toHaveCount(2);
expect("div[name=res_trululu] a.o_form_uri").toHaveAttribute("href", "/odoo/res.partner/1");
expect("div[name=trululu] a.o_form_uri").toHaveAttribute("href", "/odoo/m-partner/4");
});
test("many2one in non edit mode (without value)", async () => {
Partner._records[0].trululu = false;
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form edit="0">
<field name="trululu" />
</form>`,
});
// Remove value from many2one and then save, there should be no link anymore
expect("a.o_form_uri").toHaveCount(0);
});
test("many2one with co-model whose name field is a many2one", async () => {
Product._fields.name = fields.Many2one({
string: "User Name",
relation: "res.users",
});
Product._records = [
{
id: 37,
name: 1,
},
{
id: 41,
name: 2,
},
];
Product._views = {
form: `
<form>
<field name="name" />
</form>`,
};
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="product_id" />
</form>`,
});
await contains("div[name=product_id] input").edit("ABC", { confirm: false });
await runAllTimers();
await contains("div[name=product_id] .o_m2o_dropdown_option_create_edit").click();
expect(".modal .o_form_view").toHaveCount(1);
// quick create 'new value'
await contains(".modal div[name=name] input").edit("new value", { confirm: false });
await runAllTimers();
await contains(".modal div[name=name] .o_m2o_dropdown_option").click();
expect(".modal div[name=name] input").toHaveValue("new value");
await contains(".modal .o_form_button_save").click();
expect(".modal .o_form_view").toHaveCount(0);
expect("div[name=product_id] input").toHaveValue("new value");
});
test("many2one searches with correct value", async () => {
onRpc("name_search", ({ kwargs }) => {
expect.step(`search: ${kwargs.name}`);
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<field name="trululu" />
</sheet>
</form>`,
});
expect(".o_field_many2one input").toHaveValue("aaa");
await contains(".o_field_many2one input").click();
// unset the many2one -> should search again with ''
await contains(".o_field_many2one input").clear({ confirm: false });
await runAllTimers();
await contains(".o_field_many2one input").edit("p", { confirm: false });
await runAllTimers();
// close and re-open the dropdown -> should search with 'p' again
await contains(".o_field_many2one input").click();
await runAllTimers();
await contains(".o_field_many2one input").click();
await runAllTimers();
expect.verifySteps(["search: ", "search: ", "search: p", "search: p"]);
});
test("many2one search with server returning multiple lines", async () => {
const namegets = {
2: "fizz\nbuzz\nfizzbuzz",
4: "aaa\nAAA\nRecord",
};
onRpc("web_read", async ({ parent }) => {
expect.step("web_read");
const result = await parent();
result[0].trululu = {
id: result[0].trululu.id,
display_name: namegets[result[0].trululu.id],
};
return result;
});
onRpc("name_search", () => {
expect.step("name_search");
return Object.keys(namegets).map((id) => [parseInt(id), namegets[id]]);
});
onRpc("web_save", ({ args }) => {
expect.step("web_save");
expect(args[1]).toEqual({
trululu: 2,
});
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<group>
<field name="trululu" />
</group>
</sheet>
</form>`,
});
expect.verifySteps(["web_read"]);
// Initial value
expect(".o_field_widget input").toHaveValue("aaa");
expect(".o_field_many2one_extra").toHaveInnerHTML("<span>AAA</span><br><span>Record</span>", {
type: "html",
});
// Change the value
await contains(".o_field_widget input").edit("fizz", { confirm: false });
await runAllTimers();
// should display only the first line of the returned value from the server
expect.verifySteps(["name_search"]);
expect(queryAllTexts(".dropdown-menu li:not(.o_m2o_dropdown_option")).toEqual(["fizz", "aaa"]);
await contains(".dropdown-menu li").click();
expect.verifySteps([]);
expect(".o_field_widget input").toHaveValue("fizz");
expect(".o_field_many2one_extra").toHaveInnerHTML(
"<span>buzz</span><br><span>fizzbuzz</span>",
{ type: "html" }
);
await clickSave();
expect.verifySteps(["web_save"]);
});
test("many2one search with trailing and leading spaces", async () => {
onRpc("name_search", ({ kwargs }) => {
expect.step("search: " + kwargs.name);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="trululu" />
</form>`,
});
const input = ".o_field_many2one[name='trululu'] input";
await contains(input).click();
expect(".o_field_many2one[name='trululu'] .dropdown-menu").toBeVisible();
expect(
".o_field_many2one[name='trululu'] .dropdown-menu li:not(.o_m2o_dropdown_option)"
).toHaveCount(4);
// search with leading spaces
await contains(input).edit(" first", { confirm: false });
await runAllTimers();
expect(
".o_field_many2one[name='trululu'] .dropdown-menu li:not(.o_m2o_dropdown_option)"
).toHaveCount(1);
// search with trailing spaces
await contains(input).edit("first ", { confirm: false });
await runAllTimers();
expect(
".o_field_many2one[name='trululu'] .dropdown-menu li:not(.o_m2o_dropdown_option)"
).toHaveCount(1);
// search with leading and trailing spaces
await contains(input).edit(" first ", { confirm: false });
await runAllTimers();
expect(
".o_field_many2one[name='trululu'] .dropdown-menu li:not(.o_m2o_dropdown_option)"
).toHaveCount(1);
expect.verifySteps(["search: ", "search: first", "search: first", "search: first"]);
});
// Should be removed ?
test("many2one field with option always_reload (edit)", async () => {
onRpc("web_read", async ({ parent }) => {
const result = await parent();
result[0].trululu = {
...result[0].trululu,
display_name: "first record\nand some address",
};
return result;
});
await mountView({
type: "form",
resModel: "partner",
resId: 2,
arch: `
<form>
<field name="trululu" />
</form>`,
});
expect(".o_field_widget[name='trululu'] input").toHaveValue("first record");
expect(".o_field_many2one_extra").toHaveCount(1);
expect(".o_field_many2one_extra").toHaveText("and some address");
});
test("many2one field and list navigation", async () => {
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="bottom">
<field name="trululu"/>
</list>`,
});
// edit first input, to trigger autocomplete
await contains(".o_data_row .o_data_cell").click();
await contains(".o_data_cell input").clear();
// press keydown, to select first choice
await press("arrowdown");
// we now check that the dropdown is open (and that the focus did not go
// to the next line)
expect(".o_field_many2one").toHaveCount(1);
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
expect(".o_data_row:eq(1)").not.toHaveClass("o_selected_row");
});
test("standalone many2one field", async () => {
class Comp extends Component {
static components = { Record, Field };
static template = xml`
<Record resModel="'coucou'" fields="fields" fieldNames="['partner_id']" values="values" mode="'edit'" t-slot-scope="scope">
<Field name="'partner_id'" record="scope.record" canOpen="false" />
</Record>
`;
static props = ["*"];
setup() {
this.fields = {
partner_id: {
display_name: "partner_id",
type: "many2one",
relation: "partner",
},
};
this.values = {
partner_id: [1, "first partner"],
};
}
}
onRpc(({ method }) => {
expect.step(method);
});
await mountWithCleanup(Comp);
await contains(".o_field_widget input").edit("xyzzrot", { confirm: false });
await runAllTimers();
await contains(".o_field_widget .ui-menu-item").click();
expect(".o_field_widget .o_external_button").toHaveCount(0);
expect.verifySteps(["name_search", "name_create"]);
});
test("form: quick create then save directly", async () => {
expect.assertions(3);
const def = new Deferred();
const newRecordId = 5; // with the current records, the created record will be assigned id 5
onRpc("name_create", async () => {
expect.step("name_create");
await def;
});
onRpc("web_save", ({ args }) => {
expect.step("web_save");
expect(args[1].trululu).toBe(newRecordId);
});
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="trululu" /></form>',
});
await contains(".o_field_widget[name=trululu] input").edit("b", { confirm: false });
await runAllTimers();
await contains(".ui-menu-item").click();
await contains(".o_form_button_save").click();
// should wait for the name_create before creating the record
expect.verifySteps(["name_create"]);
def.resolve();
await animationFrame();
expect.verifySteps(["web_save"]);
});
test("form: quick create for field that returns false after name_create call", async () => {
onRpc("name_create", () => {
expect.step("name_create");
// Resolve the name_create call to false. This is possible if
// _rec_name for the model of the field is unassigned.
return false;
});
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="trululu" /></form>',
});
await contains(".o_field_widget[name=trululu] input").edit("beam", { confirm: false });
await runAllTimers();
await contains(".ui-menu-item").click();
expect.verifySteps(["name_create"]);
expect(".o_input_dropdown input").toHaveValue("");
});
test("list: quick create then save directly", async () => {
expect.assertions(8);
const def = new Deferred();
const newRecordId = 5;
onRpc("name_create", async () => {
expect.step("name_create");
await def;
});
onRpc("web_save", ({ args }) => {
expect.step("web_save");
const values = args[1];
expect(values.trululu).toBe(newRecordId);
});
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="top">
<field name="trululu" />
</list>`,
});
expect(".o_data_row").toHaveCount(3);
await contains(".o_control_panel_main_buttons .o_list_button_add").click();
expect(".o_data_row").toHaveCount(4);
await contains(".o_field_widget[name=trululu] input").edit("b", { confirm: false });
await runAllTimers();
await contains(".ui-menu-item").click();
await contains(".o_list_button_save").click();
// should wait for the name_create before creating the record
expect.verifySteps(["name_create"]);
expect(".o_data_row").toHaveCount(4);
def.resolve();
await animationFrame();
expect.verifySteps(["web_save"]);
expect(".o_data_row").toHaveCount(4);
expect(".o_data_row .o_data_cell:eq(0)").toHaveText("b");
});
test("list in form: quick create then save directly", async () => {
expect.assertions(4);
const def = new Deferred();
const newRecordId = 5; // with the current records, the created record will be assigned id 5
onRpc("name_create", async () => {
expect.step("name_create");
await def;
});
onRpc("web_save", ({ args }) => {
expect.step("web_save");
expect(args[1].p[0][2].trululu).toBe(newRecordId);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="p">
<list editable="bottom">
<field name="trululu" />
</list>
</field>
</sheet>
</form>`,
});
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_field_widget[name=trululu] input").edit("b", { confirm: false });
await runAllTimers();
await contains(".ui-menu-item").click();
await contains(".o_form_button_save").click();
// should wait for the name_create before creating the record
expect.verifySteps(["name_create"]);
await def.resolve();
await animationFrame();
expect.verifySteps(["web_save"]);
expect(".o_data_row .o_data_cell").toHaveText("b");
});
test("name_create in form dialog", async () => {
onRpc("name_create", () => {
expect.step("name_create");
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<group>
<field name="p">
<list>
<field name="bar"/>
</list>
<form>
<field name="product_id"/>
</form>
</field>
</group>
</form>`,
});
await contains(".o_field_x2many_list_row_add a").click();
await contains(".modal .o_field_widget[name=product_id] input").edit("new record", {
confirm: false,
});
await runAllTimers();
await contains(".modal .o_field_widget[name=product_id] .ui-menu-item").click();
expect.verifySteps(["name_create"]);
});
test("many2one inside one2many form view, with domain", async () => {
expect.assertions(4);
Partner._fields.trululu.domain = "[['id', '<', 1000]]";
Partner._records = [
{ id: 1, name: "a1", p: [1] },
{ id: 2, name: "a2" },
{ id: 3, name: "a3" },
{ id: 4, name: "a4" },
{ id: 5, name: "a5" },
{ id: 6, name: "a6" },
{ id: 7, name: "a7" },
{ id: 8, name: "a8" },
{ id: 9, name: "a9" },
];
Partner._views = {
list: '<list><field name="name"/></list>',
search: "<search></search>",
};
onRpc("name_search", ({ kwargs }) => {
expect(kwargs.args).toEqual([["id", ">", 1]]);
});
onRpc("web_search_read", ({ kwargs }) => {
expect(kwargs.domain).toEqual([["id", ">", 1]]);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="p">
<list>
<field name="name"/>
<field name="trululu" domain="[]"/>
</list>
<form>
<field name="trululu" domain="[['id', '>', 1]]"/>
</form>
</field>
</sheet>
</form>`,
resId: 1,
});
await contains(".o_data_row .o_data_cell").click();
await contains(".o_field_widget[name=trululu] input").click();
await runAllTimers();
await clickFieldDropdownItem("trululu", "Search More...");
expect(".modal .o_list_view").toHaveCount(1);
expect(".modal .o_data_row").toHaveCount(8);
});
test("list in form: quick create then add a new line directly", async () => {
// required many2one inside a one2many list: directly after quick creating
// a new many2one value (before the name_create returns), click on add an item:
// at this moment, the many2one has still no value, and as it is required,
// the row is discarded if a saveLine is requested. However, it should
// wait for the name_create to return before trying to save the line.
expect.assertions(8);
Partner._onChanges = {
trululu: () => {},
};
const def = new Deferred();
const newRecordId = 5; // with the current records, the created record will be assigned id 5
onRpc("name_create", async () => {
await def;
});
onRpc("web_save", ({ args }) => {
expect(args[1].p[0][2].trululu).toBe(newRecordId);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="p">
<list editable="bottom">
<field name="trululu" required="1" />
</list>
</field>
</sheet>
</form>`,
});
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_field_widget[name=trululu] input").edit("b", { confirm: false });
await runAllTimers();
await contains(".ui-menu-item").click();
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(1);
expect(".o_data_row").toHaveClass("o_selected_row");
def.resolve();
await animationFrame();
expect(".o_data_row .o_data_cell:eq(0)").toHaveText("b");
expect(".o_data_row").toHaveCount(2);
expect(".o_data_row:eq(1)").toHaveClass("o_selected_row");
await clickSave();
expect(".o_data_row").toHaveCount(1);
expect(".o_data_row .o_data_cell").toHaveText("b");
});
test("list in form: create with one2many with many2one", async () => {
Partner._fields.p = fields.One2many({
string: "one2many field",
relation: "partner",
relation_field: "trululu",
default: [[0, 0, { name: "new record", p: [] }]],
});
onRpc("read", ({ args }) => {
if (args[1].length === 1 && args[1][0] === "name") {
throw new Error("read(['name']) should not be called");
}
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="p">
<list editable="bottom">
<field name="name" />
<field name="trululu" />
</list>
</field>
</sheet>
</form>`,
});
expect("td.o_data_cell:eq(0)").toHaveText("new record");
});
test("list in form: create with one2many with many2one (version 2)", async () => {
// This test simulates the exact same scenario as the previous one,
// except that the value for the many2one is explicitely set to false,
// which is stupid, but this happens, so we have to handle it
Partner._fields.p = fields.One2many({
string: "one2many field",
relation: "partner",
relation_field: "trululu",
default: [[0, 0, { name: "new record", trululu: false, p: [] }]],
});
onRpc("read", ({ args }) => {
if (args[1].length === 1 && args[1][0] === "name") {
throw new Error("read(['name']) should not be called");
}
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="p">
<list editable="bottom">
<field name="name" />
<field name="trululu" />
</list>
</field>
</sheet>
</form>`,
});
expect("td.o_data_cell:eq(0)").toHaveText("new record");
});
test("item not dropped on discard with empty required field (default_get)", async () => {
// This test simulates discarding a record that has been created with
// one of its required field that is empty. When we discard the changes
// on this empty field, it should not assume that this record should be
// abandonned, since it has been added (even though it is a new record).
Partner._fields.p = fields.One2many({
string: "one2many field",
relation: "partner",
relation_field: "trululu",
default: [[0, 0, { name: "new record", trululu: false, p: [] }]],
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="p">
<list editable="bottom">
<field name="name" />
<field name="trululu" required="1" />
</list>
</field>
</sheet>
</form>`,
});
expect("tr.o_data_row").toHaveCount(1);
expect("td.o_data_cell:eq(0)").toHaveText("new record");
expect("td.o_data_cell:eq(1)").toHaveText("");
await contains(".o_data_row .o_data_cell").click();
expect(".o_selected_row .o_data_cell:eq(1)").toHaveClass("o_required_modifier");
// discard by clicking on body
await contains(getFixture()).click();
expect("tr.o_data_row").toHaveCount(1);
expect("td.o_data_cell:eq(0)").toHaveText("new record");
expect("td.o_data_cell:eq(1)").toHaveText("");
await contains(".o_data_row .o_data_cell").click();
expect(".o_selected_row .o_data_cell:eq(1)").toHaveClass("o_required_modifier");
});
test("list in form: read with unique ids (default_get)", async () => {
Partner._records[0].name = "MyTrululu";
Partner._fields.p = fields.One2many({
string: "one2many field",
relation: "partner",
relation_field: "trululu",
default: [
[0, 0, { trululu: 1, p: [] }],
[0, 0, { trululu: 1, p: [] }],
],
});
onRpc("read", ({ args }) => {
if (args[1].length === 1 && args[1][0] === "name") {
throw new Error("read(['name']) should not called");
}
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="p">
<list editable="bottom">
<field name="trululu" />
</list>
</field>
</sheet>
</form>`,
});
expect(queryAllTexts("td.o_data_cell")).toEqual(["MyTrululu", "MyTrululu"]);
});
test("list in form: show name of many2one fields in multi-page (default_get)", async () => {
Partner._fields.p = fields.One2many({
string: "one2many field",
relation: "partner",
relation_field: "trululu",
default: [
[0, 0, { name: "record1", trululu: 1, p: [] }],
[0, 0, { name: "record2", trululu: 2, p: [] }],
],
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="p">
<list editable="bottom" limit="1">
<field name="name" />
<field name="trululu" />
</list>
</field>
</sheet>
</form>`,
});
expect(queryAllTexts("td.o_data_cell")).toEqual([
"record1",
"first record",
"record2",
"second record",
]);
});
test("list in form: item not dropped on discard with empty required field (onchange in default_get)", async () => {
// variant of the test "list in form: discard newly added element with
// empty required field (default_get)", in which the `default_get`
// performs an `onchange` at the same time. This `onchange` may create
// some records, which should not be abandoned on discard, similarly
// to records created directly by `default_get`
Partner._fields.product_id = fields.Many2one({
string: "Product",
relation: "product",
default: 37,
onChange: (obj) => {
if (obj.product_id === 37) {
obj.p = [[0, 0, { name: "entry", trululu: false }]];
}
},
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="product_id" />
<field name="p">
<list editable="bottom">
<field name="name" />
<field name="trululu" required="1" />
</list>
</field>
</form>`,
});
// check that there is a record in the editable list with empty string as required field
expect(".o_data_row").toHaveCount(1);
expect("td.o_data_cell:eq(0)").toHaveText("entry");
expect("td.o_data_cell.o_required_modifier").toHaveCount(1);
expect("td.o_data_cell.o_required_modifier").toHaveText("");
// click on empty required field in editable list record
await contains("td.o_data_cell.o_required_modifier").click();
// click off so that the required field still stay empty
await contains(getFixture()).click();
// record should not be dropped
expect(".o_data_row").toHaveCount(1);
expect("td.o_data_cell:eq(0)").toHaveText("entry");
expect("td.o_data_cell.o_required_modifier").toHaveText("");
});
test("list in form: item not dropped on discard with empty required field (onchange on list after default_get)", async () => {
// discarding a record from an `onchange` in a `default_get` should not
// abandon the record. This should not be the case for following
// `onchange`, except if an onchange make some changes on the list:
// in particular, if an onchange make changes on the list such that
// a record is added, this record should not be dropped on discard
Partner._onChanges = {
product_id: (obj) => {
if (obj.product_id === 37) {
obj.p = [[0, 0, { name: "entry", trululu: false }]];
}
},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="product_id" />
<field name="p">
<list editable="bottom">
<field name="name" />
<field name="trululu" required="1" />
</list>
</field>
</form>`,
});
// check no record in list
expect(".o_data_row").toHaveCount(0);
// select product_id to force on_change in editable list
await contains("div[name=product_id] input").click();
await contains(".ui-menu-item").click();
// check that there is a record in the editable list with empty string as required field
expect(".o_data_row").toHaveCount(1);
expect("td.o_data_cell:eq(0)").toHaveText("entry");
expect("td.o_required_modifier").toHaveCount(1);
expect("td.o_required_modifier").toHaveText("");
// click on empty required field in editable list record
await contains("td.o_required_modifier").click();
// click off so that the required field still stay empty
await contains(getFixture()).click();
// record should not be dropped
expect(".o_data_row").toHaveCount(1);
expect(queryAllTexts("td.o_data_cell")).toEqual(["entry", ""]);
});
test('item dropped on discard with empty required field with "Add an item" (invalid on "ADD")', async () => {
// when a record in a list is added with "Add an item", it should
// always be dropped on discard if some required field are empty
// at the record creation.
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="p">
<list editable="bottom">
<field name="name" />
<field name="trululu" required="1" />
</list>
</field>
</form>`,
});
// Click on "Add an item"
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_field_widget.o_required_modifier[name=trululu]").toHaveCount(1);
expect(".o_field_widget.o_required_modifier[name=trululu] input").toHaveValue("");
// click on empty required field in editable list record
await contains(".o_field_widget.o_required_modifier[name=trululu] input").click();
// click off so that the required field still stay empty
await contains(getFixture()).click();
// record should be dropped
expect(".o_data_row").toHaveCount(0);
});
test('item not dropped on discard with empty required field with "Add an item" (invalid on "UPDATE")', async () => {
// when a record in a list is added with "Add an item", it should
// be temporarily added to the list when it is valid (e.g. required
// fields are non-empty). If the record is updated so that the required
// field is empty, and it is discarded, then the record should not be
// dropped.
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="p">
<list editable="bottom">
<field name="name" />
<field name="trululu" required="1" />
</list>
</field>
</form>`,
});
expect(".o_data_row").toHaveCount(0);
// Click on "Add an item"
await contains(".o_field_x2many_list_row_add a").click();
expect(".o_data_row").toHaveCount(1);
expect(".o_field_widget.o_required_modifier[name=trululu] input").toHaveCount(1);
expect(".o_field_widget.o_required_modifier[name=trululu] input").toHaveValue("");
// add something to required field and leave edit mode of the record
await contains(".o_field_widget.o_required_modifier[name=trululu] input").click();
await contains("li.ui-menu-item").click();
await contains(getFixture()).click();
expect(".o_data_row").toHaveCount(1);
expect(".o_data_cell:eq(1)").toHaveText("first record");
// leave edit mode of the record
await contains(getFixture()).click();
expect(".o_data_row").toHaveCount(1);
expect(".o_data_cell:eq(1)").toHaveText("first record");
});
// WARNING: this does not seem to be a many2one field test
test("list in form: default_get with x2many create", async () => {
expect.assertions(3);
Partner._fields.timmy = fields.Many2many({
string: "pokemon",
relation: "partner.type",
default: [[0, 0, { name: "brandon is the new timmy" }]],
onChange: (obj) => {
obj.int_field = obj.timmy.length;
},
});
onRpc("web_save", ({ args }) => {
expect(args[1]).toEqual({
int_field: 1,
timmy: [[0, args[1].timmy[0][1], { name: "new value" }]],
});
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="timmy">
<list editable="bottom">
<field name="name" />
</list>
</field>
<field name="int_field" />
</sheet>
</form>`,
});
expect("td.o_data_cell").toHaveText("brandon is the new timmy");
expect(".o_field_integer input").toHaveValue("1");
// edit the subrecord and save
await contains(".o_data_cell").click();
await contains(".o_data_cell input").edit("new value", { confirm: false });
await clickSave();
});
// WARNING: this does not seem to be a many2one field test
test("list in form: default_get with x2many create and onchange", async () => {
expect.assertions(1);
Partner._fields.turtles = fields.One2many({
string: "one2many turtle field",
relation: "turtle",
relation_field: "turtle_trululu",
default: [
[4, 2],
[4, 3],
],
});
onRpc("web_save", ({ args }) => {
expect(args[1].turtles).toEqual([
[4, 2],
[4, 3],
]);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="turtles">
<list editable="bottom">
<field name="turtle_foo" />
</list>
</field>
<field name="int_field" />
</sheet>
</form>`,
});
await clickSave();
});
test("list in form: call button in sub view", async () => {
Partner._records[0].p = [2];
Product._views = {
form: `
<form>
<header>
<button name="action" type="action" string="Just do it !" />
<button name="object" type="object" string="Just don't do it !" />
<field name="name" />
</header>
</form>`,
};
const def = new Deferred();
mockService("action", {
doActionButton(params) {
const { name, resModel, resId, resIds } = params;
expect.step(name);
expect(resModel).toBe("product");
expect(resId).toBe(37);
expect(resIds).toEqual([37]);
return def.then(() => {
params.onClose();
});
},
});
onRpc("get_formview_id", () => {
return false;
});
await mountViewInDialog({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="p">
<list editable="bottom">
<field name="product_id"/>
</list>
</field>
</form>`,
});
expect(".modal").toHaveCount(1);
await contains("td.o_data_cell").click();
await contains(".o_external_button", { visible: false }).click();
expect(".modal").toHaveCount(2);
const buttons = queryAll(".o_dialog:not(.o_inactive_modal) .o_form_statusbar button");
await contains(buttons[0]).click();
expect.verifySteps(["action"]);
expect(buttons[1]).not.toBeEnabled();
def.resolve();
await animationFrame();
await contains(".modal:eq(1) .o_form_button_cancel").click();
expect(".modal").toHaveCount(1);
await contains(".o_external_button", { visible: false }).click();
expect(".modal").toHaveCount(2);
await contains(".o_dialog:not(.o_inactive_modal) .o_form_statusbar button:eq(1)").click();
expect.verifySteps(["object"]);
});
test("X2Many sequence list in modal", async () => {
Partner._fields.sequence = fields.Integer({
string: "Sequence",
type: "integer",
onChange: (obj) => {
if (obj.id === 2) {
obj.sequence = 1;
expect.step("onchange sequence");
}
},
});
Partner._records[0].sequence = 1;
Partner._records[1].sequence = 2;
Product._fields.turtle_ids = fields.One2many({
string: "Turtles",
relation: "turtle",
});
Product._records[0].turtle_ids = [1];
Product._records[0].name = "leonardo";
Product._records[0].name = "xphone";
Turtle._fields.partnertypes_ids = fields.One2many({
string: "Partner",
relation: "partner",
});
Turtle._fields.type_id = fields.Many2one({
string: "Partner Type",
relation: "partner.type",
});
Turtle._records[0].type_id = 12;
PartnerType._fields.partner_ids = fields.One2many({
string: "Partner",
relation: "partner",
});
PartnerType._records[0].partner_ids = [1, 2];
PartnerType._views = {
form: `
<form>
<field name="partner_ids" />
</form>`,
};
Partner._views = {
list: `
<list>
<field name="name" />
<field name="sequence" widget="handle" />
</list>`,
};
onRpc("partner.type", "get_formview_id", () => {
return false;
});
onRpc("partner.type", "web_save", () => {
expect.step("partner.type web_save");
});
await mountViewInDialog({
type: "form",
resModel: "product",
resId: 37,
arch: `
<form>
<field name="name" />
<field name="turtle_ids" widget="one2many">
<list editable="bottom">
<field name="type_id"/>
</list>
</field>
</form>`,
});
expect(".modal").toHaveCount(1);
await contains(".o_data_cell").click();
await contains(".o_external_button", { visible: false }).click();
expect(".modal").toHaveCount(2);
expect(".modal:eq(1) .ui-sortable-handle").toHaveCount(2);
await contains(
".o_dialog:not(.o_inactive_modal) .o_data_row:nth-child(2) .ui-sortable-handle"
).dragAndDrop(".o_dialog:not(.o_inactive_modal) tbody tr");
// Saving the modal and then the original model
await contains(".modal:eq(1) .o_form_button_save").click();
await clickSave();
expect.verifySteps(["onchange sequence", "partner.type web_save"]);
});
test("autocompletion in a many2one, in form view with a domain", async () => {
expect.assertions(1);
onRpc("name_search", ({ kwargs }) => {
expect(kwargs.args).toEqual([]);
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
domain: [["trululu", "=", 4]],
arch: '<form><field name="product_id" /></form>',
});
await contains(".o_field_widget[name=product_id] input").click();
});
test("autocompletion in a many2one, in form view with a date field", async () => {
expect.assertions(1);
onRpc("name_search", ({ kwargs }) => {
expect(kwargs.args).toEqual([["bar", "=", true]]);
});
await mountView({
type: "form",
resModel: "partner",
resId: 2,
arch: `
<form>
<field name="bar" />
<field name="date" />
<field name="trululu" domain="[('bar', '=', True)]" />
</form>`,
});
await contains(".o_field_widget[name='trululu'] input").click();
});
test("creating record with many2one with option always_reload", async () => {
Partner._fields.trululu = fields.Many2one({
string: "Trululu",
relation: "partner",
default: 1,
onChange: (obj) => {
obj.trululu = 2; //[2, "second record"];
},
});
onRpc("onchange", async ({ parent }) => {
const result = await parent();
result.value.trululu = {
...result.value.trululu,
display_name: "hello world\nso much noise",
};
return result;
});
onRpc(({ method }) => {
expect.step(method);
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="trululu" />
</form>`,
});
expect(".o_field_widget[name='trululu'] input").toHaveValue("hello world");
expect.verifySteps(["get_views", "onchange"]);
});
test("empty list with sample data and many2one with option always_reload", async () => {
await mountView({
type: "list",
resModel: "partner",
arch: `
<list sample="1">
<field name="product_id" />
</list>`,
context: { search_default_empty: true },
searchViewArch: `
<search>
<filter name="empty" domain="[('id', '&lt;', 0)]"/>
</search>`,
});
expect(".o_list_view .o_content").toHaveClass("o_view_sample_data");
expect(".o_list_table").toHaveCount(1);
expect(".o_data_row").toHaveCount(10);
expect("thead tr th").toHaveCount(2);
});
test("selecting a many2one, then discarding", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: '<form><field name="product_id" /></form>',
});
expect(".o_field_widget[name='product_id'] input").toHaveValue("");
await contains(".o_field_widget[name='product_id'] input").click();
await contains(".o_field_widget[name='product_id'] .dropdown-item").click();
expect(".o_field_widget[name='product_id'] input").toHaveValue("xphone");
await contains(".o_form_button_cancel").click();
expect(".o_field_widget[name='product_id'] input").toHaveValue("");
});
test("domain and context are correctly used when doing a name_search in a m2o", async () => {
expect.assertions(4);
Partner._records[0].timmy = [12];
// Need to take into account the company service which populates context at startup
const DEFAULT_USER_CTX = { ...user.context, allowed_company_ids: [1] };
serverState.userContext = { hey: "ho" };
onRpc("product", "name_search", ({ kwargs }) => {
expect(kwargs.args).toEqual(["&", ["foo", "=", "bar"], ["foo", "=", "yop"]]);
expect(kwargs.context).toEqual({
...DEFAULT_USER_CTX,
hey: "ho",
hello: "world",
test: "yop",
});
return [];
});
onRpc("partner", "name_search", ({ kwargs }) => {
expect(kwargs.args).toEqual([["id", "in", [12]]]);
expect(kwargs.context).toEqual({
...DEFAULT_USER_CTX,
hey: "ho",
timmy: [12],
});
return [];
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="product_id" domain="[('foo', '=', 'bar'), ('foo', '=', foo)]" context="{'hello': 'world', 'test': foo}" />
<field name="foo" />
<field name="trululu" context="{'timmy': timmy}" domain="[('id', 'in', timmy)]" />
<field name="timmy" widget="many2many_tags" invisible="1" />
</form>`,
});
await contains(".o_field_widget[name='product_id'] input").click();
await contains(".o_field_widget[name='trululu'] input").click();
});
test("quick create on a many2one", async () => {
expect.assertions(1);
onRpc("name_create", ({ args }) => {
expect(args[0]).toBe("new partner");
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="product_id" />
</sheet>
</form>`,
});
await contains(".o_field_many2one input").edit("new partner", { confirm: false });
await runAllTimers();
await press("tab");
});
test("failing quick create on a many2one because ValidationError", async () => {
expect.assertions(5);
Product._views = {
form: '<form><field name="name" /></form>',
};
onRpc("name_create", () => {
throw makeServerError({ type: "ValidationError" });
});
onRpc("web_save", ({ args }) => {
expect(args[1]).toEqual({ name: "xyz" });
});
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="product_id" /></form>',
});
await contains(".o_field_widget[name='product_id'] input").edit("abcd", { confirm: false });
await runAllTimers();
await contains(".o_field_widget[name='product_id'] .dropdown-item").click();
await animationFrame(); // wait for the error service to ensure that there's no error dialog
expect(".o_error_dialog").toHaveCount(0);
expect(".modal .o_form_view").toHaveCount(1);
expect(".modal .o_field_widget[name='name'] input").toHaveValue("abcd");
await contains(".modal .o_field_widget[name='name'] input").edit("xyz");
await contains(".modal .o_form_button_save").click();
expect(".o_field_widget[name='product_id'] input").toHaveValue("xyz");
});
test("failing quick create on a many2one", async () => {
expect.assertions(2);
expect.errors(1);
Product._views = {
form: '<form><field name="name" /></form>',
};
onRpc("name_create", () => {
throw makeServerError();
});
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="product_id" /></form>',
});
await contains(".o_field_widget[name='product_id'] input").edit("abcd", { confirm: false });
await runAllTimers();
await contains(".o_field_widget[name='product_id'] .dropdown-item").click();
await animationFrame(); // wait for the error service
expect(".o_error_dialog").toHaveCount(1);
expect(".modal .o_form_view").toHaveCount(0);
});
test("failing quick create on a many2one inside a one2many because ValidationError", async () => {
expect.assertions(4);
Partner._views = {
list: `
<list editable="bottom">
<field name="product_id" />
</list>`,
};
Product._views = {
form: '<form><field name="name" /></form>',
};
onRpc("name_create", () => {
throw makeServerError({ type: "ValidationError" });
});
onRpc("web_save", ({ args }) => {
expect(args[1]).toEqual({ name: "xyz" });
});
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="p" /></form>',
});
await contains(".o_field_x2many_list_row_add a").click();
await contains(".o_field_widget[name='product_id'] input").edit("abcd", { confirm: false });
await runAllTimers();
await contains(".o_field_widget[name='product_id'] .dropdown-item").click();
expect(".modal .o_form_view").toHaveCount(1);
expect(".modal .o_field_widget[name='name'] input").toHaveValue("abcd");
await contains(".modal .o_field_widget[name='name'] input").edit("xyz");
await contains(".modal .o_form_button_save").click();
expect(".o_field_widget[name='product_id'] input").toHaveValue("xyz");
});
test("slow create on a many2one", async () => {
Product._views = {
form: '<form><field name="name" /></form>',
};
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="product_id" options="{'no_quick_create': 1}" />
</sheet>
</form>`,
});
await contains(".o_field_many2one input").edit("new product", { confirm: false });
await runAllTimers();
await press("tab");
await animationFrame();
expect(".modal").toHaveCount(1);
// cancel the many2one creation with Discard button
await contains(".modal .modal-footer .btn:not(.btn-primary)").click();
expect(".modal").toHaveCount(0);
expect(".o_field_many2one input").toHaveValue("");
// cancel the many2one creation with Close button
await contains(".o_field_many2one input").edit("new product", { confirm: false });
await runAllTimers();
await press("tab");
await animationFrame();
expect(".modal").toHaveCount(1);
await contains(".modal .modal-header button").click();
expect(".o_field_many2one input").toHaveValue("");
expect(".modal").toHaveCount(0);
// select a new value then cancel the creation of the new one --> restore the previous
await contains(".o_field_widget[name=product_id] input").click();
await contains(".ui-menu-item").click();
expect(".o_field_many2one input").toHaveValue("xphone");
await contains(".o_field_many2one input").edit("new product", { confirm: false });
await runAllTimers();
await press("tab");
await animationFrame();
expect(".modal").toHaveCount(1);
await contains(".modal .modal-footer .btn:not(.btn-primary)").click();
expect(".o_field_many2one input").toHaveValue("");
// confirm the many2one creation
await contains(".o_field_many2one input").edit("new product", { confirm: false });
await runAllTimers();
await press("tab");
await animationFrame();
expect(".modal .o_form_view").toHaveCount(1);
await contains(".modal .o_form_button_cancel").click();
});
test("select a many2one value by pressing tab", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="product_id" /></form>',
});
await contains(".o_field_many2one input").edit("xph", { confirm: false });
await runAllTimers();
await press("tab");
await animationFrame();
expect(".modal").toHaveCount(0);
expect(".o_field_many2one input").toHaveValue("xphone");
expect(".o_external_button").toHaveCount(1);
});
test("no_create option on a many2one", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="product_id" options="{'no_create': 1}" />
</sheet>
</form>`,
});
await contains(".o_field_many2one input").edit("new partner", { confirm: false });
await runAllTimers();
await press("tab");
expect(".modal").toHaveCount(0);
expect(".o_field_many2one input").toHaveValue("");
});
test("no_create option on a many2one when can_create is absent", async () => {
Partner._fields.product_id.readonly = true;
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="product_id" options="{'no_create': 1}" readonly="0" />
</sheet>
</form>`,
});
await contains(".o_field_many2one input").edit("new partner", { confirm: false });
await runAllTimers();
await press("tab");
expect(".modal").toHaveCount(0);
expect(".o_field_many2one input").toHaveValue("");
});
test("no_quick_create option on a many2one when can_create is absent", async () => {
Partner._fields.product_id.readonly = true;
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="product_id" options="{'no_quick_create': 1}" readonly="0" />
</sheet>
</form>`,
});
await contains(".o_field_many2one input").edit("new partner", { confirm: false });
await runAllTimers();
expect(".ui-autocomplete .o_m2o_dropdown_option").toHaveCount(1);
expect(".ui-autocomplete .o_m2o_dropdown_option").toHaveClass(
"o_m2o_dropdown_option_create_edit"
);
});
test("can_create and can_write option on a many2one", async () => {
Product.options = {
can_create: "false",
can_write: "false",
};
Product._views = {
form: `
<form>
<field name="name" />
</form>`,
};
onRpc("product", "get_formview_id", () => {
return false;
});
await mountViewInDialog({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="product_id" can_create="false" can_write="false"/>
</sheet>
</form>`,
});
expect(".modal").toHaveCount(1);
await contains(".o_field_many2one input").click();
expect(".o_m2o_dropdown_option.o_m2o_dropdown_option_create").toHaveCount(0);
await contains(".ui-menu-item:eq(1)").click();
expect(".o_field_many2one input").toHaveValue("xpad");
expect(".o_field_many2one .o_external_button").toHaveCount(1);
await contains(".o_field_many2one .o_external_button", { visible: false }).click();
expect(".modal").toHaveCount(2);
expect(".modal .o_form_view .o_form_readonly").toHaveCount(1);
await contains(".o_dialog:not(.o_inactive_modal) .modal-footer .btn-primary").click();
await contains(".o_field_many2one input").edit("new product");
expect(".modal").toHaveCount(1);
});
test("create_name_field option on a many2one", async () => {
// when the 'create_name_field' option is set, the value entered in the
// many2one input should be used to populate this specified field,
// instead of the generic 'name' field.
Partner._views = {
form: `
<form>
<field name="foo" />
</form>`,
};
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="trululu" options="{'create_name_field': 'foo'}" />
</sheet>
</form>`,
});
await contains(".o_field_widget[name=trululu] input").edit("yz", { confirm: false });
await runAllTimers();
await contains(".o_field_widget[name=trululu] input").click();
await selectFieldDropdownItem("trululu", "Create and edit...");
expect(".o_field_widget[name=foo] input").toHaveValue("yz");
await contains(".o_form_button_cancel").click();
});
test("propagate can_create onto the search popup", async () => {
Product._records = [
{ id: 1, name: "Tromblon1" },
{ id: 2, name: "Tromblon2" },
{ id: 3, name: "Tromblon3" },
{ id: 4, name: "Tromblon4" },
{ id: 5, name: "Tromblon5" },
{ id: 6, name: "Tromblon6" },
{ id: 7, name: "Tromblon7" },
{ id: 8, name: "Tromblon8" },
...Product._records,
];
Product._views = {
list: `
<list>
<field name="name"/>
</list>`,
search: `
<search>
<field name="name"/>
</search>`,
};
onRpc("get_formview_id", () => {
return false;
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="name"/>
<field name="product_id" can_create="false"/>
</form>`,
});
await contains(".o_field_widget[name=product_id] input").click();
expect(".o-autocomplete a:contains(Start typing...)").toHaveCount(0);
await contains(".o_field_widget[name=product_id] input").edit("a", { confirm: false });
await runAllTimers();
expect(".ui-autocomplete a:contains(Create and Edit)").toHaveCount(0);
await contains(".o_field_many2one[name=product_id] input").edit("", { confirm: false });
await runAllTimers();
await clickFieldDropdownItem("product_id", "Search More...");
expect(".modal-dialog.modal-lg").toHaveCount(1);
expect(queryAllTexts(".modal-footer button")).toEqual(["Close"]);
});
test("many2one with can_create=false shows no result item when searched something that doesn't exist", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="product_id" can_create="false" can_write="false" />
</sheet>
</form>`,
});
await contains(".o_field_many2one input").click();
await contains(".o_field_many2one[name=product_id] input").edit("abc", { confirm: false });
await runAllTimers();
expect(".o_field_many2one[name=product_id] .o_m2o_dropdown_option_create").toHaveCount(0);
expect(".o_field_many2one[name=product_id] .o_m2o_no_result").toHaveCount(1);
await contains(getFixture()).click();
expect(".o_field_many2one[name=product_id] .o_m2o_no_result").toHaveCount(0);
});
test("pressing enter in a m2o in an editable list", async () => {
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="bottom">
<field name="product_id" />
</list>`,
});
await contains("td.o_data_cell").click();
expect(".o_selected_row").toHaveCount(1);
// we now write 'a' and press enter to check that the selection is
// working, and prevent the navigation
await contains("[name=product_id] input").edit("a", { confirm: false });
await runAllTimers();
expect("[name=product_id] .o-autocomplete--dropdown-menu").toHaveCount(1);
// we now trigger ENTER to select first choice
await press("Enter");
await animationFrame();
expect("[name=product_id] input").toBeFocused();
expect("[name=product_id] .o-autocomplete--dropdown-menu").toHaveCount(0);
// we now trigger again ENTER to make sure we can move to next line
await press("Enter");
await animationFrame();
expect("tr.o_data_row:nth-child(1) [name=product_id] input").toHaveCount(0);
expect("tr.o_data_row:nth-child(2)").toHaveClass("o_selected_row");
// we now write again 'a' in the cell to select xpad. We will now
// test with the tab key
await contains("[name=product_id] input").edit("a", { confirm: false });
await runAllTimers();
expect(
"tr.o_data_row:nth-child(2) [name=product_id] .o-autocomplete--dropdown-menu"
).toHaveCount(1);
await press("Tab");
await animationFrame();
expect("tr.o_data_row:nth-child(2) [name=product_id] input").toHaveCount(0);
expect("tr.o_data_row:nth-child(3)").toHaveClass("o_selected_row");
});
test("pressing ENTER on a 'no_quick_create' many2one should open a M2ODialog", async () => {
Partner._views = {
form: `
<form>
<field name="name" />
</form>`,
};
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="trululu" options="{'no_quick_create': 1}" />
<field name="foo" />
</form>`,
});
await contains(".o_field_many2one input").edit("Something that does not exist", {
confirm: false,
});
await runAllTimers();
await press("Enter");
await animationFrame();
expect(".modal").toHaveCount(1);
// Check that discarding clears $input
await contains(".modal .o_form_button_cancel").click();
expect(".o_field_many2one input").toHaveValue("");
});
test("select a value by pressing TAB on a many2one with onchange", async () => {
Partner._onChanges = {
trululu: () => {},
};
const def = new Deferred();
onRpc("onchange", () => def);
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="trululu" />
<field name="name" />
</form>`,
});
await contains(".o_field_many2one input").edit("first", { confirm: "tab" });
// simulate a focusout (e.g. because the user clicks outside)
// before the onchange returns
await click(".o_field_char");
expect(".modal").toHaveCount(0);
// unlock the onchange
def.resolve();
await runAllTimers();
expect(".o_field_many2one input").toHaveValue("first record");
expect(".modal").toHaveCount(0);
});
test("leaving a many2one by pressing tab", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="trululu"/>
<field name="name"/>
</form>`,
});
await contains(".o_field_many2one input").click();
await runAllTimers();
await press("tab");
await animationFrame();
expect(".o_field_many2one input").toHaveValue("");
// open autocomplete dropdown and manually select item by UP/DOWN key and press TAB
await contains(".o_field_many2one input").click();
await runAllTimers();
await press("arrowdown");
await press("tab");
await animationFrame();
expect(".o_field_many2one input").toHaveValue("second record");
// clear many2one and then open autocomplete, write something and press TAB
await contains(".o_field_many2one input").edit("", { confirm: false });
await runAllTimers();
await contains(".o_field_many2one input").click();
await contains(".o_field_many2one input").edit("se", { confirm: "tab" });
await runAllTimers();
expect(".o_field_many2one input").toHaveValue("second record");
});
test("leaving an empty many2one by pressing tab (after backspace or delete)", async () => {
expect.assertions(4);
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="trululu"/>
<field name="name"/>
</form>`,
});
expect(".o_field_many2one input").toHaveValue();
// simulate backspace to remove values and press TAB
await contains(".o_field_many2one input").edit("", { confirm: false });
await runAllTimers();
await press("backspace");
await press("tab");
await animationFrame();
expect(".o_field_many2one input").toHaveValue("");
// reset a value
await selectFieldDropdownItem("trululu", "first record");
expect(".o_field_many2one input").toHaveValue("first record");
// simulate delete to remove values and press TAB
await contains(".o_field_many2one input").edit("", { confirm: false });
await runAllTimers();
await press("delete");
await press("tab");
// TODO: fix owl
await animationFrame();
expect(".o_field_many2one input").toHaveValue("");
});
test("many2one in editable list + onchange, with enter", async () => {
Partner._onChanges = {
product_id: (obj) => {
obj.int_field = obj.product_id || 0;
},
};
const def = new Deferred();
onRpc("onchange", () => def);
onRpc(({ method }) => {
expect.step(method);
});
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="bottom">
<field name="product_id" />
<field name="int_field" />
</list>`,
});
await contains("td.o_data_cell").click();
await contains("td.o_data_cell input").edit("a", { confirm: false });
await runAllTimers();
await press("enter");
def.resolve();
await animationFrame();
await press("enter");
await animationFrame();
expect(".modal").toHaveCount(0);
expect.verifySteps([
"get_views",
"web_search_read", // to display results in the dialog
"has_group",
"name_search",
"onchange",
"web_save",
]);
});
test("many2one in editable list + onchange, with enter, part 2", async () => {
// this is the same test as the previous one, but the onchange is just
// resolved slightly later
Partner._onChanges = {
product_id: (obj) => {
obj.int_field = obj.product_id || 0;
},
};
const def = new Deferred();
onRpc("onchange", () => def);
onRpc(({ method }) => {
expect.step(method);
});
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="bottom">
<field name="product_id" />
<field name="int_field" />
</list>`,
});
await contains("td.o_data_cell").click();
await contains("td.o_data_cell input").edit("a", { confirm: false });
await runAllTimers();
await press("enter");
await press("enter");
def.resolve();
await animationFrame();
expect(".modal").toHaveCount(0);
expect.verifySteps([
"get_views",
"web_search_read", // to display results in the dialog
"has_group",
"name_search",
"onchange",
"web_save",
]);
});
test("many2one: dynamic domain set in the field's definition", async () => {
expect.assertions(2);
Partner._fields.trululu = fields.Many2one({
string: "Trululu",
relation: "partner",
domain: "[('foo' ,'=', foo)]",
});
onRpc("name_search", ({ kwargs }) => {
expect(kwargs.args).toEqual([["foo", "=", "yop"]]);
});
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="top">
<field name="foo" invisible="1" />
<field name="trululu" />
</list>`,
});
await contains(".o_data_cell:eq(0)").click();
await contains(".o_field_many2one input").click();
expect(".o_field_many2one .o-autocomplete--dropdown-item").toHaveCount(2);
});
test("many2one: domain set in view and on field", async () => {
expect.assertions(2);
Partner._fields.trululu = fields.Many2one({
string: "Trululu",
relation: "partner",
domain: "[('foo' ,'=', 'boum')]",
});
onRpc("name_search", ({ kwargs }) => {
// should only use the domain set in the view
expect(kwargs.args).toEqual([["foo", "=", "blip"]]);
});
await mountView({
type: "list",
resModel: "partner",
arch: `
<list editable="top">
<field name="foo" invisible="1"/>
<field name="trululu" domain="[['foo', '=', 'blip']]"/>
</list>`,
});
await contains(".o_data_cell:eq(0)").click();
await contains(".o_field_many2one input").click();
expect(".o_field_many2one .o-autocomplete--dropdown-item").toHaveCount(2);
});
test("many2one: domain updated by an onchange", async () => {
expect.assertions(2);
Partner._onChanges = {
int_field: () => {},
};
let domain = [];
onRpc("onchange", () => {
domain = [["id", "in", [10]]];
return {
domain: {
trululu: domain,
unexisting_field: domain,
},
};
});
onRpc("name_search", ({ kwargs }) => {
expect(kwargs.args).toEqual(domain);
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="int_field" />
<field name="trululu" />
</form>`,
});
// trigger a name_search (domain should be [])
await contains(".o_field_widget[name=trululu] input").click();
// close the dropdown
await contains(".o_field_widget[name=trululu] input").click();
// trigger an onchange that will update the domain
// trigger a name_search (domain should be [['id', 'in', [10]]])
await contains(".o_field_widget[name='trululu'] input").click();
});
test("search more in many2one: no text in input", async () => {
// when the user clicks on 'Search More...' in a many2one dropdown, and there is no text
// in the input (i.e. no value to search on), we bypass the name_search that is meant to
// return a list of preselected ids to filter on in the list view (opened in a dialog)
expect.assertions(2);
for (let i = 0; i < 8; i++) {
Partner._records.push({ id: 100 + i, name: `test_${i}` });
}
Partner._views = {
list: `
<list>
<field name="name" />
</list>`,
search: `<search />`,
};
onRpc(({ method }) => {
expect.step(method);
});
onRpc("web_search_read", ({ kwargs }) => {
expect(kwargs.domain).toEqual([]);
});
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="trululu" /></form>',
});
await contains(`.o_field_widget[name="trululu"] input`).clear();
await contains(`.o_field_widget[name="trululu"] input`).click();
await contains(`.o_field_widget[name="trululu"] .o_m2o_dropdown_option_search_more`).click();
expect.verifySteps([
"get_views", // main form view
"onchange",
"name_search", // to display results in the dropdown
"get_views", // list view in dialog
"has_group",
"web_search_read", // to display results in the dialog
]);
});
test("search more in many2one: text in input", async () => {
// when the user clicks on 'Search More...' in a many2one dropdown, and there is some
// text in the input, we perform a name_search to get a (limited) list of preselected
// ids and we add a dynamic filter (with those ids) to the search view in the dialog, so
// that the user can remove this filter to bypass the limit
expect.assertions(5);
for (let i = 0; i < 8; i++) {
Partner._records.push({ id: 100 + i, name: `test_${i}` });
}
Partner._views = {
list: `
<list>
<field name="name" />
</list>`,
search: `<search />`,
};
let expectedDomain;
onRpc(({ method }) => {
expect.step(method);
});
onRpc("web_search_read", ({ kwargs }) => {
expect(kwargs.domain).toEqual(expectedDomain);
});
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="trululu" /></form>',
});
expectedDomain = [["id", "in", [100, 101, 102, 103, 104, 105, 106, 107]]];
await contains(`.o_field_widget[name="trululu"] input`).click();
await contains(".o_field_widget[name='trululu'] input").edit("test", { confirm: false });
await runAllTimers();
await contains(`.o_field_widget[name="trululu"] .o_m2o_dropdown_option_search_more`).click();
expect(".modal .o_list_view").toHaveCount(1);
expect(".modal .o_cp_searchview .o_facet_values").toHaveCount(1);
// remove the filter on ids
expectedDomain = [];
await contains(".modal .o_cp_searchview .o_facet_remove").click();
expect.verifySteps([
"get_views", // main form view
"onchange",
"name_search", // empty search, triggered when the user clicks in the input
"name_search", // to display results in the dropdown
"name_search", // to get preselected ids matching the search
"get_views", // list view in dialog
"has_group",
"web_search_read", // to display results in the dialog
"web_search_read", // after removal of dynamic filter
]);
});
test("search more in many2one: dropdown click", async () => {
for (let i = 0; i < 8; i++) {
Partner._records.push({ id: 100 + i, name: `test_${i}` });
}
Partner._views = {
list: `
<list>
<field name="name" />
</list>`,
search: `<search />`,
};
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="trululu" /></form>',
});
await contains(`.o_field_widget[name="trululu"] input`).click();
await contains(".o_field_widget[name='trululu'] input").edit("test", { confirm: false });
await runAllTimers();
await contains(`.o_field_widget[name="trululu"] .o_m2o_dropdown_option_search_more`).click();
// dropdown selector
const searchDropdown = ".o_control_panel_actions .o-dropdown";
await contains(searchDropdown).click();
expect(searchDropdown).toHaveClass("show");
expect(getDropdownMenu(searchDropdown)).toBeVisible();
});
test("updating a many2one from a many2many", async () => {
expect.assertions(5);
Turtle._records[1].turtle_trululu = 1;
Partner._views = {
form: `
<form>
<field name="name" />
</form>`,
};
onRpc("get_formview_id", ({ args }) => {
expect(args[0]).toEqual([1]);
return false;
});
await mountViewInDialog({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="turtles">
<list editable="bottom">
<field name="name" />
<field name="turtle_trululu"/>
</list>
</field>
</form>`,
});
expect(".modal").toHaveCount(1);
// Opening the modal
await contains(".o_data_row td:eq(1)").click();
await contains(".o_external_button", { visible: false }).click();
expect(".modal").toHaveCount(2);
// Changing the 'trululu' value
await contains(".o_dialog:not(.o_inactive_modal) div[name=name] input").edit("test");
await contains(".modal:eq(1) .o_form_button_save").click();
expect(".modal").toHaveCount(1);
// Test whether the value has changed
expect(".o_dialog:not(.o_inactive_modal) div[name=turtle_trululu] input").toHaveValue("test");
});
test("search more in many2one: resequence inside dialog", async () => {
// when the user clicks on 'Search More...' in a many2one dropdown, resequencing inside
// the dialog works
Partner._fields.sequence = fields.Integer();
for (let i = 0; i < 8; i++) {
Partner._records.push({ id: 100 + i, name: `test_${i}` });
}
Partner._views = {
list: `
<list>
<field name="sequence" widget="handle" />
<field name="name" />
</list>`,
search: `<search />`,
};
onRpc("web_search_read", ({ kwargs }) => {
expect(kwargs.domain).toEqual([]);
});
onRpc("/web/dataset/resequence", () => {
expect.step("/web/dataset/resequence");
});
onRpc(({ method }) => {
expect.step(method);
});
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="trululu" /></form>',
});
await contains(".o_field_widget[name='trululu'] input").click();
await runAllTimers();
await contains(`.o_field_widget[name="trululu"] .o_m2o_dropdown_option_search_more`).click();
expect(".modal").toHaveCount(1);
expect(".modal .ui-sortable-handle").toHaveCount(11);
await contains(".modal .o_data_row:nth-child(2) .ui-sortable-handle").dragAndDrop(
".modal tbody tr"
);
await animationFrame();
expect.verifySteps([
"get_views",
"onchange",
"name_search", // to display results in the dropdown
"get_views", // list view in dialog
"web_search_read", // to display results in the dialog
"has_group",
"/web/dataset/resequence", // resequencing lines
"read",
]);
});
test("many2one dropdown disappears on scroll", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<div style="height: 2000px;">
<field name="trululu" />
</div>
</form>`,
});
await contains(".o_field_many2one input").click();
expect(".o_field_many2one .dropdown-menu").toHaveCount(1);
const dropdown = queryOne(".o_field_many2one .dropdown-menu");
dropdown.style = "max-height: 40px;";
await scroll(dropdown, { top: 50 });
expect(dropdown).toHaveProperty("scrollTop", 50);
expect(".o_field_many2one .dropdown-menu").toHaveCount(1);
await scroll(".o_content", { top: 50 });
await animationFrame();
expect(".o_field_many2one .dropdown-menu").toHaveCount(0);
});
test("search more in many2one: group and use the pager", async () => {
Partner._records.push(
{
id: 5,
name: "Partner 4",
},
{
id: 6,
name: "Partner 5",
},
{
id: 7,
name: "Partner 6",
},
{
id: 8,
name: "Partner 7",
},
{
id: 9,
name: "Partner 8",
},
{
id: 10,
name: "Partner 9",
}
);
Partner._views = {
list: `
<list limit="7">
<field name="name" />
</list>`,
search: `
<search><group>
<filter name="bar" string="Bar" context="{'group_by': 'bar'}" />
</group></search>`,
};
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: '<form><field name="trululu" /></form>',
});
await selectFieldDropdownItem("trululu", "Search More...");
await toggleSearchBarMenu(".modal");
await toggleMenuItem("Bar");
await contains(".modal .o_group_header:eq(1)").click();
expect(".modal .o_data_row").toHaveCount(7);
await contains(".modal .o_group_header .o_pager_next").click();
expect(".modal .o_data_row").toHaveCount(1);
});
test("focus when closing many2one modal in many2one modal", async () => {
Partner._views = {
form: '<form><field name="trululu"/></form>',
};
onRpc("get_formview_id", () => {
return false;
});
await mountViewInDialog({
type: "form",
resModel: "partner",
resId: 2,
arch: '<form><field name="trululu"/></form>',
});
expect(".o_dialog").toHaveCount(1);
expect(document.body).toHaveClass("modal-open");
// Open many2one modal
await contains(".o_external_button", { visible: false }).click();
const originalModal = queryOne(".o_dialog:eq(1)");
expect(".o_dialog").toHaveCount(2);
expect(originalModal).not.toHaveClass("o_inactive_modal");
expect(document.body).toHaveClass("modal-open");
// Open many2one modal of field in many2one modal
await contains(".o_dialog:eq(1) .o_external_button", { visible: false }).click();
expect(".o_dialog").toHaveCount(3);
expect(".o_dialog:eq(2)").not.toHaveClass("o_inactive_modal");
expect(document.body).toHaveClass("modal-open");
// Close second modal
await contains(".o_dialog:eq(2) button[class='btn-close']").click();
expect(".o_dialog").toHaveCount(2);
expect(originalModal).toBe(queryOne(".o_dialog:eq(1)"), {
message: "First modal is still opened",
});
expect(".o_dialog:eq(1)").not.toHaveClass("o_inactive_modal");
expect(document.body).toHaveClass("modal-open");
// Close first modal
await contains(".o_dialog:eq(1) button[class='btn-close']").click();
expect(".o_dialog").toHaveCount(1);
});
test("search more pager is reset when doing a new search", async () => {
Partner._fields.datetime = fields.Datetime({ string: "Datetime Field", searchable: true });
Partner._records.push(
...new Array(170).fill().map((_, i) => ({ id: i + 10, name: "Partner " + i }))
);
Partner._views = {
list: `
<list>
<field name="name"/>
</list>`,
search: `
<search>
<field name="datetime"/>
<field name="name"/>
</search>`,
};
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<sheet>
<group>
<field name="trululu"/>
</group>
</sheet>
</form>`,
});
await selectFieldDropdownItem("trululu", "Search More...");
await contains(".modal .o_pager_next").click();
expect(".modal .o_pager_limit").toHaveText("173");
expect(".modal .o_pager_value").toHaveText("81-160");
expect(".modal tr.o_data_row").toHaveCount(80);
await editSearch("first");
await validateSearch();
expect(".modal .o_pager_limit").toHaveText("1");
expect(".modal .o_pager_value").toHaveText("1-1");
expect(".modal tr.o_data_row").toHaveCount(1);
});
test("click on many2one link in list view", async () => {
Turtle._records[1].product_id = 37;
Partner._views = {
form: '<form> <field name="turtles"/> </form>',
search: "<search></search>",
};
Turtle._views = {
list: `
<list readonly="1">
<field name="product_id" widget="many2one" context="{'field': 'Yes'}"/>
</list>`,
};
Product._views = {
search: "<search></search>",
form: "<form></form>",
};
onRpc("get_formview_action", (args) => {
expect.step("get_formview_action");
expect(args.kwargs.context.field).toBe("Yes");
expect(args.kwargs.context).not.toInclude("global");
return {
type: "ir.actions.act_window",
res_model: "product",
view_type: "form",
view_mode: "form",
views: [[false, "form"]],
target: "current",
res_id: args[0],
};
});
await mountWithCleanup(WebClient);
await getService("action").doAction({
name: "Partner",
res_model: "partner",
res_id: 1,
type: "ir.actions.act_window",
views: [[false, "form"]],
context: { global: "No" },
});
expect("a.o_form_uri").toHaveCount(1);
expect(".o_breadcrumb").toHaveCount(1);
await contains("a.o_form_uri").click();
expect.verifySteps(["get_formview_action"]);
expect(".breadcrumb-item").toHaveCount(1);
expect(".o_breadcrumb").toHaveCount(1);
});
test("Many2oneField with placeholder", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="trululu" placeholder="Placeholder"/></form>',
});
expect(".o_field_widget[name='trululu'] input").toHaveAttribute("placeholder", "Placeholder");
});
test("external_button performs a doAction by default", async () => {
Partner._views = {
form: '<form><field name="trululu"/></form>',
search: "<search></search>",
};
onRpc("get_formview_action", () => {
expect.step("get_formview_action");
return {
type: "ir.actions.act_window",
res_model: "partner",
view_type: "form",
view_mode: "form",
views: [[false, "form"]],
target: "current",
res_id: false,
};
});
await mountWithCleanup(WebClient);
await getService("action").doAction({
name: "Partner",
res_model: "partner",
res_id: 1,
type: "ir.actions.act_window",
views: [[false, "form"]],
});
await selectFieldDropdownItem("trululu", "first record");
expect(".o_field_widget .o_external_button.oi-arrow-right").toHaveCount(1);
await contains(".o_field_widget .o_external_button", { visible: false }).click();
expect.verifySteps(["get_formview_action"]);
expect(".breadcrumb").toHaveText("first record");
});
test("external_button opens a FormViewDialog in dialogs", async () => {
onRpc("get_formview_id", () => {
expect.step("get_formview_id");
return false;
});
await mountViewInDialog({
type: "form",
resModel: "partner",
arch: '<form><field name="trululu"/></form>',
});
expect(".modal").toHaveCount(1);
await selectFieldDropdownItem("trululu", "first record");
expect(".o_field_widget .o_external_button.oi-launch").toHaveCount(1);
await contains(".o_field_widget .o_external_button", { visible: false }).click();
expect.verifySteps(["get_formview_id"]);
expect(".modal").toHaveCount(2);
});
test("keep changes when editing related record in a dialog", async () => {
Partner._views = {
[["form", 98]]: '<form><field name="int_field"/></form>',
};
onRpc("get_formview_id", () => {
return 98;
});
onRpc("web_save", () => {
expect.step("web_save");
});
await mountViewInDialog({
type: "form",
resModel: "partner",
arch: '<form><field name="foo"/><field name="trululu"/></form>',
});
expect(".modal").toHaveCount(1);
await contains(".o_field_widget[name=foo] input").edit("some value", { confirm: false });
await runAllTimers();
await selectFieldDropdownItem("trululu", "first record");
expect(".o_field_widget .o_external_button.oi-launch").toHaveCount(1);
await contains(".o_field_widget .o_external_button", { visible: false }).click();
expect(".modal").toHaveCount(2);
await contains(".o_dialog:not(.o_inactive_modal) .o_field_widget[name=int_field] input").edit(
"5464"
);
await contains(
".o_dialog:not(.o_inactive_modal) .modal-footer .btn-primary:not(.d-none)"
).click();
expect(".modal").toHaveCount(1);
expect(".o_field_widget[name=foo] input").toHaveValue("some value");
expect.verifySteps(["web_save"]);
});
test("create and edit, save and then discard", async () => {
Partner.views = {
[[98, "form"]]: '<form><field name="name"/></form>',
};
onRpc("get_formview_id", () => {
return 98;
});
await mountView({
type: "form",
resModel: "partner",
arch: '<form><field name="trululu"/></form>',
resId: 1,
});
expect(".o_field_widget[name=trululu] input").toHaveValue("aaa");
await contains(".o_field_widget[name=trululu] input").edit("new m2o", { confirm: false });
await runAllTimers();
await contains(".o_field_widget[name=trululu] input").click();
await selectFieldDropdownItem("trululu", "Create and edit...");
expect(".modal").toHaveCount(1);
await contains(".modal-footer .btn-primary:not(.d-none)").click();
expect(".modal").toHaveCount(0);
expect(".o_field_widget[name=trululu] input").toHaveValue("new m2o");
await contains(".o_form_button_cancel").click();
expect(".o_field_widget[name=trululu] input").toHaveValue("aaa");
});
test("external button must be displayed after the update caused by an onchange", async () => {
Partner._onChanges = {
name: (obj) => {
if (obj.name) {
obj.trululu = 1;
}
},
};
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="name"/>
<field name="trululu"/>
</form>`,
});
expect(".o_external_button").toHaveCount(0);
await contains("[name=name] input").edit("new value");
expect(".o_external_button").toHaveCount(1);
});
test("many2one field with false as name", async () => {
Partner._records[0].name = false;
await mountView({
type: "form",
resModel: "partner",
resId: 2,
arch: `
<form>
<field name="trululu"/>
</form>`,
});
expect(".o_field_widget[name='trululu'] input").toHaveValue("Unnamed");
});
test("many2one search with false as name", async () => {
onRpc("name_search", () => {
return [[1, false]];
});
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="trululu" />
</form>`,
});
await contains(".o_field_many2one input").click();
expect(".o_field_many2one[name='trululu'] .dropdown-menu a.dropdown-item:eq(0)").toHaveText(
"Unnamed"
);
});