1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
import type { BrowserClient } from "../client";
import { safeJs } from "../util/js-template";
import { type Result, err, ok } from "../util/result";
import type { ToolErr } from "../util/tool";
import { cdpCall, evalJs } from "./cdp-call";
import { resolveRefToObjectId } from "./ref-resolve";
export type PageResult = {
readonly ok: boolean;
readonly reason?: string;
readonly kind?: string;
readonly value?: unknown;
readonly text?: string;
readonly checked?: boolean;
readonly changed?: boolean;
readonly tag?: string;
readonly options?: ReadonlyArray<{ value: string; text: string }>;
};
const isOption = (v: unknown): v is { readonly value: string; readonly text: string } =>
typeof v === "object" && v !== null
&& "value" in v && typeof v.value === "string"
&& "text" in v && typeof v.text === "string";
export const isPageResult = (v: unknown): v is PageResult => {
if (typeof v !== "object" || v === null) return false;
if (!("ok" in v) || typeof v.ok !== "boolean") return false;
if ("reason" in v && typeof v.reason !== "string") return false;
if ("kind" in v && typeof v.kind !== "string") return false;
if ("text" in v && typeof v.text !== "string") return false;
if ("checked" in v && typeof v.checked !== "boolean") return false;
if ("changed" in v && typeof v.changed !== "boolean") return false;
if ("tag" in v && typeof v.tag !== "string") return false;
if ("options" in v && !(Array.isArray(v.options) && v.options.every(isOption))) return false;
return true;
};
export const runOnElement = async (
client: BrowserClient,
opts: {
ref?: string | undefined;
selector?: string | undefined;
fnBody: string;
arg: unknown;
notFound?: unknown;
},
): Promise<Result<unknown, ToolErr>> => {
if (opts.ref !== undefined) {
const objectId = await resolveRefToObjectId(client, opts.ref);
if (!objectId.success) return objectId;
const r = await cdpCall(client, "Runtime.callFunctionOn", {
objectId: objectId.data,
functionDeclaration: `function (arg) { ${opts.fnBody} }`,
arguments: [{ value: opts.arg }],
returnByValue: true,
});
if (!r.success) return r;
if (r.data.exceptionDetails !== undefined) {
return err({
kind: "cdp_error",
message: `page function threw: ${r.data.exceptionDetails.text}`,
details: { ref: opts.ref },
});
}
return ok(r.data.result.value);
}
if (opts.selector === undefined) {
return err({ kind: "invalid_state", message: "Provide either `ref` or `selector`." });
}
// fnBody is harness-authored, not user input; only the selector and value are interpolated, and those go through safeJs.
const prelude = safeJs`
(() => {
const el = document.querySelector(${opts.selector});
if (!el) return ${opts.notFound ?? { status: "not_found" }};
const __arg = ${opts.arg};
return (function (arg) {`;
const expr = `${prelude} ${opts.fnBody} }).call(el, __arg); })()`;
const r = await evalJs(client, expr);
if (!r.success) return r;
return ok(r.data);
};
export const resolveAndCall = async (
client: BrowserClient,
ref: string,
functionDeclaration: string,
args: ReadonlyArray<unknown>,
): Promise<Result<PageResult, ToolErr>> => {
const objectId = await resolveRefToObjectId(client, ref);
if (!objectId.success) return objectId;
const handle = objectId.data;
try {
const called = await cdpCall(client, "Runtime.callFunctionOn", {
objectId: handle,
functionDeclaration,
arguments: args.map((v) => ({ value: v })),
returnByValue: true,
awaitPromise: true,
});
if (!called.success) return err({ ...called.error, details: { ref } });
if (called.data.exceptionDetails !== undefined) {
return err({ kind: "cdp_error", message: `page function threw: ${JSON.stringify(called.data.exceptionDetails)}`, details: { ref } });
}
const value = called.data.result.value;
if (!isPageResult(value)) {
return err({ kind: "internal", message: "page function returned no result object", details: { ref } });
}
return ok(value);
} finally {
await cdpCall(client, "Runtime.releaseObject", { objectId: handle });
}
};
export const detailsOf = (ref: string, res: PageResult, extra?: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>> => {
const { ok: _ok, ...rest } = res;
return { ref, ...rest, ...(extra ?? {}) };
};
|