Files
enterprise-addons/documents_spreadsheet/static/tests/pivot/pivot_menu_items.test.js
T

520 lines
20 KiB
JavaScript

import {
defineDocumentSpreadsheetModels,
defineDocumentSpreadsheetTestAction,
getBasicServerData,
} from "@documents_spreadsheet/../tests/helpers/data";
import {
createSpreadsheetFromPivotView,
createSpreadsheetWithPivot,
} from "@documents_spreadsheet/../tests/helpers/pivot_helpers";
import { createSpreadsheet } from "@documents_spreadsheet/../tests/helpers/spreadsheet_test_utils";
import { getHighlightsFromStore } from "@documents_spreadsheet/../tests/helpers/store_helpers";
import { beforeEach, describe, expect, getFixture, test } from "@odoo/hoot";
import { hover, leave } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { helpers, registries } from "@odoo/o-spreadsheet";
import {
addGlobalFilter,
selectCell,
setCellContent,
setGlobalFilterValue,
} from "@spreadsheet/../tests/helpers/commands";
import { getBasicPivotArch } from "@spreadsheet/../tests/helpers/data";
import {
getCell,
getCellFormula,
getCellValue,
getCorrespondingCellFormula,
getEvaluatedCell,
} from "@spreadsheet/../tests/helpers/getters";
import {
getZoneOfInsertedDataSource,
insertPivotInSpreadsheet,
} from "@spreadsheet/../tests/helpers/pivot";
import { doMenuAction } from "@spreadsheet/../tests/helpers/ui";
import { contains, MockServer } from "@web/../tests/web_test_helpers";
import { user } from "@web/core/user";
import { mockActionService } from "../helpers/spreadsheet_test_utils";
const { cellMenuRegistry, topbarMenuRegistry } = registries;
const { toCartesian, toZone } = helpers;
defineDocumentSpreadsheetModels();
defineDocumentSpreadsheetTestAction();
describe.current.tags("desktop");
let target;
const reinsertDynamicPivotPath = ["data", "reinsert_dynamic_pivot", "reinsert_dynamic_pivot_1"];
const reinsertStaticPivotPath = ["data", "reinsert_static_pivot", "reinsert_static_pivot_1"];
beforeEach(() => {
target = getFixture();
});
test("Reinsert a dynamic pivot", async function () {
const { model, env } = await createSpreadsheetWithPivot();
selectCell(model, "D8");
await doMenuAction(topbarMenuRegistry, reinsertDynamicPivotPath, env);
expect(getCorrespondingCellFormula(model, "E10")).toBe(`=PIVOT(1)`, {
message: "It should be part of a pivot formula",
});
});
test("Reinsert a static pivot", async function () {
const { model, env } = await createSpreadsheetWithPivot();
selectCell(model, "D8");
await doMenuAction(topbarMenuRegistry, reinsertStaticPivotPath, env);
expect(getCellFormula(model, "E10")).toBe(
`=PIVOT.VALUE(1,"probability:avg","bar",FALSE,"foo",1)`,
{
message: "It should contain a pivot formula",
}
);
});
test("Reinsert a pivot with a contextual search domain", async function () {
const serverData = getBasicServerData();
const uid = user.userId;
serverData.models.partner.records = [{ id: 1, probability: 0.5, foo: uid }];
serverData.views["partner,false,search"] = /* xml */ `
<search>
<filter string="Filter" name="filter" domain="[('foo', '=', uid)]"/>
</search>
`;
const { model, env } = await createSpreadsheetFromPivotView({
serverData,
additionalContext: { search_default_filter: 1 },
});
selectCell(model, "D8");
await doMenuAction(topbarMenuRegistry, reinsertStaticPivotPath, env);
expect(getCellFormula(model, "E10")).toBe(
`=PIVOT.VALUE(1,"probability:avg","bar",FALSE,"foo",${uid})`,
{ message: "It should contain a pivot formula" }
);
});
test("Reinsert pivot menu item should be hidden if the pivot is invalid", async function () {
const { model, env } = await createSpreadsheet();
setCellContent(model, "A1", "Customer");
selectCell(model, "A1");
const pivot = topbarMenuRegistry
.getMenuItems()
.find((item) => item.id === "insert")
.children(env)
.find((item) => item.id === "insert_pivot")
.children(env)
.find((item) => item.id === "insert_pivot_from_range");
await pivot.execute(env);
const reinsertDynamicPivot = topbarMenuRegistry
.getMenuItems()
.find((item) => item.id === "data")
.children(env)
.find((item) => item.id === "reinsert_dynamic_pivot");
expect(reinsertDynamicPivot.isVisible(env)).toBe(true);
setCellContent(model, "A1", "", "Sheet1");
expect(reinsertDynamicPivot.isVisible(env)).toBe(false);
});
test("Reinsert a pivot in a too small sheet", async function () {
const { model, env } = await createSpreadsheetWithPivot();
const sheetId = model.getters.getActiveSheetId();
model.dispatch("CREATE_SHEET", { cols: 1, rows: 1, sheetId: "111" });
model.dispatch("ACTIVATE_SHEET", {
sheetIdFrom: sheetId,
sheetIdTo: "111",
});
selectCell(model, "A1");
await doMenuAction(topbarMenuRegistry, reinsertDynamicPivotPath, env);
expect(model.getters.getNumberCols("111")).toBe(6);
expect(model.getters.getNumberRows("111")).toBe(5);
expect(getCorrespondingCellFormula(model, "B3")).toBe(`=PIVOT(1)`, {
message: "It should be part of a pivot formula",
});
});
test("Reinsert a pivot with new data", async function () {
const { model, env } = await createSpreadsheetWithPivot();
MockServer.env["partner"].create({
active: true,
foo: 25, // <- New value inserted
bar: false,
date: "2016-12-11",
product_id: 41,
probability: 15,
field_with_array_agg: 4,
create_date: "2016-12-11",
tag_ids: [],
});
selectCell(model, "D8");
await doMenuAction(topbarMenuRegistry, reinsertStaticPivotPath, env);
expect(getCellFormula(model, "I8")).toBe(`=PIVOT.HEADER(1,"foo",25)`);
expect(getCellFormula(model, "I10")).toBe(
`=PIVOT.VALUE(1,"probability:avg","bar",FALSE,"foo",25)`
);
});
test("Reinsert a pivot with an updated record", async function () {
const { model, env } = await createSpreadsheetWithPivot();
expect(getCellValue(model, "B1")).toBe(1);
expect(getCellValue(model, "C1")).toBe(2);
expect(getCellValue(model, "D1")).toBe(12);
const partnerRecords = MockServer.env["partner"];
partnerRecords[0].foo = 99;
partnerRecords[1].foo = 99;
// updated measures
partnerRecords[0].probability = 88;
partnerRecords[1].probability = 77;
selectCell(model, "A10");
await doMenuAction(topbarMenuRegistry, reinsertDynamicPivotPath, env);
await animationFrame();
expect(getCellValue(model, "D10")).toBe(99, {
message: "The header should have been updated",
});
expect(getCellValue(model, "D14")).toBe(77 + 88, {
message: "The value should have been updated",
});
});
test("Reinsert an Odoo pivot which has no formula on the sheet (meaning the data is not loaded)", async function () {
const spreadsheetData = {
version: 16,
sheets: [
{
id: "sheet1",
},
],
pivots: {
1: {
id: 1,
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: ["bar"],
context: {},
},
},
};
const serverData = getBasicServerData();
serverData.models["documents.document"].records = [
{
id: 45,
spreadsheet_data: JSON.stringify(spreadsheetData),
name: "Spreadsheet",
handler: "spreadsheet",
},
];
const { model, env } = await createSpreadsheet({
serverData,
spreadsheetId: 45,
});
await doMenuAction(topbarMenuRegistry, reinsertStaticPivotPath, env);
expect(getCellFormula(model, "C1")).toBe(`=PIVOT.HEADER(1,"foo",2)`);
expect(getCellFormula(model, "C2")).toBe(`=PIVOT.HEADER(1,"foo",2,"measure","probability")`);
expect(getCellFormula(model, "C3")).toBe(`=PIVOT.VALUE(1,"probability","bar",FALSE,"foo",2)`);
await animationFrame();
expect(getCellValue(model, "C1")).toBe(2);
expect(getCellValue(model, "C2")).toBe("Probability");
expect(getCellValue(model, "C3")).toBe(15);
});
test("Keep applying filter when pivot is re-inserted", async function () {
const { model, env, pivotId } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="bar" type="col"/>
<field name="product_id" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
pivotType: "static",
});
await addGlobalFilter(
model,
{
id: "42",
type: "relation",
label: "Filter",
},
{
pivot: {
[pivotId]: {
chain: "product_id",
type: "many2one",
},
},
}
);
await animationFrame();
await setGlobalFilterValue(model, {
id: "42",
value: { operator: "in", ids: [41] },
});
await animationFrame();
expect(getCellValue(model, "B3")).toBe("", {
message: "The value should have been filtered",
});
expect(getCellValue(model, "C3")).toBe("", {
message: "The value should have been filtered",
});
await doMenuAction(topbarMenuRegistry, reinsertDynamicPivotPath, env);
await animationFrame();
expect(getCellValue(model, "B3")).toBe("", {
message: "The value should still be filtered",
});
expect(getCellValue(model, "C3")).toBe("", {
message: "The value should still be filtered",
});
});
test("undo pivot reinsert", async function () {
const { model, env } = await createSpreadsheetWithPivot();
selectCell(model, "D8");
await doMenuAction(topbarMenuRegistry, reinsertDynamicPivotPath, env);
expect(getCorrespondingCellFormula(model, "E10")).toBe(`=PIVOT(1)`, {
message: "It should contain a pivot formula",
});
model.dispatch("REQUEST_UNDO");
expect(getCell(model, "E10")).toBe(undefined, {
message: "It should have removed the re-inserted pivot",
});
});
test("reinsert pivot with anchor on merge but not top left", async function () {
const { model, env } = await createSpreadsheetWithPivot({ pivotType: "static" });
const sheetId = model.getters.getActiveSheetId();
const [pivotId] = model.getters.getPivotIds();
const pivotZone = getZoneOfInsertedDataSource(model, "pivot", pivotId);
model.dispatch("REMOVE_TABLE", { sheetId, target: [pivotZone] });
expect(getCellFormula(model, "B2")).toBe(
`=PIVOT.HEADER(1,"foo",1,"measure","probability:avg")`,
{
message: "It should contain a pivot formula",
}
);
model.dispatch("ADD_MERGE", {
sheetId,
target: [{ top: 0, bottom: 1, left: 0, right: 0 }],
force: true,
});
selectCell(model, "A2"); // A1 and A2 are merged; select A2
const { col, row } = toCartesian("A2");
expect(model.getters.isInMerge({ sheetId, col, row })).toBe(true);
await doMenuAction(topbarMenuRegistry, reinsertDynamicPivotPath, env);
expect(getCellFormula(model, "B2")).toBe(
`=PIVOT.HEADER(1,"foo",1,"measure","probability:avg")`,
{
message: "It should contain a pivot formula",
}
);
});
test("Verify presence of pivot properties on pivot cell", async function () {
const { model, env } = await createSpreadsheetWithPivot();
selectCell(model, "B2");
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_properties");
expect(root.isVisible(env)).toBe(true);
});
test("Verify absence of pivot properties on non-pivot cell", async function () {
const { model, env } = await createSpreadsheetWithPivot();
selectCell(model, "Z26");
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_properties");
expect(root.isVisible(env)).toBe(false);
});
test("Verify absence of pivot properties on formula with invalid pivot Id", async function () {
const { model, env } = await createSpreadsheetWithPivot();
setCellContent(model, "A1", `=PIVOT.HEADER("fakeId")`);
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_properties");
expect(root.isVisible(env)).toBe(false);
setCellContent(model, "A1", `=PIVOT.VALUE("fakeId", "probability", "foo", 2)`);
expect(root.isVisible(env)).toBe(false);
});
test("verify absence of pivots in top menu bar in a spreadsheet without a pivot", async function () {
await createSpreadsheet();
expect("div[data-id='pivots']").toHaveCount(0);
});
test("Verify presence of pivots in top menu bar in a spreadsheet with a pivot", async function () {
const { model, env } = await createSpreadsheetFromPivotView();
await insertPivotInSpreadsheet(model, "PIVOT#2", { arch: getBasicPivotArch() });
expect(target.querySelector("div[data-id='data']")).not.toBe(null, {
message: "The 'Pivots' menu should be in the dom",
});
const root = topbarMenuRegistry.getMenuItems().find((item) => item.id === "data");
const childrenNames = root.children(env).map((name) => name.name(env).toString()); // toString to force translation
expect(childrenNames.find((name) => name === "(#1) Partners by Foo")).not.toBe(undefined);
expect(childrenNames.find((name) => name === "(#2) Partner Pivot")).not.toBe(undefined);
// bottom children
expect(childrenNames.find((name) => name === "Refresh all data")).not.toBe(undefined);
expect(childrenNames.find((name) => name === "Re-insert dynamic pivot")).not.toBe(undefined);
expect(childrenNames.find((name) => name === "Re-insert static pivot")).not.toBe(undefined);
expect(childrenNames.find((name) => name === "Re-insert list")).not.toBe(undefined);
});
test("Pivot focus changes on top bar menu click", async function () {
const { model, env } = await createSpreadsheetFromPivotView();
await insertPivotInSpreadsheet(model, "PIVOT#2", { arch: getBasicPivotArch() });
await doMenuAction(topbarMenuRegistry, ["data", "item_pivot_1"], env);
await animationFrame();
expect(".os-pivot-title").toHaveValue("Partners by Foo");
await doMenuAction(topbarMenuRegistry, ["data", "item_pivot_2"], env);
await animationFrame();
expect(".os-pivot-title").toHaveValue("Partner Pivot");
});
test("A warning is displayed in the menu item if the pivot is unused", async function () {
const { model } = await createSpreadsheetFromPivotView();
model.dispatch("CREATE_SHEET", { sheetId: "sh2", name: "Sheet2" });
await insertPivotInSpreadsheet(model, "PIVOT#2", { sheetId: "sh2" });
await contains("div[data-id='data']").click();
const menuItemPivot1 = target.querySelector("div[data-name='item_pivot_1']");
const menuItemPivot2 = target.querySelector("div[data-name='item_pivot_2']");
expect(menuItemPivot1.querySelectorAll(".o-unused-pivot-icon")).toHaveLength(0);
expect(menuItemPivot2.querySelectorAll(".o-unused-pivot-icon")).toHaveLength(0);
model.dispatch("DELETE_SHEET", { sheetId: "sh2" });
await animationFrame();
expect(menuItemPivot1.querySelectorAll(".o-unused-pivot-icon")).toHaveLength(0);
expect(menuItemPivot2.querySelectorAll(".o-unused-pivot-icon")).toHaveLength(1);
});
test("Can rebuild the Odoo domain of records based on the according merged pivot cell", async function () {
const { webClient, model } = await createSpreadsheetFromPivotView();
const env = {
...webClient.env,
model,
services: {
...model.config.custom.env.services,
action: {
doAction: (params) => {
expect.step(params.res_model);
expect.step(params.domain);
},
},
},
};
const sheetId = model.getters.getActiveSheetId();
const [pivotId] = model.getters.getPivotIds();
const pivotZone = getZoneOfInsertedDataSource(model, "pivot", pivotId);
model.dispatch("REMOVE_TABLE", { sheetId, target: [pivotZone] });
model.dispatch("ADD_MERGE", {
sheetId,
target: [toZone("C3:D3")],
force: true, // there are data in D3
});
selectCell(model, "D3");
await animationFrame();
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
await root.execute(env);
expect.verifySteps(["partner", ["&", ["foo", "=", 2], ["bar", "=", false]]]);
});
test("See records is visible even if the formula is lowercase", async function () {
const { env, model } = await createSpreadsheetWithPivot();
selectCell(model, "B4");
await animationFrame();
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
expect(root.isVisible(env)).toBe(true);
setCellContent(model, "B4", getCellFormula(model, "B4").replace("PIVOT.VALUE", "pivot.value"));
expect(root.isVisible(env)).toBe(true);
});
test("See records is not visible if the formula is in error", async function () {
const { env, model } = await createSpreadsheetWithPivot({ pivotType: "static" });
selectCell(model, "B4");
await animationFrame();
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
expect(root.isVisible(env)).toBe(true);
setCellContent(
model,
"B4",
getCellFormula(model, "B4").replace(`PIVOT.VALUE(1`, `PIVOT.VALUE("5)`)
); //Invalid id
expect(getEvaluatedCell(model, "B4").message).not.toBe(undefined);
expect(root.isVisible(env)).toBe(false);
});
test("'See records' loads a specific action if set in the pivot definition", async function () {
const actionXmlId = "spreadsheet.partner_action";
const { webClient, model } = await createSpreadsheetFromPivotView({ 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.domain);
},
},
},
};
selectCell(model, "C3");
await animationFrame();
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
await root.execute(env);
expect.verifySteps(["partner", ["&", ["foo", "=", 2], ["bar", "=", false]]]);
});
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 { env, model } = await createSpreadsheetFromPivotView({
serverData,
additionalContext: { search_default_filter: 1 },
actionXmlId,
});
mockActionService((action) => {
expect.step("loadAction");
expect(action.context).toEqual({ search_default_filter: 1 });
});
selectCell(model, "C2");
await animationFrame();
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
await root.execute(env);
expect.verifySteps(["loadAction"]);
});
test("Pivot cells are highlighted when hovering their menu item", async function () {
const { model, env } = await createSpreadsheetFromPivotView();
const sheetId = model.getters.getActiveSheetId();
await contains(".o-sidePanelClose").click();
await contains(".o-topbar-top div[data-id='data']").click();
await hover("div[data-name='item_pivot_1']");
const pivotId = model.getters.getPivotIds()[0];
const range = model.getters.getRangeFromZone(
sheetId,
getZoneOfInsertedDataSource(model, "pivot", pivotId)
);
expect(getHighlightsFromStore(env)).toEqual([{ color: "#017E84", range, noFill: true }]);
await leave("div[data-name='item_pivot_1");
expect(getHighlightsFromStore(env)).toEqual([]);
});