133 lines
7.2 KiB
TypeScript
133 lines
7.2 KiB
TypeScript
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { Catalog, CatalogComponent, FlowNode } from "../../src/types";
|
|
import type { RunContext } from "../../src/engine/types";
|
|
import { getHandler } from "../../src/engine/registry";
|
|
import "../../src/capabilities/data-extended";
|
|
|
|
const ids = [
|
|
"Collection_Create", "Collection_HasItem", "Collection_Count", "Collection_Remove", "Collection_Add",
|
|
"Ini_Read", "Ini_Write", "Ini_ListSection", "Ini_ListKey", "Ini_DeleteKey", "Ini_DeleteSection",
|
|
"Log_Info", "Log_Error", "ExceptionUtil_Throws",
|
|
"Bivartable_CreateDf", "Bivartable_CreateDfV1", "Bivartable_DoCreate",
|
|
"Bivartable_GetMaxValue", "Bivartable_GetMinValue", "Bivartable_GetSumValue", "Bivartable_GetAvgValue",
|
|
"Bivartable_SummaryCalculationDf", "Bivartable_DeleteDuplicateRow", "Bivartable_MergeDf",
|
|
"Bivartable_DoMerge", "Bivartable_FilterDf", "Bivartable_DoFilter"
|
|
] as const;
|
|
|
|
const temporaryDirectories: string[] = [];
|
|
const log = vi.fn();
|
|
const context = { log } as unknown as RunContext;
|
|
const component = {} as CatalogComponent;
|
|
const node = (componentId: string): FlowNode => ({
|
|
id: `data_${componentId}`,
|
|
componentId,
|
|
position: { x: 0, y: 0 },
|
|
values: {},
|
|
options: {}
|
|
});
|
|
|
|
async function temporaryDirectory(): Promise<string> {
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-data-extended-"));
|
|
temporaryDirectories.push(directory);
|
|
return directory;
|
|
}
|
|
|
|
async function run(componentId: string, args: Record<string, unknown> = {}): Promise<unknown> {
|
|
const handler = getHandler(componentId);
|
|
expect(handler, `${componentId} should be registered`).toBeTruthy();
|
|
return handler!(context, node(componentId), args, component);
|
|
}
|
|
|
|
afterEach(async () => {
|
|
log.mockReset();
|
|
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
|
|
});
|
|
|
|
describe("extended local data 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(15);
|
|
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("creates and mutates unique local collections", async () => {
|
|
const collection = await run("Collection_Create") as unknown[];
|
|
await run("Collection_Add", { collection, value: "A" });
|
|
await run("Collection_Add", { collection, value: "A" });
|
|
await run("Collection_Add", { collection, value: "B" });
|
|
await expect(run("Collection_HasItem", { collection, item: "A" })).resolves.toBe(true);
|
|
await expect(run("Collection_Count", { collection })).resolves.toBe(2);
|
|
await run("Collection_Remove", { collection, value: "A" });
|
|
expect(collection).toEqual(["B"]);
|
|
});
|
|
|
|
it("persists, lists, reads, and deletes INI values", async () => {
|
|
const directory = await temporaryDirectory();
|
|
const file = path.join(directory, "nested", "settings.ini");
|
|
await run("Ini_Write", { file, section: "App", key: "name", value: "RPA Studio" });
|
|
await run("Ini_Write", { file, section: "App", key: "workers", value: 3 });
|
|
await run("Ini_Write", { file, section: "Network", key: "timeout", value: 30 });
|
|
await expect(run("Ini_Read", { file, section: "App", key: "name" })).resolves.toBe("RPA Studio");
|
|
await expect(run("Ini_Read", { file, section: "Missing", key: "key", defaultValue: "fallback" })).resolves.toBe("fallback");
|
|
await expect(run("Ini_ListSection", { file })).resolves.toEqual(["App", "Network"]);
|
|
await expect(run("Ini_ListKey", { file, section: "App" })).resolves.toEqual(["name", "workers"]);
|
|
await run("Ini_DeleteKey", { file, section: "App", key: "workers" });
|
|
await run("Ini_DeleteSection", { file, section: "Network" });
|
|
await expect(run("Ini_ListKey", { file, section: "App" })).resolves.toEqual(["name"]);
|
|
await expect(run("Ini_ListSection", { file })).resolves.toEqual(["App"]);
|
|
await expect(readFile(file, "utf8")).resolves.toContain("name=RPA Studio");
|
|
});
|
|
|
|
it("writes runtime logs and raises a located user exception", async () => {
|
|
await run("Log_Info", { msg: "started" });
|
|
await run("Log_Error", { msg: "failed" });
|
|
expect(log).toHaveBeenNthCalledWith(1, "started", "info", "data_Log_Info");
|
|
expect(log).toHaveBeenNthCalledWith(2, "failed", "error", "data_Log_Error");
|
|
await expect(run("ExceptionUtil_Throws", { msg: "stop now" })).rejects.toMatchObject({
|
|
name: "RuntimeError", code: "user_exception", message: "stop now", nodeId: "data_ExceptionUtil_Throws"
|
|
});
|
|
});
|
|
|
|
it("creates tables from arrays, records, and column dictionaries", async () => {
|
|
await expect(run("Bivartable_CreateDf", { data: [[1, 2], [3, 4]], columnList: ["A", "B"] }))
|
|
.resolves.toEqual([["A", "B"], [1, 2], [3, 4]]);
|
|
await expect(run("Bivartable_CreateDfV1", { data: [{ name: "A", score: 2 }, { name: "B", score: 4 }] }))
|
|
.resolves.toEqual([["name", "score"], ["A", 2], ["B", 4]]);
|
|
await expect(run("Bivartable_DoCreate", { data: { name: ["A", "B"], score: [2, 4] } }))
|
|
.resolves.toEqual([["name", "score"], ["A", 2], ["B", 4]]);
|
|
});
|
|
|
|
it("calculates scalar and grouped table aggregates", async () => {
|
|
const input = [["team", "score"], ["A", 2], ["A", 4], ["B", 3]];
|
|
await expect(run("Bivartable_GetMaxValue", { df: input, maxvaluecols: "score" })).resolves.toBe(4);
|
|
await expect(run("Bivartable_GetMinValue", { df: input, minvaluecols: "score" })).resolves.toBe(2);
|
|
await expect(run("Bivartable_GetSumValue", { df: input, sumvaluecols: "score" })).resolves.toBe(9);
|
|
await expect(run("Bivartable_GetAvgValue", { df: input, avgvaluecols: "score", groupby: "team" }))
|
|
.resolves.toEqual([["team", "score"], ["A", 3], ["B", 3]]);
|
|
await expect(run("Bivartable_SummaryCalculationDf", { df: input, name: "score", method: "sum" })).resolves.toBe(9);
|
|
});
|
|
|
|
it("deduplicates, merges, and filters local tables", async () => {
|
|
const duplicated = [["id", "name"], [1, "A"], [1, "A2"], [2, "B"]];
|
|
await expect(run("Bivartable_DeleteDuplicateRow", { df: duplicated, change_type: "name", col_name: ["id"], mode: "first" }))
|
|
.resolves.toEqual([["id", "name"], [1, "A"], [2, "B"]]);
|
|
const left = [["id", "name"], [1, "A"], [2, "B"]];
|
|
const right = [["id", "score"], [1, 90], [3, 70]];
|
|
await expect(run("Bivartable_MergeDf", { df1: left, df2: right, on_name: "id", merge_type: "left" }))
|
|
.resolves.toEqual([["id", "name", "score"], [1, "A", 90], [2, "B", null]]);
|
|
await expect(run("Bivartable_DoMerge", { df1: left, df2: right, onx: "id", howx: "inner" }))
|
|
.resolves.toEqual([["id", "name", "score"], [1, "A", 90]]);
|
|
await expect(run("Bivartable_FilterDf", { df: left, col_name: "id", choice: ">", criteria: 1 }))
|
|
.resolves.toEqual([["id", "name"], [2, "B"]]);
|
|
await expect(run("Bivartable_DoFilter", { df: [["id", "score"], [1, 70], [2, 90], [3, 95]], filter: "(df['id'] > 1) & (df['score'] >= 90)" }))
|
|
.resolves.toEqual([["id", "score"], [2, 90], [3, 95]]);
|
|
});
|
|
});
|