PRA-only-demo/packages/rpa-core/test/capabilities/capabilities.test.ts

172 lines
7.9 KiB
TypeScript

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 "../../src/engine/registry";
import type { RunContext } from "../../src/engine/types";
import type { CatalogComponent, FlowNode } from "../../src/types";
import "../../src/capabilities/desktop";
import "../../src/capabilities/office";
import "../../src/capabilities/pdf";
import "../../src/capabilities/message";
import "../../src/capabilities/word";
import { findImage } from "../../src/capabilities/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<string> {
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<void>((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<void>((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);
});