feat: 持久化运行记录并补充分发验证

这个提交包含在:
yu 于 2026-10-27 11:35:13 +08:00 提交
1 父节点 c03380168d
当前提交 aff2d5cd95
8 个文件已更改
+344 -7

没有与搜索条件相匹配的文件

+151
查看文件
@@ -0,0 +1,151 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import { getHandler } from "../engine/registry";
import { RuntimeError, type BrowserLike, type RunContext } from "../engine/types";
import "./browser";
const component = {} as CatalogComponent;
function node(componentId: string): FlowNode {
return { id: `browser_${componentId}`, componentId, position: { x: 0, y: 0 }, values: {}, options: {} };
}
function fakeBrowser(): BrowserLike {
return {
navigate: vi.fn(async (url: string) => ({ url, title: "Fake page" })),
click: vi.fn(async (selector: unknown) => ({ selector })),
type: vi.fn(async (selector: unknown, text: string, clearFirst?: boolean) => ({ selector, text, clearFirst })),
getText: vi.fn(async () => "fake text"),
exists: vi.fn(async () => true),
select: vi.fn(async (selector: unknown, value: string) => ({ selector, value })),
runJs: vi.fn(async (script: string) => ({ script })),
download: vi.fn(async (_selector: unknown, folder: string, filename?: string) => `${folder}/${filename || "download.bin"}`),
screenshot: vi.fn(async (target: string) => target),
currentUrl: vi.fn(async () => "about:blank"),
close: vi.fn(async () => undefined)
};
}
function context(browser: BrowserLike): RunContext {
return {
runId: "browser-test-run", flowId: "browser-test-flow", flow: {} as RunContext["flow"],
vars: {}, secrets: {}, signal: new AbortController().signal, evidenceDir: "C:/temp/evidence",
browser, emit: vi.fn(), log: vi.fn(), componentOf: vi.fn()
};
}
async function invoke(ctx: RunContext, componentId: string, args: Record<string, unknown>): Promise<unknown> {
const handler = getHandler(componentId);
expect(handler, `${componentId} should be registered`).toBeTypeOf("function");
return handler!(ctx, node(componentId), args, component);
}
async function expectValidationError(promise: Promise<unknown>): Promise<void> {
await expect(promise).rejects.toSatisfy((error: unknown) =>
error instanceof RuntimeError && error.code === "validation_error"
);
}
describe("browser capability parameter routing", () => {
let browser: BrowserLike;
let ctx: RunContext;
beforeEach(() => { browser = fakeBrowser(); ctx = context(browser); });
it.each(["WebBrowser_CreateV3", "WebBrowser_Create", "WebBrowser_CreateBackground"])(
"%s routes its URL to browser.navigate",
async (componentId) => {
await expect(invoke(ctx, componentId, { url: "https://offline.test/create" })).resolves.toEqual({
url: "https://offline.test/create", title: "Fake page"
});
expect(browser.navigate).toHaveBeenCalledOnce();
expect(browser.navigate).toHaveBeenCalledWith("https://offline.test/create");
}
);
it("routes navigation URL", async () => {
await invoke(ctx, "WebBrowser_GoUrl", { url: "https://offline.test/next" });
expect(browser.navigate).toHaveBeenCalledWith("https://offline.test/next");
});
it("routes click selector", async () => {
const selector = { type: "css", value: "#submit" };
await invoke(ctx, "Element_Click_V6", { uiSelector: selector });
expect(browser.click).toHaveBeenCalledWith(selector);
});
it("routes input selector and text and handles enter/blur", async () => {
const selector = { type: "aria", value: "textbox=Name" };
await invoke(ctx, "Element_SetValue_V6", { uiSelector: selector, text: "robot", isEnter: "1" });
expect(browser.type).toHaveBeenCalledWith(selector, "robot", true);
expect(browser.runJs).toHaveBeenCalledWith("document.activeElement && document.activeElement.blur()");
});
it("routes text selector and requested attribute", async () => {
const selector = { type: "css", value: ".result" };
await expect(invoke(ctx, "Element_GetText_V5", {
uiSelector: selector, attr_type: "attribute", attr_name: "data-result"
})).resolves.toBe("fake text");
expect(browser.getText).toHaveBeenCalledWith(selector, "data-result");
});
it("routes element-exists selector", async () => {
const selector = { type: "text", value: "Completed" };
await expect(invoke(ctx, "Element_Exists", { uiSelector: selector })).resolves.toBe(true);
expect(browser.exists).toHaveBeenCalledWith(selector);
});
it("routes select selector and itemsToSelect value", async () => {
const selector = { type: "css", value: "select[name=status]" };
await invoke(ctx, "Element_SelectOperation_v1", { uiSelector: selector, itemsToSelect: "approved" });
expect(browser.select).toHaveBeenCalledWith(selector, "approved");
});
it.each([
"WebBrowser_RunJsInIeByTitle", "WebBrowser_RunJsInIeBySelector", "WebBrowser_RunJsInIeBySelector_v1",
"WebBrowser_RunJsInIeBySelector_v2", "WebBrowser_RunJsInIeBySelector_v3", "WebBrowser_RunJsInIeBySelectorGround"
])("%s routes jsCode to browser.runJs", async (componentId) => {
await invoke(ctx, componentId, { jsCode: "return document.title" });
expect(browser.runJs).toHaveBeenCalledWith("return document.title");
});
it.each(["WebBrowser_SaveAs", "WebBrowser_SaveAs_V1"])(
"%s routes selector and download options",
async (componentId) => {
const selector = { type: "css", value: "a.download" };
await expect(invoke(ctx, componentId, {
uiSelector: selector, path: "C:/offline/downloads", filename: "report.csv", timeout: "2500"
})).resolves.toBe("C:/offline/downloads/report.csv");
expect(browser.download).toHaveBeenCalledWith(selector, "C:/offline/downloads", "report.csv", 2500);
}
);
});
describe("browser capability validation", () => {
let browser: BrowserLike;
let ctx: RunContext;
beforeEach(() => { browser = fakeBrowser(); ctx = context(browser); });
it("rejects a missing navigation URL", async () => {
await expectValidationError(invoke(ctx, "WebBrowser_GoUrl", {}));
expect(browser.navigate).not.toHaveBeenCalled();
});
it("rejects a missing element selector", async () => {
await expectValidationError(invoke(ctx, "Element_Click_V6", {}));
expect(browser.click).not.toHaveBeenCalled();
});
it("rejects a missing JavaScript script", async () => {
await expectValidationError(invoke(ctx, "WebBrowser_RunJsInIeByTitle", {}));
expect(browser.runJs).not.toHaveBeenCalled();
});
it("rejects a missing download directory before starting a download", async () => {
await expectValidationError(invoke(ctx, "WebBrowser_SaveAs", {
uiSelector: { type: "css", value: "a.download" }
}));
expect(browser.download).not.toHaveBeenCalled();
});
});