mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 03:52:01 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -17,6 +17,7 @@ const FAKE_RECORD = {
|
|||
isTimeHidden: false,
|
||||
rawRecord: {
|
||||
name: "Meeting",
|
||||
description: "<p>Test description</p>",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ test(`mount a CalendarCommonPopover`, async () => {
|
|||
expect(`.popover-header`).toHaveText("Meeting");
|
||||
expect(`.list-group`).toHaveCount(2);
|
||||
expect(`.list-group.o_cw_popover_fields_secondary`).toHaveCount(1);
|
||||
expect(`.list-group.o_cw_popover_fields_secondary div[name="description"]`).toHaveClass("text-wrap");
|
||||
expect(`.card-footer .o_cw_popover_edit`).toHaveCount(1);
|
||||
expect(`.card-footer .o_cw_popover_delete`).toHaveCount(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ export const FAKE_FIELDS = {
|
|||
default: 1,
|
||||
},
|
||||
name: { string: "Name", type: "char" },
|
||||
description: { string: "Description", type: "html" },
|
||||
start_date: { string: "Start Date", type: "date" },
|
||||
stop_date: { string: "Stop Date", type: "date" },
|
||||
start: { string: "Start Datetime", type: "datetime" },
|
||||
|
|
@ -189,6 +190,12 @@ export const FAKE_MODEL = {
|
|||
"event",
|
||||
"calendar"
|
||||
),
|
||||
description: Field.parseFieldNode(
|
||||
createElement("field", { name: "description" , class: "text-wrap"}),
|
||||
{ event: { fields: FAKE_FIELDS } },
|
||||
"event",
|
||||
"calendar"
|
||||
),
|
||||
},
|
||||
activeFields: {
|
||||
name: {
|
||||
|
|
@ -198,6 +205,13 @@ export const FAKE_MODEL = {
|
|||
required: false,
|
||||
onChange: false,
|
||||
},
|
||||
description: {
|
||||
context: "{}",
|
||||
invisible: false,
|
||||
readonly: false,
|
||||
required: false,
|
||||
onChange: false,
|
||||
},
|
||||
},
|
||||
rangeEnd: DEFAULT_DATE.endOf("month"),
|
||||
rangeStart: DEFAULT_DATE.startOf("month"),
|
||||
|
|
|
|||
|
|
@ -1445,7 +1445,6 @@ test(`create event with timezone in week mode European locale`, async () => {
|
|||
</calendar>
|
||||
`,
|
||||
});
|
||||
|
||||
await selectTimeRange("2016-12-13 08:00:00", "2016-12-13 10:00:00");
|
||||
expect(`.fc-event-main .fc-event-time`).toHaveText("08:00 - 10:00");
|
||||
|
||||
|
|
@ -1460,6 +1459,23 @@ test(`create event with timezone in week mode European locale`, async () => {
|
|||
expect(`.fc-event-main`).toHaveCount(0);
|
||||
});
|
||||
|
||||
test(`create multi day event in week mode`, async () => {
|
||||
mockTimeZone(2);
|
||||
|
||||
patchWithCleanup(CalendarCommonRenderer.prototype, {
|
||||
get options() {
|
||||
return { ...super.options, selectAllow: () => true };
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
resModel: "event",
|
||||
type: "calendar",
|
||||
arch: `<calendar date_start="start" date_stop="stop" mode="week"/>`,
|
||||
});
|
||||
await selectTimeRange("2016-12-13 11:00:00", "2016-12-14 16:00:00");
|
||||
expect(`.fc-event-main .fc-event-time`).toHaveText("11:00 - 16:00");
|
||||
});
|
||||
|
||||
test(`default week start (US)`, async () => {
|
||||
// if not given any option, default week start is on Sunday
|
||||
mockTimeZone(-7);
|
||||
|
|
@ -2187,6 +2203,51 @@ test(`set filter with many2many field on mobile`, async () => {
|
|||
expect(`.o_event[data-event-id="5"] .fc-event-main`).toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("many2many filter handles archived records without crashing on desktop", async () => {
|
||||
CalendarPartner._fields.active = fields.Boolean({ default: true });
|
||||
CalendarPartner._records.push({
|
||||
id: 99,
|
||||
name: "Joni",
|
||||
active: false,
|
||||
});
|
||||
Event._records[0].attendee_ids = [99];
|
||||
|
||||
await mountView({
|
||||
resModel: "event",
|
||||
type: "calendar",
|
||||
arch: `
|
||||
<calendar date_start="start">
|
||||
<field name="attendee_ids" filters="1"/>
|
||||
</calendar>
|
||||
`,
|
||||
});
|
||||
expect(`.o_calendar_filter_item`).toHaveCount(4);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("many2many filter handles archived records without crashing on mobile", async () => {
|
||||
CalendarPartner._fields.active = fields.Boolean({ default: true });
|
||||
CalendarPartner._records.push({
|
||||
id: 99,
|
||||
name: "Joni",
|
||||
active: false,
|
||||
});
|
||||
Event._records[0].attendee_ids = [99];
|
||||
|
||||
await mountView({
|
||||
resModel: "event",
|
||||
type: "calendar",
|
||||
arch: `
|
||||
<calendar date_start="start">
|
||||
<field name="attendee_ids" filters="1"/>
|
||||
</calendar>
|
||||
`,
|
||||
});
|
||||
await contains(`.o_filter`).click();
|
||||
expect(`.o_calendar_filter_item`).toHaveCount(4);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`set filter with one2many field on desktop`, async () => {
|
||||
Event._fields.attendee_ids = fields.One2many({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ test("properly compile notebook", () => {
|
|||
const expected = /*xml*/ `
|
||||
<t t-translation="off">
|
||||
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<Notebook defaultPage="__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[0]" onPageUpdate="(page) => __comp__.props.onNotebookPageChange(0, page)">
|
||||
<Notebook defaultPage="__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[0]" onPageUpdate="(page) => __comp__.props.onNotebookPageChange(0, page)" onWillActivatePage="(page) => __comp__.onWillChangeNotebookPage?.(0, page)">
|
||||
<t t-set-slot="page_1" title="\`Page1\`" name="\`p1\`" isVisible="true" fieldnames="["charfield"]">
|
||||
<Field id="'charfield'" name="'charfield'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['charfield']" readonly="__comp__.props.readonly"/>
|
||||
</t>
|
||||
|
|
|
|||
|
|
@ -353,6 +353,16 @@ test(`button box rendering on big screen`, async () => {
|
|||
}
|
||||
});
|
||||
|
||||
test(`button box rendering invisible`, async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
arch: `<form><div name="button_box" invisible="1"><button id="btn1">MyButton</button></div></form>`,
|
||||
resId: 2,
|
||||
});
|
||||
expect(`.o_control_panel .o_control_panel_actions`).toHaveInnerHTML("");
|
||||
});
|
||||
|
||||
test(`form view gets size class on small and big screens`, async () => {
|
||||
let uiSize = SIZES.MD;
|
||||
const bus = new EventBus();
|
||||
|
|
@ -6245,7 +6255,7 @@ test(`onchange returns an error`, async () => {
|
|||
|
||||
await contains(`.o_field_widget[name=int_field] input`).edit("64");
|
||||
expect.verifyErrors(["Some business message"]);
|
||||
expect(`.modal`).toHaveCount(1);
|
||||
await waitFor(`.modal`);
|
||||
expect(`.modal-body`).toHaveText(/Some business message/);
|
||||
expect(`.o_field_widget[name="int_field"] input`).toHaveValue("9");
|
||||
|
||||
|
|
@ -9112,6 +9122,7 @@ test(`form view is not broken if save operation fails with redirect warning`, as
|
|||
|
||||
test.tags("desktop");
|
||||
test("Redirect Warning full feature: additional context, action_id, leaving while dirty", async function () {
|
||||
expect.errors(1);
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
|
|
@ -12785,6 +12796,33 @@ test(`cog menu action is executed with up to date context`, async () => {
|
|||
expect.verifySteps(["doAction y", "doAction z"]);
|
||||
});
|
||||
|
||||
test("CogMenu receives the model in env", async () => {
|
||||
class CogItem extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<button class="test-cog" t-on-click="onClick">Test</button>`;
|
||||
onClick() {
|
||||
expect.step([`cog clicked`, this.env.model.root.resModel, this.env.model.root.resId]);
|
||||
}
|
||||
}
|
||||
registry.category("cogMenu").add("test-cog", {
|
||||
Component: CogItem,
|
||||
isDisplayed: (env) => {
|
||||
expect.step([`cog displayed`, env.model.root.resModel, env.model.root.resId]);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "form",
|
||||
resId: 5,
|
||||
arch: `<form><field name="display_name"/></form>`,
|
||||
});
|
||||
expect.verifySteps([["cog displayed", "partner", 5]]);
|
||||
await contains(".o_cp_action_menus button").click();
|
||||
await contains("button.test-cog").click();
|
||||
expect.verifySteps([["cog clicked", "partner", 5]]);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test(`preserve current scroll position on form view while closing dialog`, async () => {
|
||||
Partner._views = {
|
||||
|
|
|
|||
|
|
@ -1256,6 +1256,27 @@ test("no content helper after update", async () => {
|
|||
expect(".abc").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("display the provided no content helper when search has no matching data", async () => {
|
||||
Foo._records = [];
|
||||
|
||||
await mountView({
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
noContentHelp: /* xml */ `
|
||||
<p class="abc">This helper should be displayed</p>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_graph_canvas_container canvas").toHaveCount(1);
|
||||
expect(".o_view_nocontent").toHaveCount(0);
|
||||
|
||||
await toggleSearchBarMenu();
|
||||
await toggleMenuItem("color");
|
||||
|
||||
expect(".o_graph_canvas_container canvas").toHaveCount(0);
|
||||
expect(".o_nocontent_help:contains(This helper should be displayed)").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("can reload with other group by", async () => {
|
||||
const view = await mountView({
|
||||
type: "graph",
|
||||
|
|
@ -1850,6 +1871,7 @@ test("clicking on bar charts triggers a do_action", async () => {
|
|||
domain: [["bar", "=", false]],
|
||||
name: "Foo Analysis",
|
||||
res_model: "foo",
|
||||
search_view_id: [67, "search"],
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
|
|
@ -1864,6 +1886,7 @@ test("clicking on bar charts triggers a do_action", async () => {
|
|||
const view = await mountView({
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
searchViewId: 67,
|
||||
arch: /* xml */ `
|
||||
<graph string="Foo Analysis">
|
||||
<field name="bar" />
|
||||
|
|
@ -1889,6 +1912,7 @@ test("middle click on bar charts triggers a do_action", async () => {
|
|||
domain: [["bar", "=", false]],
|
||||
name: "Foo Analysis",
|
||||
res_model: "foo",
|
||||
search_view_id: [67, "search"],
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
|
|
@ -1903,6 +1927,7 @@ test("middle click on bar charts triggers a do_action", async () => {
|
|||
const view = await mountView({
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
searchViewId: 67,
|
||||
arch: /* xml */ `
|
||||
<graph string="Foo Analysis">
|
||||
<field name="bar" />
|
||||
|
|
@ -1928,6 +1953,7 @@ test("Clicking on bar charts removes group_by and search_default_* context keys"
|
|||
domain: [["bar", "=", false]],
|
||||
name: "Foo Analysis",
|
||||
res_model: "foo",
|
||||
search_view_id: [67, "search"],
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
|
|
@ -1942,6 +1968,7 @@ test("Clicking on bar charts removes group_by and search_default_* context keys"
|
|||
const view = await mountView({
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
searchViewId: 67,
|
||||
arch: /* xml */ `
|
||||
<graph string="Foo Analysis">
|
||||
<field name="bar" />
|
||||
|
|
@ -1969,6 +1996,7 @@ test("clicking on a pie chart trigger a do_action with correct views", async ()
|
|||
domain: [["bar", "=", false]],
|
||||
name: "Foo Analysis",
|
||||
res_model: "foo",
|
||||
search_view_id: [67, "search"],
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
|
|
@ -1983,6 +2011,7 @@ test("clicking on a pie chart trigger a do_action with correct views", async ()
|
|||
const view = await mountView({
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
searchViewId: 67,
|
||||
arch: /* xml */ `
|
||||
<graph string="Foo Analysis" type="pie">
|
||||
<field name="bar" />
|
||||
|
|
@ -2017,6 +2046,7 @@ test("middle click on a pie chart trigger a do_action with correct views", async
|
|||
domain: [["bar", "=", false]],
|
||||
name: "Foo Analysis",
|
||||
res_model: "foo",
|
||||
search_view_id: [67, "search"],
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
|
|
@ -2031,6 +2061,7 @@ test("middle click on a pie chart trigger a do_action with correct views", async
|
|||
const view = await mountView({
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
searchViewId: 67,
|
||||
arch: /* xml */ `
|
||||
<graph string="Foo Analysis" type="pie">
|
||||
<field name="bar" />
|
||||
|
|
@ -2111,6 +2142,24 @@ test("graph view with invisible attribute on field", async () => {
|
|||
expect(".o_menu_item:contains(Revenue)").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("graph view reserved word", async () => {
|
||||
// Check that the use of reserved words does not interfere with the view.
|
||||
Product._records.push({ id: 150, name: "constructor" });
|
||||
Foo._records.at(-1).product_id = 150;
|
||||
|
||||
const view = await mountView({
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
arch: /* xml */ `
|
||||
<graph order="DESC">
|
||||
<field name="product_id" />
|
||||
</graph>
|
||||
`,
|
||||
});
|
||||
checkLabels(view, ["xphone", "xpad", "constructor"]);
|
||||
checkDatasets(view, ["data", "label"], [{ data: [4, 3, 1], label: "Count" }]);
|
||||
});
|
||||
|
||||
test("graph view sort by measure", async () => {
|
||||
// change last record from foo as there are 4 records count for each product
|
||||
Product._records.push({ id: 150, name: "zphone" });
|
||||
|
|
@ -2292,8 +2341,6 @@ test("empty graph view with sample data", async () => {
|
|||
|
||||
expect(".o_graph_view .o_content").toHaveClass("o_view_sample_data");
|
||||
expect(".o_view_nocontent").toHaveCount(1);
|
||||
expect(".ribbon").toHaveCount(1);
|
||||
expect(".ribbon").toHaveText("SAMPLE DATA");
|
||||
expect(".o_graph_canvas_container canvas").toHaveCount(1);
|
||||
|
||||
await toggleSearchBarMenu();
|
||||
|
|
@ -2301,7 +2348,6 @@ test("empty graph view with sample data", async () => {
|
|||
|
||||
expect(".o_graph_view .o_content").not.toHaveClass("o_view_sample_data");
|
||||
expect(".o_view_nocontent").toHaveCount(0);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
expect(".o_graph_canvas_container canvas").toHaveCount(1);
|
||||
});
|
||||
|
||||
|
|
@ -2326,7 +2372,6 @@ test("non empty graph view with sample data", async () => {
|
|||
expect(".o_content").not.toHaveClass("o_view_sample_data");
|
||||
expect(".o_view_nocontent").toHaveCount(0);
|
||||
expect(".o_graph_canvas_container canvas").toHaveCount(1);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
|
||||
await toggleSearchBarMenu();
|
||||
await toggleMenuItem("False Domain");
|
||||
|
|
@ -2334,7 +2379,6 @@ test("non empty graph view with sample data", async () => {
|
|||
expect(".o_content").not.toHaveClass("o_view_sample_data");
|
||||
expect(".o_graph_canvas_container canvas").toHaveCount(0);
|
||||
expect(".o_view_nocontent").toHaveCount(1);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("empty graph view without sample data after filter", async () => {
|
||||
|
|
|
|||
|
|
@ -70,8 +70,10 @@ import {
|
|||
patchWithCleanup,
|
||||
quickCreateKanbanColumn,
|
||||
quickCreateKanbanRecord,
|
||||
removeFacet,
|
||||
serverState,
|
||||
stepAllNetworkCalls,
|
||||
switchView,
|
||||
toggleKanbanColumnActions,
|
||||
toggleKanbanRecordDropdown,
|
||||
toggleMenuItem,
|
||||
|
|
@ -88,6 +90,7 @@ import { FileInput } from "@web/core/file_input/file_input";
|
|||
import { browser } from "@web/core/browser/browser";
|
||||
import { currencies } from "@web/core/currency";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { user } from "@web/core/user";
|
||||
import { RelationalModel } from "@web/model/relational_model/relational_model";
|
||||
import { SampleServer } from "@web/model/sample_server";
|
||||
import { KanbanCompiler } from "@web/views/kanban/kanban_compiler";
|
||||
|
|
@ -5806,7 +5809,6 @@ test("delete an empty column, then a column with records.", async () => {
|
|||
__extra_domain: [["product_id", "=", 7]],
|
||||
product_id: [7, "empty group"],
|
||||
__count: 0,
|
||||
__fold: false,
|
||||
__records: [],
|
||||
});
|
||||
result.length = 3;
|
||||
|
|
@ -6204,7 +6206,6 @@ test("count of folded groups in empty kanban with sample data", async () => {
|
|||
product_id: [2, "In Progress"],
|
||||
__count: 0,
|
||||
__extra_domain: [],
|
||||
__fold: true,
|
||||
},
|
||||
],
|
||||
length: 2,
|
||||
|
|
@ -7019,8 +7020,6 @@ test("empty kanban with sample data", async () => {
|
|||
message: "there should be 10 sample records",
|
||||
});
|
||||
expect(".o_view_nocontent").toHaveCount(1);
|
||||
expect(".ribbon").toHaveCount(1);
|
||||
expect(".ribbon").toHaveText("SAMPLE DATA");
|
||||
|
||||
await toggleSearchBarMenu();
|
||||
await toggleMenuItem("Match nothing");
|
||||
|
|
@ -7028,7 +7027,6 @@ test("empty kanban with sample data", async () => {
|
|||
expect(".o_content").not.toHaveClass("o_view_sample_data");
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(0);
|
||||
expect(".o_view_nocontent").toHaveCount(1);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("empty grouped kanban with sample data and many2many_tags", async () => {
|
||||
|
|
@ -7148,14 +7146,12 @@ test("non empty kanban with sample data", async () => {
|
|||
expect(".o_content").not.toHaveClass("o_view_sample_data");
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(4);
|
||||
expect(".o_view_nocontent").toHaveCount(0);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
|
||||
await toggleSearchBarMenu();
|
||||
await toggleMenuItem("Match nothing");
|
||||
|
||||
expect(".o_content").not.toHaveClass("o_view_sample_data");
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(0);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("empty grouped kanban with sample data: add a column", async () => {
|
||||
|
|
@ -7398,7 +7394,7 @@ test("kanban with sample data grouped by m2o and existing groups", async () => {
|
|||
__extra_domain: [["product_id", "=", "3"]],
|
||||
},
|
||||
],
|
||||
length: 2,
|
||||
length: 1,
|
||||
}));
|
||||
|
||||
await mountView({
|
||||
|
|
@ -7422,6 +7418,43 @@ test("kanban with sample data grouped by m2o and existing groups", async () => {
|
|||
expect(".o_kanban_record").toHaveText("hello");
|
||||
});
|
||||
|
||||
test(`kanban grouped by m2o with sample data with more than 5 real groups`, async () => {
|
||||
Partner._records = [];
|
||||
onRpc("web_read_group", () => ({
|
||||
// simulate 6, empty, real groups
|
||||
groups: [1, 2, 3, 4, 5, 6].map((id) => ({
|
||||
__count: 0,
|
||||
__records: [],
|
||||
product_id: [id, `Value ${id}`],
|
||||
__extra_domain: [["product_id", "=", id]],
|
||||
})),
|
||||
length: 6,
|
||||
}));
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "kanban",
|
||||
arch: `
|
||||
<kanban sample="1">
|
||||
<templates>
|
||||
<div t-name="card">
|
||||
<field name="product_id"/>
|
||||
</div>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
groupBy: ["product_id"],
|
||||
});
|
||||
expect(".o_content").toHaveClass("o_view_sample_data");
|
||||
expect(queryAllTexts(`.o_kanban_group .o_column_title`)).toEqual([
|
||||
"Value 1",
|
||||
"Value 2",
|
||||
"Value 3",
|
||||
"Value 4",
|
||||
"Value 5",
|
||||
"Value 6",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("bounce create button when no data and click on empty area", async () => {
|
||||
await mountView({
|
||||
|
|
@ -13714,6 +13747,35 @@ test("selection can be enabled by pressing 'space' key", async () => {
|
|||
expect(".o_record_selected").toHaveCount(4);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("selection can be enabled by pressing 'shift + space' key", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
expect(".o_selection_box").toHaveCount(0);
|
||||
await press("ArrowDown");
|
||||
await keyDown("Shift");
|
||||
await press("Space");
|
||||
await animationFrame();
|
||||
expect(".o_record_selected").toHaveCount(1);
|
||||
await keyUp("Shift");
|
||||
await press("ArrowDown");
|
||||
await press("ArrowDown");
|
||||
await keyDown("Shift");
|
||||
await press("Space");
|
||||
await animationFrame();
|
||||
expect(".o_record_selected").toHaveCount(3);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("drag and drop records and quickly open a record", async () => {
|
||||
Partner._views.kanban = /* xml */ `
|
||||
|
|
@ -13819,9 +13881,9 @@ test("groups will be scrolled to on unfold if outside of viewport", async () =>
|
|||
"the next group (which is folded) should stick to the right of the screen after the scroll",
|
||||
});
|
||||
expect(".o_column_folded:eq(0)").toHaveText("column 7\n(1)");
|
||||
await contains('.o_kanban_group:contains("column 7\n(1)")').click();
|
||||
await contains('.o_kanban_group:contains("column 7 (1)")').click();
|
||||
expect(".o_content").toHaveProperty("scrollLeft", 2154);
|
||||
({ x, width } = queryRect('.o_kanban_group:contains("column 7\n(1)")'));
|
||||
({ x, width } = queryRect('.o_kanban_group:contains("column 7 (1)")'));
|
||||
// TODO JUM: change digits option
|
||||
expect(x + width).toBeCloseTo(window.innerWidth, {
|
||||
digits: 0,
|
||||
|
|
@ -13833,7 +13895,7 @@ test("groups will be scrolled to on unfold if outside of viewport", async () =>
|
|||
expect(".o_content").toHaveProperty("scrollLeft", 3302);
|
||||
await contains(".o_kanban_group:last").click();
|
||||
expect(".o_content").toHaveProperty("scrollLeft", 3562);
|
||||
({ x, width } = queryRect('.o_kanban_group:contains("column 11\n(1)")'));
|
||||
({ x, width } = queryRect('.o_kanban_group:contains("column 11 (1)")'));
|
||||
// TODO JUM: change digits option
|
||||
expect(x + width).toBeCloseTo(window.innerWidth, {
|
||||
digits: 0,
|
||||
|
|
@ -14618,3 +14680,346 @@ test("Cache: unfolded is now folded", async () => {
|
|||
expect(getKanbanColumn(1)).toHaveClass("o_column_folded");
|
||||
expect(queryText(getKanbanColumn(1))).toBe("xmo\n(2)");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Cache: kanban view progressbar, filter, open a record, edit, come back", async () => {
|
||||
// This test encodes a very specify scenario involving a kanban with progressbar, where the
|
||||
// filter was lost when coming back due to the cache callback, which removed the groups
|
||||
// information.
|
||||
Product._records[1].fold = false;
|
||||
|
||||
let def;
|
||||
onRpc("web_read_group", () => def);
|
||||
|
||||
Partner._views = {
|
||||
"kanban,false": `
|
||||
<kanban default_group_by="product_id" on_create="quick_create" quick_create_view="some_view_ref">
|
||||
<progressbar field="foo" colors='{"yop": "success", "gnap": "warning", "blip": "danger"}'/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
"form,false": `<form><field name="product_id" widget="statusbar" options="{'clickable': true}"/></form>`,
|
||||
"search,false": `<search/>`,
|
||||
};
|
||||
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
name: "Partners Action",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "kanban"],
|
||||
[false, "form"],
|
||||
],
|
||||
search_view_id: [false, "search"],
|
||||
},
|
||||
]);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction(1);
|
||||
expect(".o_kanban_group").toHaveCount(2);
|
||||
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(2);
|
||||
|
||||
// Filter the first column with the progressbar
|
||||
await contains(".o_column_progress .progress-bar", { root: getKanbanColumn(0) }).click();
|
||||
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(1);
|
||||
|
||||
// Open a record, then go back, s.t. we populate the cache with the current params of the kanban
|
||||
await contains(".o_kanban_group:eq(1) .o_kanban_record").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
await contains(".o_back_button").click();
|
||||
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(1);
|
||||
|
||||
// Open again and make a change which will have an impact on the kanban, then go back
|
||||
await contains(".o_kanban_group:eq(1) .o_kanban_record").click();
|
||||
expect(".o_form_view").toHaveCount(1);
|
||||
await contains(".o_field_widget[name=product_id] button[data-value='3']").click();
|
||||
// Slow down the rpc s.t. we first use data from the cache, and then we update
|
||||
def = new Deferred();
|
||||
await contains(".o_back_button").click();
|
||||
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(1);
|
||||
|
||||
// Resolve the promise
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(1);
|
||||
|
||||
// Open a last time and come back => the filter should still be applied correctly
|
||||
await contains(".o_kanban_group:eq(1) .o_kanban_record").click();
|
||||
await contains(".o_back_button").click();
|
||||
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("scroll position is restored when coming back to kanban view", async () => {
|
||||
Partner._views = {
|
||||
kanban: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: `<list><field name="foo"/></list>`,
|
||||
search: `<search />`,
|
||||
};
|
||||
|
||||
for (let i = 1; i < 10; i++) {
|
||||
Product._records.push({ id: 100 + i, name: `Product ${i}` });
|
||||
for (let j = 1; j < 20; j++) {
|
||||
Partner._records.push({
|
||||
id: 100 * i + j,
|
||||
product_id: 100 + i,
|
||||
foo: `Record ${i}/${j}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let def;
|
||||
onRpc("web_read_group", () => def);
|
||||
await resize({ width: 800, height: 300 });
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "kanban"],
|
||||
[false, "list"],
|
||||
],
|
||||
context: {
|
||||
group_by: ["product_id"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
// simulate scrolls in the kanban view
|
||||
queryOne(".o_content").scrollTop = 100;
|
||||
queryOne(".o_content").scrollLeft = 400;
|
||||
|
||||
await getService("action").switchView("list");
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
// the kanban is "lazy", so it displays the control panel directly, and the renderer later with
|
||||
// the data => simulate this and check that the scroll position is correctly restored
|
||||
def = new Deferred();
|
||||
await getService("action").switchView("kanban");
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(".o_kanban_renderer").toHaveCount(0);
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_renderer").toHaveCount(1);
|
||||
expect(".o_content").toHaveProperty("scrollTop", 100);
|
||||
expect(".o_content").toHaveProperty("scrollLeft", 400);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("scroll position is restored when coming back to kanban view (mobile)", async () => {
|
||||
Partner._views = {
|
||||
kanban: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: `<list><field name="foo"/></list>`,
|
||||
search: `<search />`,
|
||||
};
|
||||
|
||||
for (let i = 1; i < 20; i++) {
|
||||
Partner._records.push({
|
||||
id: 100 + i,
|
||||
foo: `Record ${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
let def;
|
||||
onRpc("web_search_read", () => def);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "kanban"],
|
||||
[false, "list"],
|
||||
],
|
||||
});
|
||||
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
// simulate a scroll in the kanban view
|
||||
queryOne(".o_kanban_view").scrollTop = 100;
|
||||
|
||||
await getService("action").switchView("list");
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
// the kanban is "lazy", so it displays the control panel directly, and the renderer later with
|
||||
// the data => simulate this and check that the scroll position is correctly restored
|
||||
def = new Deferred();
|
||||
await getService("action").switchView("kanban");
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(".o_kanban_renderer").toHaveCount(0);
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_renderer").toHaveCount(1);
|
||||
expect(".o_kanban_view").toHaveProperty("scrollTop", 100);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("scroll position is restored when coming back to kanban view (grouped, mobile)", async () => {
|
||||
Partner._views = {
|
||||
kanban: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: `<list><field name="foo"/></list>`,
|
||||
search: `<search />`,
|
||||
};
|
||||
|
||||
Partner._records = [];
|
||||
for (let i = 1; i < 5; i++) {
|
||||
Product._records.push({ id: 100 + i, name: `Product ${i}` });
|
||||
for (let j = 1; j < 20; j++) {
|
||||
Partner._records.push({
|
||||
id: 100 * i + j,
|
||||
product_id: 100 + i,
|
||||
foo: `Record ${i}/${j}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let def;
|
||||
onRpc("web_read_group", () => def);
|
||||
await resize({ width: 375, height: 667 }); // iphone se
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "kanban"],
|
||||
[false, "list"],
|
||||
],
|
||||
context: {
|
||||
group_by: ["product_id"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
// simulate scrolls in the kanban view
|
||||
queryOne(".o_kanban_renderer").scrollLeft = 656; // scroll to the third column
|
||||
queryAll(".o_kanban_group")[2].scrollTop = 200;
|
||||
|
||||
await getService("action").switchView("list");
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
// the kanban is "lazy", so it displays the control panel directly, and the renderer later with
|
||||
// the data => simulate this and check that the scroll position is correctly restored
|
||||
def = new Deferred();
|
||||
await getService("action").switchView("kanban");
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
expect(".o_kanban_renderer").toHaveCount(0);
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_kanban_renderer").toHaveCount(1);
|
||||
expect(".o_kanban_group:eq(2)").toHaveProperty("scrollTop", 200);
|
||||
expect(".o_kanban_renderer").toHaveProperty("scrollLeft", 656);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("limit is reset when restoring a view after ungrouping", async () => {
|
||||
Partner._views["kanban"] = `
|
||||
<kanban sample="1">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`;
|
||||
Partner._views["list"] = '<list><field name="foo"/></list>';
|
||||
Partner._views.search = `
|
||||
<search>
|
||||
<group>
|
||||
<filter name="foo" string="Foo" context="{'group_by': 'foo'}"/>
|
||||
</group>
|
||||
</search>
|
||||
`;
|
||||
|
||||
onRpc("partner", "web_search_read", ({ kwargs }) => {
|
||||
const { domain, limit } = kwargs;
|
||||
if (!domain.length) {
|
||||
expect.step(`limit=${limit}`);
|
||||
}
|
||||
});
|
||||
|
||||
patchWithCleanup(user, {
|
||||
hasGroup: () => true,
|
||||
});
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
await getService("action").doAction({
|
||||
type: "ir.actions.act_window",
|
||||
id: 450,
|
||||
xml_id: "action_450",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
views: [
|
||||
[false, "kanban"],
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
context: { search_default_foo: true },
|
||||
});
|
||||
|
||||
await switchView("list");
|
||||
await removeFacet("Foo");
|
||||
expect.verifySteps(["limit=80"]);
|
||||
await switchView("kanban");
|
||||
expect.verifySteps(["limit=40"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("add o-navigable to buttons with dropdown-item class and view buttons", async () => {
|
||||
Partner._records.splice(1, 3); // keep one record only
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="menu">
|
||||
<a role="menuitem" class="dropdown-item">Item</a>
|
||||
<a role="menuitem" type="set_cover" class="dropdown-item">Item</a>
|
||||
<a role="menuitem" type="object" class="dropdown-item">Item</a>
|
||||
</t>
|
||||
<t t-name="card">
|
||||
<div/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
expect(".o-dropdown--menu").toHaveCount(0);
|
||||
await toggleKanbanRecordDropdown();
|
||||
expect(".o-dropdown--menu .dropdown-item.o-navigable").toHaveCount(3);
|
||||
expect(".o-dropdown--menu .dropdown-item.o-navigable.focus").toHaveCount(0);
|
||||
|
||||
// Check that navigation is working
|
||||
await hover(".o-dropdown--menu .dropdown-item.o-navigable");
|
||||
expect(".o-dropdown--menu .dropdown-item.o-navigable.focus").toHaveCount(1);
|
||||
|
||||
await press("arrowdown");
|
||||
expect(".o-dropdown--menu .dropdown-item.o-navigable:nth-child(2)").toHaveClass("focus");
|
||||
|
||||
await press("arrowdown");
|
||||
expect(".o-dropdown--menu .dropdown-item.o-navigable:nth-child(3)").toHaveClass("focus");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import {
|
||||
animationFrame,
|
||||
clear,
|
||||
click,
|
||||
Deferred,
|
||||
edit,
|
||||
expect,
|
||||
getFixture,
|
||||
hover,
|
||||
keyDown,
|
||||
keyUp,
|
||||
middleClick,
|
||||
mockDate,
|
||||
mockTimeZone,
|
||||
pointerDown,
|
||||
pointerUp,
|
||||
press,
|
||||
|
|
@ -17,17 +22,12 @@ import {
|
|||
queryOne,
|
||||
queryRect,
|
||||
queryText,
|
||||
runAllTimers,
|
||||
test,
|
||||
tick,
|
||||
unload,
|
||||
waitFor,
|
||||
} from "@odoo/hoot-dom";
|
||||
import {
|
||||
animationFrame,
|
||||
Deferred,
|
||||
mockDate,
|
||||
mockTimeZone,
|
||||
runAllTimers,
|
||||
tick,
|
||||
} from "@odoo/hoot-mock";
|
||||
} from "@odoo/hoot";
|
||||
import { Component, markup, onRendered, onWillStart, useRef, xml } from "@odoo/owl";
|
||||
import { getPickerCell } from "@web/../tests/core/datetime/datetime_test_helpers";
|
||||
import {
|
||||
|
|
@ -1906,7 +1906,7 @@ test(`basic grouped list rendering with widget="handle" col`, async () => {
|
|||
expect(`thead th[data-name=int_field]`).toHaveCount(1);
|
||||
expect(`tr.o_group_header`).toHaveCount(2);
|
||||
expect(`th.o_group_name`).toHaveCount(2);
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(3); // group name + colspan 2 + cog placeholder
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(2); // group name + cog placeholder
|
||||
expect(`.o_group_header:eq(0) .o_list_number`).toHaveCount(0);
|
||||
});
|
||||
|
||||
|
|
@ -1938,6 +1938,18 @@ test(`basic grouped list rendering with a date field between two fields with a a
|
|||
expect(queryAllTexts(`.o_group_header:eq(0) td`)).toEqual(["-4", "", "-4"]);
|
||||
});
|
||||
|
||||
test(`basic grouped list rendering 1 col without selector and with optional field`, async () => {
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
type: "list",
|
||||
arch: `<list><field name="foo"/><field name="bar" optional="hidden"/></list>`,
|
||||
groupBy: ["bar"],
|
||||
allowSelectors: false,
|
||||
});
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(2);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "1");
|
||||
});
|
||||
|
||||
test(`basic grouped list rendering 1 col without selector`, async () => {
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
|
|
@ -1982,8 +1994,8 @@ test(`basic grouped list rendering 2 cols without selector`, async () => {
|
|||
groupBy: ["bar"],
|
||||
allowSelectors: false,
|
||||
});
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(3);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "1");
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(2);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "2");
|
||||
});
|
||||
|
||||
test(`basic grouped list rendering 3 cols without selector`, async () => {
|
||||
|
|
@ -1994,8 +2006,27 @@ test(`basic grouped list rendering 3 cols without selector`, async () => {
|
|||
groupBy: ["bar"],
|
||||
allowSelectors: false,
|
||||
});
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(3);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "2");
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(2);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "3");
|
||||
});
|
||||
|
||||
test(`basic grouped list rendering 3 cols without selector and with optional fields`, async () => {
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
<field name="text"/>
|
||||
<field name="date" optional="hidden"/>
|
||||
</list>
|
||||
`,
|
||||
groupBy: ["bar"],
|
||||
allowSelectors: false,
|
||||
});
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(2);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "3");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
|
|
@ -2007,8 +2038,8 @@ test(`basic grouped list rendering 2 col with selector on desktop`, async () =>
|
|||
groupBy: ["bar"],
|
||||
allowSelectors: true,
|
||||
});
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(3);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "2");
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(2);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "3");
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
|
|
@ -2020,8 +2051,8 @@ test(`basic grouped list rendering 2 col with selector on mobile`, async () => {
|
|||
groupBy: ["bar"],
|
||||
allowSelectors: true,
|
||||
});
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(3);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "1");
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(2);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "2");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
|
|
@ -2034,8 +2065,8 @@ test(`basic grouped list rendering 3 cols with selector on desktop`, async () =>
|
|||
allowSelectors: true,
|
||||
});
|
||||
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(3);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "3");
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(2);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "4");
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
|
|
@ -2048,8 +2079,8 @@ test(`basic grouped list rendering 3 cols with selector on mobile`, async () =>
|
|||
allowSelectors: true,
|
||||
});
|
||||
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(3);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "2");
|
||||
expect(`.o_group_header:eq(0) th`).toHaveCount(2);
|
||||
expect(`.o_group_header th:eq(0)`).toHaveAttribute("colspan", "3");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
|
|
@ -4867,6 +4898,22 @@ test(`aggregates are formatted according to field widget`, async () => {
|
|||
});
|
||||
});
|
||||
|
||||
test(`aggregates of monetary widget with no currency data in grouped list`, async () => {
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
type: "list",
|
||||
groupBy: ["bar"],
|
||||
arch: `
|
||||
<list>
|
||||
<field name="qux" widget="monetary" options="{'currency_field': 'currency_id'}" sum="Sum"/>
|
||||
<field name="currency_id" column_invisible="True"/>
|
||||
</list>`,
|
||||
});
|
||||
expect(`tfoot`).toHaveText("19.40", {
|
||||
message: "aggregates monetary should still be displayed without currency",
|
||||
});
|
||||
});
|
||||
|
||||
test(`aggregates of monetary field with no currency field`, async () => {
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
|
|
@ -4971,6 +5018,54 @@ test(`aggregates monetary (currency field in view)`, async () => {
|
|||
expect(`tfoot`).toHaveText("$ 2,000.00");
|
||||
});
|
||||
|
||||
test(`aggregates monetary (currency field not set)`, async () => {
|
||||
Foo._fields.amount = fields.Monetary({ currency_field: "currency_test" });
|
||||
Foo._fields.currency_test = fields.Many2one({ relation: "res.currency" });
|
||||
Foo._records[0].currency_test = 1;
|
||||
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="amount" widget="monetary" sum="Sum"/>
|
||||
<field name="currency_test"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
expect(queryAllTexts(`tbody .o_monetary_cell`)).toEqual([
|
||||
"$ 1,200.00",
|
||||
"500.00",
|
||||
"300.00",
|
||||
"0.00",
|
||||
]);
|
||||
expect(`tfoot`).toHaveText("$ 0.00?");
|
||||
});
|
||||
|
||||
test(`aggregates monetary (currency field not set on first record)`, async () => {
|
||||
Foo._fields.amount = fields.Monetary({ currency_field: "currency_test" });
|
||||
Foo._fields.currency_test = fields.Many2one({ relation: "res.currency" });
|
||||
Foo._records[1].currency_test = 1;
|
||||
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="amount" widget="monetary" sum="Sum"/>
|
||||
<field name="currency_test"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
expect(queryAllTexts(`tbody .o_monetary_cell`)).toEqual([
|
||||
"1,200.00",
|
||||
"$ 500.00",
|
||||
"300.00",
|
||||
"0.00",
|
||||
]);
|
||||
expect(`tfoot`).toHaveText("$ 0.00?");
|
||||
});
|
||||
|
||||
test(`aggregates monetary with custom digits (same currency)`, async () => {
|
||||
Foo._records = Foo._records.map((record) => ({
|
||||
...record,
|
||||
|
|
@ -7224,8 +7319,6 @@ test(`empty list with sample data`, async () => {
|
|||
expect(`.o_list_table`).toHaveCount(1);
|
||||
expect(`.o_data_row`).toHaveCount(10);
|
||||
expect(`.o_nocontent_help`).toHaveCount(1);
|
||||
expect(".ribbon").toHaveCount(1);
|
||||
expect(".ribbon").toHaveText("SAMPLE DATA");
|
||||
|
||||
// Check list sample data
|
||||
expect(`.o_data_row .o_data_cell:eq(0)`).toHaveText("", {
|
||||
|
|
@ -7254,7 +7347,6 @@ test(`empty list with sample data`, async () => {
|
|||
expect(`.o_list_view .o_content`).not.toHaveClass("o_view_sample_data");
|
||||
expect(`.o_list_table`).toHaveCount(1);
|
||||
expect(`.o_nocontent_help`).toHaveCount(1);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
|
||||
await toggleMenuItem("False Domain");
|
||||
await toggleMenuItem("True Domain");
|
||||
|
|
@ -7262,7 +7354,6 @@ test(`empty list with sample data`, async () => {
|
|||
expect(`.o_list_table`).toHaveCount(1);
|
||||
expect(`.o_data_row`).toHaveCount(4);
|
||||
expect(`.o_nocontent_help`).toHaveCount(0);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
});
|
||||
|
||||
test(`refresh empty list with sample data`, async () => {
|
||||
|
|
@ -7413,7 +7504,6 @@ test(`non empty list with sample data`, async () => {
|
|||
expect(`.o_list_table`).toHaveCount(1);
|
||||
expect(`.o_data_row`).toHaveCount(4);
|
||||
expect(`.o_list_view .o_content`).not.toHaveClass("o_view_sample_data");
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
|
||||
await toggleSearchBarMenu();
|
||||
await toggleMenuItem("true_domain");
|
||||
|
|
@ -7421,7 +7511,6 @@ test(`non empty list with sample data`, async () => {
|
|||
expect(`.o_list_table`).toHaveCount(1);
|
||||
expect(`.o_data_row`).toHaveCount(0);
|
||||
expect(`.o_list_view .o_content`).not.toHaveClass("o_view_sample_data");
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
});
|
||||
|
||||
test(`click on header in empty list with sample data`, async () => {
|
||||
|
|
@ -7448,6 +7537,38 @@ test(`click on header in empty list with sample data`, async () => {
|
|||
});
|
||||
});
|
||||
|
||||
test(`list grouped by m2o with sample data with more than 5 real groups`, async () => {
|
||||
Foo._records = [];
|
||||
onRpc("web_read_group", () => ({
|
||||
// simulate 6, empty, real groups
|
||||
groups: [1, 2, 3, 4, 5, 6].map((id) => ({
|
||||
__count: 0,
|
||||
__records: [],
|
||||
m2o: [id, `Value ${id}`],
|
||||
__extra_domain: [["m2o", "=", id]],
|
||||
})),
|
||||
length: 6,
|
||||
}));
|
||||
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
type: "list",
|
||||
arch: `<list sample="1"><field name="foo"/></list>`,
|
||||
groupBy: ["m2o"],
|
||||
});
|
||||
expect(`.o_list_view .o_content`).toHaveClass("o_view_sample_data");
|
||||
expect(`.o_list_table`).toHaveCount(1);
|
||||
expect(`.o_group_header`).toHaveCount(6);
|
||||
expect(queryAllTexts(`.o_group_header`)).toEqual([
|
||||
"Value 1 (3)",
|
||||
"Value 2 (3)",
|
||||
"Value 3 (3)",
|
||||
"Value 4 (3)",
|
||||
"Value 5 (2)",
|
||||
"Value 6 (2)",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`non empty editable list with sample data: delete all records`, async () => {
|
||||
await mountView({
|
||||
|
|
@ -7870,6 +7991,40 @@ test(`groupby node with edit button`, async () => {
|
|||
expect.verifySteps(["doAction"]);
|
||||
});
|
||||
|
||||
test(`edit button does not trigger fold group`, async () => {
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
expect.step("doAction");
|
||||
expect(action).toEqual({
|
||||
context: { create: false },
|
||||
res_id: 1,
|
||||
res_model: "res.currency",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="foo"/>
|
||||
<groupby name="currency_id">
|
||||
<button name="edit" type="edit" icon="fa-edit" title="Edit"/>
|
||||
</groupby>
|
||||
</list>
|
||||
`,
|
||||
groupBy: ["currency_id"],
|
||||
});
|
||||
expect(`.o_group_open`).toHaveCount(0);
|
||||
await contains(`.o_group_header:eq(0)`).click();
|
||||
expect(`.o_group_open`).toHaveCount(1);
|
||||
await contains(`.o_group_header .o_group_buttons button:eq(0)`).click();
|
||||
expect(`.o_group_open`).toHaveCount(1);
|
||||
expect.verifySteps(["doAction"]);
|
||||
});
|
||||
|
||||
test(`groupby node with subfields, and onchange`, async () => {
|
||||
Foo._onChanges = {
|
||||
foo() {},
|
||||
|
|
@ -7923,9 +8078,9 @@ test(`list view, editable, without data`, async () => {
|
|||
type: "list",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="foo"/>
|
||||
<field name="date"/>
|
||||
<field name="m2o"/>
|
||||
<field name="foo"/>
|
||||
<button type="object" icon="fa-plus-square" name="method"/>
|
||||
</list>
|
||||
`,
|
||||
|
|
@ -7947,7 +8102,7 @@ test(`list view, editable, without data`, async () => {
|
|||
expect(`tbody tr:eq(0)`).toHaveClass("o_selected_row", {
|
||||
message: "the date field td should be in edit mode",
|
||||
});
|
||||
expect(`tbody tr:eq(0) td:eq(1)`).toHaveText("Feb 10, 2017", {
|
||||
expect(`tbody tr:eq(0) td:eq(2)`).toHaveText("Feb 10, 2017", {
|
||||
message: "the date field td should have the default value",
|
||||
});
|
||||
expect(`tr.o_selected_row .o_list_record_selector input`).toHaveProperty("disabled", true, {
|
||||
|
|
@ -8099,6 +8254,10 @@ test(`editable list view, should refocus date field`, async () => {
|
|||
|
||||
await contains(getPickerCell("15")).click();
|
||||
expect(`.o_datetime_picker`).toHaveCount(0);
|
||||
|
||||
// the datetime field is rendered multiple times before `picker.activeInput`
|
||||
// is reset, and so before the field displays a button instead of the input
|
||||
await waitFor(`.o_field_widget[name=date] button`);
|
||||
expect(`.o_field_widget[name=date] button`).toHaveValue("02/15/2017");
|
||||
expect(`.o_field_widget[name=date] button`).toBeFocused();
|
||||
});
|
||||
|
|
@ -10881,7 +11040,7 @@ test(`multi edit field with daterange widget (edition without using the picker)`
|
|||
await contains(
|
||||
`.o_data_row .o_data_cell .o_field_daterange[name='date_start'] input[data-field='date_start']`
|
||||
).edit("2016-04-01 11:00:00", { confirm: "enter" });
|
||||
expect(`.modal`).toHaveCount(1, {
|
||||
expect(await waitFor(".modal")).toHaveCount(1, {
|
||||
message: "The confirm dialog should appear to confirm the multi edition.",
|
||||
});
|
||||
expect(queryAllTexts(`.modal-body .o_modal_changes td`)).toEqual([
|
||||
|
|
@ -10934,6 +11093,48 @@ test(`list daterange with empty start date and end date`, async () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test(`list daterange in form: open/close picker`, async () => {
|
||||
Foo._fields.foo_o2m = fields.One2many({ relation: "foo" });
|
||||
Foo._fields.date_end = fields.Date();
|
||||
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="foo_o2m">
|
||||
<list editable="bottom">
|
||||
<field name="date" widget="daterange" options="{'end_date_field': 'date_end', 'always_range': '1'}"/>
|
||||
</list>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await contains(`.o_field_x2many_list_row_add a`).click();
|
||||
await contains(".o_field_daterange[name=date]").click();
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toBeDisplayed();
|
||||
expect("input[data-field=date]").toBeFocused();
|
||||
|
||||
await contains(getPickerCell("15")).click();
|
||||
await contains(getPickerCell("20")).click();
|
||||
|
||||
// Close picker
|
||||
await pointerDown(`.o_view_controller`);
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
|
||||
// Wait to check if the picker is still closed
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`editable list view: contexts are correctly sent`, async () => {
|
||||
serverState.userContext = { someKey: "some value" };
|
||||
|
|
@ -12637,6 +12838,47 @@ test(`grouped list view move to previous page of group when all records from las
|
|||
expect(`.o_data_row`).toHaveCount(2);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`grouped list view move to previous page of group when all records from last page deleted with more pages`, async () => {
|
||||
Foo._records.push({ id: 6, foo: "foo", m2o: 1 });
|
||||
Foo._records.push({ id: 7, foo: "foo", m2o: 1 });
|
||||
onRpc("web_search_read", ({ kwargs }) => {
|
||||
expect.step(`web_search_read ${kwargs.limit} - ${kwargs.offset}`);
|
||||
});
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
type: "list",
|
||||
arch: `<list limit="2"><field name="display_name"/></list>`,
|
||||
actionMenus: {},
|
||||
groupBy: ["m2o"],
|
||||
});
|
||||
expect(`th:contains(Value 1 (5))`).toHaveCount(1, {
|
||||
message: "Value 1 should contain 3 records",
|
||||
});
|
||||
expect(`th:contains(Value 2 (1))`).toHaveCount(1, {
|
||||
message: "Value 2 should contain 1 record",
|
||||
});
|
||||
await contains(`.o_group_header:eq(0)`).click();
|
||||
expect(getPagerValue(queryFirst(`.o_group_header`))).toEqual([1, 2]);
|
||||
expect(getPagerLimit(queryFirst(`.o_group_header`))).toBe(5);
|
||||
expect.verifySteps(["web_search_read 2 - 0"]);
|
||||
|
||||
// move to next page
|
||||
await pagerNext(queryFirst(`.o_group_header`));
|
||||
await pagerNext(queryFirst(`.o_group_header`));
|
||||
expect(getPagerValue(queryFirst(`.o_group_header`))).toEqual([5, 5]);
|
||||
expect(getPagerLimit(queryFirst(`.o_group_header`))).toBe(5);
|
||||
expect.verifySteps(["web_search_read 2 - 2", "web_search_read 2 - 4"]);
|
||||
|
||||
// delete a record
|
||||
await contains(`.o_data_row .o_list_record_selector input`).click();
|
||||
await contains(`.o_cp_action_menus .dropdown-toggle`).click();
|
||||
await contains(`.dropdown-item:contains(Delete)`).click();
|
||||
await contains(`.modal .btn-primary`).click();
|
||||
expect(`th.o_group_name:eq(0) .o_pager_counter`).toHaveCount(1);
|
||||
expect(`.o_data_row`).toHaveCount(2);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`grouped list view move to next page when all records from the current page deleted`, async () => {
|
||||
Foo._records = [1, 2, 3, 4, 5, 6]
|
||||
|
|
@ -19199,3 +19441,92 @@ test(`multi edition: many2many_tags add few tags in one time`, async () => {
|
|||
message: "should have display_name in badge",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("multi_edit: must work for copy/paster or operation", async () => {
|
||||
Foo._records[1].datetime = "1989-05-03 12:51:35";
|
||||
Foo._records[2].datetime = "1987-11-13 12:12:34";
|
||||
Foo._records[3].datetime = "2019-04-09 03:21:35";
|
||||
await mountView({
|
||||
resModel: "foo",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list multi_edit="1">
|
||||
<field name="foo"/>
|
||||
<field name="datetime"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
await contains(`.o_list_record_selector`).click();
|
||||
await contains(`.o_data_cell[name=datetime]`).click();
|
||||
await animationFrame();
|
||||
await waitFor(`.o_datetime_picker`);
|
||||
await contains(`input[data-field=datetime]`).edit("+125d", { confirm: "tab" });
|
||||
expect(`tbody tr:eq(0) td[name=datetime]`).toHaveText("Jul 14, 11:30 AM");
|
||||
await contains(`.modal button:contains(update)`).click();
|
||||
expect(".modal").toHaveCount(0);
|
||||
expect(queryAllTexts(`.o_data_cell`)).toEqual([
|
||||
"yop",
|
||||
"Jul 14, 11:30 AM",
|
||||
"blip",
|
||||
"Jul 14, 11:30 AM",
|
||||
"gnap",
|
||||
"Jul 14, 11:30 AM",
|
||||
"blip",
|
||||
"Jul 14, 11:30 AM",
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("scroll position is restored when coming back to list view", async () => {
|
||||
Foo._views = {
|
||||
kanban: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
list: `<list><field name="foo"/></list>`,
|
||||
search: `<search />`,
|
||||
};
|
||||
|
||||
for (let i = 1; i < 30; i++) {
|
||||
Foo._records.push({ id: 100 + i, foo: `Record ${i}` });
|
||||
}
|
||||
|
||||
let def;
|
||||
onRpc("web_search_read", () => def);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
res_model: "foo",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "kanban"],
|
||||
[false, "list"],
|
||||
],
|
||||
});
|
||||
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
await getService("action").switchView("list");
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
// simulate a scroll in the list view
|
||||
queryOne(".o_list_view").scrollTop = 200;
|
||||
|
||||
await getService("action").switchView("kanban");
|
||||
expect(".o_kanban_view").toHaveCount(1);
|
||||
|
||||
// the list is "lazy", so it displays the control panel directly, and the renderer later with
|
||||
// the data => simulate this and check that the scroll position is correctly restored
|
||||
def = new Deferred();
|
||||
await getService("action").switchView("list");
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
expect(".o_list_renderer").toHaveCount(0);
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_list_renderer").toHaveCount(1);
|
||||
expect(".o_list_view").toHaveProperty("scrollTop", 200);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryAllTexts, queryFirst, queryOne, queryText } from "@odoo/hoot-dom";
|
||||
import { queryAll, queryAllTexts, queryFirst, queryOne, queryText, resize } from "@odoo/hoot-dom";
|
||||
import { animationFrame, Deferred, mockDate } from "@odoo/hoot-mock";
|
||||
import { markup } from "@odoo/owl";
|
||||
import {
|
||||
|
|
@ -520,6 +520,7 @@ test("clicking on a cell triggers a doAction", async () => {
|
|||
domain: [["product_id", "=", 37]],
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
search_view_id: [67, "search"],
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
view_mode: "list",
|
||||
|
|
@ -535,6 +536,7 @@ test("clicking on a cell triggers a doAction", async () => {
|
|||
await mountView({
|
||||
type: "pivot",
|
||||
resModel: "partner",
|
||||
searchViewId: 67,
|
||||
arch: `
|
||||
<pivot string="Partners">
|
||||
<field name="product_id" type="row"/>
|
||||
|
|
@ -2799,12 +2801,9 @@ test("empty pivot view with sample data", async () => {
|
|||
|
||||
expect(".o_pivot_view .o_content").toHaveClass("o_view_sample_data");
|
||||
expect(".o_view_nocontent .abc").toHaveCount(1);
|
||||
expect(".ribbon").toHaveCount(1);
|
||||
expect(".ribbon").toHaveText("SAMPLE DATA");
|
||||
await removeFacet();
|
||||
expect(".o_pivot_view .o_content").not.toHaveClass("o_view_sample_data");
|
||||
expect(".o_view_nocontent .abc").toHaveCount(0);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
expect("table").toHaveCount(1);
|
||||
});
|
||||
|
||||
|
|
@ -2828,13 +2827,11 @@ test("non empty pivot view with sample data", async () => {
|
|||
|
||||
expect(".o_content").not.toHaveClass("o_view_sample_data");
|
||||
expect(".o_view_nocontent .abc").toHaveCount(0);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
expect("table").toHaveCount(1);
|
||||
await toggleSearchBarMenu();
|
||||
await toggleMenuItem("Small Than 0");
|
||||
expect(".o_content").not.toHaveClass("o_view_sample_data");
|
||||
expect(".o_view_nocontent .abc").toHaveCount(1);
|
||||
expect(".ribbon").toHaveCount(0);
|
||||
expect("table").toHaveCount(0);
|
||||
});
|
||||
|
||||
|
|
@ -3865,6 +3862,7 @@ test("middle clicking on a cell triggers a doAction", async () => {
|
|||
domain: [["product_id", "=", 37]],
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
search_view_id: [67, "search"],
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
view_mode: "list",
|
||||
|
|
@ -3883,6 +3881,7 @@ test("middle clicking on a cell triggers a doAction", async () => {
|
|||
await mountView({
|
||||
type: "pivot",
|
||||
resModel: "partner",
|
||||
searchViewId: 67,
|
||||
arch: `
|
||||
<pivot string="Partners">
|
||||
<field name="product_id" type="row"/>
|
||||
|
|
@ -3994,3 +3993,102 @@ test("pivot view with monetary with multiple currencies", async () => {
|
|||
expect(".o_pivot table tbody tr:eq(1)").toHaveText("USD \n$ 1,000.00");
|
||||
expect(".o_pivot table tbody tr:last").toHaveText("EUR \n400.00 €");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("scroll position is restored when coming back to pivot view", async () => {
|
||||
Partner._views = {
|
||||
kanban: `
|
||||
<pivot>
|
||||
<field name="foo" type="row"/>
|
||||
</pivot>`,
|
||||
list: `<list><field name="foo"/></list>`,
|
||||
search: `<search />`,
|
||||
};
|
||||
|
||||
for (let i = 1; i < 20; i++) {
|
||||
Partner._records.push({ id: 100 + i, foo: 100 + i });
|
||||
}
|
||||
|
||||
let def;
|
||||
onRpc("formatted_read_grouping_sets", () => def);
|
||||
await resize({ width: 800, height: 300 });
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "pivot"],
|
||||
[false, "list"],
|
||||
],
|
||||
context: {
|
||||
group_by: ["foo"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_pivot_view").toHaveCount(1);
|
||||
// simulate a scroll in the pivot view
|
||||
queryOne(".o_content").scrollTop = 200;
|
||||
|
||||
await getService("action").switchView("list");
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
// the pivot is "lazy", so it displays the control panel directly, and the renderer later with
|
||||
// the data => simulate this and check that the scroll position is correctly restored
|
||||
def = new Deferred();
|
||||
await getService("action").switchView("pivot");
|
||||
expect(".o_pivot_view").toHaveCount(1);
|
||||
expect(".o_content .o_pivot").toHaveCount(0);
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_content .o_pivot").toHaveCount(1);
|
||||
expect(".o_content").toHaveProperty("scrollTop", 200);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("scroll position is restored when coming back to pivot view (mobile)", async () => {
|
||||
Partner._views = {
|
||||
kanban: `
|
||||
<pivot>
|
||||
<field name="foo" type="row"/>
|
||||
</pivot>`,
|
||||
list: `<list><field name="foo"/></list>`,
|
||||
search: `<search />`,
|
||||
};
|
||||
|
||||
for (let i = 1; i < 20; i++) {
|
||||
Partner._records.push({ id: 100 + i, foo: 100 + i });
|
||||
}
|
||||
|
||||
let def;
|
||||
onRpc("formatted_read_grouping_sets", () => def);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "pivot"],
|
||||
[false, "list"],
|
||||
],
|
||||
context: {
|
||||
group_by: ["foo"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_pivot_view").toHaveCount(1);
|
||||
// simulate a scroll in the pivot view
|
||||
queryOne(".o_pivot_view").scrollTop = 200;
|
||||
|
||||
await getService("action").switchView("list");
|
||||
expect(".o_list_view").toHaveCount(1);
|
||||
|
||||
// the pivot is "lazy", so it displays the control panel directly, and the renderer later with
|
||||
// the data => simulate this and check that the scroll position is correctly restored
|
||||
def = new Deferred();
|
||||
await getService("action").switchView("pivot");
|
||||
expect(".o_pivot_view").toHaveCount(1);
|
||||
expect(".o_content .o_pivot").toHaveCount(0);
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_content .o_pivot").toHaveCount(1);
|
||||
expect(".o_pivot_view").toHaveProperty("scrollTop", 200);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue