Files
enterprise-addons/documents_spreadsheet/static/tests/list/list_view.test.js
T

1249 lines
47 KiB
JavaScript

import {
defineDocumentSpreadsheetModels,
defineDocumentSpreadsheetTestAction,
getBasicData,
getBasicServerData,
getDocumentBasicData,
} from "@documents_spreadsheet/../tests/helpers/data";
import {
createSpreadsheetFromListView,
invokeInsertListInSpreadsheetDialog,
spawnListViewForSpreadsheet,
} from "@documents_spreadsheet/../tests/helpers/list_helpers";
import { createSpreadsheet } from "@documents_spreadsheet/../tests/helpers/spreadsheet_test_utils";
import { getHighlightsFromStore } from "@documents_spreadsheet/../tests/helpers/store_helpers";
import { SpreadsheetAction } from "@documents_spreadsheet/bundle/actions/spreadsheet_action";
import { describe, expect, getFixture, test } from "@odoo/hoot";
import { hover, leave } from "@odoo/hoot-dom";
import { animationFrame, mockDate } from "@odoo/hoot-mock";
import * as spreadsheet from "@odoo/o-spreadsheet";
import { onMounted } from "@odoo/owl";
import {
addGlobalFilter,
selectCell,
setCellContent,
} from "@spreadsheet/../tests/helpers/commands";
import { Partner, Product, ResUsers } from "@spreadsheet/../tests/helpers/data";
import {
getCellFormula,
getCellValue,
getEvaluatedCell,
} from "@spreadsheet/../tests/helpers/getters";
import { insertListInSpreadsheet } from "@spreadsheet/../tests/helpers/list";
import { getZoneOfInsertedDataSource } from "@spreadsheet/../tests/helpers/pivot";
import { doMenuAction, getActionMenu } from "@spreadsheet/../tests/helpers/ui";
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
import { getSpreadsheetActionModel } from "@spreadsheet_edition/../tests/helpers/webclient_helpers";
import { InsertListSpreadsheetMenu } from "@spreadsheet_edition/assets/list_view/insert_list_spreadsheet_menu_owl";
import { SpreadsheetSelectorDialog } from "@spreadsheet_edition/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_dialog";
import { insertList } from "@spreadsheet_edition/bundle/list/list_init_callback";
import * as dsHelpers from "@web/../tests/core/tree_editor/condition_tree_editor_test_helpers";
import {
contains,
fields,
mountView,
onRpc,
pagerNext,
patchWithCleanup,
toggleActionMenu,
makeServerError,
serverState,
mockService,
} from "@web/../tests/web_test_helpers";
import { registry } from "@web/core/registry";
import { user } from "@web/core/user";
import { Deferred } from "@web/core/utils/concurrency";
import { mockActionService } from "../helpers/spreadsheet_test_utils";
defineDocumentSpreadsheetModels();
defineDocumentSpreadsheetTestAction();
describe.current.tags("desktop");
const { topbarMenuRegistry, cellMenuRegistry } = spreadsheet.registries;
const { toZone } = spreadsheet.helpers;
test("insert list formulas", async () => {
const { model } = await createSpreadsheetFromListView({
linesNumber: 3,
serverData: {
models: getBasicData(),
views: {
"partner,false,list": `
<list>
<field name="foo"/>
<field name="bar"/>
</list>`,
},
},
});
expect(model.getters.getListDefinition("1").columns).toEqual(["foo", "bar"]);
expect(getCellFormula(model, "A1")).toBe('=ODOO.LIST.HEADER(1,"foo","Foo")');
expect(getCellFormula(model, "B1")).toBe('=ODOO.LIST.HEADER(1,"bar","Bar")');
expect(getCellFormula(model, "A2")).toBe('=ODOO.LIST(1,1,"foo")');
expect(getCellFormula(model, "A3")).toBe('=ODOO.LIST(1,2,"foo")');
expect(getCellFormula(model, "A4")).toBe('=ODOO.LIST(1,3,"foo")');
expect(getCellFormula(model, "A5")).toBe("");
expect(getCellFormula(model, "B2")).toBe('=ODOO.LIST(1,1,"bar")');
expect(getCellFormula(model, "B3")).toBe('=ODOO.LIST(1,2,"bar")');
expect(getCellFormula(model, "B4")).toBe('=ODOO.LIST(1,3,"bar")');
expect(getCellFormula(model, "B5")).toBe("");
});
test("List export with a invisible field", async () => {
const { model } = await createSpreadsheetFromListView({
serverData: {
models: getBasicData(),
views: {
"partner,false,list": `
<list string="Partners">
<field name="foo" column_invisible="1"/>
<field name="bar"/>
</list>`,
},
},
});
expect(model.getters.getListDefinition("1").columns).toEqual(["bar"]);
});
test("List export with a widget handle", async () => {
const { model } = await createSpreadsheetFromListView({
serverData: {
models: getBasicData(),
views: {
"partner,false,list": `
<list string="Partners">
<field name="foo" widget="handle"/>
<field name="bar"/>
</list>`,
},
},
});
expect(model.getters.getListDefinition("1").columns).toEqual(["bar"]);
});
test("Lists are inserted with a table in the webclient", async () => {
const { model } = await createSpreadsheetFromListView();
const sheetId = model.getters.getActiveSheetId();
const table = model.getters.getTable({ sheetId, col: 2, row: 2 });
expect(table.range.zone).toEqual(toZone("A1:D11"));
});
test("property fields are not exported", async () => {
const data = getBasicData();
const propertyDefinition = {
type: "char",
name: "property_char",
string: "Property char",
};
const product = Product._records[0];
product.properties_definitions = [propertyDefinition];
data.partner.records = [
{
id: 1,
bar: true,
product_id: product.id,
partner_properties: {
[propertyDefinition.name]: "CHAR",
},
},
];
const propertyField = fields.Properties({
string: "Property char",
definition_record: "product_id",
definition_record_field: "properties_definitions",
});
Partner._fields.partner_properties = propertyField;
const { model } = await createSpreadsheetFromListView({
actions: async () => {
// display the property which is an optional column
await contains(".o_optional_columns_dropdown_toggle").click();
await contains(".o-dropdown--menu input[type='checkbox']").click();
expect(".o_list_renderer th[data-name='partner_properties.property_char']").toHaveCount(
1
);
expect.step("display_property");
},
serverData: {
models: data,
views: {
"partner,false,list": /*xml*/ `
<list>
<field name="product_id"/>
<field name="bar"/>
<field name="partner_properties"/>
</list>`,
},
},
});
expect(model.getters.getListDefinition("1").columns).toEqual(["product_id", "bar"]);
expect.verifySteps(["display_property"]);
});
test("json fields are not exported", async () => {
const { model } = await createSpreadsheetFromListView({
serverData: {
models: getBasicData(),
views: {
"partner,false,list": `
<list string="Partners">
<field name="jsonField"/>
<field name="bar"/>
</list>`,
},
},
});
expect(model.getters.getListDefinition("1").columns).toEqual(["bar"]);
});
test("list side panel is open at insertion", async function () {
await createSpreadsheetFromListView();
expect(".o-listing-details-side-panel").toHaveCount(1);
});
test("An error is displayed in the side panel if the list has invalid model", async function () {
const { model, env } = await createSpreadsheetFromListView({
mockRPC: async function (route, { model, method, kwargs }) {
if (model === "unknown" && method === "fields_get") {
throw makeServerError({ code: 404 });
}
},
});
await contains(".o-sidePanelClose").click();
const listId = model.getters.getListIds()[0];
const listDefinition = model.getters.getListModelDefinition(listId);
model.dispatch("UPDATE_ODOO_LIST", {
listId,
list: {
...listDefinition,
metaData: {
...listDefinition.metaData,
resModel: "unknown",
},
},
});
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
expect(".o-validation-error").toHaveCount(1);
});
test("Open list properties", async function () {
const { env } = await createSpreadsheetFromListView();
await doMenuAction(topbarMenuRegistry, ["data", "item_list_1"], env);
await animationFrame();
const target = getFixture();
const title = target.querySelector(".o-sidePanelTitle").innerText;
expect(title).toBe("List #1");
const sections = target.querySelectorAll(".o-section");
expect(sections.length).toBe(6, { message: "it should have 6 sections" });
const [listName, listModel, columns, domain] = sections;
expect(listName.children[0]).toHaveText("List Name");
expect(listName.children[1].querySelector("input").value).toBe("Partners");
expect(listModel.children[0]).toHaveText("Model");
expect(listModel.children[1]).toHaveText("Partner (partner)");
expect(columns.children[0]).toHaveText("Columns");
expect(domain.children[0]).toHaveText("Domain");
expect(domain.children[1]).toHaveText("Match all records\nInclude archived");
});
test("A warning is displayed in the menu item if the list is unused", async function () {
const { model } = await createSpreadsheetFromListView();
model.dispatch("CREATE_SHEET", { sheetId: "sh2", name: "Sheet2" });
insertListInSpreadsheet(model, {
sheetId: "sh2",
model: "product",
columns: ["name", "active"],
});
const target = getFixture();
await contains("div[data-id='data']").click();
const menuItemList1 = target.querySelector("div[data-name='item_list_1']");
const menuItemList2 = target.querySelector("div[data-name='item_list_2']");
expect(menuItemList1.querySelector(".o-unused-list-icon")).toEqual(null);
expect(menuItemList2.querySelector(".o-unused-list-icon")).toEqual(null);
model.dispatch("DELETE_SHEET", { sheetId: "sh2" });
await animationFrame();
expect(menuItemList1.querySelector(".o-unused-list-icon")).toEqual(null);
expect(menuItemList2.querySelector(".o-unused-list-icon")).toHaveCount(1);
});
test("A warning is displayed in the side panel if the list is unused", async function () {
const { model, env } = await createSpreadsheetFromListView();
const [listId] = model.getters.getListIds();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
expect(".o-validation-warning").toHaveCount(0);
model.dispatch("CREATE_SHEET", { sheetId: "sh2", name: "Sheet2" });
const activeSheetId = model.getters.getActiveSheetId();
model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: "sh2" });
model.dispatch("DELETE_SHEET", { sheetId: activeSheetId });
await animationFrame();
expect(".o-validation-warning").toHaveCount(1);
model.dispatch("REQUEST_UNDO");
await animationFrame();
expect(".o-validation-warning").toHaveCount(0);
});
test("Deleting the list closes the side panel", async function () {
const { model, env } = await createSpreadsheetFromListView();
const [listId] = model.getters.getListIds();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
const titleSelector = ".o-sidePanelTitle";
expect(titleSelector).toHaveText("List #1");
model.dispatch("REMOVE_ODOO_LIST", { listId });
await animationFrame();
expect(titleSelector).toHaveCount(0);
});
test("Undo a list insertion closes the side panel", async function () {
const { model, env } = await createSpreadsheetFromListView();
const [listId] = model.getters.getListIds();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
const titleSelector = ".o-sidePanelTitle";
expect(titleSelector).toHaveText("List #1");
model.dispatch("REQUEST_UNDO");
model.dispatch("REQUEST_UNDO");
await animationFrame();
expect(titleSelector).toHaveCount(0);
});
test("Add list in an existing spreadsheet", async () => {
const { model, env } = await createSpreadsheetFromListView();
const list = model.getters.getListDefinition("1");
const fields = model.getters.getListDataSource("1").getFields();
const callback = insertList.bind({ isEmptySpreadsheet: false })({
list: {
...list,
columns: list.columns.map((col) => ({ name: col })),
},
threshold: 10,
fields: fields,
name: "my list",
});
model.dispatch("CREATE_SHEET", { sheetId: "42", position: 1 });
const activeSheetId = model.getters.getActiveSheetId();
expect(model.getters.getSheetIds()).toEqual([activeSheetId, "42"]);
await callback(model, env.__spreadsheet_stores__);
expect(model.getters.getSheetIds().length).toBe(3);
expect(model.getters.getSheetIds()[0]).toEqual(activeSheetId);
expect(model.getters.getSheetIds()[1]).toBe("42");
expect(".o-listing-details-side-panel").toHaveCount(1);
});
test("Verify absence of list properties on non-list cell", async function () {
const { model, env } = await createSpreadsheetFromListView();
selectCell(model, "Z26");
const root = cellMenuRegistry.getAll().find((item) => item.id === "listing_properties");
expect(root.isVisible(env)).toBe(false);
});
test("Verify absence of list properties on formula with invalid list Id", async function () {
const { model, env } = await createSpreadsheetFromListView();
setCellContent(model, "A1", `=ODOO.LIST.HEADER("fakeId", "foo")`);
const root = cellMenuRegistry.getAll().find((item) => item.id === "listing_properties");
expect(root.isVisible(env)).toBe(false);
setCellContent(model, "A1", `=ODOO.LIST("fakeId", "2", "bar")`);
expect(root.isVisible(env)).toBe(false);
});
test("Re-insert a list correctly ask for lines number", async function () {
const { model, env } = await createSpreadsheetFromListView();
selectCell(model, "Z26");
await doMenuAction(topbarMenuRegistry, ["data", "reinsert_list", "reinsert_list_1"], env);
await animationFrame();
expect(".modal-body input").toHaveCount(1);
expect(".modal-body input").toHaveProperty("type", "number");
await contains(".o_dialog .btn-secondary").click(); // cancel
expect(getCellFormula(model, "Z26")).toBe("", { message: "the list is not re-inserted" });
await doMenuAction(topbarMenuRegistry, ["data", "reinsert_list", "reinsert_list_1"], env);
await animationFrame();
await contains(".o_dialog .btn-primary").click(); // confirm
expect(getCellFormula(model, "Z26")).toBe('=ODOO.LIST.HEADER(1,"foo","Foo")', {
message: "the list is re-inserted",
});
});
test("Validates input and shows error message when input is invalid", async function () {
const { model, env } = await createSpreadsheetFromListView();
selectCell(model, "Z1");
await doMenuAction(topbarMenuRegistry, ["data", "reinsert_list", "reinsert_list_1"], env);
await animationFrame();
await contains(".modal-body input").edit("");
await contains(".modal-content > .modal-footer > .btn-primary").click();
expect(".modal-body span.text-danger:only").toHaveText("Please enter a valid number.");
});
test("Re-insert a list with a selected number of records", async function () {
const { model, env } = await createSpreadsheetFromListView();
selectCell(model, "Z1");
await doMenuAction(topbarMenuRegistry, ["data", "reinsert_list", "reinsert_list_1"], env);
await animationFrame();
await contains(".modal-body input").edit("2000");
await contains(".modal-content > .modal-footer > .btn-primary").click();
expect(model.getters.getNumberRows(model.getters.getActiveSheetId())).toBe(2001);
});
test("Re-insert a list also applies a table", async function () {
const { model, env } = await createSpreadsheetFromListView();
const sheetId = model.getters.getActiveSheetId();
let table = model.getters.getTable({ sheetId, col: 0, row: 49 });
expect(table).toBe(undefined);
selectCell(model, "A50");
await doMenuAction(topbarMenuRegistry, ["data", "reinsert_list", "reinsert_list_1"], env);
await animationFrame();
await contains(".modal-body input").edit("10");
await contains(".modal-content > .modal-footer > .btn-primary").click();
table = model.getters.getTable({ sheetId, col: 0, row: 49 });
expect(table.range.zone).toEqual(toZone("A50:D60"));
});
test("user related context is not saved in the spreadsheet", async function () {
ResUsers._records = getDocumentBasicData().models["res.users"].records;
registry.category("favoriteMenu").add(
"insert-in-spreadsheet-menu",
{
Component: InsertListSpreadsheetMenu,
groupNumber: 4,
},
{ sequence: 5 }
);
patchWithCleanup(SpreadsheetSelectorDialog.prototype, {
setup() {
super.setup();
expect(this.props.actionOptions.preProcessingAsyncActionData.list.context).toEqual(
{
default_stage_id: 5,
},
{ message: "user related context is not stored in context" }
);
},
});
serverState.companies = [{ id: 15, name: "Hermit" }];
const userCtx = user.context;
patchWithCleanup(user, {
get context() {
return Object.assign({}, userCtx, {
allowed_company_ids: [15],
tz: "bx",
lang: "FR",
uid: 4,
});
},
});
patchWithCleanup(user, { userId: 4 });
const context = {
...user.context,
default_stage_id: 5,
};
const { env } = await mountView({
type: "list",
resModel: "partner",
context,
arch: `
<list string="Partners">
<field name="bar"/>
<field name="product_id"/>
</list>
`,
config: {
actionType: "ir.actions.act_window",
getDisplayName: () => "Test",
viewType: "list",
},
});
await invokeInsertListInSpreadsheetDialog(env);
await contains(".modal button.btn-primary").click();
});
test("Selected records from current page are inserted correctly", async function () {
const def = new Deferred();
let spreadsheetAction = {};
patchWithCleanup(SpreadsheetAction.prototype, {
setup() {
super.setup();
onMounted(() => {
spreadsheetAction = this;
def.resolve();
});
},
});
const serverData = {
models: getBasicData(),
views: {
"partner,false,list": `
<list limit="2">
<field name="foo"/>
</list>`,
},
};
await spawnListViewForSpreadsheet({
serverData,
});
/** Insert the selected records from current page in a new spreadsheet */
const target = getFixture();
await contains(target.querySelectorAll("td.o_list_record_selector input")[1]).click();
await pagerNext();
await contains(target.querySelectorAll("td.o_list_record_selector input")[0]).click();
await toggleActionMenu();
const insertMenuItem = [...target.querySelectorAll(".o-dropdown--menu .o_menu_item")].filter(
(el) => el.innerText === "Insert in spreadsheet"
)[0];
await contains(insertMenuItem).click();
await contains(".modal-content > .modal-footer > .btn-primary").click();
await def;
const model = getSpreadsheetActionModel(spreadsheetAction);
await waitForDataLoaded(model);
expect(getCellValue(model, "A2")).toBe(17, {
message: "First record from page 2 (i.e. 3 of 4 records) should be inserted",
});
expect(getCellValue(model, "A3")).toBe(null);
});
test("Selected all records from current page are inserted correctly", async function () {
const def = new Deferred();
let spreadsheetAction = {};
patchWithCleanup(SpreadsheetAction.prototype, {
setup() {
super.setup();
onMounted(() => {
spreadsheetAction = this;
def.resolve();
});
},
});
const serverData = {
models: getBasicData(),
views: {
"partner,false,list": `
<list limit="2">
<field name="foo"/>
</list>`,
},
};
await spawnListViewForSpreadsheet({
serverData,
});
/** Insert the selected records from current page in a new spreadsheet */
const target = getFixture();
await contains(target.querySelectorAll("td.o_list_record_selector input")[1]).click();
await contains(target.querySelectorAll("td.o_list_record_selector input")[0]).click();
await contains(".o_select_domain").click();
await toggleActionMenu();
const insertMenuItem = [...target.querySelectorAll(".o-dropdown--menu .o_menu_item")].filter(
(el) => el.innerText === "Insert in spreadsheet"
)[0];
await contains(insertMenuItem).click();
await contains(".modal-content > .modal-footer > .btn-primary").click();
await def;
const model = getSpreadsheetActionModel(spreadsheetAction);
await waitForDataLoaded(model);
expect(getCellValue(model, "A2")).toBe(12, {
message: "First record from page 2 (i.e. 3 of 4 records) should be inserted",
});
expect(getCellValue(model, "A3")).toBe(1);
expect(getCellValue(model, "A4")).toBe(17);
expect(getCellValue(model, "A5")).toBe(2);
});
test("Insert in spreadsheet is avaiblable on a list grouped by m2m field", async function () {
let spreadsheetAction;
patchWithCleanup(SpreadsheetAction.prototype, {
setup() {
super.setup();
spreadsheetAction = this;
},
});
const serverData = {
models: getBasicData(),
views: {
"partner,false,list": `
<list>
<field name="foo"/>
</list>`,
},
};
await spawnListViewForSpreadsheet({
serverData,
groupBy: ["tag_ids"],
});
const target = getFixture();
await contains(target.querySelectorAll(".o_list_record_selector input")[0]).click();
await toggleActionMenu();
await contains(".o-dropdown--menu .o_menu_item:has(.oi-view-list)").click();
await contains(".modal button.btn-primary").click();
await animationFrame();
const model = getSpreadsheetActionModel(spreadsheetAction);
await waitForDataLoaded(model);
expect(getCellValue(model, "A2")).toBe(12);
expect(getCellValue(model, "A3")).toBe(1);
expect(getCellValue(model, "A4")).toBe(17);
expect(getCellValue(model, "A5")).toBe(2);
});
test("Can see record of a list", async function () {
const { webClient, model } = await createSpreadsheetFromListView();
const listId = model.getters.getListIds()[0];
const dataSource = model.getters.getListDataSource(listId);
const env = {
...webClient.env,
model,
services: {
...model.config.custom.env.services,
action: {
doAction: (params) => {
expect.step(params.res_model);
expect.step(params.res_id.toString());
},
},
},
};
selectCell(model, "A2");
const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record");
await root.execute(env);
expect.verifySteps(["partner", dataSource.getIdFromPosition(0).toString()]);
selectCell(model, "A3");
await root.execute(env);
expect.verifySteps(["partner", dataSource.getIdFromPosition(1).toString()]);
// From a cell inside a merge
model.dispatch("ADD_MERGE", {
sheetId: model.getters.getActiveSheetId(),
target: [toZone("A3:B3")],
force: true, // there are data in B3
});
selectCell(model, "B3");
await root.execute(env);
expect.verifySteps(["partner", dataSource.getIdFromPosition(1).toString()]);
});
test("See record of list is only displayed on list formula with only one list formula", async function () {
const { webClient, model } = await createSpreadsheetFromListView();
const env = {
...webClient.env,
model,
services: model.config.custom.env.services,
};
setCellContent(model, "A1", "test");
setCellContent(model, "A2", `=ODOO.LIST("1","1","foo")`);
setCellContent(model, "A3", `=ODOO.LIST("1","1","foo")+LIST("1","1","foo")`);
const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record");
selectCell(model, "A1");
expect(root.isVisible(env)).toBe(false);
selectCell(model, "A2");
expect(root.isVisible(env)).toBe(true);
selectCell(model, "A3");
expect(root.isVisible(env)).toBe(false);
});
test("See records is visible even if the formula is lowercase", async function () {
const { env, model } = await createSpreadsheetFromListView();
selectCell(model, "B2");
const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record");
expect(root.isVisible(env)).toBe(true);
setCellContent(model, "B2", getCellFormula(model, "B2").replace("ODOO.LIST", "odoo.list"));
expect(root.isVisible(env)).toBe(true);
});
test("See records is not visible if the formula is in error", async function () {
const { env, model } = await createSpreadsheetFromListView();
selectCell(model, "B2");
const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record");
expect(root.isVisible(env)).toBe(true);
setCellContent(
model,
"B2",
getCellFormula(model, "B2").replace(`ODOO.LIST(1`, `ODOO.LIST("5)`)
); //Invalid id
expect(getEvaluatedCell(model, "B2").message).not.toBe(undefined);
expect(root.isVisible(env)).toBe(false);
});
test("See record.isVisible() don't throw on spread values", async function () {
const { env, model } = await createSpreadsheet();
setCellContent(model, "A1", "A1");
setCellContent(model, "A2", "A2");
setCellContent(model, "C1", "=TRANSPOSE(A1:A2)");
selectCell(model, "D1");
await animationFrame();
const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record");
expect(root.isVisible(env)).toBe(false);
});
test("Cannot see record of list formula without value", async function () {
const { env, model } = await createSpreadsheetFromListView();
expect(getCellFormula(model, "A6")).toBe(`=ODOO.LIST(1,5,"foo")`);
expect(getCellValue(model, "A6")).toBe("", { message: "A6 is empty" });
selectCell(model, "A6");
const action = await getActionMenu(cellMenuRegistry, ["list_see_record"], env);
expect(action.isVisible(env)).toBe(false);
});
test("'See records' loads a specific action if set in the list definition", async function () {
const actionXmlId = "spreadsheet.partner_action";
const { webClient, model } = await createSpreadsheetFromListView({ actionXmlId });
const actionService = webClient.env.services.action;
const env = {
...webClient.env,
model,
services: {
...model.config.custom.env.services,
action: {
...actionService,
doAction: (params) => {
expect(params.id).not.toBe(undefined);
expect(params.xml_id).not.toBe(undefined);
expect.step(params.res_model);
expect.step(params.res_id.toString());
},
},
},
};
selectCell(model, "C3");
await animationFrame();
const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record");
await root.execute(env);
expect.verifySteps(["partner", "2"]);
});
test("Context is passed correctly to the action service", async function () {
const serverData = getBasicServerData();
const actionXmlId = "spreadsheet.partner_action";
serverData.views["partner,false,search"] = /* xml */ `
<search>
<filter string="Filter" name="filter" context="{'search_default_filter': 1}" />
</search>
`;
const { model, env } = await createSpreadsheetFromListView({
serverData,
actionXmlId,
additionalContext: { search_default_filter: 1 },
});
mockActionService((action) => {
expect.step("loadAction");
expect(action.context).toEqual({ search_default_filter: 1 });
});
selectCell(model, "A2");
await animationFrame();
const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record");
await root.execute(env);
expect.verifySteps(["loadAction"]);
});
test("Update the list title from the side panel", async function () {
const { model, env } = await createSpreadsheetFromListView();
const [listId] = model.getters.getListIds();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
const target = await contains(".os-input");
await target.click();
await target.edit("new name");
expect(model.getters.getListName(listId)).toBe("new name");
});
test("list with a contextual domain", async () => {
// TODO: the date is coded at 12PM so the test won't fail if the timezone is not UTC. It will still fail on some
// timezones (GMT +13). The good way to do the test would be to patch the time zone and the date correctly.
// But PyDate uses new Date() instead of luxon, which cannot be correctly patched.
mockDate("2016-05-14 12:00:00");
const serverData = getBasicServerData();
serverData.models.partner.records = [
{
id: 1,
probability: 0.5,
date: "2016-05-14",
},
];
serverData.views["partner,false,search"] = /* xml */ `
<search>
<filter string="Filter" name="filter" domain="[('date', '=', context_today())]"/>
</search>
`;
serverData.views["partner,false,list"] = /* xml */ `
<list>
<field name="foo"/>
</list>
`;
const { model } = await createSpreadsheetFromListView({
serverData,
additionalContext: { search_default_filter: 1 },
mockRPC: function (route, args) {
if (args.method === "web_search_read") {
expect(args.kwargs.domain).toEqual([["date", "=", "2016-05-14"]], {
message: "data should be fetched with the evaluated the domain",
});
expect.step("web_search_read");
}
},
});
const listId = "1";
expect(model.getters.getListDefinition(listId).domain).toEqual(
'[("date", "=", context_today())]'
);
expect(model.exportData().lists[listId].domain).toBe('[("date", "=", context_today())]', {
message: "domain is exported with the dynamic value",
});
expect.verifySteps([
"web_search_read", // list view is loaded
"web_search_read", // the data is loaded in the spreadsheet
]);
});
test("Update the list domain from the side panel", async function () {
onRpc("/web/domain/validate", () => true);
const { model, env } = await createSpreadsheetFromListView();
const [listId] = model.getters.getListIds();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
const fixture = getFixture();
await contains(".o_edit_domain").click();
await dsHelpers.addNewRule();
await contains(".modal-footer .btn-primary").click();
expect(model.getters.getListDefinition(listId).domain).toEqual([["id", "=", 1]]);
expect(dsHelpers.getConditionText(fixture)).toBe("Id = 1");
});
test("Update the list sorting from the side panel", async function () {
const { model, env } = await createSpreadsheetFromListView();
const [listId] = model.getters.getListIds();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
await contains(".add-dimension").click();
for (let i = 0; i < 5; i++) {
await contains(".o-popover input").press("ArrowDown");
}
await contains(".o-autocomplete-value-focus").click();
expect(model.getters.getListDefinition(listId).orderBy).toEqual([{ name: "date", asc: true }]);
await contains(".add-dimension").click();
for (let i = 0; i < 7; i++) {
await contains(".o-popover input").press("ArrowDown");
}
await contains(".o-autocomplete-value-focus").click();
const fixture = getFixture();
fixture.querySelector(".o-select-order").value = false;
fixture.querySelector(".o-select-order").dispatchEvent(new Event("change"));
expect(model.getters.getListDefinition(listId).orderBy).toEqual([
{ name: "date", asc: false },
{ name: "foo", asc: true },
]);
model.dispatch("REQUEST_UNDO");
expect(model.getters.getListDefinition(listId).orderBy).toEqual([
{ name: "date", asc: true },
{ name: "foo", asc: true },
]);
await contains(".o-delete-rule").click();
expect(model.getters.getListDefinition(listId).orderBy).toEqual([{ name: "foo", asc: true }]);
});
test("List sorting selector should display only sortable fields", async function () {
Partner._fields.foo = fields.Integer({ sortable: false });
Partner._fields.bar = fields.Boolean({ sortable: false });
const { model, env } = await createSpreadsheetFromListView({});
const [listId] = model.getters.getListIds();
const fixture = getFixture();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
await contains(".add-dimension").click();
const options = [...fixture.querySelectorAll(".o-popover .o-autocomplete-dropdown > div")];
const availableFields = options.map((el) => el.innerText);
expect(availableFields).toEqual([
"Active",
"Creation Date",
"Currency",
"Date",
"Display name",
"field_with_array_agg",
"Id",
"Json Field",
"Last Modified on",
"Money!",
"name",
"Probability",
"Product",
"Properties",
"Tags",
"Users",
]);
});
test("List sorting selector should not display already used fields", async function () {
Partner._fields.foo = fields.Integer({ sortable: false });
Partner._fields.bar = fields.Boolean({ sortable: false });
const { model, env } = await createSpreadsheetFromListView({});
const [listId] = model.getters.getListIds();
const fixture = getFixture();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
await contains(".add-dimension").click();
await contains(".o-popover input").press("ArrowDown");
await contains(".o-popover input").press("ArrowDown");
await contains(".o-autocomplete-value-focus").click();
await contains(".add-dimension").click();
let options = [...fixture.querySelectorAll(".o-popover .o-autocomplete-dropdown > div")];
let availableFields = options.map((el) => el.innerText);
expect(availableFields).toEqual([
"Active",
/* "Creation Date", this field should not be available anymore as it is used */
"Currency",
"Date",
"Display name",
"field_with_array_agg",
"Id",
"Json Field",
"Last Modified on",
"Money!",
"name",
"Probability",
"Product",
"Properties",
"Tags",
"Users",
]);
await contains(".o-popover input").press("ArrowDown");
await contains(".o-popover input").press("ArrowDown");
await contains(".o-autocomplete-value-focus").click();
await contains(".add-dimension").click();
options = [...fixture.querySelectorAll(".o-popover .o-autocomplete-dropdown > div")];
availableFields = options.map((el) => el.innerText);
expect(availableFields).toEqual([
"Active",
/* "Creation Date", this field should not be available anymore as it is used */
/* "Currency", this field should not be available anymore as it is used */
"Date",
"Display name",
"field_with_array_agg",
"Id",
"Json Field",
"Last Modified on",
"Money!",
"name",
"Probability",
"Product",
"Properties",
"Tags",
"Users",
]);
});
test("List sorting selector should display used fields again when corresponding rule is deleted", async function () {
Partner._fields.foo = fields.Integer({ sortable: false });
Partner._fields.bar = fields.Boolean({ sortable: false });
const { model, env } = await createSpreadsheetFromListView({});
const [listId] = model.getters.getListIds();
const fixture = getFixture();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
await contains(".add-dimension").click();
await contains(".o-popover input").press("ArrowDown");
await contains(".o-popover input").press("ArrowDown");
await contains(".o-autocomplete-value-focus").click();
await contains(".add-dimension").click();
let options = [...fixture.querySelectorAll(".o-popover .o-autocomplete-dropdown > div")];
let availableFields = options.map((el) => el.innerText);
expect(availableFields).toEqual([
"Active",
/* "Creation Date", this field should not be available anymore as it is used */
"Currency",
"Date",
"Display name",
"field_with_array_agg",
"Id",
"Json Field",
"Last Modified on",
"Money!",
"name",
"Probability",
"Product",
"Properties",
"Tags",
"Users",
]);
await contains(".o-delete-rule").click();
await contains(".add-dimension").click();
options = [...fixture.querySelectorAll(".o-popover .o-autocomplete-dropdown > div")];
availableFields = options.map((el) => el.innerText);
expect(availableFields).toEqual([
"Active",
"Creation Date",
"Currency",
"Date",
"Display name",
"field_with_array_agg",
"Id",
"Json Field",
"Last Modified on",
"Money!",
"name",
"Probability",
"Product",
"Properties",
"Tags",
"Users",
]);
});
test("Inserting a list preserves the ascending sorting from the list", async function () {
const serverData = getBasicServerData();
Partner._fields.foo.sortable = true;
const { model } = await createSpreadsheetFromListView({
serverData,
orderBy: [{ name: "foo", asc: true }],
linesNumber: 4,
});
expect(getEvaluatedCell(model, "A2").value <= getEvaluatedCell(model, "A3").value).not.toBe(
undefined
);
expect(getEvaluatedCell(model, "A3").value <= getEvaluatedCell(model, "A4").value).not.toBe(
undefined
);
expect(getEvaluatedCell(model, "A4").value <= getEvaluatedCell(model, "A5").value).not.toBe(
undefined
);
});
test("Inserting a list preserves the descending sorting from the list", async function () {
const serverData = getBasicServerData();
Partner._fields.foo.sortable = true;
const { model } = await createSpreadsheetFromListView({
serverData,
orderBy: [{ name: "foo", asc: false }],
linesNumber: 4,
});
expect(getEvaluatedCell(model, "A2").value >= getEvaluatedCell(model, "A3").value).not.toBe(
undefined
);
expect(getEvaluatedCell(model, "A3").value >= getEvaluatedCell(model, "A4").value).not.toBe(
undefined
);
expect(getEvaluatedCell(model, "A4").value >= getEvaluatedCell(model, "A5").value).not.toBe(
undefined
);
});
test("Sorting from the list is displayed in the properties panel", async function () {
const serverData = getBasicServerData();
Partner._fields.foo.sortable = true;
Partner._fields.bar.sortable = true;
const { model, env } = await createSpreadsheetFromListView({
serverData,
orderBy: [
{ name: "foo", asc: true },
{ name: "bar", asc: false },
],
linesNumber: 4,
});
const [listId] = model.getters.getListIds();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
expect(".o_sorting_rule_column:eq(0)").toHaveText("Bar");
expect(".o_sorting_rule_column:eq(1)").toHaveText("Foo");
/** Bar should be descending */
expect(".o-select-order:eq(0)").toHaveValue("false");
/** Foo should be ascending */
expect(".o-select-order:eq(1)").toHaveValue("true");
});
test("Opening the sidepanel of a list while the panel of another list is open updates the side panel", async function () {
const { model, env } = await createSpreadsheetFromListView({});
insertListInSpreadsheet(model, {
model: "product",
columns: ["name", "active"],
});
const listIds = model.getters.getListIds();
const fixture = getFixture();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId: listIds[0] });
await animationFrame();
let modelName = fixture.querySelector(".o-section .o_model_name");
expect(modelName).toHaveText("Partner (partner)");
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId: listIds[1] });
await animationFrame();
modelName = fixture.querySelector(".o-section .o_model_name");
expect(modelName).toHaveText("Product (product)");
});
test("Duplicate a list from the side panel", async function () {
const serverData = getBasicServerData();
Partner._fields.foo.sortable = true;
const { model, env } = await createSpreadsheetFromListView({
serverData,
orderBy: [{ name: "foo", asc: true }],
});
const [listId] = model.getters.getListIds();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId });
await animationFrame();
expect(model.getters.getListIds().length).toBe(1);
expect(".o-sidePanelTitle").toHaveText("List #1");
await contains(".os-cog-wheel-menu-icon").click();
await contains(".o-popover .fa-clone").click();
expect(model.getters.getListIds().length).toBe(2);
expect(".o-sidePanelTitle").toHaveText("List #2");
});
test("List export from an action with an xml ID", async function () {
const actionXmlId = "spreadsheet.partner_action";
const { model } = await createSpreadsheetFromListView({ actionXmlId });
expect(model.getters.getListDefinition("1").actionXmlId).toBe("spreadsheet.partner_action");
});
test("List cells are highlighted when their side panel is open", async function () {
const { model, env } = await createSpreadsheetFromListView();
const sheetId = model.getters.getActiveSheetId();
env.openSidePanel("LIST_PROPERTIES_PANEL", { listId: "1" });
await animationFrame();
const range = model.getters.getRangeFromZone(
sheetId,
getZoneOfInsertedDataSource(model, "list", "1")
);
expect(getHighlightsFromStore(env)).toEqual([{ range, noFill: true }]);
await contains(".o-sidePanelClose").click();
expect(getHighlightsFromStore(env)).toEqual([]);
});
test("List cells are highlighted when hovering the list menu item", async function () {
const { model, env } = await createSpreadsheetFromListView();
await contains(".o-sidePanelClose").click();
const sheetId = model.getters.getActiveSheetId();
await contains(".o-topbar-top div[data-id='data']").click();
await hover("div[data-name='item_list_1']");
const range = model.getters.getRangeFromZone(
sheetId,
getZoneOfInsertedDataSource(model, "list", "1")
);
expect(getHighlightsFromStore(env)).toEqual([{ range, noFill: true }]);
await leave("div[data-name='item_list_1']");
expect(getHighlightsFromStore(env)).toEqual([]);
});
test("Inserting a grouped list ignore groups", async function () {
const serverData = getBasicServerData();
Partner._fields.foo.sortable = true;
onRpc("partner", "web_read_group", ({ kwargs }) => {
if (kwargs.groupby) {
// The mock server cannot handle orderby count
kwargs.order = "";
}
});
const { model } = await createSpreadsheetFromListView({
actions: async (fixture) => {
// display the property which is an optional column
await contains(".o_searchview_dropdown_toggler").click();
await contains(".o_add_custom_group_menu").select("bar");
await contains(".o_searchview_facet_label").click();
},
serverData,
linesNumber: 4,
});
expect(getEvaluatedCell(model, "A2").value <= getEvaluatedCell(model, "A3").value).not.toBe(
undefined
);
});
test("Middle-click on 'See record' opens the record in a new tab", async function () {
const { model, env } = await createSpreadsheetFromListView({
serverData: {
models: getBasicData(),
views: {
"partner,false,list": `
<list string="Partners">
<field name="foo"/>
<field name="bar"/>
</list>`,
"partner,false,search": "<search/>",
},
},
});
mockService("action", {
doAction(_, options) {
expect.step("doAction");
expect(options).toEqual({
newWindow: true,
viewType: "form",
});
return Promise.resolve(true);
},
});
selectCell(model, "A2");
const root = cellMenuRegistry.getAll().find((item) => item.id === "list_see_record");
await root.execute(env, true);
expect.verifySteps(["doAction"]);
});
test("readonly list side panel", async function () {
const { model, env } = await createSpreadsheetFromListView();
await doMenuAction(topbarMenuRegistry, ["data", "item_list_1"], env);
await animationFrame();
expect(".o-sidePanelBody > div.pe-none.opacity-50").toHaveCount(0);
model.updateMode("readonly");
await animationFrame();
expect(".o-sidePanelBody > div.pe-none.opacity-50").toHaveCount(1);
expect(".o-sidePanelBody > div.pe-none.opacity-50").toHaveAttribute("inert");
});
test("display list related filters panel", async function () {
const { model } = await createSpreadsheetFromListView();
const listId = model.getters.getListIds()[0];
await addGlobalFilter(
model,
{ id: "42", type: "relation", label: "Filter" },
{
list: {
[listId]: {
chain: "product_id",
type: "many2one",
},
},
}
);
await addGlobalFilter(model, { id: "43", type: "relation", label: "Filter 2" });
expect(".o_side_panel_collapsible_title:contains(Matching 1 / 2 filters)").toHaveCount(1);
});