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 */ ` `; 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*/ ` `, 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 */ ` `; 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([]); });