修正 PID 空结果、目标类型判断与转换、毫秒等待。 让关闭程序和命令行按所选模式显示并校验参数。 统一 PowerShell 输出编码,修正 BAT 路径引号处理。 明确屏保和屏幕唤醒的实际能力,补充缺陷回归测试。 验证:类型检查、构建与组件回归检查通过。 BREAKING CHANGE: 判断数据类型改为返回布尔值。 需要类型名称的旧流程请改用 Basic_Type。 屏保 V1 移除未实现参数,旧自定义密码节点需重新添加。
131 lines
7.3 KiB
TypeScript
131 lines
7.3 KiB
TypeScript
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import type { Catalog, CatalogComponent, FlowDefinition, FlowNode } from "../../shared/types";
|
|
import type { RunContext } from "../engine/types";
|
|
import { getHandler } from "../engine/registry";
|
|
import "./utility-extended";
|
|
|
|
const ids = [
|
|
"Array_Reshape2Dim", "Time_GetDatetimeObj", "Time_TimeTypeConversion_V1", "Basic_SetVariable_V1",
|
|
"Basic_GetProcessInfo", "System_ExecuteCommandV1", "DataService_Diff", "DataService_WorkingDay",
|
|
"Databook_Clear", "Databook_GetRowAndColCount", "Databook_WriteContent", "Databook_ReadContent",
|
|
"Databook_DeleteRow", "Databook_DeleteColumn"
|
|
] as const;
|
|
|
|
const directories: string[] = [];
|
|
const flow: FlowDefinition = {
|
|
schemaVersion: 1, id: "flow-1", name: "本地工具测试", description: "utility",
|
|
variables: [], nodes: [], edges: [], meta: { createdAt: "2026-10-27", updatedAt: "2026-10-27", appVersion: "0.1.0" }
|
|
};
|
|
const context = { vars: {}, flow, runId: "run-1" } as unknown as RunContext;
|
|
const component = {} as CatalogComponent;
|
|
const node = (id: string, outputVar?: string): FlowNode => ({
|
|
id: `utility_${id}`, componentId: id, position: { x: 0, y: 0 }, values: {}, options: outputVar ? { outputVar } : {}
|
|
});
|
|
|
|
async function run(id: string, args: Record<string, unknown> = {}, outputVar?: string): Promise<unknown> {
|
|
const handler = getHandler(id);
|
|
expect(handler, `${id} should be registered`).toBeTruthy();
|
|
return handler!(context, node(id, outputVar), args, component);
|
|
}
|
|
|
|
afterEach(async () => {
|
|
context.vars = {};
|
|
await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
|
|
});
|
|
|
|
describe("utility extended capabilities", () => {
|
|
it("registers only real catalog component ids", async () => {
|
|
const catalog = JSON.parse(await readFile("resources/catalog.json", "utf8")) as Catalog;
|
|
const catalogIds = new Set(catalog.components.map((item) => item.id));
|
|
expect(ids.length).toBeGreaterThanOrEqual(10);
|
|
for (const id of ids) {
|
|
expect(catalogIds.has(id), `${id} should exist in catalog`).toBe(true);
|
|
expect(getHandler(id), `${id} should be registered`).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
it("reshapes arrays and rejects incompatible dimensions", async () => {
|
|
await expect(run("Array_Reshape2Dim", { arr: "[1,2,3,4,5,6]", x: 2, y: 3 }))
|
|
.resolves.toEqual([[1, 2, 3], [4, 5, 6]]);
|
|
await expect(run("Array_Reshape2Dim", { arr: [1, 2, 3], x: 2, y: 2 }))
|
|
.rejects.toMatchObject({ code: "validation_error" });
|
|
});
|
|
|
|
it("returns datetime details and formats Python-style patterns", async () => {
|
|
const detail = await run("Time_GetDatetimeObj", { date_input: "2026-10-27 08:09:10" }) as Record<string, unknown>;
|
|
expect(detail).toMatchObject({ year: 2026, month: 10, day: 27, hour: 8, minute: 9, second: 10 });
|
|
await expect(run("Time_TimeTypeConversion_V1", { datetime: "2026-10-27 08:09:10", get_type: "%Y/%m/%d %H:%M:%S" }))
|
|
.resolves.toBe("2026/10/27 08:09:10");
|
|
await expect(run("Time_TimeTypeConversion_V1", { datetime: "12:34", get_type: "%H:%M:%S" }))
|
|
.resolves.toBe("12:34:00");
|
|
await expect(run("Time_TimeTypeConversion_V1", { datetime: "2026-02-30" }))
|
|
.rejects.toMatchObject({ code: "validation_error" });
|
|
});
|
|
|
|
it("converts variable types and exposes current flow information", async () => {
|
|
await expect(run("Basic_SetVariable_V1", { variableType: "rpa_int", value: "42" }, "answer")).resolves.toBe(42);
|
|
expect(context.vars.answer).toBe(42);
|
|
await expect(run("Basic_SetVariable_V1", { variableType: "rpa_list", value: "[1,2]" })).resolves.toEqual([1, 2]);
|
|
await expect(run("Basic_GetProcessInfo")).resolves.toMatchObject({ id: "flow-1", name: "本地工具测试", runId: "run-1" });
|
|
});
|
|
|
|
it("executes a local command and returns stdout", async () => {
|
|
const args = process.platform === "win32"
|
|
? { command_type: "1", dos_command: "echo utility-ok", get_ret: true }
|
|
: { command_type: "1", dos_command: "printf utility-ok", get_ret: true };
|
|
await expect(run("System_ExecuteCommandV1", args)).resolves.toContain("utility-ok");
|
|
});
|
|
|
|
it("executes BAT files whose path contains spaces and Chinese characters", async () => {
|
|
if (process.platform !== "win32") return;
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-utility-中文 空格-"));
|
|
directories.push(directory);
|
|
const file = path.join(directory, "测试 脚本.bat");
|
|
await writeFile(file, "@echo RPA-BAT-PATH-OK\r\n", "ascii");
|
|
await expect(run("System_ExecuteCommandV1", { command_type: "2", bat_file: file, get_ret: "1" })).resolves.toBe("RPA-BAT-PATH-OK");
|
|
});
|
|
|
|
it("keeps PowerShell command output in UTF-8", async () => {
|
|
if (process.platform !== "win32") return;
|
|
await expect(run("System_ExecuteCommandV1", { command_type: "3", ps_command: "Write-Output '中文输出测试'", get_ret: "1" })).resolves.toBe("中文输出测试");
|
|
});
|
|
|
|
it("compares local text files with structured line changes", async () => {
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-utility-"));
|
|
directories.push(directory);
|
|
const source = path.join(directory, "source.txt");
|
|
const target = path.join(directory, "target.txt");
|
|
await writeFile(source, "A\nB\nC", "utf8");
|
|
await writeFile(target, "A\nD\nC", "utf8");
|
|
const result = await run("DataService_Diff", { sourceFilePath: source, targetFilePath: target, showTable: true }) as Record<string, unknown>;
|
|
expect(result).toMatchObject({ identical: false, addedCount: 1, removedCount: 1, sourceLineCount: 3, targetLineCount: 3 });
|
|
});
|
|
|
|
it("evaluates weekends and local holiday/workday overrides", async () => {
|
|
await expect(run("DataService_WorkingDay", { day: "2026-10-27" })).resolves.toBe(true);
|
|
await expect(run("DataService_WorkingDay", { day: "2026-10-31" })).resolves.toBe(false);
|
|
await expect(run("DataService_WorkingDay", { day: "2026-10-31", working_cal: '{"workdays":["2026-10-31"]}' })).resolves.toBe(true);
|
|
await expect(run("DataService_WorkingDay", { day: "2026-10-27", working_cal: '["2026-10-27"]' })).resolves.toBe(false);
|
|
});
|
|
|
|
it("writes, reads, counts, and clears the in-run databook", async () => {
|
|
await run("Databook_WriteContent", { write_type: "region", column_name: "A", row_num: 1, content: [["name", "score"], ["Alice", 90], ["Bob", 80]] });
|
|
await expect(run("Databook_ReadContent", { read_type: "cell", column_name: "B", row_num: 2 })).resolves.toBe(90);
|
|
await expect(run("Databook_ReadContent", { read_type: "column", column_name: "A" })).resolves.toEqual(["name", "Alice", "Bob"]);
|
|
await expect(run("Databook_ReadContent", { read_type: "region", column_name: "A", row_num: 2, end_column_name: "B", end_row_num: 3 }))
|
|
.resolves.toEqual([["Alice", 90], ["Bob", 80]]);
|
|
await expect(run("Databook_GetRowAndColCount")).resolves.toMatchObject({ rowCount: 3, columnCount: 2 });
|
|
await expect(run("Databook_Clear")).resolves.toEqual([]);
|
|
});
|
|
|
|
it("deletes selected databook rows and columns", async () => {
|
|
context.vars.__rpaDatabook = [["A", "B", "C"], [1, 2, 3], [4, 5, 6]];
|
|
await run("Databook_DeleteRow", { row_nums: [2] });
|
|
await run("Databook_DeleteColumn", { column_names: ["B"] });
|
|
expect(context.vars.__rpaDatabook).toEqual([["A", "C"], [4, 6]]);
|
|
});
|
|
});
|