152 lines
8.2 KiB
TypeScript
152 lines
8.2 KiB
TypeScript
import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
|
||
import { existsSync } from "node:fs";
|
||
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 "../../src/types";
|
||
import type { RunContext } from "../../src/engine/types";
|
||
import { getHandler } from "../../src/engine/registry";
|
||
import "../../src/capabilities/index";
|
||
import { EXCEL_COMMON_IDS } from "../../src/capabilities/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();
|
||
}
|
||
|
||
// 宿主冲突检查是 demo 仓库专有关注点:独立宿主(如对面 backend)无 src/main/capabilities,跳过即可
|
||
if (existsSync("src/main/capabilities")) {
|
||
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");
|
||
});
|
||
});
|