PRA-only-demo/src/main/capabilities/excel-common.test.ts
2026-10-27 16:29:52 +08:00

148 lines
7.9 KiB
TypeScript

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 "../../shared/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<string> {
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<string, unknown>): Promise<unknown> {
const handler = getHandler(componentId);
expect(handler, `${componentId} should be registered`).toBeTruthy();
return handler!(context, node(componentId), args, component);
}
async function readWorkbook(filePath: string): Promise<ExcelJS.Workbook> {
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");
});
});