/** * dyad-recorder-client.js * * Injected into the preview iframe by the proxy server (see proxy_server.js). * Observes user interactions while recording is active and reports a normalized * action stream to the renderer via postMessage. OBSERVE-ONLY: it never calls * preventDefault, so the app behaves exactly as it would without the recorder. * * Capture semantics are a small port of Playwright's in-page RecordActionTool: * text entry becomes a single `fill` with the full value, form-control toggles * come from `change`, and selectors prefer stable, human-readable locators. * Clicks are reported immediately — a click that navigates would otherwise be * lost with the unloading document — and only the first click of a multi-click * gesture is reported, so the renderer's `collapseActions` folds exactly one * click into the `dblclick` that follows it. * * Plain, dependency-free IIFE JS: it is read verbatim and injected into every * previewed HTML document. See `src/lib/test_recorder/types.ts` for the schemas * of the actions and locator descriptors it emits. * * Protocol: * down (from parent): { type: "activate-dyad-recorder" | "deactivate-dyad-recorder", token } * { type: "flush-dyad-recorder", token, requestId } * up (to parent): { type: "dyad-recorder-initialized" } * { type: "dyad-recorder-action", action: RecordedAction } * { type: "dyad-recorder-flushed", requestId } * * Known limitation — the top-level preview document only. This script is * injected into nested frames too, but Dyad activates only the frame it embeds, * and `window.parent` from a nested document is the app, not the renderer. So * interactions inside an app's own iframes are absent from the recording rather * than misrecorded. Covering them needs the activation relayed down, the actions * relayed back up, and a frame reference carried on each action so replay can * address it via `page.frameLocator` — without that last part a relayed action * would replay against the main frame and silently assert the wrong document. * Tracked as follow-up work; see the PR discussion. */ (() => { // Read once, here, and then take the global away. // // The vm/happy-dom unit test sets this before the script runs so it can drive // the recorder with synthetic events. Nothing else legitimately sets it, and // the proxy splices this script in immediately after `` — so every one // of the app's own scripts, inline or bundled or from a CDN, runs strictly // after this line. Snapshotting is therefore enough to stop a page script // flipping the recorder into accepting untrusted events mid-recording and // fabricating clicks and fills that land in a spec the user commits and runs. const allowUntrustedEvents = window.__DYAD_RECORDER_ALLOW_UNTRUSTED__ === true; // Minted per proxy and known only to the trusted renderer. Without this, an // unrelated page can frame the localhost preview, arm the recorder, and // receive every captured fill value through the parent message channel. // // Removed from the DOM once read, for the same reason the global below is // deleted: the app's own scripts all run after this line, but the attribute // would otherwise stay in the document for any of them — including CDN code // in an AI-generated app — to lift with one `querySelector`. const recorderToken = document.currentScript?.dataset.dyadRecorderToken; try { document.currentScript?.removeAttribute("data-dyad-recorder-token"); } catch { // Best effort: the value is already snapshotted above either way. } try { delete window.__DYAD_RECORDER_ALLOW_UNTRUSTED__; } catch { // Non-configurable, so someone is holding it deliberately. The snapshot // above already decided this session's answer either way. } const OVERLAY_CLASS = "__dyad_recorder_overlay__"; // Absorbs the synthetic duplicate the browser dispatches when a