@descryy/runtime-browser 0.4.8 → 0.5.1

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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Recording: turns a HUMAN's own interactions with an already-open page into
3
+ * an ordered, timestamped stream a host can hand straight to a scenario
4
+ * builder -- the descry-core follow-up this package owes
5
+ * (`documents/decisions-inbox/DEC-NEXT-browser-recording-primitive.md`,
6
+ * `descry-core`'s `packages/mcp/src/browser/driver.ts`:
7
+ * `BrowserPageSession.startRecording`/`stopRecording`). Nothing here writes
8
+ * anything or drives the page itself -- the opposite direction from
9
+ * `BrowserActionCollector`, which *commands* the page and reports what it
10
+ * did. This collector only listens.
11
+ *
12
+ * Four kinds of interaction, matching the port's own `BrowserActionKind`
13
+ * vocabulary so the host can forward a captured event as a
14
+ * `PerformedTarget`/`PerformedStep` with no translation of the action name
15
+ * itself:
16
+ * - `click`, on the same interactive-element set `playwright-driver.ts`'s
17
+ * own `INTERACTIVE` constant already uses for a snapshot
18
+ * (`a, button, input, select, textarea, [role], [onclick], summary`) --
19
+ * clicking a checkbox or radio is captured here, not by the `change`
20
+ * listener below (see that listener's own comment on why).
21
+ * - `fill`, for a text field's value once it is COMMITTED -- the native
22
+ * `change` event, which a text `<input>`/`<textarea>` fires on blur only
23
+ * when its value actually changed since focus. Not `input`: capturing
24
+ * every keystroke would record a value no scenario replay wants (a
25
+ * partially-typed string), and `fill`, not `type`, because a recording
26
+ * only ever knows a field's final value, never the keystroke sequence
27
+ * that produced it -- the same distinction `browser-action-collector.ts`
28
+ * draws between its two text verbs, resolved here the only way it can be.
29
+ * - `select`, for a `<select>`'s `change` event -- its own branch, not the
30
+ * text-field one, because "a chooser's value changed" and "a field's text
31
+ * was committed" are different facts (mirrors `BrowserActionCollector
32
+ * .select()`).
33
+ * - `navigate`, for a real top-level (main-frame) navigation, observed
34
+ * directly off Playwright's own `framenavigated` rather than through the
35
+ * page-side script -- the frame that navigated *is* the fact, and asking
36
+ * the page to report its own navigation would miss the first instant of
37
+ * the new document.
38
+ *
39
+ * **Passwords never leave the page.** A field this collector's page-side
40
+ * script recognises as a password field (`type="password"`, or
41
+ * `autocomplete="current-password"`/`"new-password"`) reports `secret: true`
42
+ * and never reads `.value` into the payload it sends across the binding at
43
+ * all -- not captured-then-redacted, never captured. `text` on the resulting
44
+ * `RecordedInteraction` is `null` whenever `secret` is `true`; assembling the
45
+ * actual `${credential:NAME}` placeholder is the host's job
46
+ * (`recordedSecretCredentialName`/`recordedSecretPlaceholder`,
47
+ * `descry-core`'s `driver.ts`) because only the host knows this session's
48
+ * running count of secrets recorded so far -- the ordinal that keeps two
49
+ * distinct password fields from colliding onto the same credential name.
50
+ *
51
+ * **Selector preference** reuses the identical tiered rule
52
+ * `playwright-driver.ts`'s own `computeSelector` already applies for a
53
+ * snapshot element: a `data-testid`/`data-test` hook first, then a
54
+ * non-generated `id`, then a named form control, then an
55
+ * aria-labelled-and-roled element, then a bounded structural path anchored at
56
+ * the nearest ancestor carrying one of the above -- each tier accepted only
57
+ * when it is unique in the element's own root (`document`, or the open
58
+ * shadow root it lives in). It is restated here, not imported, for the same
59
+ * reason `driver.ts` restates `PerformedTarget` rather than importing a
60
+ * scenario type: the string below is serialised into the page verbatim
61
+ * (`page.addInitScript`/`page.evaluate`), and a page-side script cannot
62
+ * import a Node module.
63
+ *
64
+ * **Listener lifecycle.** `page.exposeFunction` and `page.addInitScript` are
65
+ * each installed at most once per collector instance (lazily, on the first
66
+ * `start()`) -- Playwright refuses a second `exposeFunction` under the same
67
+ * name, and a second `addInitScript` would double-register the in-page
68
+ * listeners on every navigation after that. Because `addInitScript` "doesn't
69
+ * reach an already-loaded document" (the same contract every other probe in
70
+ * this package documents -- `frontend-initiator-capture.ts`,
71
+ * `identity-grounding`'s probes), `start()` ALSO runs the identical
72
+ * installer directly via `page.evaluate` against whatever document is
73
+ * current right now, idempotently (a `window` marker skips a second
74
+ * install), so a recording that starts mid-session on an already-open page
75
+ * is not silently missing its first document's events.
76
+ *
77
+ * A capturing session can therefore be started and stopped more than once
78
+ * without re-installing anything: `start()`/`stop()` do not attach or detach
79
+ * the page-side listeners themselves (Playwright exposes no way to
80
+ * un-expose a function or un-register an init script), they gate a
81
+ * `capturing` flag the Node-side binding handler checks before pushing to
82
+ * the buffer -- the identical "an unattached listener produces nothing"
83
+ * discipline `descry-core`'s fake driver already documents for its own
84
+ * `recordHuman*` methods. The one exception is the `framenavigated`
85
+ * listener, which genuinely is `page.on`/`page.off` in `start()`/`stop()`,
86
+ * matching `BrowserConsoleCollector`'s own precedent, because Playwright
87
+ * offers a real way to detach it.
88
+ */
89
+ import type { BrowserSession } from "./browser-session.ts";
90
+ /** The element actually acted on -- role, accessible name, a selector when one is addressable. Mirrors the port's own `PerformedTarget` field-for-field so a host can forward this unmodified. */
91
+ export interface RecordedTarget {
92
+ readonly role: string;
93
+ readonly name: string;
94
+ /** Null when no candidate was unique in this element's own root -- the same "no guessed selector" rule every other selector computation in this repo follows. */
95
+ readonly selector: string | null;
96
+ }
97
+ /** One human-driven interaction, in the order it was observed. */
98
+ export interface RecordedInteraction {
99
+ readonly action: "click" | "fill" | "select" | "navigate";
100
+ /** The element acted on. Null for `navigate`, which acts on the page, not an element. */
101
+ readonly target: RecordedTarget | null;
102
+ /** Where a `navigate` went. Null for every other action. */
103
+ readonly url: string | null;
104
+ /** The committed value, for `fill`/`select`. Null for `click`/`navigate`, and ALWAYS null when `secret` is true -- see this module's header. */
105
+ readonly text: string | null;
106
+ /** True only for a `fill` whose source field this collector's page-side script recognised as a password field. When true, `text` is null and was never populated from the field's real value in the first place. */
107
+ readonly secret: boolean;
108
+ readonly at: string;
109
+ }
110
+ export interface BrowserRecordingCollectorOptions {
111
+ readonly session: BrowserSession;
112
+ }
113
+ export interface BrowserRecordingCollector {
114
+ /** Starts (or resumes) capturing. Throws if the session's page has already crashed or closed, or if this collector is already capturing -- call `stop()` first. */
115
+ start(): Promise<void>;
116
+ /** Stops capturing and returns every interaction captured since `start()`, in order. Throws if this collector is not currently capturing. */
117
+ stop(): Promise<readonly RecordedInteraction[]>;
118
+ }
119
+ export declare function createBrowserRecordingCollector(options: BrowserRecordingCollectorOptions): BrowserRecordingCollector;
120
+ //# sourceMappingURL=browser-recording-collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-recording-collector.d.ts","sourceRoot":"","sources":["../src/browser-recording-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,kMAAkM;AAClM,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iKAAiK;IACjK,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,kEAAkE;AAClE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;IAC1D,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gJAAgJ;IAChJ,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,oNAAoN;IACpN,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC,mKAAmK;IACnK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,6IAA6I;IAC7I,IAAI,IAAI,OAAO,CAAC,SAAS,mBAAmB,EAAE,CAAC,CAAC;CACjD;AAqOD,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,gCAAgC,GAAG,yBAAyB,CAuEpH"}
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Recording: turns a HUMAN's own interactions with an already-open page into
3
+ * an ordered, timestamped stream a host can hand straight to a scenario
4
+ * builder -- the descry-core follow-up this package owes
5
+ * (`documents/decisions-inbox/DEC-NEXT-browser-recording-primitive.md`,
6
+ * `descry-core`'s `packages/mcp/src/browser/driver.ts`:
7
+ * `BrowserPageSession.startRecording`/`stopRecording`). Nothing here writes
8
+ * anything or drives the page itself -- the opposite direction from
9
+ * `BrowserActionCollector`, which *commands* the page and reports what it
10
+ * did. This collector only listens.
11
+ *
12
+ * Four kinds of interaction, matching the port's own `BrowserActionKind`
13
+ * vocabulary so the host can forward a captured event as a
14
+ * `PerformedTarget`/`PerformedStep` with no translation of the action name
15
+ * itself:
16
+ * - `click`, on the same interactive-element set `playwright-driver.ts`'s
17
+ * own `INTERACTIVE` constant already uses for a snapshot
18
+ * (`a, button, input, select, textarea, [role], [onclick], summary`) --
19
+ * clicking a checkbox or radio is captured here, not by the `change`
20
+ * listener below (see that listener's own comment on why).
21
+ * - `fill`, for a text field's value once it is COMMITTED -- the native
22
+ * `change` event, which a text `<input>`/`<textarea>` fires on blur only
23
+ * when its value actually changed since focus. Not `input`: capturing
24
+ * every keystroke would record a value no scenario replay wants (a
25
+ * partially-typed string), and `fill`, not `type`, because a recording
26
+ * only ever knows a field's final value, never the keystroke sequence
27
+ * that produced it -- the same distinction `browser-action-collector.ts`
28
+ * draws between its two text verbs, resolved here the only way it can be.
29
+ * - `select`, for a `<select>`'s `change` event -- its own branch, not the
30
+ * text-field one, because "a chooser's value changed" and "a field's text
31
+ * was committed" are different facts (mirrors `BrowserActionCollector
32
+ * .select()`).
33
+ * - `navigate`, for a real top-level (main-frame) navigation, observed
34
+ * directly off Playwright's own `framenavigated` rather than through the
35
+ * page-side script -- the frame that navigated *is* the fact, and asking
36
+ * the page to report its own navigation would miss the first instant of
37
+ * the new document.
38
+ *
39
+ * **Passwords never leave the page.** A field this collector's page-side
40
+ * script recognises as a password field (`type="password"`, or
41
+ * `autocomplete="current-password"`/`"new-password"`) reports `secret: true`
42
+ * and never reads `.value` into the payload it sends across the binding at
43
+ * all -- not captured-then-redacted, never captured. `text` on the resulting
44
+ * `RecordedInteraction` is `null` whenever `secret` is `true`; assembling the
45
+ * actual `${credential:NAME}` placeholder is the host's job
46
+ * (`recordedSecretCredentialName`/`recordedSecretPlaceholder`,
47
+ * `descry-core`'s `driver.ts`) because only the host knows this session's
48
+ * running count of secrets recorded so far -- the ordinal that keeps two
49
+ * distinct password fields from colliding onto the same credential name.
50
+ *
51
+ * **Selector preference** reuses the identical tiered rule
52
+ * `playwright-driver.ts`'s own `computeSelector` already applies for a
53
+ * snapshot element: a `data-testid`/`data-test` hook first, then a
54
+ * non-generated `id`, then a named form control, then an
55
+ * aria-labelled-and-roled element, then a bounded structural path anchored at
56
+ * the nearest ancestor carrying one of the above -- each tier accepted only
57
+ * when it is unique in the element's own root (`document`, or the open
58
+ * shadow root it lives in). It is restated here, not imported, for the same
59
+ * reason `driver.ts` restates `PerformedTarget` rather than importing a
60
+ * scenario type: the string below is serialised into the page verbatim
61
+ * (`page.addInitScript`/`page.evaluate`), and a page-side script cannot
62
+ * import a Node module.
63
+ *
64
+ * **Listener lifecycle.** `page.exposeFunction` and `page.addInitScript` are
65
+ * each installed at most once per collector instance (lazily, on the first
66
+ * `start()`) -- Playwright refuses a second `exposeFunction` under the same
67
+ * name, and a second `addInitScript` would double-register the in-page
68
+ * listeners on every navigation after that. Because `addInitScript` "doesn't
69
+ * reach an already-loaded document" (the same contract every other probe in
70
+ * this package documents -- `frontend-initiator-capture.ts`,
71
+ * `identity-grounding`'s probes), `start()` ALSO runs the identical
72
+ * installer directly via `page.evaluate` against whatever document is
73
+ * current right now, idempotently (a `window` marker skips a second
74
+ * install), so a recording that starts mid-session on an already-open page
75
+ * is not silently missing its first document's events.
76
+ *
77
+ * A capturing session can therefore be started and stopped more than once
78
+ * without re-installing anything: `start()`/`stop()` do not attach or detach
79
+ * the page-side listeners themselves (Playwright exposes no way to
80
+ * un-expose a function or un-register an init script), they gate a
81
+ * `capturing` flag the Node-side binding handler checks before pushing to
82
+ * the buffer -- the identical "an unattached listener produces nothing"
83
+ * discipline `descry-core`'s fake driver already documents for its own
84
+ * `recordHuman*` methods. The one exception is the `framenavigated`
85
+ * listener, which genuinely is `page.on`/`page.off` in `start()`/`stop()`,
86
+ * matching `BrowserConsoleCollector`'s own precedent, because Playwright
87
+ * offers a real way to detach it.
88
+ */
89
+ const RECORDING_BINDING_NAME = "__descryRecordInteraction";
90
+ /** The same interactive-element set `playwright-driver.ts`'s `INTERACTIVE` constant already scopes a snapshot to -- kept identical so a click this collector captures is always one a snapshot-based replay could also have resolved. */
91
+ const INTERACTIVE_SELECTOR = "a, button, input, select, textarea, [role], [onclick], summary";
92
+ /**
93
+ * Serialised and run inside the page (`page.addInitScript`/`page.evaluate`)
94
+ * -- closes over nothing in this module. Idempotent: re-running it against a
95
+ * document that already has the listeners installed (the `window` marker)
96
+ * is a deliberate no-op, so calling it once via `addInitScript`'s
97
+ * registration and once more directly via `page.evaluate` at `start()` time
98
+ * (for the document already open when recording starts) never double-fires
99
+ * an event.
100
+ */
101
+ function installerScriptSource() {
102
+ return `(() => {
103
+ const MARKER = "__descryRecordingInstalled";
104
+ if (window[MARKER] === true) return;
105
+ const bind = window[${JSON.stringify(RECORDING_BINDING_NAME)}];
106
+ if (typeof bind !== "function") return;
107
+ window[MARKER] = true;
108
+
109
+ const GENERATED_ID = /[0-9a-f]{8}|:r[0-9a-z]+:|^\\s*$/i;
110
+ const FORM_CONTROLS = ["input", "select", "textarea", "button"];
111
+ const TEXT_INPUT_TYPES = ["", "text", "email", "tel", "url", "search", "number", "password"];
112
+
113
+ function escapeAttrValue(value) {
114
+ return value.replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"');
115
+ }
116
+
117
+ function rootOf(el) {
118
+ return typeof el.getRootNode === "function" ? el.getRootNode() : el.ownerDocument;
119
+ }
120
+
121
+ function isUnique(root, candidate) {
122
+ try {
123
+ return root.querySelectorAll(candidate).length === 1;
124
+ } catch (e) {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ function stableHookSelector(node) {
130
+ const testId = node.getAttribute("data-testid");
131
+ if (testId && testId.trim() !== "") return '[data-testid="' + escapeAttrValue(testId) + '"]';
132
+ const testAttr = node.getAttribute("data-test");
133
+ if (testAttr && testAttr.trim() !== "") return '[data-test="' + escapeAttrValue(testAttr) + '"]';
134
+ const id = node.getAttribute("id");
135
+ if (id && !GENERATED_ID.test(id)) return '[id="' + escapeAttrValue(id) + '"]';
136
+ return null;
137
+ }
138
+
139
+ function nthOfTypeSegment(node, parent) {
140
+ const tag = node.tagName.toLowerCase();
141
+ const siblings = parent.querySelectorAll(":scope > " + tag);
142
+ if (siblings.length <= 1) return tag;
143
+ for (let i = 0; i < siblings.length; i += 1) {
144
+ if (siblings[i] === node) return tag + ":nth-of-type(" + (i + 1) + ")";
145
+ }
146
+ return tag;
147
+ }
148
+
149
+ function structuralPath(target, root) {
150
+ const segments = [];
151
+ let node = target;
152
+ for (let depth = 0; depth < 6; depth += 1) {
153
+ const parent = node.parentElement;
154
+ if (!parent) return null;
155
+ segments.unshift(nthOfTypeSegment(node, parent));
156
+ const hook = stableHookSelector(parent);
157
+ if (hook) {
158
+ const candidate = hook + " > " + segments.join(" > ");
159
+ return isUnique(root, candidate) ? candidate : null;
160
+ }
161
+ node = parent;
162
+ }
163
+ return null;
164
+ }
165
+
166
+ function computeSelector(target) {
167
+ const root = rootOf(target);
168
+ const testId = target.getAttribute("data-testid");
169
+ if (testId && testId.trim() !== "") {
170
+ const candidate = '[data-testid="' + escapeAttrValue(testId) + '"]';
171
+ if (isUnique(root, candidate)) return candidate;
172
+ }
173
+ const testAttr = target.getAttribute("data-test");
174
+ if (testAttr && testAttr.trim() !== "") {
175
+ const candidate = '[data-test="' + escapeAttrValue(testAttr) + '"]';
176
+ if (isUnique(root, candidate)) return candidate;
177
+ }
178
+ const id = target.getAttribute("id");
179
+ if (id && !GENERATED_ID.test(id)) {
180
+ const candidate = '[id="' + escapeAttrValue(id) + '"]';
181
+ if (isUnique(root, candidate)) return candidate;
182
+ }
183
+ const tag = target.tagName.toLowerCase();
184
+ const name = target.getAttribute("name");
185
+ if (name && name.trim() !== "" && FORM_CONTROLS.indexOf(tag) !== -1) {
186
+ const candidate = tag + '[name="' + escapeAttrValue(name) + '"]';
187
+ if (isUnique(root, candidate)) return candidate;
188
+ }
189
+ const explicitRole = target.getAttribute("role");
190
+ const ariaLabel = target.getAttribute("aria-label");
191
+ if (explicitRole && ariaLabel && ariaLabel.trim() !== "") {
192
+ const candidate = '[role="' + escapeAttrValue(explicitRole) + '"][aria-label="' + escapeAttrValue(ariaLabel) + '"]';
193
+ if (isUnique(root, candidate)) return candidate;
194
+ }
195
+ return structuralPath(target, root);
196
+ }
197
+
198
+ function derivedRole(el) {
199
+ const tag = el.tagName.toLowerCase();
200
+ const explicit = el.getAttribute("role");
201
+ if (explicit) return explicit;
202
+ const type = (el.getAttribute("type") || "").toLowerCase();
203
+ if (tag === "a") return "link";
204
+ if (tag === "button") return "button";
205
+ if (tag === "textarea") return "textbox";
206
+ if (tag === "summary") return "button";
207
+ if (tag === "select") {
208
+ const multiple = el.getAttribute("multiple") !== null;
209
+ const size = Number(el.getAttribute("size") || "1");
210
+ return multiple || size > 1 ? "listbox" : "combobox";
211
+ }
212
+ if (tag !== "input") return tag;
213
+ switch (type) {
214
+ case "button":
215
+ case "submit":
216
+ case "reset":
217
+ case "image":
218
+ return "button";
219
+ case "checkbox":
220
+ return "checkbox";
221
+ case "radio":
222
+ return "radio";
223
+ case "range":
224
+ return "slider";
225
+ case "number":
226
+ return "spinbutton";
227
+ case "search":
228
+ return "searchbox";
229
+ default:
230
+ return "textbox";
231
+ }
232
+ }
233
+
234
+ function derivedName(el) {
235
+ const tag = el.tagName.toLowerCase();
236
+ const type = (el.getAttribute("type") || "").toLowerCase();
237
+ const aria = el.getAttribute("aria-label");
238
+ if (aria && aria.trim() !== "") return aria;
239
+ if (tag !== "select") {
240
+ const inner = (el.innerText || "").trim();
241
+ if (inner !== "") return inner;
242
+ const text = (el.textContent || "").trim();
243
+ if (text !== "") return text;
244
+ }
245
+ if (tag === "input" && (type === "button" || type === "submit" || type === "reset")) {
246
+ const value = el.getAttribute("value");
247
+ if (value && value.trim() !== "") return value.trim();
248
+ }
249
+ return el.getAttribute("placeholder") || el.getAttribute("title") || el.getAttribute("name") || "";
250
+ }
251
+
252
+ function isPasswordField(el) {
253
+ if (el.tagName.toLowerCase() !== "input") return false;
254
+ const type = (el.getAttribute("type") || "").toLowerCase();
255
+ if (type === "password") return true;
256
+ const autocomplete = (el.getAttribute("autocomplete") || "").toLowerCase();
257
+ return autocomplete === "current-password" || autocomplete === "new-password";
258
+ }
259
+
260
+ function describeTarget(el) {
261
+ return { role: derivedRole(el), name: derivedName(el).slice(0, 200), selector: computeSelector(el) };
262
+ }
263
+
264
+ document.addEventListener(
265
+ "click",
266
+ function (ev) {
267
+ const el = ev.target && typeof ev.target.closest === "function" ? ev.target.closest(${JSON.stringify(INTERACTIVE_SELECTOR)}) : null;
268
+ if (!el) return;
269
+ const described = describeTarget(el);
270
+ bind({ action: "click", role: described.role, name: described.name, selector: described.selector, value: null, secret: false }).catch(function () {});
271
+ },
272
+ true,
273
+ );
274
+
275
+ document.addEventListener(
276
+ "change",
277
+ function (ev) {
278
+ const el = ev.target;
279
+ if (!el || !el.tagName) return;
280
+ const tag = el.tagName.toLowerCase();
281
+ if (tag === "select") {
282
+ const described = describeTarget(el);
283
+ const value = el.multiple ? Array.prototype.map.call(el.selectedOptions, function (o) { return o.value; }).join(",") : el.value;
284
+ bind({ action: "select", role: described.role, name: described.name, selector: described.selector, value: value, secret: false }).catch(function () {});
285
+ return;
286
+ }
287
+ if (tag === "input" || tag === "textarea") {
288
+ const type = (el.getAttribute("type") || "").toLowerCase();
289
+ // Only a text-shaped control's committed value is a "fill" -- a checkbox/radio's
290
+ // own change is a state toggle, not a value entered, and is already reported by
291
+ // the click listener above (the click that toggled it matches INTERACTIVE too).
292
+ if (tag === "input" && TEXT_INPUT_TYPES.indexOf(type) === -1) return;
293
+ const secret = isPasswordField(el);
294
+ const described = describeTarget(el);
295
+ // The whole point: a password field's real value is never read into this payload at all.
296
+ bind({ action: "fill", role: described.role, name: described.name, selector: described.selector, value: secret ? null : el.value, secret: secret }).catch(function () {});
297
+ }
298
+ },
299
+ true,
300
+ );
301
+ })();`;
302
+ }
303
+ export function createBrowserRecordingCollector(options) {
304
+ const { session } = options;
305
+ const page = session.page;
306
+ let capturing = false;
307
+ let buffer = [];
308
+ let bindingInstalled = false;
309
+ function onRecordedInteraction(payload) {
310
+ if (!capturing)
311
+ return; // an unattached listener producing nothing -- see this module's header.
312
+ buffer.push({
313
+ action: payload.action,
314
+ target: { role: payload.role, name: payload.name, selector: payload.selector },
315
+ url: null,
316
+ text: payload.secret ? null : payload.value,
317
+ secret: payload.secret,
318
+ at: new Date().toISOString(),
319
+ });
320
+ }
321
+ function onFrameNavigated(frame) {
322
+ if (!capturing)
323
+ return;
324
+ if (frame !== page.mainFrame())
325
+ return; // "top-level" -- an iframe's own navigation is not this session's page moving.
326
+ buffer.push({ action: "navigate", target: null, url: frame.url(), text: null, secret: false, at: new Date().toISOString() });
327
+ }
328
+ async function ensureInstalled() {
329
+ if (!bindingInstalled) {
330
+ // Playwright refuses a second `exposeFunction` under the same name and would
331
+ // double-register in-page listeners on a second `addInitScript` -- both installed
332
+ // at most once per collector instance, ever.
333
+ await page.exposeFunction(RECORDING_BINDING_NAME, onRecordedInteraction);
334
+ await page.addInitScript({ content: installerScriptSource() });
335
+ bindingInstalled = true;
336
+ }
337
+ // `addInitScript` only reaches documents navigated to AFTER it is registered -- this
338
+ // session may already be sitting on a page when recording starts, so the identical,
339
+ // idempotent installer also runs directly against whatever document is current now.
340
+ await page.evaluate(installerScriptSource()).catch(() => {
341
+ // No accessible document to evaluate against right now (e.g. mid-navigation). Not
342
+ // fatal: `addInitScript` still covers every document from here forward, so a
343
+ // human's first observable interaction is only missed if it happens in the exact
344
+ // instant no document is present to click or type into.
345
+ });
346
+ }
347
+ return {
348
+ async start() {
349
+ if (!session.isUsable()) {
350
+ throw new Error(`browser session ${session.sessionId}: the page has already crashed or closed.`);
351
+ }
352
+ if (capturing) {
353
+ throw new Error(`browser session ${session.sessionId}: already recording. Call stop() before starting again.`);
354
+ }
355
+ await ensureInstalled();
356
+ buffer = [];
357
+ capturing = true;
358
+ page.on("framenavigated", onFrameNavigated);
359
+ },
360
+ async stop() {
361
+ if (!capturing) {
362
+ throw new Error(`browser session ${session.sessionId}: not currently recording. Call start() first.`);
363
+ }
364
+ page.off("framenavigated", onFrameNavigated);
365
+ capturing = false;
366
+ const captured = buffer;
367
+ buffer = [];
368
+ return captured;
369
+ },
370
+ };
371
+ }
372
+ //# sourceMappingURL=browser-recording-collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-recording-collector.js","sourceRoot":"","sources":["../src/browser-recording-collector.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ export type { DomElementSnapshot } from "./dom-evidence.ts";
4
4
  export { captureElementSnapshot } from "./dom-evidence.ts";
5
5
  export type { BrowserActionCollectorOptions, BrowserActionCollector, WaitForOptions } from "./browser-action-collector.ts";
6
6
  export { createBrowserActionCollector } from "./browser-action-collector.ts";
7
+ export type { BrowserRecordingCollectorOptions, BrowserRecordingCollector, RecordedTarget, RecordedInteraction, } from "./browser-recording-collector.ts";
8
+ export { createBrowserRecordingCollector } from "./browser-recording-collector.ts";
7
9
  export type { BrowserConsoleCollectorOptions } from "./browser-console-collector.ts";
8
10
  export { createBrowserConsoleCollector } from "./browser-console-collector.ts";
9
11
  export type { BrowserNetworkCollectorOptions, CapturedResponseBody } from "./browser-network-collector.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,2BAA2B,EAC3B,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,0BAA0B,EAC1B,2BAA2B,EAC3B,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,EAIvB,0BAA0B,EAC1B,8BAA8B,GAC/B,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,YAAY,EAAE,6BAA6B,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC3H,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E,YAAY,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AACrF,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAE/E,YAAY,EAAE,8BAA8B,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAC3G,OAAO,EAAE,6BAA6B,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAE7H,YAAY,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAEvE,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,2BAA2B,EAC3B,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,0BAA0B,EAC1B,2BAA2B,EAC3B,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,EAIvB,0BAA0B,EAC1B,8BAA8B,GAC/B,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,YAAY,EAAE,6BAA6B,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC3H,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E,YAAY,EACV,gCAAgC,EAChC,yBAAyB,EACzB,cAAc,EACd,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,+BAA+B,EAAE,MAAM,kCAAkC,CAAC;AAEnF,YAAY,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AACrF,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAE/E,YAAY,EAAE,8BAA8B,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAC3G,OAAO,EAAE,6BAA6B,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAE7H,YAAY,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAEvE,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { launchBrowserSession, launchBrowserSessionOrRefuse, resolveChromiumExec
5
5
  createSharedBrowserManager, SHARED_BROWSER_IDLE_TIMEOUT_MS, } from "./browser-session.js";
6
6
  export { captureElementSnapshot } from "./dom-evidence.js";
7
7
  export { createBrowserActionCollector } from "./browser-action-collector.js";
8
+ export { createBrowserRecordingCollector } from "./browser-recording-collector.js";
8
9
  export { createBrowserConsoleCollector } from "./browser-console-collector.js";
9
10
  export { createBrowserNetworkCollector, captureResponseBody, MAX_RESPONSE_BODY_BYTES } from "./browser-network-collector.js";
10
11
  export { extractHeaderCorrelationIds } from "./network-correlation.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB;AACvB,8EAA8E;AAC9E,qFAAqF;AACrF,mFAAmF;AACnF,0BAA0B,EAC1B,8BAA8B,GAC/B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAG7E,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAG/E,OAAO,EAAE,6BAA6B,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAG7H,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAGvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB;AACvB,8EAA8E;AAC9E,qFAAqF;AACrF,mFAAmF;AACnF,0BAA0B,EAC1B,8BAA8B,GAC/B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAQ7E,OAAO,EAAE,+BAA+B,EAAE,MAAM,kCAAkC,CAAC;AAGnF,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAG/E,OAAO,EAAE,6BAA6B,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAG7H,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAGvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descryy/runtime-browser",
3
- "version": "0.4.8",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "Browser runtime: launch, isolated context, navigation, click/type/wait, DOM inspection, screenshots, console/page-error/network capture, all through the Collector contract (plan §7/§8). See documents/decisions-inbox/PROPOSAL-B-playwright-for-browser-runtime.md for the Playwright-vs-CDP call. BrowserSession.injectStorageState (0.4.5) is the mid-session counterpart to the launch-time auth seed -- lands cookies/localStorage into a session that is already open, for a scenario's sign-in step.",
6
6
  "license": "UNLICENSED",
@@ -25,17 +25,17 @@
25
25
  "registry": "https://registry.npmjs.org",
26
26
  "access": "public"
27
27
  },
28
- "scripts": {
29
- "build": "tsc -b"
30
- },
31
28
  "dependencies": {
32
29
  "@descryy/runtime-contracts": "0.3.3",
33
30
  "@descryy/runtime-backend-observation": "0.3.6",
34
31
  "playwright-core": "^1.62.1",
35
- "@descryy/runtime-adapter-typescript": "0.3.7"
32
+ "@descryy/runtime-adapter-typescript": "0.3.9"
36
33
  },
37
34
  "devDependencies": {
38
- "@descryy/runtime-controller": "0.3.7",
35
+ "@descryy/runtime-controller": "0.3.9",
39
36
  "@descryy/runtime-evidence-store": "0.4.2"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -b"
40
40
  }
41
- }
41
+ }