import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import net from "node:net"; import { describe, expect, it, afterEach } from "vitest"; import { PDFDocument } from "pdf-lib"; import { PNG } from "pngjs"; import { getHandler } from "../engine/registry"; import type { RunContext } from "../engine/types"; import type { CatalogComponent, FlowNode } from "../../shared/types"; import "./desktop"; import "./office"; import "./pdf"; import "./message"; import "./word"; import { findImage } from "./vision"; const tempDirs: string[] = []; const node = (componentId: string): FlowNode => ({ id: `smoke_${componentId}`, componentId, position: { x: 0, y: 0 }, values: {}, options: {} }); const component = {} as CatalogComponent; const context = {} as RunContext; async function tempDir(): Promise { const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-capability-smoke-")); tempDirs.push(directory); return directory; } afterEach(async () => { await Promise.all(tempDirs.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); }); describe("capability registration smoke", () => { it("creates, writes, reads, and saves an Excel workbook", async () => { const directory = await tempDir(); const create = getHandler("Excel_Cs_NewV5"); const write = getHandler("Excel_Cs_WriteCell"); const read = getHandler("Excel_Cs_ReadRangeV2"); const insert = getHandler("Excel_Cs_Insert"); const sheets = getHandler("Excel_GetSheetsName"); expect(create && write && read && insert && sheets).toBeTruthy(); const handle = await create!(context, node("Excel_Cs_NewV5"), { path: directory, name: "smoke.xlsx", override: "1" }, component); await write!(context, node("Excel_Cs_WriteCell"), { workbook: handle, sheet: "Sheet1", row: "1", column: "A", content: "RPA", save: "1" }, component); const table = await read!(context, node("Excel_Cs_ReadRangeV2"), { workbook: handle, sheet: "Sheet1", range: "A1:A1" }, component); await insert!(context, node("Excel_Cs_Insert"), { workbook: handle, name: "结果", save: "1" }, component); expect(table).toEqual([["RPA"]]); await expect(sheets!(context, node("Excel_GetSheetsName"), { workbook: handle }, component)).resolves.toEqual(["Sheet1", "结果"]); await expect(readFile(path.join(directory, "smoke.xlsx"))).resolves.toBeTruthy(); }); it("counts and concatenates PDF files", async () => { const directory = await tempDir(); const first = await PDFDocument.create(); first.addPage([120, 120]); const second = await PDFDocument.create(); second.addPage([120, 120]); const firstPath = path.join(directory, "one.pdf"); const secondPath = path.join(directory, "two.pdf"); await writeFile(firstPath, await first.save()); await writeFile(secondPath, await second.save()); const count = getHandler("PDF_GetPdfPageNum"); const concat = getHandler("PDF_ConcatPdf"); expect(count && concat).toBeTruthy(); await expect(count!(context, node("PDF_GetPdfPageNum"), { fileInPath: firstPath }, component)).resolves.toBe(1); const output = await concat!(context, node("PDF_ConcatPdf"), { newPdfPath: directory, newPdfName: "merged.pdf", pdfList: JSON.stringify([firstPath, secondPath]) }, component); expect(output).toBe(path.join(directory, "merged.pdf")); const merged = await PDFDocument.load(await readFile(output as string)); expect(merged.getPageCount()).toBe(2); }); it("executes a local desktop command through the registered bridge", async () => { const execute = getHandler("System_ExecuteCommand"); expect(execute).toBeTruthy(); await expect(execute!(context, node("System_ExecuteCommand"), { action: "Write-Output smoke" }, component)).resolves.toContain("smoke"); await expect(execute!(context, node("System_ExecuteCommand"), { action: "Write-Output '中文输出测试'" }, component)).resolves.toBe("中文输出测试"); }); it("finds an exact PNG template inside a larger screenshot", async () => { const directory = await tempDir(); const screen = new PNG({ width: 48, height: 36 }); const template = new PNG({ width: 8, height: 8 }); screen.data.fill(255); for (let y = 0; y < template.height; y += 1) { for (let x = 0; x < template.width; x += 1) { const templateIndex = (y * template.width + x) * 4; template.data[templateIndex] = 220; template.data[templateIndex + 1] = 20; template.data[templateIndex + 2] = 30; template.data[templateIndex + 3] = 255; const screenIndex = ((12 + y) * screen.width + 17 + x) * 4; template.data.copy(screen.data, screenIndex, templateIndex, templateIndex + 4); } } const screenPath = path.join(directory, "screen.png"); const templatePath = path.join(directory, "template.png"); await writeFile(screenPath, PNG.sync.write(screen)); await writeFile(templatePath, PNG.sync.write(template)); const match = await findImage(templatePath, screenPath, 0.02); expect(match).toMatchObject({ x: 17, y: 12, width: 8, height: 8, score: 1 }); }); it("sends mail to a local SMTP server", async () => { let received = ""; const server = net.createServer((socket) => { socket.setEncoding("utf8"); socket.write("220 localhost ESMTP\r\n"); let buffer = ""; let dataMode = false; socket.on("data", (chunk) => { buffer += chunk; const lines = buffer.split("\r\n"); buffer = lines.pop() || ""; for (const line of lines) { if (dataMode) { if (line === ".") { dataMode = false; socket.write("250 queued\r\n"); } else received += `${line}\n`; } else if (/^EHLO/i.test(line)) socket.write("250-localhost\r\n250 OK\r\n"); else if (/^DATA/i.test(line)) { dataMode = true; socket.write("354 end with dot\r\n"); } else if (/^QUIT/i.test(line)) { socket.write("221 bye\r\n"); socket.end(); } else socket.write("250 OK\r\n"); } }); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; try { const send = getHandler("Mail_Send"); expect(send).toBeTruthy(); await send!(context, node("Mail_Send"), { host: "127.0.0.1", port: String(port), account: "robot@example.com", to: "owner@example.com", subject: "RPA smoke", content: "mail delivered", is_ssl: "False" }, component); expect(received).toContain("RPA smoke"); expect(received).toContain("mail delivered"); const union = getHandler("Mail_UnionSend_v1"); expect(union).toBeTruthy(); await union!(context, node("Mail_UnionSend_v1"), { account_obj: { host: "127.0.0.1", port, account: "robot@example.com" }, receiver: "owner@example.com", subject: "Union smoke", content: "union delivered" }, component); expect(received).toContain("Union smoke"); expect(received).toContain("union delivered"); } finally { await new Promise((resolve) => server.close(() => resolve())); } }); it("creates and writes a Word document through COM", async () => { if (process.platform !== "win32") return; const directory = await tempDir(); const create = getHandler("Word_New"); const write = getHandler("Word_Write"); expect(create && write).toBeTruthy(); const handle = await create!(context, node("Word_New"), { filePath: directory, name: "smoke.docx" }, component); await write!(context, node("Word_Write"), { objWord: handle, text: "RPA Word smoke" }, component); const file = await readFile(path.join(directory, "smoke.docx")); expect(file.byteLength).toBeGreaterThan(1000); }, 30000); });