Skip to content

Commit e794249

Browse files
Share perform helper dispatch across replay paths
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent e183813 commit e794249

3 files changed

Lines changed: 133 additions & 81 deletions

File tree

‎src/agent/index.ts‎

Lines changed: 6 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ import { setDebugOptions } from "@/debug/options";
6060
import { initializeRuntimeContext } from "./shared/runtime-context";
6161
import { performAction } from "./actions/shared/perform-action";
6262
import { createScriptFromActionCache } from "./shared/action-cache-script";
63-
import { attachCachedActionHelpers } from "./shared/action-cache-exec";
63+
import {
64+
attachCachedActionHelpers,
65+
dispatchPerformHelper,
66+
isPageActionMethod,
67+
} from "./shared/action-cache-exec";
6468
import { AgentDeps } from "@/types/agent/types";
6569
import { parseExtractOutput } from "./shared/parse-extract-output";
6670
import { executeReplaySpecialAction } from "./shared/replay-special-actions";
@@ -603,66 +607,6 @@ export class HyperAgent<T extends BrowserProviders = "Local"> {
603607
return finalSuccess;
604608
};
605609

606-
/**
607-
* Type-safe dispatch for HyperPage perform* methods.
608-
* Explicitly routes to the correct method with proper typing.
609-
*
610-
* Methods that require a value argument (second param): type, fill, press, selectOptionFromDropdown, scrollToPercentage
611-
* Methods with only xpath and options: click, hover, check, uncheck, scrollToElement, nextChunk, prevChunk
612-
*/
613-
const dispatchPerformHelper = (
614-
hp: HyperPage,
615-
method: string,
616-
xpath: string,
617-
value: string | undefined,
618-
options: PerformOptions
619-
): Promise<TaskOutput> => {
620-
switch (method) {
621-
case "click":
622-
return hp.performClick(xpath, options);
623-
case "hover":
624-
return hp.performHover(xpath, options);
625-
case "type":
626-
return hp.performType(xpath, value ?? "", options);
627-
case "fill":
628-
return hp.performFill(xpath, value ?? "", options);
629-
case "press":
630-
return hp.performPress(xpath, value ?? "", options);
631-
case "selectOptionFromDropdown":
632-
return hp.performSelectOption(xpath, value ?? "", options);
633-
case "check":
634-
return hp.performCheck(xpath, options);
635-
case "uncheck":
636-
return hp.performUncheck(xpath, options);
637-
case "scrollToElement":
638-
return hp.performScrollToElement(xpath, options);
639-
case "scrollToPercentage":
640-
return hp.performScrollToPercentage(xpath, value ?? "", options);
641-
case "nextChunk":
642-
return hp.performNextChunk(xpath, options);
643-
case "prevChunk":
644-
return hp.performPrevChunk(xpath, options);
645-
default:
646-
throw new Error(`Unknown perform helper method: ${method}`);
647-
}
648-
};
649-
650-
/** Set of valid method names that can be dispatched */
651-
const validHelperMethods = new Set([
652-
"click",
653-
"fill",
654-
"type",
655-
"press",
656-
"selectOptionFromDropdown",
657-
"check",
658-
"uncheck",
659-
"hover",
660-
"scrollToElement",
661-
"scrollToPercentage",
662-
"nextChunk",
663-
"prevChunk",
664-
]);
665-
666610
for (const step of [...cache.steps].sort(
667611
(a, b) => a.stepIndex - b.stepIndex
668612
)) {
@@ -682,7 +626,7 @@ export class HyperAgent<T extends BrowserProviders = "Local"> {
682626
result = replaySpecialResult;
683627
} else {
684628
const method = step.method;
685-
if (method && validHelperMethods.has(method)) {
629+
if (method && isPageActionMethod(method)) {
686630
const xpath = step.xpath;
687631
const hasXPath =
688632
typeof xpath === "string" && xpath.trim().length > 0;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {
2+
dispatchPerformHelper,
3+
isPageActionMethod,
4+
} from "@/agent/shared/action-cache-exec";
5+
import type { HyperPage } from "@/types/agent/types";
6+
7+
function createMockHyperPage(): HyperPage {
8+
const ok = Promise.resolve({
9+
taskId: "task-id",
10+
status: "completed",
11+
steps: [],
12+
output: "ok",
13+
});
14+
return {
15+
performClick: jest.fn().mockReturnValue(ok),
16+
performHover: jest.fn().mockReturnValue(ok),
17+
performType: jest.fn().mockReturnValue(ok),
18+
performFill: jest.fn().mockReturnValue(ok),
19+
performPress: jest.fn().mockReturnValue(ok),
20+
performSelectOption: jest.fn().mockReturnValue(ok),
21+
performCheck: jest.fn().mockReturnValue(ok),
22+
performUncheck: jest.fn().mockReturnValue(ok),
23+
performScrollToElement: jest.fn().mockReturnValue(ok),
24+
performScrollToPercentage: jest.fn().mockReturnValue(ok),
25+
performNextChunk: jest.fn().mockReturnValue(ok),
26+
performPrevChunk: jest.fn().mockReturnValue(ok),
27+
} as unknown as HyperPage;
28+
}
29+
30+
describe("action-cache perform helper dispatch", () => {
31+
it("validates known page action methods", () => {
32+
expect(isPageActionMethod("click")).toBe(true);
33+
expect(isPageActionMethod("not-a-method")).toBe(false);
34+
});
35+
36+
it("dispatches fill with value argument", async () => {
37+
const page = createMockHyperPage();
38+
const options = { maxSteps: 2 };
39+
40+
await dispatchPerformHelper(page, "fill", "//input[1]", "hello", options);
41+
42+
expect(page.performFill).toHaveBeenCalledWith("//input[1]", "hello", {
43+
maxSteps: 2,
44+
});
45+
});
46+
47+
it("dispatches click without value argument", async () => {
48+
const page = createMockHyperPage();
49+
const options = { maxSteps: 1 };
50+
51+
await dispatchPerformHelper(page, "click", "//button[1]", undefined, options);
52+
53+
expect(page.performClick).toHaveBeenCalledWith("//button[1]", {
54+
maxSteps: 1,
55+
});
56+
});
57+
});

‎src/agent/shared/action-cache-exec.ts‎

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,77 @@
1-
import { AgentDeps, HyperPage, TaskOutput } from "@/types/agent/types";
1+
import {
2+
AgentDeps,
3+
HyperPage,
4+
PerformOptions,
5+
TaskOutput,
6+
} from "@/types/agent/types";
27
import * as cachedRunner from "./run-cached-action";
38

49
const DEFAULT_MAX_STEPS = 3;
510

6-
type PageAction =
7-
| "click"
8-
| "fill"
9-
| "type"
10-
| "press"
11-
| "selectOptionFromDropdown"
12-
| "check"
13-
| "uncheck"
14-
| "hover"
15-
| "scrollToElement"
16-
| "scrollToPercentage"
17-
| "nextChunk"
18-
| "prevChunk";
19-
20-
interface PerformOptions {
21-
frameIndex?: number | null;
22-
performInstruction?: string | null;
23-
maxSteps?: number;
11+
export const PAGE_ACTION_METHODS = [
12+
"click",
13+
"fill",
14+
"type",
15+
"press",
16+
"selectOptionFromDropdown",
17+
"check",
18+
"uncheck",
19+
"hover",
20+
"scrollToElement",
21+
"scrollToPercentage",
22+
"nextChunk",
23+
"prevChunk",
24+
] as const;
25+
26+
type Includes<T extends readonly string[]> = (
27+
haystack: readonly string[],
28+
needle: string
29+
) => needle is T[number];
30+
31+
const includes = ((haystack: readonly string[], needle: string): boolean =>
32+
haystack.includes(needle)) as Includes<typeof PAGE_ACTION_METHODS>;
33+
34+
export type PageAction = (typeof PAGE_ACTION_METHODS)[number];
35+
36+
export function isPageActionMethod(method: string): method is PageAction {
37+
return includes(PAGE_ACTION_METHODS, method);
38+
}
39+
40+
export function dispatchPerformHelper(
41+
hp: HyperPage,
42+
method: PageAction,
43+
xpath: string,
44+
value: string | undefined,
45+
options: PerformOptions
46+
): Promise<TaskOutput> {
47+
switch (method) {
48+
case "click":
49+
return hp.performClick(xpath, options);
50+
case "hover":
51+
return hp.performHover(xpath, options);
52+
case "type":
53+
return hp.performType(xpath, value ?? "", options);
54+
case "fill":
55+
return hp.performFill(xpath, value ?? "", options);
56+
case "press":
57+
return hp.performPress(xpath, value ?? "", options);
58+
case "selectOptionFromDropdown":
59+
return hp.performSelectOption(xpath, value ?? "", options);
60+
case "check":
61+
return hp.performCheck(xpath, options);
62+
case "uncheck":
63+
return hp.performUncheck(xpath, options);
64+
case "scrollToElement":
65+
return hp.performScrollToElement(xpath, options);
66+
case "scrollToPercentage":
67+
return hp.performScrollToPercentage(xpath, value ?? "", options);
68+
case "nextChunk":
69+
return hp.performNextChunk(xpath, options);
70+
case "prevChunk":
71+
return hp.performPrevChunk(xpath, options);
72+
default:
73+
throw new Error(`Unknown perform helper method: ${method}`);
74+
}
2475
}
2576

2677
function runCachedAction(

0 commit comments

Comments
 (0)