pi-browser-harness 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +86 -0
  3. package/package.json +12 -6
  4. package/skills/pi-browser-harness/SKILL.md +57 -2
  5. package/src/cdp/commands.ts +200 -14
  6. package/src/cdp/console-buffer.ts +37 -23
  7. package/src/cdp/discovery.ts +57 -25
  8. package/src/cdp/events.ts +8 -3
  9. package/src/cdp/network-buffer.ts +38 -32
  10. package/src/cdp/ownership.ts +22 -4
  11. package/src/cdp/record-store.ts +2 -2
  12. package/src/cdp/session.ts +320 -217
  13. package/src/cdp/target-factory.ts +34 -15
  14. package/src/cdp/types.ts +17 -1
  15. package/src/client.ts +76 -43
  16. package/src/daemon/bridge.ts +110 -349
  17. package/src/daemon/connection.ts +323 -0
  18. package/src/daemon/index.ts +77 -48
  19. package/src/daemon/lines.ts +40 -0
  20. package/src/daemon/main.ts +12 -0
  21. package/src/daemon/protocol.ts +55 -25
  22. package/src/daemon/server.ts +100 -63
  23. package/src/daemon/socket.ts +10 -0
  24. package/src/daemon/spawn.ts +21 -22
  25. package/src/daemon/transport.ts +80 -49
  26. package/src/domains/ax-tree.ts +8 -8
  27. package/src/domains/cdp-call.ts +14 -7
  28. package/src/domains/click.ts +2 -2
  29. package/src/domains/concurrency.ts +50 -0
  30. package/src/domains/console.ts +20 -64
  31. package/src/domains/drag.ts +26 -28
  32. package/src/domains/element-call.ts +102 -68
  33. package/src/domains/files.ts +13 -9
  34. package/src/domains/form-batch.ts +13 -10
  35. package/src/domains/form.ts +78 -107
  36. package/src/domains/headers.ts +2 -2
  37. package/src/domains/history.ts +4 -4
  38. package/src/domains/io.ts +35 -0
  39. package/src/domains/isolated-tab.ts +6 -12
  40. package/src/domains/js.ts +65 -44
  41. package/src/domains/keyboard.ts +14 -11
  42. package/src/domains/navigate.ts +102 -66
  43. package/src/domains/network.ts +55 -95
  44. package/src/domains/page/fill.page.ts +139 -0
  45. package/src/domains/page/form.page.ts +61 -0
  46. package/src/domains/page/inject.ts +37 -0
  47. package/src/domains/page/keyboard.page.ts +16 -0
  48. package/src/domains/page/live-value.page.ts +10 -0
  49. package/src/domains/page/read-page.page.ts +51 -0
  50. package/src/domains/page/serp.page.ts +39 -0
  51. package/src/domains/page.ts +18 -51
  52. package/src/domains/poll.ts +42 -0
  53. package/src/domains/readpage/read-page.ts +48 -53
  54. package/src/domains/ref-resolve.ts +43 -29
  55. package/src/domains/ref-signature.ts +11 -0
  56. package/src/domains/render.ts +12 -23
  57. package/src/domains/screenshot-capture.ts +4 -2
  58. package/src/domains/screenshot.ts +13 -10
  59. package/src/domains/scroll.ts +4 -4
  60. package/src/domains/search/google-serp.ts +1 -1
  61. package/src/domains/search/web-search.ts +38 -33
  62. package/src/domains/snapshot.ts +71 -73
  63. package/src/domains/tab-resolve.ts +36 -0
  64. package/src/domains/tabs.ts +18 -69
  65. package/src/domains/target.ts +1 -1
  66. package/src/index.ts +90 -33
  67. package/src/profile/bind.ts +45 -14
  68. package/src/profile/browser-process.ts +50 -12
  69. package/src/profile/command.ts +20 -12
  70. package/src/profile/launch.ts +13 -9
  71. package/src/profile/list.ts +9 -13
  72. package/src/profile/store.ts +2 -1
  73. package/src/prompt.ts +32 -7
  74. package/src/registry.ts +65 -4
  75. package/src/schemas/parse.ts +2 -2
  76. package/src/setup.ts +28 -19
  77. package/src/state.ts +2 -1
  78. package/src/util/debug.ts +7 -1
  79. package/src/util/guards.ts +3 -0
  80. package/src/util/mutex.ts +1 -1
  81. package/src/util/paths.ts +3 -2
  82. package/src/util/result.ts +8 -0
  83. package/src/util/sharp-shim.ts +18 -11
  84. package/src/util/sleep.ts +18 -0
  85. package/src/util/tool.ts +163 -114
  86. package/src/util/truncate.ts +1 -1
  87. package/tsconfig.json +2 -1
  88. package/src/domains/fill-engine.ts +0 -80
package/src/util/tool.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { TSchema, Static } from "typebox";
1
+ import { isRecord } from "./guards";
2
+ import { Type, type TSchema, type Static } from "typebox";
3
+ import { Compile } from "typebox/compile";
2
4
  import type {
3
5
  AgentToolResult,
4
6
  ExtensionAPI,
@@ -7,14 +9,44 @@ import type {
7
9
  ToolDefinition,
8
10
  ToolRenderResultOptions,
9
11
  } from "@mariozechner/pi-coding-agent";
10
- import type { Component } from "@mariozechner/pi-tui";
12
+ import { type Component, Text } from "@mariozechner/pi-tui";
11
13
  import { daemonSocketMayExist } from "../daemon/spawn";
12
14
  import type { BrowserClient } from "../client";
13
- import type { Result } from "./result";
15
+ import { type Result, err } from "./result";
14
16
 
15
- export type ToolOk = {
17
+ // Details are stored by pi as JSON and handed back on replay, so a renderer only ever reads them.
18
+ export type DeepReadonly<T> = T extends ReadonlyArray<infer U>
19
+ ? ReadonlyArray<DeepReadonly<U>>
20
+ : T extends object
21
+ ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
22
+ : T;
23
+
24
+ // pi serializes details to JSON, so a Map, a function or an undefined-valued key would be silently lost or
25
+ // mangled on replay; the untyped contract admits only what survives the round trip.
26
+ export type JsonValue = string | number | boolean | null | ReadonlyArray<JsonValue> | JsonObject;
27
+ export type JsonObject = { readonly [key: string]: JsonValue };
28
+
29
+ const isJsonValue = (v: unknown): v is JsonValue => {
30
+ if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") return true;
31
+ if (Array.isArray(v)) return v.every(isJsonValue);
32
+ if (typeof v !== "object" || Object.getPrototypeOf(v) !== Object.prototype) return false;
33
+ return Object.values(v).every(isJsonValue);
34
+ };
35
+
36
+ export const isJsonObject = (v: unknown): v is JsonObject => isRecord(v) && isJsonValue(v);
37
+
38
+ // A tool that declares no `details` schema may still attach any JSON object for logs; its renderer (if any)
39
+ // sees that object untyped, or undefined when the handler attached nothing.
40
+ export const UntypedDetailsSchema = Type.Union([Type.Record(Type.String(), Type.Unknown()), Type.Undefined()]);
41
+ export type UntypedDetailsSchema = typeof UntypedDetailsSchema;
42
+ export type UntypedDetails = JsonObject | undefined;
43
+
44
+ // The value type a details schema describes, as the handler produces it and the renderer reads it back.
45
+ export type DetailsOf<D extends TSchema> = D extends UntypedDetailsSchema ? UntypedDetails : DeepReadonly<Static<D>>;
46
+
47
+ export type ToolOk<T = UntypedDetails> = {
16
48
  readonly text: string;
17
- readonly details?: Readonly<Record<string, unknown>>;
49
+ readonly details?: T;
18
50
  };
19
51
 
20
52
  export type ToolErrKind =
@@ -22,6 +54,7 @@ export type ToolErrKind =
22
54
  | "cdp_error"
23
55
  | "timeout"
24
56
  | "invalid_state"
57
+ | "invalid_args"
25
58
  | "io_error"
26
59
  | "internal";
27
60
 
@@ -31,80 +64,131 @@ export type ToolErr = {
31
64
  readonly details?: Readonly<Record<string, unknown>>;
32
65
  };
33
66
 
34
- export type HandlerContext = {
67
+ export type HandlerContext<T = UntypedDetails> = {
35
68
  readonly client: BrowserClient;
36
69
  readonly signal: AbortSignal | undefined;
37
- readonly onUpdate: (update: ToolOk) => void;
70
+ readonly onUpdate: (update: ToolOk<T>) => void;
38
71
  readonly extensionCtx: ExtensionContext;
39
72
  };
40
73
 
41
- export type ToolHandler<S extends TSchema> = (
74
+ export type ToolHandler<S extends TSchema, T = UntypedDetails> = (
42
75
  args: Static<S>,
43
- ctx: HandlerContext,
44
- ) => Promise<Result<ToolOk, ToolErr>>;
76
+ ctx: HandlerContext<T>,
77
+ ) => Promise<Result<ToolOk<T>, ToolErr>>;
45
78
 
46
- export type BrowserToolDefinition<S extends TSchema> = {
79
+ export type ToolConcurrency = "serialized" | "parallel";
80
+
81
+ // S types the arguments, D the structured details the handler attaches to a successful result. pi persists
82
+ // details and hands them back to renderResult on replay; declaring D lets the tool read them back typed, after
83
+ // the contract has validated them against the schema. A tool with no details leaves D at its default.
84
+ export type BrowserToolDefinition<S extends TSchema, D extends TSchema = UntypedDetailsSchema> = {
47
85
  readonly name: string;
48
86
  readonly label: string;
49
87
  readonly description: string;
50
88
  readonly promptSnippet: string;
51
89
  readonly promptGuidelines: ReadonlyArray<string>;
52
90
  readonly parameters: S;
53
- readonly handler: ToolHandler<S>;
91
+ readonly details?: D;
92
+ readonly handler: ToolHandler<S, DetailsOf<D>>;
54
93
  readonly renderCall?: (args: Static<S>, theme: Theme) => Component;
55
- readonly renderResult?: (result: AgentToolResult<unknown>, expanded: boolean, theme: Theme) => Component;
56
- readonly concurrency: "serialized" | "parallel";
94
+ readonly renderResult?: (result: AgentToolResult<DetailsOf<D>>, expanded: boolean, theme: Theme) => Component;
95
+ readonly concurrency: ToolConcurrency;
57
96
  readonly ensureAlive?: boolean;
58
97
  };
59
98
 
60
- // renderCall accepts `never` so any concrete args type satisfies it via function-parameter contravariance.
61
- export type AnyBrowserToolDefinition = Omit<BrowserToolDefinition<TSchema>, "renderCall" | "renderResult"> & {
62
- readonly renderCall?: (args: never, theme: Theme) => Component;
63
- readonly renderResult?: (result: AgentToolResult<unknown>, expanded: boolean, theme: Theme) => Component;
99
+ // The schema-erased view of a tool. Everything that depends on S or D (handler, renderers, the details
100
+ // validator) stays closed over inside `register`, which was built while they were still bound, so no cast is
101
+ // needed to hold tools of different schemas in one list.
102
+ export type BrowserTool = Pick<
103
+ BrowserToolDefinition<TSchema>,
104
+ "name" | "label" | "description" | "promptSnippet" | "promptGuidelines" | "concurrency" | "ensureAlive"
105
+ > & {
106
+ readonly register: (pi: Pick<ExtensionAPI, "registerTool">, client: BrowserClient) => void;
64
107
  };
65
108
 
66
- export const defineBrowserTool = <S extends TSchema>(
67
- def: BrowserToolDefinition<S>,
68
- ): BrowserToolDefinition<S> => def;
69
-
70
- type OkDetails = { readonly ok: true } & Readonly<Record<string, unknown>>;
71
- type ErrDetails = { readonly ok: false; readonly kind: ToolErrKind; readonly message: string } & Readonly<Record<string, unknown>>;
72
-
73
- type ErrorFlagged<D> = AgentToolResult<D> & { readonly isError?: boolean };
74
-
75
- const toToolResult = (
76
- r: Result<ToolOk, ToolErr>,
77
- toolName: string,
78
- ): ErrorFlagged<OkDetails | ErrDetails> => {
79
- if (r.success) {
80
- const details: OkDetails = { ok: true, ...(r.data.details ?? {}) };
81
- return {
82
- content: [{ type: "text", text: r.data.text }],
83
- details,
84
- };
109
+ // A defined tool is both: tests reach the typed handler and parameters, the registry sees BrowserTool.
110
+ export type DefinedBrowserTool<S extends TSchema, D extends TSchema = UntypedDetailsSchema> =
111
+ BrowserToolDefinition<S, D> & BrowserTool;
112
+
113
+ // pi does not export ToolRenderContext from its index, so it is read off the renderer signature.
114
+ type RenderResultContext<S extends TSchema> = Parameters<NonNullable<ToolDefinition<S>["renderResult"]>>[3];
115
+
116
+ export const renderToolError = (toolName: string, e: ToolErr): string =>
117
+ `${toolName} failed (${e.kind}): ${e.message}`;
118
+
119
+ // Stored details are whatever the handler attached; the schema (not a marker) is what tells a renderer they
120
+ // are usable on replay.
121
+ const okResult = <T>(u: ToolOk<T>): AgentToolResult<unknown> => ({
122
+ content: [{ type: "text", text: u.text }],
123
+ details: u.details,
124
+ });
125
+
126
+ const storedText = (result: AgentToolResult<unknown>, fallback: string): string => {
127
+ const first = result.content[0];
128
+ return first?.type === "text" ? first.text : fallback;
129
+ };
130
+
131
+ // Readonly-ness is a compile-time view, so a value the compiled schema accepts is a DetailsOf<D> value. This
132
+ // is the one place the predicate is asserted; tools get their details typed through it.
133
+ // Compiled once: most tools declare no schema, and compiling the shared fallback per tool is wasted codegen.
134
+ const UNTYPED_DETAILS_VALIDATOR = Compile(UntypedDetailsSchema);
135
+
136
+ const detailsGuard = <D extends TSchema>(schema: D | undefined): ((v: unknown) => v is DetailsOf<D>) => {
137
+ const validator = schema ? Compile(schema) : UNTYPED_DETAILS_VALIDATOR;
138
+ return (v: unknown): v is DetailsOf<D> => validator.Check(v);
139
+ };
140
+
141
+ const notConnected = (message: string): Result<never, ToolErr> =>
142
+ err({ kind: "not_connected", message });
143
+
144
+ const runHandler = async <S extends TSchema, D extends TSchema>(
145
+ def: BrowserToolDefinition<S, D>,
146
+ client: BrowserClient,
147
+ args: Static<S>,
148
+ signal: AbortSignal | undefined,
149
+ onUpdate: ((partial: AgentToolResult<unknown>) => void) | undefined,
150
+ extensionCtx: ExtensionContext,
151
+ ): Promise<Result<ToolOk<DetailsOf<D>>, ToolErr>> => {
152
+ if (def.ensureAlive !== false) {
153
+ if (!(await daemonSocketMayExist())) {
154
+ return notConnected("Browser harness not initialized. Run /browser-setup first");
155
+ }
156
+ const alive = await client.ensureAlive();
157
+ if (!alive.success) return notConnected(alive.error.message);
158
+ }
159
+ let release: (() => void) | undefined;
160
+ if (def.concurrency !== "parallel") {
161
+ release = await client.mutationMutex().acquire();
162
+ if (signal?.aborted) {
163
+ release();
164
+ return err({ kind: "invalid_state", message: "Tool execution aborted before entering serialized lane" });
165
+ }
166
+ }
167
+ try {
168
+ return await def.handler(args, {
169
+ client,
170
+ signal,
171
+ onUpdate: (u) => onUpdate?.(okResult(u)),
172
+ extensionCtx,
173
+ });
174
+ } catch (e) {
175
+ // A throw is a bug in the handler, not a tool outcome; it still has to reach the model as an error.
176
+ return err({ kind: "internal", message: e instanceof Error ? e.message : String(e) });
177
+ } finally {
178
+ release?.();
85
179
  }
86
- const details: ErrDetails = {
87
- ok: false,
88
- kind: r.error.kind,
89
- message: r.error.message,
90
- ...(r.error.details ?? {}),
91
- };
92
- return {
93
- isError: true,
94
- content: [{ type: "text", text: `${toolName} failed (${r.error.kind}): ${r.error.message}` }],
95
- details,
96
- };
97
180
  };
98
181
 
99
- export const registerBrowserTool = (
100
- pi: ExtensionAPI,
182
+ // pi has no error field on a resolved tool result: the agent loop marks a result isError only when
183
+ // execute rejects, and then stores the thrown message as the result text. So a failed handler throws.
184
+ const toPiTool = <S extends TSchema, D extends TSchema>(
185
+ def: BrowserToolDefinition<S, D>,
101
186
  client: BrowserClient,
102
- def: AnyBrowserToolDefinition,
103
- ): void => {
104
- type S = TSchema;
187
+ ): ToolDefinition<S> => {
105
188
  const defRenderCall = def.renderCall;
106
189
  const defRenderResult = def.renderResult;
107
- const td: ToolDefinition<S> = {
190
+ const isDetails = detailsGuard(def.details);
191
+ return {
108
192
  name: def.name,
109
193
  label: def.label,
110
194
  description: def.description,
@@ -112,11 +196,7 @@ export const registerBrowserTool = (
112
196
  promptGuidelines: [...def.promptGuidelines],
113
197
  parameters: def.parameters,
114
198
  ...(defRenderCall
115
- ? {
116
- renderCall: (args: Static<S>, theme: Theme, _ctx: Parameters<NonNullable<ToolDefinition<S>["renderCall"]>>[2]) =>
117
- // Sanctioned cast: at runtime args is always the concrete Static<S> value the tool was defined with.
118
- (defRenderCall as (args: unknown, theme: Theme) => Component)(args, theme),
119
- }
199
+ ? { renderCall: (args: Static<S>, theme: Theme) => defRenderCall(args, theme) }
120
200
  : {}),
121
201
  ...(defRenderResult
122
202
  ? {
@@ -124,64 +204,33 @@ export const registerBrowserTool = (
124
204
  result: AgentToolResult<unknown>,
125
205
  options: ToolRenderResultOptions,
126
206
  theme: Theme,
127
- _ctx: Parameters<NonNullable<ToolDefinition<S>["renderResult"]>>[3],
128
- ) => defRenderResult(result, options.expanded ?? false, theme),
207
+ ctx: RenderResultContext<S>,
208
+ ) => {
209
+ // A replayed error carries only the thrown text (details are empty), so the tool's own renderer never sees it.
210
+ if (ctx.isError) {
211
+ const text = storedText(result, `${def.name} failed (internal): unknown error`);
212
+ return new Text(theme.fg("error", text), 0, 0);
213
+ }
214
+ // Replayed details may predate the schema (older session, changed shape); then the stored text stands in.
215
+ const details: unknown = result.details;
216
+ if (!isDetails(details)) return new Text(storedText(result, ""), 0, 0);
217
+ return defRenderResult({ ...result, details }, options.expanded, theme);
218
+ },
129
219
  }
130
220
  : {}),
131
221
  async execute(_toolCallId, args, signal, onUpdate, extensionCtx) {
132
- if (def.ensureAlive !== false) {
133
- if (!(await daemonSocketMayExist())) {
134
- return toToolResult(
135
- {
136
- success: false,
137
- error: {
138
- kind: "not_connected",
139
- message: "Browser harness not initialized. Run /browser-setup first",
140
- },
141
- },
142
- def.name,
143
- );
144
- }
145
- const alive = await client.ensureAlive();
146
- if (!alive.success) {
147
- return toToolResult(
148
- { success: false, error: { kind: "not_connected", message: alive.error.message } },
149
- def.name,
150
- );
151
- }
152
- }
153
- let release: (() => void) | undefined;
154
- if (def.concurrency !== "parallel") {
155
- const acquire = await client.mutationMutex().acquire();
156
- release = acquire;
157
- if (signal?.aborted) {
158
- release();
159
- return toToolResult(
160
- { success: false, error: { kind: "invalid_state", message: "Tool execution aborted before entering serialized lane" } },
161
- def.name,
162
- );
163
- }
164
- }
165
- try {
166
- const result = await def.handler(args, {
167
- client,
168
- signal,
169
- onUpdate: (u) => {
170
- if (onUpdate) {
171
- const update: AgentToolResult<OkDetails> = {
172
- content: [{ type: "text", text: u.text }],
173
- details: { ok: true, ...(u.details ?? {}) },
174
- };
175
- onUpdate(update);
176
- }
177
- },
178
- extensionCtx,
179
- });
180
- return toToolResult(result, def.name);
181
- } finally {
182
- release?.();
183
- }
222
+ const result = await runHandler(def, client, args, signal, onUpdate, extensionCtx);
223
+ if (!result.success) throw new Error(renderToolError(def.name, result.error));
224
+ return okResult(result.data);
184
225
  },
185
226
  };
186
- pi.registerTool(td);
187
227
  };
228
+
229
+ export const defineBrowserTool = <S extends TSchema, D extends TSchema = UntypedDetailsSchema>(
230
+ def: BrowserToolDefinition<S, D>,
231
+ ): DefinedBrowserTool<S, D> => ({
232
+ ...def,
233
+ register: (pi, client) => {
234
+ pi.registerTool(toPiTool(def, client));
235
+ },
236
+ });
@@ -13,7 +13,7 @@ const tempDirs: string[] = [];
13
13
 
14
14
  export const cleanupTempDirs = async (): Promise<void> => {
15
15
  const dirs = tempDirs.splice(0);
16
- await Promise.all(dirs.map((d) => rm(d, { recursive: true, force: true }).catch(() => {})));
16
+ await Promise.all(dirs.map((d) => rm(d, { recursive: true, force: true }).catch(() => undefined)));
17
17
  };
18
18
 
19
19
  export type TruncatedOutput = {
package/tsconfig.json CHANGED
@@ -8,5 +8,6 @@
8
8
  "sourceMap": true
9
9
  },
10
10
  "include": ["src/**/*.ts"],
11
- "exclude": ["node_modules", "dist"]
11
+ "exclude": ["node_modules", "dist", "src/**/*.page.ts"],
12
+ "references": [{ "path": "./tsconfig.page.json" }]
12
13
  }
@@ -1,80 +0,0 @@
1
- export type FillOptions = {
2
- readonly rejectSelect: boolean;
3
- readonly focusFirst: boolean;
4
- };
5
-
6
- const ENGINE = `
7
- const el = this;
8
- if (!el || el.nodeType !== 1) return { ok: false, reason: "ref does not point to an element" };
9
- const tagName = el.tagName || "";
10
- const tag = tagName.toLowerCase();
11
- const type = (el.type || "").toLowerCase();
12
-
13
- if (__rejectSelect && tag === "select") {
14
- return { ok: false, reason: "element is a <select>", kind: "select", tag: tagName };
15
- }
16
- if (el.disabled) return { ok: false, reason: "element is disabled", tag: tagName };
17
-
18
- const fire = function (t) { el.dispatchEvent(new Event(t, { bubbles: true })); };
19
- if (__focusFirst) { try { el.focus(); } catch (e) {} }
20
-
21
- if (tag === "select") {
22
- const want = String(value);
23
- let matched = null;
24
- for (let i = 0; i < el.options.length; i++) {
25
- const o = el.options[i];
26
- if (o.value === want || o.label === want || o.text === want) { matched = o; break; }
27
- }
28
- if (!matched) {
29
- const opts = [];
30
- for (let i = 0; i < el.options.length; i++) opts.push({ value: el.options[i].value, text: el.options[i].text });
31
- return { ok: false, reason: "no matching option", kind: "select", options: opts, tag: tagName };
32
- }
33
- el.value = matched.value;
34
- fire("input"); fire("change");
35
- return { ok: true, kind: "select", value: el.value, text: matched.text, tag: tagName };
36
- }
37
-
38
- if (tag === "input" && (type === "checkbox" || type === "radio")) {
39
- const want = value === true || value === "true" || value === "on" || value === 1;
40
- let changed = false;
41
- if (el.checked !== want) {
42
- try { el.click(); } catch (e) {}
43
- if (el.checked !== want) { el.checked = want; fire("input"); fire("change"); }
44
- changed = true;
45
- }
46
- return { ok: true, kind: type, checked: el.checked, changed: changed, tag: tagName };
47
- }
48
-
49
- if (el.isContentEditable) {
50
- try { el.focus(); } catch (e) {}
51
- let done = false;
52
- try {
53
- const sel = window.getSelection();
54
- if (sel) { sel.removeAllRanges(); const rng = document.createRange(); rng.selectNodeContents(el); sel.addRange(rng); }
55
- done = document.execCommand("insertText", false, String(value));
56
- } catch (e) { done = false; }
57
- if (!done) {
58
- el.textContent = String(value);
59
- el.dispatchEvent(new InputEvent("input", { bubbles: true }));
60
- }
61
- return { ok: true, kind: "contenteditable", value: el.textContent || "", tag: tagName };
62
- }
63
-
64
- if (tag === "input" || tag === "textarea") {
65
- const proto = tag === "textarea" ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
66
- const desc = Object.getOwnPropertyDescriptor(proto, "value");
67
- if (desc && desc.set) desc.set.call(el, String(value));
68
- else el.value = String(value);
69
- fire("input"); fire("change");
70
- return { ok: true, kind: el.type || tag, value: el.value, tag: tagName };
71
- }
72
-
73
- return { ok: false, reason: "element is not a fillable field (tag=" + tag + ")", tag: tagName };
74
- `;
75
-
76
- export const fillBody = (opts: FillOptions): string =>
77
- `const __rejectSelect = ${opts.rejectSelect}; const __focusFirst = ${opts.focusFirst};\n${ENGINE}`;
78
-
79
- export const fillDeclaration = (opts: FillOptions): string =>
80
- `function (value) { ${fillBody(opts)} }`;