172 lines
7.9 KiB
TypeScript
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);
|
|
});
|