PRA-only-demo/src/main/capabilities/excel-store.test.ts
yu 9de0eeaa88 fix: 统一 Excel 工作簿共享存储,修复多组件互相覆盖
office.ts(内存句柄表)、office-file-extended.ts(每次重读磁盘)、
excel-common.ts(按路径独立缓存)、extended.ts(loadSheet 每次重建)
各自持有工作簿实例并整簿写回同一文件,后保存者会整体覆盖先保存者:
列宽/行高/合并被背景色冲掉、复制的 Sheet 被新建 Sheet 冲掉等。

- 新增 excel-store.ts:按路径共享同一 ExcelJS 实例,四个模块统一接入
- resolveWorkbook 返回流程变量持有的原始句柄对象,另存为改写 path
  能同步到后续节点(此前改在副本上,下游仍写旧路径)
- 另存为后以新路径重新登记实例并释放旧路径缓存
- ReadColumn 裁掉尾部空值(共享实例下触碰空行会撑大 rowCount)
- 新增 3 个共享存储回归测试(格式保持/复制Sheet保持/另存后续写)
2026-10-28 13:32:01 +08:00

69 lines
4.5 KiB
TypeScript

import { mkdtemp, 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 { CatalogComponent, FlowNode } from "../../shared/types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./office";
import "./extended";
import "./office-file-extended";
import "./office-aliases";
import "./excel-common";
import "./common-final";
const dirs: string[] = [];
const context = {} as RunContext;
const component = {} as CatalogComponent;
const node = (id: string): FlowNode => ({ id, componentId: id, position: { x: 0, y: 0 }, values: {}, options: {} });
async function run(id: string, args: Record<string, unknown>) { const handler = getHandler(id); expect(handler, id).toBeTruthy(); return handler!(context, node(id), args, component); }
afterEach(async () => Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))));
describe("excel shared workbook store", () => {
it("keeps widths/heights set by office-file-extended when excel-common saves afterwards", async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-store-")); dirs.push(directory);
const handle = await run("Excel_NewExcel", { filePath: directory, name: "a.xlsx", isMakedirs: "True" }) as { path: string };
await run("Excel_Cs_WriteRangeV2", { workbook: handle, sheet: "Sheet1", row: "1", column: "A", contents: [["名称", "数量", "备注"], ["鼠标", 20, "A;B"]], isSave: "True" });
await run("Excel_Cs_SetWidth", { workbook: handle, sheet: "Sheet1", columns: "A:C", width: "20", save: "True" });
await run("Excel_Cs_SetHeight", { workbook: handle, sheet: "Sheet1", rows: "1", height: "26", save: "True" });
await run("Excel_Cs_MergeCells", { workbook: handle, sheet: "Sheet1", range: "A1:C1", merge: "0", save: "True" });
await run("Excel_Cs_SetRangeBackground", { workbook: handle, sheet: "Sheet1", range: "A1:C1", color: "FFF2CC", save: "True" });
await run("Excel_SetCellFontColor", { objExcel: handle, sheet: "Sheet1", cellPos: "A1", cellColor: "FF0000", isSave: "True" });
const wb = new ExcelJS.Workbook();
await wb.xlsx.readFile(handle.path);
const ws = wb.worksheets[0];
expect(ws.getColumn("A").width).toBe(20);
expect(ws.getRow(1).height).toBe(26);
expect(ws.model.merges).toContain("A1:C1");
expect((ws.getCell("A1").fill as ExcelJS.FillPattern).fgColor?.argb).toBe("FFFFF2CC");
expect((ws.getCell("A1").font?.color as { argb?: string } | undefined)?.argb).toBe("FFFF0000");
expect(ws.getCell("A1").value).toBe("名称");
});
it("keeps copied sheets created by office-file-extended when sheets are created and deleted later", async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-store-")); dirs.push(directory);
const handle = await run("Excel_NewExcel", { filePath: directory, name: "b.xlsx", isMakedirs: "True" }) as { path: string };
await run("Excel_Cs_WriteRangeV2", { workbook: handle, sheet: "Sheet1", row: "1", column: "A", contents: [["名称"]], isSave: "True" });
await run("Excel_Cs_CopyV2", { workbook: handle, sheet: "Sheet1", newSheet: "Sheet1副本", override: "True", save: "True" });
await run("Excel_CreateSheet", { workbook: handle, sheet: "临时表", save: "True" });
await run("Excel_DeleteSheet", { workbook: handle, sheet: "临时表", save: "True" });
const wb = new ExcelJS.Workbook();
await wb.xlsx.readFile(handle.path);
expect(wb.worksheets.map((sheet) => sheet.name)).toEqual(["Sheet1", "Sheet1副本"]);
});
it("keeps the shared instance usable after save-as switches the handle path", async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-store-")); dirs.push(directory);
const handle = await run("Excel_NewExcel", { filePath: directory, name: "c.xlsx", isMakedirs: "True" }) as { path: string };
await run("Excel_Cs_SetWidth", { workbook: handle, sheet: "Sheet1", columns: "A", width: "18", save: "True" });
const saved = await run("Excel_Cs_SaveAs", { workbook: handle, path: path.join(directory, "d.xlsx") }) as { path: string };
expect(saved.path).toBe(path.resolve(directory, "d.xlsx"));
await run("Excel_Cs_SetHeight", { workbook: handle, sheet: "Sheet1", rows: "1", height: "30", save: "True" });
const wb = new ExcelJS.Workbook();
await wb.xlsx.readFile(path.resolve(directory, "d.xlsx"));
expect(wb.worksheets[0].getColumn("A").width).toBe(18);
expect(wb.worksheets[0].getRow(1).height).toBe(30);
});
});