mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 09:52:02 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -46,7 +46,7 @@ test("BinaryField is correctly rendered (readonly)", async () => {
|
|||
onRpc("/web/content", async (request) => {
|
||||
expect.step("/web/content");
|
||||
|
||||
const body = await request.text();
|
||||
const body = await request.formData();
|
||||
expect(body).toBeInstanceOf(FormData);
|
||||
expect(body.get("field")).toBe("document", {
|
||||
message: "we should download the field document",
|
||||
|
|
@ -103,7 +103,7 @@ test("BinaryField is correctly rendered", async () => {
|
|||
onRpc("/web/content", async (request) => {
|
||||
expect.step("/web/content");
|
||||
|
||||
const body = await request.text();
|
||||
const body = await request.formData();
|
||||
expect(body).toBeInstanceOf(FormData);
|
||||
expect(body.get("field")).toBe("document", {
|
||||
message: "we should download the field document",
|
||||
|
|
@ -469,10 +469,10 @@ test("should accept file with allowed MIME type and reject others", async () =>
|
|||
|
||||
test("doesn't crash if value is not a string", async () => {
|
||||
class Dummy extends models.Model {
|
||||
document = fields.Binary()
|
||||
document = fields.Binary();
|
||||
_applyComputesAndValidate() {}
|
||||
}
|
||||
defineModels([Dummy])
|
||||
defineModels([Dummy]);
|
||||
Dummy._records.push({ id: 1, document: {} });
|
||||
await mountView({
|
||||
type: "form",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
stepAllNetworkCalls,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Color extends models.Model {
|
||||
|
|
@ -27,7 +28,7 @@ class Color extends models.Model {
|
|||
form: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="hex_color" widget="color" />
|
||||
<field name="hex_color" widget="color"/>
|
||||
</group>
|
||||
</form>`,
|
||||
list: /* xml */ `
|
||||
|
|
@ -137,3 +138,70 @@ test("color field change via anoter field's onchange", async () => {
|
|||
expect(".o_field_color input").toHaveValue("#fefefe");
|
||||
expect(".o_field_color div").toHaveStyle({ backgroundColor: "rgb(254, 254, 254)" });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`color field in form view => no automatic save by default`, async () => {
|
||||
stepAllNetworkCalls();
|
||||
await mountView({
|
||||
resModel: "color",
|
||||
type: "form",
|
||||
});
|
||||
|
||||
await contains(`input[type=color]`, { visible: false }).edit("#fefefe");
|
||||
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"get_views",
|
||||
"onchange",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`color field in list view => automatic save by default`, async () => {
|
||||
stepAllNetworkCalls();
|
||||
await mountView({
|
||||
resModel: "color",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="text"/>
|
||||
<field name="hex_color" widget="color"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(`.o_data_row:eq(0) input[type=color]`, { visible: false }).edit("#fefefe");
|
||||
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
"web_save",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`color field in list view => no save if autosave is false`, async () => {
|
||||
stepAllNetworkCalls();
|
||||
await mountView({
|
||||
resModel: "color",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="text"/>
|
||||
<field name="hex_color" widget="color" options="{'autosave': 0}"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(`.o_data_row:eq(0) input[type=color]`, { visible: false }).edit("#fefefe");
|
||||
|
||||
expect.verifySteps([
|
||||
"/web/webclient/translations",
|
||||
"/web/webclient/load_menus",
|
||||
"get_views",
|
||||
"web_search_read",
|
||||
"has_group",
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, press, queryAllTexts, queryOne, scroll } from "@odoo/hoot-dom";
|
||||
import { animationFrame, mockDate, mockTimeZone } from "@odoo/hoot-mock";
|
||||
import {
|
||||
Deferred,
|
||||
animationFrame,
|
||||
click,
|
||||
edit,
|
||||
expect,
|
||||
mockDate,
|
||||
mockTimeZone,
|
||||
press,
|
||||
queryAllTexts,
|
||||
queryFirst,
|
||||
queryOne,
|
||||
scroll,
|
||||
test,
|
||||
waitFor,
|
||||
} from "@odoo/hoot";
|
||||
import {
|
||||
assertDateTimePicker,
|
||||
getPickerCell,
|
||||
|
|
@ -61,6 +74,45 @@ test("toggle datepicker", async () => {
|
|||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("datepicker is automatically closed after selecting a value", async () => {
|
||||
Partner._onChanges.date = () => {};
|
||||
const def = new Deferred();
|
||||
onRpc("onchange", () => def);
|
||||
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
await contains(".o_field_date button").click();
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
await contains(getPickerCell(22)).click();
|
||||
await animationFrame();
|
||||
// The picker shouldn't be reopened, even if the onChange RPC is slow.
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
test("Ensure only one datepicker is open", async () => {
|
||||
Partner._fields.date_start = fields.Date();
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date_start"/>
|
||||
<field name="date"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await queryFirst("[data-field='date_start']").click();
|
||||
await queryFirst("[data-field='date']").click();
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("open datepicker on Control+Enter", async () => {
|
||||
defineParams({
|
||||
|
|
@ -340,7 +392,7 @@ test("multi edition of date field in list view: clear date in input", async () =
|
|||
await contains(".o_field_date button").click();
|
||||
await fieldInput("date").clear();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(await waitFor(".modal")).toHaveCount(1);
|
||||
await contains(".modal .modal-footer .btn-primary").click();
|
||||
|
||||
expect(".o_data_row:first-child .o_data_cell").toHaveText("");
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
import { _makeUser, user } from "@web/core/user";
|
||||
|
||||
function getPickerCell(expr) {
|
||||
return queryAll(`.o_datetime_picker .o_date_item_cell:contains(/^${expr}$/)`);
|
||||
return queryAll(`.o_datetime_picker .o_date_item_cell:text(${expr})`);
|
||||
}
|
||||
|
||||
class Partner extends models.Model {
|
||||
|
|
@ -1081,7 +1081,7 @@ test("list daterange: start date input width matches its span counterpart", asyn
|
|||
const initialWidth = queryFirst(".o_field_daterange span").offsetWidth;
|
||||
await contains(".o_field_daterange span:first").click();
|
||||
await animationFrame();
|
||||
expect(".o_field_daterange input").toHaveProperty("offsetWidth", initialWidth);
|
||||
expect(".o_field_daterange input").toHaveProperty("offsetWidth", initialWidth + 1);
|
||||
});
|
||||
|
||||
test("always range: related end date, both start date and end date empty", async () => {
|
||||
|
|
|
|||
|
|
@ -330,12 +330,8 @@ test("multi edition of DatetimeField in list view: edit date in input", async ()
|
|||
await click(".o_field_datetime input");
|
||||
await animationFrame();
|
||||
await edit("10/02/2019 09:00:00", { confirm: "Enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
|
||||
await click(".modal .modal-footer .btn-primary");
|
||||
await animationFrame();
|
||||
await contains(".modal:only .modal-footer .btn-primary").click();
|
||||
|
||||
expect(".o_data_row:first-child .o_data_cell:first").toHaveText("Oct 2, 9:00 AM");
|
||||
expect(".o_data_row:nth-child(2) .o_data_cell:first").toHaveText("Oct 2, 9:00 AM");
|
||||
|
|
@ -369,10 +365,7 @@ test("multi edition of DatetimeField in list view: clear date in input", async (
|
|||
await edit("", { confirm: "Enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
|
||||
await click(".modal .modal-footer .btn-primary");
|
||||
await animationFrame();
|
||||
await contains(".modal:only .modal-footer .btn-primary").click();
|
||||
|
||||
expect(".o_data_row:first-child .o_data_cell:first").toHaveText("");
|
||||
expect(".o_data_row:nth-child(2) .o_data_cell:first").toHaveText("");
|
||||
|
|
@ -641,7 +634,7 @@ test("placeholder_field shows as placeholder (datetime)", async () => {
|
|||
</form>`,
|
||||
});
|
||||
await contains("div[name='datetime'] button").click();
|
||||
expect("div[name='datetime'] input").toHaveAttribute("placeholder", "Apr 1, 2025, 9:11 AM", {
|
||||
expect("div[name='datetime'] input").toHaveAttribute("placeholder", /Apr 1, 2025, 9:11\sAM/, {
|
||||
message: "placeholder_field should be the placeholder",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
edit,
|
||||
manuallyDispatchProgrammaticEvent,
|
||||
queryAll,
|
||||
queryAllProperties,
|
||||
queryFirst,
|
||||
setInputFiles,
|
||||
waitFor,
|
||||
|
|
@ -18,6 +19,7 @@ import {
|
|||
onRpc,
|
||||
pagerNext,
|
||||
contains,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
|
|
@ -349,14 +351,7 @@ test("ImageField preview is updated when an image is uploaded", async () => {
|
|||
await click(".o_select_file_button");
|
||||
await setInputFiles(imageFile);
|
||||
// It can take some time to encode the data as a base64 url
|
||||
await runAllTimers();
|
||||
// Wait for a render
|
||||
await animationFrame();
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
{ message: "the image should have the new src" }
|
||||
);
|
||||
await waitFor(`div[name=document] img[data-src="data:image/png;base64,${MY_IMAGE}"]`);
|
||||
});
|
||||
|
||||
test("clicking save manually after uploading new image should change the unique of the image src", async () => {
|
||||
|
|
@ -878,3 +873,24 @@ test("convert image to webp", async () => {
|
|||
);
|
||||
await setFiles(imageFile);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("ImageField with width attribute in list", async () => {
|
||||
const { ResCompany, ResPartner, ResUsers } = webModels;
|
||||
defineModels([ResCompany, ResPartner, ResUsers]);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list>
|
||||
<field name="document" widget="image" width="30"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_data_row").toHaveCount(3);
|
||||
expect(".o_field_widget[name=document] img").toHaveCount(3);
|
||||
expect(queryAllProperties(".o_list_table th[data-name=document]", "offsetWidth")).toEqual([39]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer({ sortable: true });
|
||||
json_checkboxes_field = fields.Json({ string: "Json Checkboxes Field" });
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
int_field: 10,
|
||||
json_checkboxes_field: {
|
||||
key1: { checked: true, label: "First Key" },
|
||||
key2: { checked: false, label: "Second Key" },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("JsonCheckBoxesField", async () => {
|
||||
const commands = [
|
||||
{
|
||||
key1: { checked: true, label: "First Key" },
|
||||
key2: { checked: true, label: "Second Key" },
|
||||
},
|
||||
{
|
||||
key1: { checked: false, label: "First Key" },
|
||||
key2: { checked: true, label: "Second Key" },
|
||||
},
|
||||
];
|
||||
onRpc("web_save", (args) => {
|
||||
expect.step("web_save");
|
||||
expect(args.args[1].json_checkboxes_field).toEqual(commands.shift());
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="json_checkboxes_field" widget="json_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").not.toBeChecked();
|
||||
|
||||
expect("div.o_field_widget div.form-check input:disabled").toHaveCount(0);
|
||||
|
||||
// check a value by clicking on input
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
await runAllTimers();
|
||||
await clickSave();
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(2);
|
||||
|
||||
// uncheck a value by clicking on label
|
||||
await contains("div.o_field_widget div.form-check > label").click();
|
||||
await runAllTimers();
|
||||
await clickSave();
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").not.toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").toBeChecked();
|
||||
|
||||
expect.verifySteps(["web_save", "web_save"]);
|
||||
});
|
||||
|
||||
test("JsonCheckBoxesField (readonly field)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="json_checkboxes_field" widget="json_checkboxes" readonly="True" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2, {
|
||||
message: "should have fetched and displayed the 2 values of the many2many",
|
||||
});
|
||||
expect("div.o_field_widget div.form-check input:disabled").toHaveCount(2, {
|
||||
message: "the checkboxes should be disabled",
|
||||
});
|
||||
|
||||
await contains("div.o_field_widget div.form-check > label:eq(1)").click();
|
||||
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").not.toBeChecked();
|
||||
});
|
||||
|
||||
test("JsonCheckBoxesField (some readonly)", async () => {
|
||||
Partner._records[0].json_checkboxes_field = {
|
||||
key1: { checked: true, label: "First Key" },
|
||||
key2: { checked: false, readonly: true, label: "Second Key" },
|
||||
};
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="json_checkboxes_field" widget="json_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2, {
|
||||
message: "should have fetched and displayed the 2 values of the many2many",
|
||||
});
|
||||
expect("div.o_field_widget div.form-check input:eq(0):enabled").toHaveCount(1, {
|
||||
message: "first checkbox should be enabled",
|
||||
});
|
||||
expect("div.o_field_widget div.form-check input:eq(1):disabled").toHaveCount(1, {
|
||||
message: "second checkbox should be disabled",
|
||||
});
|
||||
|
||||
await contains("div.o_field_widget div.form-check > label:eq(1)").click();
|
||||
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").not.toBeChecked();
|
||||
});
|
||||
|
||||
test("JsonCheckBoxesField (question circle)", async () => {
|
||||
Partner._records[0].json_checkboxes_field = {
|
||||
key1: { checked: true, label: "First Key" },
|
||||
key2: { checked: false, label: "Second Key", question_circle: "Some info about this" },
|
||||
};
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="json_checkboxes_field" widget="json_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check:eq(0) ~ i.fa-question-circle").toHaveCount(0, {
|
||||
message: "first checkbox should not have a question circle",
|
||||
});
|
||||
expect(
|
||||
"div.o_field_widget div.form-check:eq(1) ~ i.fa-question-circle[title='Some info about this']"
|
||||
).toHaveCount(1, {
|
||||
message: "second checkbox should have a question circle",
|
||||
});
|
||||
});
|
||||
|
||||
test("JsonCheckBoxesField (implicit inline mode)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="json_checkboxes_field" widget="json_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget .d-inline-block div.form-check").toHaveCount(2, {
|
||||
message: "should show the checkboxes in inlined mode",
|
||||
});
|
||||
});
|
||||
|
||||
test("JsonCheckBoxesField (explicit inline mode)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="json_checkboxes_field" widget="json_checkboxes" options="{'stacked': 0}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget .d-inline-block div.form-check").toHaveCount(2, {
|
||||
message: "should show the checkboxes in inlined mode",
|
||||
});
|
||||
});
|
||||
|
||||
test("JsonCheckBoxesField (stacked mode)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="json_checkboxes_field" widget="json_checkboxes" options="{'stacked': 1}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget .d-block div.form-check").toHaveCount(2, {
|
||||
message: "should show the checkboxes in stacked mode",
|
||||
});
|
||||
});
|
||||
|
|
@ -1097,6 +1097,44 @@ test('many2many field with link option (kanban, create="0")', async () => {
|
|||
expect(".o-kanban-button-new").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("readonly many2many field: edit record", async () => {
|
||||
Partner._records[0].timmy = [1, 2];
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(`save ${args[1].name}`);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="color"/>
|
||||
<field name="timmy" readonly="1">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
</list>
|
||||
<form>
|
||||
<field name="name"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=timmy]").toHaveClass("o_readonly_modifier");
|
||||
expect(".o_field_x2many_list_row_add").toHaveCount(0);
|
||||
expect(".o_list_record_remove").toHaveCount(0);
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual(["gold", "silver"]);
|
||||
|
||||
await contains(".o_data_row:first .o_data_cell").click();
|
||||
expect(".o_dialog .o_form_renderer").toHaveClass("o_form_editable");
|
||||
|
||||
await contains(".o_dialog .o_field_widget[name=name] input").edit("new name");
|
||||
await contains(".o_dialog .o_form_button_save").click();
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual(["new name", "silver"]);
|
||||
expect.verifySteps(["save new name"]);
|
||||
});
|
||||
|
||||
test("many2many list: list of id as default value", async () => {
|
||||
Partner._fields.turtles = fields.Many2many({
|
||||
relation: "turtle",
|
||||
|
|
|
|||
|
|
@ -4037,7 +4037,7 @@ test("many2one search with formatted name", async () => {
|
|||
{
|
||||
id: 1,
|
||||
display_name: "Paul Eric",
|
||||
__formatted_display_name: "Test: **Paul** --Eric-- `good guy`\n\tMore text",
|
||||
__formatted_display_name: "Research & Development Test: **Paul** --Eric-- `good guy`\n\tMore text",
|
||||
},
|
||||
]);
|
||||
await mountView({
|
||||
|
|
@ -4053,7 +4053,7 @@ test("many2one search with formatted name", async () => {
|
|||
expect(
|
||||
".o_field_many2one[name='trululu'] .dropdown-menu a.dropdown-item:eq(0)"
|
||||
).toHaveInnerHTML(
|
||||
`Test: <b>Paul</b> <span class="text-muted">Eric</span> <span class="o_tag position-relative d-inline-flex align-items-center mw-100 o_badge badge rounded-pill lh-1 o_tag_color_0">good guy</span><br/><span style="margin-left: 2em"></span>More text`
|
||||
`Research & Development Test: <b>Paul</b> <span class="text-muted">Eric</span> <span class="o_tag position-relative d-inline-flex align-items-center mw-100 o_badge badge rounded-pill lh-1 o_tag_color_0">good guy</span><br/><span style="margin-left: 2em"></span>More text`
|
||||
);
|
||||
await contains(
|
||||
".o_field_many2one[name='trululu'] .dropdown-menu a.dropdown-item:eq(0)"
|
||||
|
|
@ -4110,6 +4110,37 @@ test("search typeahead", async () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("skip name search optimization", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Many2XAutocomplete
|
||||
value="test"
|
||||
resModel="'partner'"
|
||||
activeActions="{}"
|
||||
fieldString.translate="Field"
|
||||
getDomain.bind="getDomain"
|
||||
update.bind="update"
|
||||
preventMemoization="true"
|
||||
/>`;
|
||||
static components = { Many2XAutocomplete };
|
||||
static props = ["*"];
|
||||
getDomain() {
|
||||
return [];
|
||||
}
|
||||
update() {}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
onRpc("web_name_search", () => expect.step("web_name_search"));
|
||||
await contains(".o_input_dropdown input").edit("wxy", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect.verifySteps(["web_name_search"]);
|
||||
expect(`.o-autocomplete.dropdown li:not(.o_m2o_dropdown_option) a`).toHaveCount(0);
|
||||
await contains(".o_input_dropdown input").edit("wxyz", { confirm: false });
|
||||
expect(`.o-autocomplete.dropdown li:not(.o_m2o_dropdown_option) a`).toHaveCount(0);
|
||||
await runAllTimers();
|
||||
expect.verifySteps(["web_name_search"]);
|
||||
});
|
||||
|
||||
test("highlight search in many2one", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
|
|
|
|||
|
|
@ -955,6 +955,40 @@ test("delete all records in last page (in field o2m inline list view)", async ()
|
|||
expect(".o_x2m_control_panel .o_pager").toHaveText("1-2 / 3");
|
||||
});
|
||||
|
||||
test("delete all records then repopulate", async () => {
|
||||
Partner._records[0].turtles = [1];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="turtles">
|
||||
<list editable="bottom" default_order="turtle_int">
|
||||
<field name="turtle_int" widget="handle"/>
|
||||
<field name="turtle_foo"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(".o_data_row").toHaveCount(1);
|
||||
await contains(".o_list_record_remove").click();
|
||||
expect(".o_data_row").toHaveCount(0);
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
await contains(".o_field_one2many .o_list_renderer tbody input").edit("value 1", {
|
||||
confirm: "blur",
|
||||
});
|
||||
expect(".o_data_row").toHaveCount(1);
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
await contains(".o_field_one2many .o_list_renderer tbody input").edit("value 2", {
|
||||
confirm: "blur",
|
||||
});
|
||||
expect(".o_data_row").toHaveCount(2);
|
||||
await contains("tbody tr:eq(1) .o_handle_cell").dragAndDrop("tbody tr");
|
||||
expect(".o_data_row").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["value 2", "value 1"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("nested x2manys with inline form, but not list", async () => {
|
||||
Turtle._views = { list: `<list><field name="turtle_foo"/></list>` };
|
||||
|
|
@ -3662,6 +3696,56 @@ test("one2many kanban: conditional create/delete actions", async () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("one2many kanban: conditional write action", async () => {
|
||||
Partner._records[0].p = [2, 4];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<field name="p" options="{'write': [('bar', '=', True)]}">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name"/>
|
||||
<field name="bar" widget="boolean_toggle"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_kanban_record:first span").toHaveText("second record");
|
||||
expect(".o_field_widget[name=bar]:first input").toBeChecked();
|
||||
|
||||
// bar is initially true -> edit action is available
|
||||
expect(".o_kanban_record:first .o_field_widget[name=bar] input").toBeEnabled();
|
||||
expect(".o-kanban-button-new").toHaveCount(1); // can create
|
||||
await contains(".o_kanban_record:first").click();
|
||||
expect(".o_dialog .o_form_renderer").toHaveClass("o_form_editable");
|
||||
await contains(".o_dialog .o_field_widget[name=name] input").edit("second record edited");
|
||||
await contains(".modal .o_form_button_save").click();
|
||||
expect(".o_kanban_record:first span").toHaveText("second record edited");
|
||||
|
||||
// set bar false -> edit action is no longer available
|
||||
await contains('.o_field_widget[name="bar"] input').click();
|
||||
expect(".o_kanban_record:first .o_field_widget[name=bar] input").not.toBeEnabled();
|
||||
expect(".o-kanban-button-new").toHaveCount(1); // can still create
|
||||
await contains(".o_kanban_record:first").click();
|
||||
expect(".o_dialog .o_form_renderer").toHaveClass("o_form_readonly");
|
||||
expect(".o_dialog .o_form_button_save").toHaveCount(0);
|
||||
await contains(".modal .o_form_button_cancel").click();
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("editable one2many list, pager is updated on desktop", async () => {
|
||||
Turtle._records.push({ id: 4, turtle_foo: "stephen hawking" });
|
||||
|
|
@ -12248,6 +12332,70 @@ test("new record, receive more create commands than limit", async () => {
|
|||
expect(".o_x2m_control_panel .o_pager").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("existing record: receive more create commands than limit", async () => {
|
||||
Partner._records = [
|
||||
{ id: 1, name: "Initial Record 1", p: [1, 2, 3, 4] },
|
||||
{ id: 2, name: "Initial Record 2" },
|
||||
{ id: 3, name: "Initial Record 3" },
|
||||
{ id: 4, name: "Initial Record 4" },
|
||||
]
|
||||
Partner._onChanges = {
|
||||
int_field: function (obj) {
|
||||
if (obj.int_field === 16) {
|
||||
obj.p = [
|
||||
[0, 0, { display_name: "Record 1" }],
|
||||
[0, 0, { display_name: "Record 2" }],
|
||||
[0, 0, { display_name: "Record 3" }],
|
||||
[0, 0, { display_name: "Record 4" }],
|
||||
];
|
||||
}
|
||||
},
|
||||
};
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field"/>
|
||||
<group>
|
||||
<field name="p">
|
||||
<list limit="2">
|
||||
<field name="display_name"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([
|
||||
"Initial Record 1",
|
||||
"Initial Record 2",
|
||||
]);
|
||||
|
||||
await contains("[name=int_field] input").edit("16", { confirm: "blur" });
|
||||
|
||||
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([
|
||||
"Initial Record 1",
|
||||
"Initial Record 2",
|
||||
"Record 1",
|
||||
"Record 2",
|
||||
"Record 3",
|
||||
"Record 4",
|
||||
]);
|
||||
|
||||
await contains(".o_data_row :text('Record 3') ~ .o_list_record_remove").click();
|
||||
|
||||
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual([
|
||||
"Initial Record 1",
|
||||
"Initial Record 2",
|
||||
"Record 1",
|
||||
"Record 2",
|
||||
"Record 4",
|
||||
"Initial Record 3",
|
||||
]);
|
||||
});
|
||||
|
||||
test("active actions are passed to o2m field", async () => {
|
||||
Partner._records[0].turtles = [1, 2, 3];
|
||||
|
||||
|
|
@ -13490,3 +13638,85 @@ test("edit o2m with default_order on a field not in view (2)", async () => {
|
|||
await contains(".modal-footer .o_form_button_save").click();
|
||||
expect(queryAllTexts(".o_data_cell.o_list_char")).toEqual(["blip", "kawa2", "yop"]);
|
||||
});
|
||||
|
||||
test("one2many list with aggregates in first column", async () => {
|
||||
Partner._records[0].turtles = [1, 2, 3];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="turtles">
|
||||
<list>
|
||||
<field name="turtle_int" sum="My sum"/>
|
||||
<field name="display_name"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual([
|
||||
"0",
|
||||
"leonardo",
|
||||
"9",
|
||||
"donatello",
|
||||
"21",
|
||||
"raphael",
|
||||
]);
|
||||
expect(`tfoot td:first`).toHaveText("30");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("one2many list with monetary aggregates and different currencies", async () => {
|
||||
class Currency extends models.Model {
|
||||
_name = "res.currency";
|
||||
|
||||
name = fields.Char();
|
||||
symbol = fields.Char();
|
||||
position = fields.Selection({
|
||||
selection: [
|
||||
["after", "A"],
|
||||
["before", "B"],
|
||||
],
|
||||
});
|
||||
inverse_rate = fields.Float();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "USD", symbol: "$", position: "before", inverse_rate: 1 },
|
||||
{ id: 2, name: "EUR", symbol: "€", position: "after", inverse_rate: 0.5 },
|
||||
];
|
||||
}
|
||||
defineModels([Currency]);
|
||||
|
||||
Turtle._fields.amount = fields.Monetary({ currency_field: "currency", default: 100 });
|
||||
Turtle._fields.currency = fields.Many2one({ relation: "res.currency", default: 1 });
|
||||
Turtle._records[2].currency = 2;
|
||||
Partner._records[0].turtles = [1, 2, 3];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="turtles">
|
||||
<list>
|
||||
<field name="amount" sum="My sum"/>
|
||||
<field name="currency"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell.o_list_number")).toEqual([
|
||||
"$ 100.00",
|
||||
"$ 100.00",
|
||||
"100.00 €",
|
||||
]);
|
||||
expect(`tfoot`).toHaveText("$ 250.00?");
|
||||
await contains("tfoot span sup").hover();
|
||||
expect(".o_multi_currency_popover").toHaveCount(1);
|
||||
expect(".o_multi_currency_popover").toHaveText("500.00 € at $ 0.50");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -219,3 +219,21 @@ test("New record, fill in phone field, then click on call icon and save", async
|
|||
expect(".o_field_widget[name=foo] input").toHaveValue("+12345678900");
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveClass("invisible");
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("PhoneField in form view shows only icon on mobile screens", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(".o_field_phone .o_phone_form_link small").not.toBeVisible();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,19 +3,23 @@ import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
|
|||
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import {
|
||||
animationFrame,
|
||||
click,
|
||||
edit,
|
||||
expect,
|
||||
getFixture,
|
||||
mockDate,
|
||||
press,
|
||||
queryAll,
|
||||
queryAllTexts,
|
||||
queryAllValues,
|
||||
queryAttribute,
|
||||
queryFirst,
|
||||
runAllTimers,
|
||||
test,
|
||||
waitFor,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { animationFrame, mockDate, runAllTimers } from "@odoo/hoot-mock";
|
||||
} from "@odoo/hoot";
|
||||
import { editTime, getPickerCell } from "@web/../tests/core/datetime/datetime_test_helpers";
|
||||
import {
|
||||
clickCancel,
|
||||
|
|
@ -56,7 +60,7 @@ async function changeType(propertyType) {
|
|||
"tags",
|
||||
"many2one",
|
||||
"many2many",
|
||||
"separator"
|
||||
"separator",
|
||||
];
|
||||
const propertyTypeIndex = TYPES.indexOf(propertyType);
|
||||
await click(".o_field_property_definition_type input");
|
||||
|
|
@ -269,13 +273,11 @@ class ResCurrency extends models.Model {
|
|||
name = fields.Char();
|
||||
symbol = fields.Char();
|
||||
|
||||
_records = Object.entries(serverState.currencies).map(
|
||||
([id, { name, symbol }]) => ({
|
||||
id: Number(id) + 1,
|
||||
name,
|
||||
symbol,
|
||||
})
|
||||
);
|
||||
_records = Object.entries(serverState.currencies).map(([id, { name, symbol }]) => ({
|
||||
id: Number(id) + 1,
|
||||
name,
|
||||
symbol,
|
||||
}));
|
||||
}
|
||||
|
||||
defineModels([Partner, ResCompany, User, ResCurrency]);
|
||||
|
|
@ -1123,7 +1125,7 @@ test("properties: many2many", async () => {
|
|||
* modal should correspond to the selected model and should be updated dynamically.
|
||||
*/
|
||||
test.tags("desktop");
|
||||
test("properties: many2one 'Search more...'", async () => {
|
||||
test("properties: many2one 'Search more...' + internal link save keeps data", async () => {
|
||||
onRpc(({ method, model }) => {
|
||||
if (["has_access", "has_group"].includes(method)) {
|
||||
return true;
|
||||
|
|
@ -1137,6 +1139,8 @@ test("properties: many2one 'Search more...'", async () => {
|
|||
{ model: "partner", display_name: "Partner" },
|
||||
{ model: "res.users", display_name: "User" },
|
||||
];
|
||||
} else if (method === "get_formview_id") {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1164,6 +1168,14 @@ test("properties: many2one 'Search more...'", async () => {
|
|||
<field name="id"/>
|
||||
<field name="display_name"/>
|
||||
</list>`;
|
||||
User._views[["form", false]] = /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`;
|
||||
User._views[["list", false]] = /* xml */ `
|
||||
<list>
|
||||
<field name="id"/>
|
||||
|
|
@ -1237,6 +1249,21 @@ test("properties: many2one 'Search more...'", async () => {
|
|||
await animationFrame();
|
||||
// Checking the model loaded
|
||||
expect.verifySteps(["res.users"]);
|
||||
|
||||
// Select the first value
|
||||
await click(".o_list_table tbody tr:first-child td[name='display_name']");
|
||||
await animationFrame();
|
||||
|
||||
// Click on external button
|
||||
await click(".o_properties_external_button");
|
||||
await animationFrame();
|
||||
|
||||
// Click on Save & close button
|
||||
await click(".modal .o_form_button_save");
|
||||
await animationFrame();
|
||||
|
||||
// Check that value does not disappear
|
||||
expect(".o_field_property_definition_value input").toHaveValue("Alice");
|
||||
});
|
||||
|
||||
test("properties: date(time) property manipulations", async () => {
|
||||
|
|
@ -1468,9 +1495,7 @@ test("properties: kanban view", async () => {
|
|||
expect(".o_kanban_record:nth-child(2) .o_card_property_field:nth-child(1)").toHaveText(
|
||||
"char value\nsuffix"
|
||||
);
|
||||
expect(".o_kanban_record:nth-child(2) .o_card_property_field:nth-child(2)").toHaveText(
|
||||
"C"
|
||||
);
|
||||
expect(".o_kanban_record:nth-child(2) .o_card_property_field:nth-child(2)").toHaveText("C");
|
||||
|
||||
// check first card
|
||||
expect(".o_kanban_record:nth-child(1) .o_card_property_field").toHaveCount(2);
|
||||
|
|
@ -2231,10 +2256,7 @@ test("properties: open section by default", async () => {
|
|||
await click("div[property-name='property_1'] .o_field_property_group_label");
|
||||
await animationFrame();
|
||||
|
||||
expect(getGroups()).toEqual([
|
||||
[["SEPARATOR 1", "property_1"]],
|
||||
[["SEPARATOR 3", "property_3"]],
|
||||
]);
|
||||
expect(getGroups()).toEqual([[["SEPARATOR 1", "property_1"]], [["SEPARATOR 3", "property_3"]]]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
|
|
@ -2276,12 +2298,14 @@ test("properties: save separator folded state", async () => {
|
|||
assertFolded([true, false, true, false]);
|
||||
|
||||
await clickSave();
|
||||
expect.verifySteps([[
|
||||
["property_1", true],
|
||||
["property_2", false],
|
||||
["property_3", true],
|
||||
["property_4", false],
|
||||
]]);
|
||||
expect.verifySteps([
|
||||
[
|
||||
["property_1", true],
|
||||
["property_2", false],
|
||||
["property_3", true],
|
||||
["property_4", false],
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -2941,7 +2965,12 @@ test("properties: monetary without currency_field", async () => {
|
|||
|
||||
await click(".o_field_property_definition_type input");
|
||||
await animationFrame();
|
||||
expect(`.o_field_property_definition_type_menu .o-dropdown-item:contains(Monetary) > div.text-muted`).toHaveAttribute("data-tooltip", "Not possible to create monetary field because there is no currency on current model.");
|
||||
expect(
|
||||
`.o_field_property_definition_type_menu .o-dropdown-item:contains(Monetary) > div.text-muted`
|
||||
).toHaveAttribute(
|
||||
"data-tooltip",
|
||||
"Not possible to create monetary field because there is no currency on current model."
|
||||
);
|
||||
});
|
||||
|
||||
test("properties: monetary with currency_id", async () => {
|
||||
|
|
@ -2975,16 +3004,22 @@ test("properties: monetary with currency_id", async () => {
|
|||
|
||||
await click(".o_field_property_definition_type input");
|
||||
await animationFrame();
|
||||
expect(`.o_field_property_definition_type_menu .o-dropdown-item:contains(Monetary) > div:not(.text-muted)`).toHaveCount(1);
|
||||
expect(
|
||||
`.o_field_property_definition_type_menu .o-dropdown-item:contains(Monetary) > div:not(.text-muted)`
|
||||
).toHaveCount(1);
|
||||
|
||||
await contains(`.o_field_property_definition_type_menu .o-dropdown-item:contains(Monetary)`).click();
|
||||
await contains(
|
||||
`.o_field_property_definition_type_menu .o-dropdown-item:contains(Monetary)`
|
||||
).click();
|
||||
expect(`.o_field_property_definition_currency_field select`).toHaveText("Currency");
|
||||
expect(`.o_field_property_definition_currency_field select`).toHaveValue("currency_id");
|
||||
expect(".o_field_property_definition_value .o_input > span:eq(0)").toHaveText("$");
|
||||
expect(`.o_field_property_definition_value input`).toHaveValue("0.00");
|
||||
|
||||
await closePopover();
|
||||
expect(".o_property_field:nth-child(2) .o_property_field_value .o_input > span:eq(0)").toHaveText("$");
|
||||
expect(
|
||||
".o_property_field:nth-child(2) .o_property_field_value .o_input > span:eq(0)"
|
||||
).toHaveText("$");
|
||||
expect(`.o_property_field:nth-child(2) .o_property_field_value input`).toHaveValue("0.00");
|
||||
});
|
||||
|
||||
|
|
@ -3021,18 +3056,28 @@ test("properties: monetary with multiple currency field", async () => {
|
|||
|
||||
await click(".o_field_property_definition_type input");
|
||||
await animationFrame();
|
||||
expect(`.o_field_property_definition_type_menu .o-dropdown-item:contains(Monetary) > div:not(.text-muted)`).toHaveCount(1);
|
||||
expect(
|
||||
`.o_field_property_definition_type_menu .o-dropdown-item:contains(Monetary) > div:not(.text-muted)`
|
||||
).toHaveCount(1);
|
||||
|
||||
await contains(`.o_field_property_definition_type_menu .o-dropdown-item:contains(Monetary)`).click();
|
||||
expect(`.o_field_property_definition_currency_field select`).toHaveText("Currency\nAnother currency");
|
||||
await contains(
|
||||
`.o_field_property_definition_type_menu .o-dropdown-item:contains(Monetary)`
|
||||
).click();
|
||||
expect(`.o_field_property_definition_currency_field select`).toHaveText(
|
||||
"Currency\nAnother currency"
|
||||
);
|
||||
expect(`.o_field_property_definition_currency_field select`).toHaveValue("currency_id");
|
||||
|
||||
await contains(".o_field_property_definition_currency_field select").select("another_currency_id");
|
||||
await contains(".o_field_property_definition_currency_field select").select(
|
||||
"another_currency_id"
|
||||
);
|
||||
expect(`.o_field_property_definition_currency_field select`).toHaveValue("another_currency_id");
|
||||
expect(".o_field_property_definition_value .o_input > span:eq(1)").toHaveText("€");
|
||||
expect(`.o_field_property_definition_value input`).toHaveValue("0.00");
|
||||
|
||||
await closePopover();
|
||||
expect(".o_property_field:nth-child(2) .o_property_field_value .o_input > span:eq(1)").toHaveText("€");
|
||||
expect(
|
||||
".o_property_field:nth-child(2) .o_property_field_value .o_input > span:eq(1)"
|
||||
).toHaveText("€");
|
||||
expect(`.o_property_field:nth-child(2) .o_property_field_value input`).toHaveValue("0.00");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { beforeEach, expect, test, waitFor } from "@odoo/hoot";
|
||||
import { click, edit, queryAll, queryAllTexts, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame, mockDate } from "@odoo/hoot-mock";
|
||||
import { getPickerCell } from "@web/../tests/core/datetime/datetime_test_helpers";
|
||||
import { defineModels, fields, models, mountView, onRpc, contains } from "@web/../tests/web_test_helpers";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
contains,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
date = fields.Date({ string: "A date", searchable: true });
|
||||
|
|
@ -39,8 +46,8 @@ test("RemainingDaysField on a date field in list view", async () => {
|
|||
expect(cells[2]).toHaveText("Yesterday");
|
||||
expect(cells[3]).toHaveText("In 2 days");
|
||||
expect(cells[4]).toHaveText("3 days ago");
|
||||
expect(cells[5]).toHaveText("02/08/2018");
|
||||
expect(cells[6]).toHaveText("06/08/2017");
|
||||
expect(cells[5]).toHaveText("Feb 8, 2018");
|
||||
expect(cells[6]).toHaveText("Jun 8");
|
||||
expect(cells[7]).toHaveText("");
|
||||
|
||||
expect(queryOne(".o_field_widget > div", { root: cells[0] })).toHaveAttribute(
|
||||
|
|
@ -111,12 +118,11 @@ test("RemainingDaysField on a date field in multi edit list view", async () => {
|
|||
|
||||
await contains(".o_field_remaining_days button").click();
|
||||
await edit("10/10/2017", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(1);
|
||||
await waitFor(".modal");
|
||||
expect(".modal .o_field_widget").toHaveText("In 2 days", {
|
||||
message: "should have 'In 2 days' value to change",
|
||||
});
|
||||
await click(".modal .modal-footer .btn-primary");
|
||||
await click(".modal:only .modal-footer .btn-primary");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:eq(0) .o_data_cell:first").toHaveText("In 2 days", {
|
||||
|
|
@ -286,8 +292,8 @@ test("RemainingDaysField on a datetime field in list view in UTC", async () => {
|
|||
"Yesterday",
|
||||
"In 2 days",
|
||||
"3 days ago",
|
||||
"02/08/2018",
|
||||
"06/08/2017",
|
||||
"Feb 8, 2018",
|
||||
"Jun 8",
|
||||
"",
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,32 @@ test("SelectionField in a list view", async () => {
|
|||
expect(td.children).toHaveCount(1, { message: "select tag should be only child of td" });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("SelectionField in a list view with multi_edit", async () => {
|
||||
Partner._records.forEach((r) => (r.color = "red"));
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list string="Colors" multi_edit="1"><field name="color"/></list>',
|
||||
});
|
||||
// select two records and edit them
|
||||
await click(".o_data_row:eq(0) .o_list_record_selector input:first");
|
||||
await animationFrame();
|
||||
await click(".o_data_row:eq(1) .o_list_record_selector input:first");
|
||||
await animationFrame();
|
||||
|
||||
await contains(".o_field_cell[name='color']").click();
|
||||
await editSelectMenu(".o_field_widget[name='color'] input", { value: "" });
|
||||
await contains(".o_dialog footer button").click();
|
||||
expect(queryAllTexts(".o_field_cell")).toEqual(["", "", "Red"]);
|
||||
|
||||
await contains(".o_field_cell[name='color']").click();
|
||||
await editSelectMenu(".o_field_widget[name='color'] input", { value: "Black" });
|
||||
await contains(".o_dialog footer button").click();
|
||||
expect(queryAllTexts(".o_field_cell")).toEqual(["Black", "Black", "Red"]);
|
||||
});
|
||||
|
||||
test("SelectionField, edition and on many2one field", async () => {
|
||||
Partner._onChanges.product_id = () => {};
|
||||
Partner._records[0].product_id = 37;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NameAndSignature } from "@web/core/signature/name_and_signature";
|
||||
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { expect, queryOne, test } from "@odoo/hoot";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { click, drag, edit, queryFirst, waitFor } from "@odoo/hoot-dom";
|
||||
import {
|
||||
|
|
@ -342,3 +342,22 @@ test("signature field should render initials", async () => {
|
|||
});
|
||||
expect.verifySteps(["V.B."]);
|
||||
});
|
||||
|
||||
test("error loading url", async () => {
|
||||
Partner._records = [{
|
||||
id: 1,
|
||||
sign: "1 kb",
|
||||
}]
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="sign" widget="signature" />
|
||||
</form>`,
|
||||
});
|
||||
const img = queryOne(".o_field_widget img");
|
||||
img.dispatchEvent(new Event("error"));
|
||||
await waitFor(".o_notification:has(.bg-danger):contains(Could not display the selected image)");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -395,8 +395,8 @@ test('StateSelectionField edited by the smart actions "Set kanban state as <stat
|
|||
expect(".o_status_red").toHaveCount(1);
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
expect(`.o_command:contains("Set kanban state as Normal\nALT + D")`).toHaveCount(1);
|
||||
const doneItem = `.o_command:contains("Set kanban state as Done\nALT + G")`;
|
||||
expect(`.o_command:contains("Set kanban state as Normal ALT + D")`).toHaveCount(1);
|
||||
const doneItem = `.o_command:contains("Set kanban state as Done ALT + G")`;
|
||||
expect(doneItem).toHaveCount(1);
|
||||
|
||||
await click(doneItem);
|
||||
|
|
@ -405,9 +405,9 @@ test('StateSelectionField edited by the smart actions "Set kanban state as <stat
|
|||
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
expect(`.o_command:contains("Set kanban state as Normal\nALT + D")`).toHaveCount(1);
|
||||
expect(`.o_command:contains("Set kanban state as Blocked\nALT + F")`).toHaveCount(1);
|
||||
expect(`.o_command:contains("Set kanban state as Done\nALT + G")`).toHaveCount(0);
|
||||
expect(`.o_command:contains("Set kanban state as Normal ALT + D")`).toHaveCount(1);
|
||||
expect(`.o_command:contains("Set kanban state as Blocked ALT + F")`).toHaveCount(1);
|
||||
expect(`.o_command:contains("Set kanban state as Done ALT + G")`).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("StateSelectionField uses legend_* fields", async () => {
|
||||
|
|
@ -565,7 +565,8 @@ test("StateSelectionField - hotkey handling when there are more than 3 options a
|
|||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_command#o_command_2").toHaveText("Set kanban state as Done\nALT + G", {
|
||||
expect(".o_command#o_command_2").toHaveText("Set kanban state as Done ALT + G", {
|
||||
inline: true,
|
||||
message: "hotkey and command are present",
|
||||
});
|
||||
expect(".o_command#o_command_4").toHaveText("Set kanban state as Martine", {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { expect, resize, test } from "@odoo/hoot";
|
||||
import {
|
||||
click,
|
||||
Deferred,
|
||||
edit,
|
||||
press,
|
||||
queryAll,
|
||||
|
|
@ -23,6 +24,8 @@ import {
|
|||
mountWithCleanup,
|
||||
onRpc,
|
||||
serverState,
|
||||
pagerNext,
|
||||
pagerPrevious,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { EventBus } from "@odoo/owl";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
|
@ -1066,3 +1069,193 @@ test('"status" with no stages does not crash command palette', async () => {
|
|||
|
||||
expect(commands).not.toInclude("Move to next Stage");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("cache: update current status if it changed", async () => {
|
||||
class Stage extends models.Model {
|
||||
name = fields.Char();
|
||||
_records = [
|
||||
{ id: 1, name: "Stage 1" },
|
||||
{ id: 2, name: "Stage 2" },
|
||||
];
|
||||
}
|
||||
Partner._fields.stage_id = fields.Many2one({ relation: "stage" });
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "first record",
|
||||
stage_id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "second record",
|
||||
stage_id: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "third record",
|
||||
stage_id: 2,
|
||||
},
|
||||
];
|
||||
defineModels([Stage]);
|
||||
|
||||
Partner._views = {
|
||||
kanban: `
|
||||
<kanban default_group_by="stage_id">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
form: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="stage_id" widget="statusbar" />
|
||||
</header>
|
||||
</form>`,
|
||||
search: `<search></search>`,
|
||||
};
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
let def;
|
||||
onRpc("web_read", () => def);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
id: 1,
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
cache: true,
|
||||
views: [
|
||||
[false, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
});
|
||||
|
||||
// populate the cache by visiting the 3 records
|
||||
await contains(".o_kanban_record").click();
|
||||
expect(".o_last_breadcrumb_item").toHaveText("first record");
|
||||
await pagerNext();
|
||||
expect(".o_last_breadcrumb_item").toHaveText("second record");
|
||||
await pagerNext();
|
||||
expect(".o_last_breadcrumb_item").toHaveText("third record");
|
||||
|
||||
// go back to kanban and drag the first record of stage 2 on top of stage 1 column
|
||||
await contains(".o_breadcrumb .o_back_button").click();
|
||||
const dragActions = await contains(".o_kanban_record:contains(second record)").drag();
|
||||
await dragActions.moveTo(".o_kanban_record:contains(first record)");
|
||||
await dragActions.drop();
|
||||
expect(queryAllTexts(".o_kanban_record")).toEqual([
|
||||
"second record",
|
||||
"first record",
|
||||
"third record",
|
||||
]);
|
||||
|
||||
// re-open last record and use to pager to reach the record we just moved
|
||||
await contains(".o_kanban_record:contains(third record)").click();
|
||||
await pagerPrevious();
|
||||
def = new Deferred();
|
||||
await pagerPrevious();
|
||||
// retrieved from the cache => former value
|
||||
expect(".o_last_breadcrumb_item").toHaveText("second record");
|
||||
expect('.o_statusbar_status button[data-value="2"]').toHaveClass("o_arrow_button_current");
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
// updated when the rpc returns
|
||||
expect(".o_last_breadcrumb_item").toHaveText("second record");
|
||||
expect('.o_statusbar_status button[data-value="1"]').toHaveClass("o_arrow_button_current");
|
||||
});
|
||||
|
||||
test("[adjust] statusbar with a lot of stages, click to change stage", async () => {
|
||||
// force the window width and define long stage names s.t. at most 3 stages can be displayed
|
||||
resize({ width: 800 });
|
||||
class Stage extends models.Model {
|
||||
name = fields.Char();
|
||||
_records = Array.from(Array(6).keys()).map((i) => {
|
||||
const id = i + 1;
|
||||
return { id, name: `Stage with very long name ${id}` };
|
||||
});
|
||||
}
|
||||
defineModels([Stage]);
|
||||
Partner._fields.stage_id = { type: "many2one", relation: "stage" };
|
||||
Partner._records[0].stage_id = 3;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<header>
|
||||
<field name="stage_id" widget="statusbar" options="{'clickable': 1}" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// initial rendering: there should be a dropdown before and a dropdown after
|
||||
expect(".o_statusbar_status button:visible.dropdown-toggle").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_statusbar_status button:visible:not(.dropdown-toggle)")).toEqual([
|
||||
"Stage with very long name 4",
|
||||
"Stage with very long name 3",
|
||||
"Stage with very long name 2",
|
||||
]);
|
||||
expect(".o_statusbar_status button[data-value='3']").toHaveClass("o_arrow_button_current");
|
||||
await contains(".o_statusbar_status .o_last").click();
|
||||
expect(queryAllTexts(".o-dropdown-item")).toEqual(["Stage with very long name 1"]);
|
||||
await contains(".o_statusbar_status .o_first").click();
|
||||
expect(queryAllTexts(".o-dropdown-item")).toEqual([
|
||||
"Stage with very long name 5",
|
||||
"Stage with very long name 6",
|
||||
]);
|
||||
|
||||
// choose the next value: there should still be one dropdown before and one after
|
||||
await contains(".o_statusbar_status button[data-value='4']").click();
|
||||
expect(".o_statusbar_status button:visible.dropdown-toggle").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_statusbar_status button:visible:not(.dropdown-toggle)")).toEqual([
|
||||
"Stage with very long name 5",
|
||||
"Stage with very long name 4",
|
||||
"Stage with very long name 3",
|
||||
]);
|
||||
expect(".o_statusbar_status button[data-value='4']").toHaveClass("o_arrow_button_current");
|
||||
await contains(".o_statusbar_status .o_last").click();
|
||||
expect(queryAllTexts(".o-dropdown-item")).toEqual([
|
||||
"Stage with very long name 1",
|
||||
"Stage with very long name 2",
|
||||
]);
|
||||
await contains(".o_statusbar_status .o_first").click();
|
||||
expect(queryAllTexts(".o-dropdown-item")).toEqual(["Stage with very long name 6"]);
|
||||
|
||||
// choose the next value: there should only be a dropdown before
|
||||
await contains(".o_statusbar_status button[data-value='5']").click();
|
||||
expect(".o_statusbar_status button:visible.dropdown-toggle").toHaveCount(1);
|
||||
expect(queryAllTexts(".o_statusbar_status button:visible:not(.dropdown-toggle)")).toEqual([
|
||||
"Stage with very long name 6",
|
||||
"Stage with very long name 5",
|
||||
"Stage with very long name 4",
|
||||
]);
|
||||
expect(".o_statusbar_status button[data-value='5']").toHaveClass("o_arrow_button_current");
|
||||
await contains(".o_statusbar_status .o_last").click();
|
||||
expect(queryAllTexts(".o-dropdown-item")).toEqual([
|
||||
"Stage with very long name 1",
|
||||
"Stage with very long name 2",
|
||||
"Stage with very long name 3",
|
||||
]);
|
||||
|
||||
// select the first item from the dropdown before => there should only be a dropdown after
|
||||
await contains(".o-dropdown-item:first").click();
|
||||
expect(".o_statusbar_status button:visible.dropdown-toggle").toHaveCount(1);
|
||||
expect(queryAllTexts(".o_statusbar_status button:visible:not(.dropdown-toggle)")).toEqual([
|
||||
"Stage with very long name 3",
|
||||
"Stage with very long name 2",
|
||||
"Stage with very long name 1",
|
||||
]);
|
||||
expect(".o_statusbar_status button[data-value='1']").toHaveClass("o_arrow_button_current");
|
||||
await contains(".o_statusbar_status .o_first").click();
|
||||
expect(queryAllTexts(".o-dropdown-item")).toEqual([
|
||||
"Stage with very long name 4",
|
||||
"Stage with very long name 5",
|
||||
"Stage with very long name 6",
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,6 +61,17 @@ test("basic rendering char field", async () => {
|
|||
expect(".o_field_text textarea").toHaveValue("Description\nas\ntext");
|
||||
});
|
||||
|
||||
test("char field with widget='text' trims trailing spaces", async () => {
|
||||
Product._fields.name = fields.Char({ trim: true });
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: '<form><field name="name" widget="text"/></form>',
|
||||
});
|
||||
await fieldTextArea("name").edit("test ");
|
||||
expect(".o_field_text textarea").toHaveValue("test");
|
||||
});
|
||||
|
||||
test("render following an onchange", async () => {
|
||||
Product._fields.name = fields.Char({
|
||||
onChange: (record) => {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import {
|
|||
fields,
|
||||
models,
|
||||
mountView,
|
||||
patchWithCleanup,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { TimezoneMismatchField } from "@web/views/fields/timezone_mismatch/timezone_mismatch_field";
|
||||
|
||||
class Localization extends models.Model {
|
||||
country = fields.Selection({
|
||||
|
|
@ -67,3 +69,37 @@ test("in a form view", async () => {
|
|||
);
|
||||
expect(".o_tz_warning").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("timezone_mismatch_field mismatch property", () => {
|
||||
const testCases = [
|
||||
{userOffset: "-1030", browserOffset: 630, expectedMismatch: false},
|
||||
{userOffset: "+0000", browserOffset: 0, expectedMismatch: false},
|
||||
{userOffset: "+0345", browserOffset: -225, expectedMismatch: false},
|
||||
{userOffset: "+0500", browserOffset: -300, expectedMismatch: false},
|
||||
{userOffset: "+0200", browserOffset: 120, expectedMismatch: true},
|
||||
{userOffset: "+1200", browserOffset: 0, expectedMismatch: true},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
patchWithCleanup(Date.prototype, {
|
||||
getTimezoneOffset: () => testCase.browserOffset,
|
||||
});
|
||||
|
||||
patchWithCleanup(TimezoneMismatchField.prototype, {
|
||||
props: {
|
||||
name: "tz",
|
||||
tzOffsetField: "tz_offset",
|
||||
record: {
|
||||
data: {
|
||||
tz: "Test/Test",
|
||||
tz_offset: testCase.userOffset,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockField = Object.create(TimezoneMismatchField.prototype);
|
||||
|
||||
expect(mockField.mismatch).toBe(testCase.expectedMismatch);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { queryAllAttributes, queryAllTexts, queryFirst } from "@odoo/hoot-dom";
|
||||
import { queryAllAttributes, queryAllTexts, queryFirst, click, middleClick } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
|
|
@ -193,3 +194,29 @@ test("with non falsy, but non url value", async () => {
|
|||
});
|
||||
expect(".o_field_widget[name=url] a").toHaveAttribute("href", "http://odoo://hello");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("in form x2many field, click/middleclick on the link should not open the record in modal", async () => {
|
||||
Product._fields.p = fields.One2many({
|
||||
string: "one2many_field",
|
||||
relation: "product",
|
||||
});
|
||||
Product._records = [
|
||||
{ id: 1, url: "https://www.example.com/1", p: [2] },
|
||||
{ id: 2, url: "http://www.example.com/2", p: [] },
|
||||
];
|
||||
Product._views.list = `<list><field name="url" widget="url"/></list>`;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="p" widget="one2many"/></form>`,
|
||||
});
|
||||
await click(".o_field_widget[name=url] a");
|
||||
await animationFrame();
|
||||
expect(".modal.o_technical_modal").toHaveCount(0);
|
||||
|
||||
await middleClick(".o_field_widget[name=url] a");
|
||||
await animationFrame();
|
||||
expect(".modal.o_technical_modal").toHaveCount(0);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue