Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/@webreel/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export type CDPClient = {
};
DOM: {
enable: () => Promise<void>;
getDocument: (params: { depth?: number }) => Promise<{ root: { nodeId: number } }>;
querySelector: (params: {
nodeId: number;
selector: string;
}) => Promise<{ nodeId: number }>;
setFileInputFiles: (params: { nodeId: number; files: string[] }) => Promise<void>;
};
};

Expand Down
37 changes: 37 additions & 0 deletions packages/webreel/src/lib/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,43 @@ describe("validateStep", () => {
expect(errors).toEqual([]);
});

it("accepts valid upload step", () => {
const errors = validate({
action: "upload",
selector: "input[type='file']",
filePath: "audio.mp3",
});
expect(errors).toEqual([]);
});

it("validates upload requires non-empty selector", () => {
const errors = validate({ action: "upload", selector: "", filePath: "audio.mp3" });
expect(errors).toContainEqual(
expect.objectContaining({ path: "videos.x.steps[0].selector" }),
);
});

it("validates upload rejects missing selector", () => {
const errors = validate({ action: "upload", filePath: "audio.mp3" });
expect(errors).toContainEqual(
expect.objectContaining({ path: "videos.x.steps[0].selector" }),
);
});

it("validates upload requires non-empty filePath", () => {
const errors = validate({ action: "upload", selector: "#input", filePath: "" });
expect(errors).toContainEqual(
expect.objectContaining({ path: "videos.x.steps[0].filePath" }),
);
});

it("validates upload rejects missing filePath", () => {
const errors = validate({ action: "upload", selector: "#input" });
expect(errors).toContainEqual(
expect.objectContaining({ path: "videos.x.steps[0].filePath" }),
);
});

it("accepts description field on any step", () => {
const errors = validate({
action: "pause",
Expand Down
23 changes: 23 additions & 0 deletions packages/webreel/src/lib/__tests__/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,29 @@ describe("formatStep", () => {
expect(formatStep(0, step)).toBe('[step 0] select "#country" value="US"');
});

it("formats upload step", () => {
const step: Step = {
action: "upload",
selector: "input[type='file']",
filePath: "audio.mp3",
};
expect(formatStep(0, step)).toBe(
`[step 0] upload selector="input[type='file']" file="audio.mp3"`,
);
});

it("formats upload step with description", () => {
const step: Step = {
action: "upload",
selector: "#file-input",
filePath: "tests/fixtures/heydonna.mp3",
description: "upload audio",
};
expect(formatStep(0, step)).toBe(
'[step 0] upload selector="#file-input" file="tests/fixtures/heydonna.mp3": upload audio',
);
});

it("includes description when present", () => {
const step: Step = { action: "pause", ms: 100, description: "wait for animation" };
expect(formatStep(0, step)).toBe("[step 0] pause 100ms: wait for animation");
Expand Down
17 changes: 17 additions & 0 deletions packages/webreel/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ const VALID_ACTIONS = new Set([
"navigate",
"hover",
"select",
"upload",
]);

const KNOWN_TOP_LEVEL_KEYS = new Set([
Expand Down Expand Up @@ -398,6 +399,7 @@ const KNOWN_STEP_KEYS: Record<string, Set<string>> = {
"delay",
"description",
]),
upload: new Set(["action", "selector", "filePath", "label", "delay", "description"]),
};

export interface ValidationError {
Expand Down Expand Up @@ -630,6 +632,21 @@ function validateStep(step: unknown, index: number): ValidationError[] {
}
break;
}

case "upload":
if (typeof s.selector !== "string" || s.selector.length === 0) {
errors.push({
path: `${prefix}.selector`,
message: "Must be a non-empty string",
});
}
if (typeof s.filePath !== "string" || s.filePath.length === 0) {
errors.push({
path: `${prefix}.filePath`,
message: "Must be a non-empty string",
});
}
break;
}

if (s.delay !== undefined && (!Number.isFinite(s.delay) || (s.delay as number) < 0)) {
Expand Down
22 changes: 21 additions & 1 deletion packages/webreel/src/lib/runner.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { resolve, dirname } from "node:path";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { pathToFileURL } from "node:url";
import {
type CDPClient,
Expand Down Expand Up @@ -58,6 +58,8 @@ export function formatStep(i: number, step: Step): string {
return `[step ${i}] hover ${step.text ? `text="${step.text}"` : `selector="${step.selector}"`}${desc}`;
case "select":
return `[step ${i}] select "${step.selector}" value="${step.value}"${desc}`;
case "upload":
return `[step ${i}] upload selector="${step.selector}" file="${step.filePath}"${desc}`;
default: {
const _exhaustive: never = step;
return `[step ${i}] ${(_exhaustive as Step).action}`;
Expand Down Expand Up @@ -400,6 +402,24 @@ export async function runVideo(
}
break;
}

case "upload": {
const absolutePath = resolve(configDir, step.filePath);
if (!existsSync(absolutePath)) {
throw new Error(`File not found: ${absolutePath}`);
}
await client.DOM.enable();
const { root } = await client.DOM.getDocument({ depth: 0 });
const { nodeId } = await client.DOM.querySelector({
nodeId: root.nodeId,
selector: step.selector,
});
if (!nodeId) {
throw new Error(`File input not found: selector="${step.selector}"`);
}
await client.DOM.setFileInputFiles({ nodeId, files: [absolutePath] });
break;
}
}
const stepDelay = "delay" in step ? step.delay : undefined;
const postDelay = stepDelay ?? config.defaultDelay;
Expand Down
12 changes: 11 additions & 1 deletion packages/webreel/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ export interface StepSelect {
description?: string;
}

export interface StepUpload {
action: "upload";
selector: string;
filePath: string;
label?: string;
delay?: number;
description?: string;
}

export type Step =
| StepPause
| StepClick
Expand All @@ -133,7 +142,8 @@ export type Step =
| StepScreenshot
| StepNavigate
| StepHover
| StepSelect;
| StepSelect
| StepUpload;

export interface CursorConfig {
image?: string;
Expand Down