import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import ExcelJS from "exceljs"; import { afterEach, describe, expect, it } from "vitest"; import type { Catalog, CatalogComponent, FlowNode } from "../types"; import type { RunContext } from "../engine/types"; import { getHandler } from "../engine/registry"; import "./index"; import { EXCEL_COMMON_IDS } from "./excel-common"; const directories: string[] = []; const context = {} as RunContext; const component = {} as CatalogComponent; const node = (componentId: string): FlowNode => ({ id: `excel_common_${componentId}`, componentId, position: { x: 0, y: 0 }, values: {}, options: {} }); async function temporaryWorkbook(): Promise { const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-common-")); directories.push(directory); const filePath = path.join(directory, "common.xlsx"); const workbook = new ExcelJS.Workbook(); const sheet = workbook.addWorksheet("Data"); sheet.addRows([ ["name", "score", "group", "parts"], ["Bob", 2, "B", "x,y"], ["Alice", 3, "A", "m,n"], ["Bob", 2, "B", "p,q"] ]); await workbook.xlsx.writeFile(filePath); return filePath; } async function run(componentId: string, args: Record): Promise { const handler = getHandler(componentId); expect(handler, `${componentId} should be registered`).toBeTruthy(); return handler!(context, node(componentId), args, component); } async function readWorkbook(filePath: string): Promise { const workbook = new ExcelJS.Workbook(); await workbook.xlsx.readFile(filePath); return workbook; } afterEach(async () => { await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); }); describe("common Excel capabilities", () => { it("registers at least 25 real catalog IDs without conflicting with existing modules", async () => { const catalog = JSON.parse(await readFile("resources/catalog.json", "utf8")) as Catalog; const catalogIds = new Set(catalog.components.map((item) => item.id)); expect(EXCEL_COMMON_IDS.length).toBeGreaterThanOrEqual(25); for (const id of EXCEL_COMMON_IDS) { expect(catalogIds.has(id), `${id} should exist in catalog.json`).toBe(true); expect(getHandler(id), `${id} should be registered`).toBeTruthy(); } const sourceFiles = (await readdir("src/main/capabilities")) .filter((file) => file.endsWith(".ts") && !file.endsWith(".test.ts") && file !== "excel-common.ts"); const existingSource = (await Promise.all(sourceFiles.map((file) => readFile(path.join("src/main/capabilities", file), "utf8")))).join("\n"); for (const id of EXCEL_COMMON_IDS) expect(existingSource.includes(`"${id}"`), `${id} must not already be registered`).toBe(false); }); it("writes and reads rows, columns, and ranges in a real workbook", async () => { const filePath = await temporaryWorkbook(); await run("Excel_Cs_WriteRow", { workbook: filePath, sheet: "Data", row: 6, column: "A", contents: ["Carol", 9, "C"], save: 1 }); await run("Excel_Cs_WriteColumn", { workbook: filePath, sheet: "Data", row: 2, column: "E", contents: [10, 20, 30], save: 1 }); await run("Excel_Cs_WriteRangeV2", { workbook: filePath, sheet: "Data", row: 8, column: "A", contents: [["x", 1], ["y", 2]], save: 1 }); await expect(run("Excel_Cs_ReadRow", { workbook: filePath, sheet: "Data", rowIndex: 6 })).resolves.toEqual(["Carol", 9, "C", "", ""]); await expect(run("Excel_GetColData", { objExcel: filePath, sheet: "Data", cellPos: "E2" })).resolves.toEqual([10, 20, 30, "", "", "", "", ""]); await expect(run("Excel_Cs_ReadRange", { workbook: filePath, sheet: "Data", startRow: 8, startColumn: "A", lastRow: 9, lastColumn: "B" })) .resolves.toEqual([["x", 1], ["y", 2]]); await expect(run("Excel_Cs_ReadColumnCount", { workbook: filePath, sheet: "Data", row: 2 })).resolves.toBe(5); }); it("finds, replaces, sorts, filters, clears filters, and removes duplicates", async () => { const filePath = await temporaryWorkbook(); await expect(run("Excel_Cs_Find", { workbook: filePath, sheet: "Data", what: "Alice", matchFirst: 1 })) .resolves.toEqual(["A3"]); await run("Excel_Cs_Search", { workbook: filePath, sheet: "Data", mode: "替换", input: "Alice", replace: "Alicia", wholeWord: 1, save: 1 }); await run("Excel_Cs_Sort", { workbook: filePath, sheet: "Data", sortRange: "A1:D4", sortOrder: "2", header: 1, save: 1 }); await run("Excel_Cs_Filter", { workbook: filePath, sheet: "Data", column: "C", criteria: "B", condition: "equal", startRow: 1, endRow: 4, onHeader: 1, save: 1 }); let workbook = await readWorkbook(filePath); let sheet = workbook.getWorksheet("Data")!; expect(sheet.getCell("A2").value).toBe("Bob"); expect(sheet.getRow(3).hidden || sheet.getRow(4).hidden).toBe(true); await run("Excel_Cs_ClearExcelFilter", { workbook: filePath, sheet: "Data", save: 1 }); await run("Excel_Cs_DeleteDuplicates", { workbook: filePath, sheet: "Data", columns: ["A", "B", "C"], header: 1, save: 1 }); workbook = await readWorkbook(filePath); sheet = workbook.getWorksheet("Data")!; expect(sheet.getRows(1, sheet.rowCount)!.some((row) => row.hidden)).toBe(false); expect(sheet.rowCount).toBe(3); expect(sheet.getColumn("A").values).toContain("Alicia"); }); it("copies, pastes, fills, splits, and deletes real ranges", async () => { const filePath = await temporaryWorkbook(); await run("Excel_CopyRange", { objExcel: filePath, sheet: "Data", cellPosRange: "A2:B3" }); await run("Excel_Cs_PastRange", { workbook: filePath, sheet: "Data", column: "F", row: 2, save: 1 }); await run("Excel_Cs_AutoFill", { workbook: filePath, sheet: "Data", sourceRange: "B2:B3", destination: "B2:B6", save: 1 }); await run("Excel_Cs_ColumnSplitV2", { workbook: filePath, sheet: "Data", column: "D", destination: "J1", comma: 1, tab: 0, startRow: 2, endRow: 4, save: 1 }); await run("Excel_Cs_DeleteRangeV3", { workbook: filePath, sheet: "Data", excelRng: "C2:C4", save: 1 }); const workbook = await readWorkbook(filePath); const sheet = workbook.getWorksheet("Data")!; expect(sheet.getCell("F2").value).toBe("Bob"); expect(sheet.getCell("G3").value).toBe(3); expect(sheet.getCell("B6").value).toBe(6); expect(sheet.getCell("J2").value).toBe("x"); expect(sheet.getCell("K2").value).toBe("y"); expect(sheet.getCell("C2").value).toBeNull(); }); it("manages sheets and persists active sheet state", async () => { const filePath = await temporaryWorkbook(); await run("Excel_CreateSheet", { objExcel: filePath, sheet: "Report", isSave: true }); await expect(run("Excel_Cs_GetName", { workbook: filePath })).resolves.toEqual(["Data", "Report"]); await expect(run("Excel_Cs_Active", { workbook: filePath, mode: "按名称", sheet: "Report", save: 1 })).resolves.toBe("Report"); await run("Excel_DeleteSheet", { objExcel: filePath, sheet: "Data", isSave: true }); await run("Excel_Cs_Close", { workbook: filePath, save: 1 }); const workbook = await readWorkbook(filePath); expect(workbook.worksheets.map((sheet) => sheet.name)).toEqual(["Report"]); }); it("sets and reads cell and range colors", async () => { const filePath = await temporaryWorkbook(); await run("Excel_Cs_SetCellBackground", { workbook: filePath, sheet: "Data", column: "A", row: 2, color: "#12AB34", save: 1 }); await run("Excel_Cs_SetRangeFontV2", { workbook: filePath, sheet: "Data", range: "A2:B3", color: "#445566", save: 1 }); await expect(run("Excel_Cs_ReadCellBackground", { workbook: filePath, sheet: "Data", column: "A", row: 2 })).resolves.toBe("FF12AB34"); const workbook = await readWorkbook(filePath); const sheet = workbook.getWorksheet("Data")!; expect((sheet.getCell("A2").fill as ExcelJS.FillPattern).fgColor?.argb).toBe("FF12AB34"); expect(sheet.getCell("B3").font.color?.argb).toBe("FF445566"); }); });