@tangle-network/browser-agent-driver 0.12.1 → 0.14.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 (90) hide show
  1. package/README.md +81 -0
  2. package/dist/brain/index.d.ts.map +1 -1
  3. package/dist/brain/index.js +88 -6
  4. package/dist/brain/index.js.map +1 -1
  5. package/dist/cli-design-audit.d.ts +2 -0
  6. package/dist/cli-design-audit.d.ts.map +1 -1
  7. package/dist/cli-design-audit.js +111 -303
  8. package/dist/cli-design-audit.js.map +1 -1
  9. package/dist/cli-view.d.ts +83 -0
  10. package/dist/cli-view.d.ts.map +1 -0
  11. package/dist/cli-view.js +397 -0
  12. package/dist/cli-view.js.map +1 -0
  13. package/dist/cli.js +32 -1
  14. package/dist/cli.js.map +1 -1
  15. package/dist/design/audit/classify.d.ts +20 -0
  16. package/dist/design/audit/classify.d.ts.map +1 -0
  17. package/dist/design/audit/classify.js +148 -0
  18. package/dist/design/audit/classify.js.map +1 -0
  19. package/dist/design/audit/evaluate.d.ts +49 -0
  20. package/dist/design/audit/evaluate.d.ts.map +1 -0
  21. package/dist/design/audit/evaluate.js +355 -0
  22. package/dist/design/audit/evaluate.js.map +1 -0
  23. package/dist/design/audit/measure/a11y.d.ts +25 -0
  24. package/dist/design/audit/measure/a11y.d.ts.map +1 -0
  25. package/dist/design/audit/measure/a11y.js +144 -0
  26. package/dist/design/audit/measure/a11y.js.map +1 -0
  27. package/dist/design/audit/measure/contrast.d.ts +18 -0
  28. package/dist/design/audit/measure/contrast.d.ts.map +1 -0
  29. package/dist/design/audit/measure/contrast.js +201 -0
  30. package/dist/design/audit/measure/contrast.js.map +1 -0
  31. package/dist/design/audit/measure/index.d.ts +14 -0
  32. package/dist/design/audit/measure/index.d.ts.map +1 -0
  33. package/dist/design/audit/measure/index.js +38 -0
  34. package/dist/design/audit/measure/index.js.map +1 -0
  35. package/dist/design/audit/pipeline.d.ts +33 -0
  36. package/dist/design/audit/pipeline.d.ts.map +1 -0
  37. package/dist/design/audit/pipeline.js +109 -0
  38. package/dist/design/audit/pipeline.js.map +1 -0
  39. package/dist/design/audit/roi.d.ts +49 -0
  40. package/dist/design/audit/roi.d.ts.map +1 -0
  41. package/dist/design/audit/roi.js +157 -0
  42. package/dist/design/audit/roi.js.map +1 -0
  43. package/dist/design/audit/rubric/fragments/domain-ai.md +19 -0
  44. package/dist/design/audit/rubric/fragments/domain-crypto.md +24 -0
  45. package/dist/design/audit/rubric/fragments/domain-devtools.md +21 -0
  46. package/dist/design/audit/rubric/fragments/domain-fintech.md +19 -0
  47. package/dist/design/audit/rubric/fragments/maturity-prototype.md +36 -0
  48. package/dist/design/audit/rubric/fragments/type-blog.md +22 -0
  49. package/dist/design/audit/rubric/fragments/type-docs.md +23 -0
  50. package/dist/design/audit/rubric/fragments/type-ecommerce.md +23 -0
  51. package/dist/design/audit/rubric/fragments/type-marketing.md +22 -0
  52. package/dist/design/audit/rubric/fragments/type-saas-app.md +20 -0
  53. package/dist/design/audit/rubric/fragments/universal-calibration.md +16 -0
  54. package/dist/design/audit/rubric/fragments/universal-effort-anchor.md +46 -0
  55. package/dist/design/audit/rubric/fragments/universal-foundation.md +49 -0
  56. package/dist/design/audit/rubric/loader.d.ts +49 -0
  57. package/dist/design/audit/rubric/loader.d.ts.map +1 -0
  58. package/dist/design/audit/rubric/loader.js +216 -0
  59. package/dist/design/audit/rubric/loader.js.map +1 -0
  60. package/dist/design/audit/types.d.ts +166 -0
  61. package/dist/design/audit/types.d.ts.map +1 -0
  62. package/dist/design/audit/types.js +8 -0
  63. package/dist/design/audit/types.js.map +1 -0
  64. package/dist/drivers/cursor-overlay.d.ts +26 -0
  65. package/dist/drivers/cursor-overlay.d.ts.map +1 -0
  66. package/dist/drivers/cursor-overlay.js +235 -0
  67. package/dist/drivers/cursor-overlay.js.map +1 -0
  68. package/dist/drivers/playwright.d.ts +32 -0
  69. package/dist/drivers/playwright.d.ts.map +1 -1
  70. package/dist/drivers/playwright.js +79 -0
  71. package/dist/drivers/playwright.js.map +1 -1
  72. package/dist/drivers/steel.d.ts +135 -0
  73. package/dist/drivers/steel.d.ts.map +1 -0
  74. package/dist/drivers/steel.js +236 -0
  75. package/dist/drivers/steel.js.map +1 -0
  76. package/dist/index.d.ts +2 -0
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +1 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/provider-defaults.d.ts +19 -1
  81. package/dist/provider-defaults.d.ts.map +1 -1
  82. package/dist/provider-defaults.js +34 -0
  83. package/dist/provider-defaults.js.map +1 -1
  84. package/dist/test-runner.d.ts.map +1 -1
  85. package/dist/test-runner.js +51 -14
  86. package/dist/test-runner.js.map +1 -1
  87. package/dist/types.d.ts +10 -0
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/viewer/viewer.html +940 -0
  90. package/package.json +3 -2
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Design audit types — Generation 2.
3
+ *
4
+ * Core idea: classification → rubric → measurements → evaluation → findings.
5
+ * Each stage is a pure transformation; the orchestrator (`pipeline.ts`) wires them.
6
+ */
7
+ import type { DesignFinding, DesignSystemScore } from '../../types.js';
8
+ export type { DesignFinding, DesignSystemScore } from '../../types.js';
9
+ /**
10
+ * Page classification — produced by a single cheap LLM call before any scoring.
11
+ * Drives rubric composition and measurement strategy.
12
+ */
13
+ export interface PageClassification {
14
+ /** Primary page archetype */
15
+ type: PageType;
16
+ /** Application domain (free-form, but canonical values preferred) */
17
+ domain: string;
18
+ /** Detected framework, or null if undetectable */
19
+ framework: string | null;
20
+ /** Component library / design system in use */
21
+ designSystem: DesignSystemTag;
22
+ /** Apparent maturity level — anchors calibration */
23
+ maturity: Maturity;
24
+ /** Free-form: what is this page trying to accomplish? */
25
+ intent: string;
26
+ /** Classifier confidence 0-1; values < 0.7 fall back to general rubric */
27
+ confidence: number;
28
+ }
29
+ export type PageType = 'marketing' | 'saas-app' | 'dashboard' | 'docs' | 'ecommerce' | 'social' | 'tool' | 'blog' | 'utility' | 'unknown';
30
+ export type DesignSystemTag = 'shadcn' | 'mui' | 'ant' | 'chakra' | 'tailwind-custom' | 'fully-custom' | 'unstyled' | 'unknown';
31
+ export type Maturity = 'prototype' | 'mvp' | 'shipped' | 'polished' | 'world-class';
32
+ /**
33
+ * A rubric fragment — loaded from markdown with YAML frontmatter.
34
+ * Fragments compose into a full rubric based on classification.
35
+ */
36
+ export interface RubricFragment {
37
+ /** Unique fragment id (matches filename) */
38
+ id: string;
39
+ /** Predicates that decide whether this fragment applies */
40
+ appliesWhen: AppliesWhen;
41
+ /** Relative importance when composing */
42
+ weight: 'low' | 'medium' | 'high' | 'critical';
43
+ /** Human-readable title for the report */
44
+ title: string;
45
+ /** Markdown body — gets injected into the eval prompt */
46
+ body: string;
47
+ /**
48
+ * Optional dimension this fragment contributes to the design system score.
49
+ * When set, the LLM will be asked to score this dimension 1-10 in addition
50
+ * to the universal 8 dimensions. Example: a fintech fragment might
51
+ * contribute "trust-signals", a docs fragment might contribute "readability".
52
+ */
53
+ dimension?: string;
54
+ }
55
+ export interface AppliesWhen {
56
+ /** Match if classification.type is in this set; empty/undefined = always */
57
+ type?: PageType[];
58
+ /** Match if classification.domain matches any of these (substring) */
59
+ domain?: string[];
60
+ /** Match if classification.maturity is in this set */
61
+ maturity?: Maturity[];
62
+ /** Match if classification.designSystem is in this set */
63
+ designSystem?: DesignSystemTag[];
64
+ /** Universal fragments — always apply, no predicates needed */
65
+ universal?: boolean;
66
+ }
67
+ /**
68
+ * A composed rubric — the full set of fragments selected for one classification,
69
+ * ready to inject into an LLM eval prompt.
70
+ */
71
+ export interface ComposedRubric {
72
+ fragments: RubricFragment[];
73
+ /** Pre-rendered markdown body, ready for prompt injection */
74
+ body: string;
75
+ /** Calibration anchor for this rubric */
76
+ calibration: string;
77
+ /** Custom dimensions contributed by fragments (in addition to universal 8) */
78
+ dimensions: string[];
79
+ }
80
+ /**
81
+ * Bundle of deterministic measurements taken before LLM evaluation.
82
+ * The LLM sees these as ground truth and is forbidden from inventing them.
83
+ */
84
+ export interface MeasurementBundle {
85
+ contrast: ContrastReport;
86
+ a11y: A11yReport;
87
+ /** Computed at gather time, used to short-circuit evaluation if too many failures */
88
+ hasBlockingIssues: boolean;
89
+ }
90
+ export interface ContrastReport {
91
+ /** All text-bearing elements with computed text/bg colors */
92
+ totalChecked: number;
93
+ /** Failures of WCAG 2.1 AA (4.5:1 normal, 3:1 large) */
94
+ aaFailures: ContrastFailure[];
95
+ /** Failures of WCAG 2.1 AAA (7:1 normal, 4.5:1 large) — informational */
96
+ aaaFailures: ContrastFailure[];
97
+ /** Quick stats */
98
+ summary: {
99
+ aaPassRate: number;
100
+ aaaPassRate: number;
101
+ };
102
+ }
103
+ export interface ContrastFailure {
104
+ /** CSS-style selector pointing to the element */
105
+ selector: string;
106
+ /** Truncated visible text */
107
+ text: string;
108
+ /** Computed color hex */
109
+ color: string;
110
+ /** Computed (resolved) background hex */
111
+ background: string;
112
+ /** Calculated WCAG ratio */
113
+ ratio: number;
114
+ /** Required ratio for the level being checked */
115
+ required: number;
116
+ /** Element font size in px */
117
+ fontSize: number;
118
+ /** Whether the element qualifies as "large text" per WCAG */
119
+ isLargeText: boolean;
120
+ }
121
+ export interface A11yReport {
122
+ /** axe-core ran successfully */
123
+ ran: boolean;
124
+ /** Error message if axe-core failed to run */
125
+ error?: string;
126
+ /** Violations grouped by axe impact */
127
+ violations: A11yViolation[];
128
+ /** axe pass count for context */
129
+ passes: number;
130
+ }
131
+ export interface A11yViolation {
132
+ /** axe rule id */
133
+ id: string;
134
+ /** axe impact level */
135
+ impact: 'critical' | 'serious' | 'moderate' | 'minor';
136
+ /** Human-readable description */
137
+ description: string;
138
+ /** WCAG criterion (e.g. "wcag2aa") */
139
+ tags: string[];
140
+ /** First few affected elements */
141
+ nodes: Array<{
142
+ selector: string;
143
+ html: string;
144
+ failureSummary: string;
145
+ }>;
146
+ /** axe help URL */
147
+ helpUrl: string;
148
+ }
149
+ /**
150
+ * Per-page audit result.
151
+ */
152
+ export interface PageAuditResult {
153
+ url: string;
154
+ score: number;
155
+ summary: string;
156
+ strengths: string[];
157
+ findings: DesignFinding[];
158
+ classification?: PageClassification;
159
+ rubricFragments?: string[];
160
+ measurements?: MeasurementBundle;
161
+ designSystemScore?: DesignSystemScore;
162
+ screenshotPath?: string;
163
+ tokensUsed?: number;
164
+ error?: string;
165
+ }
166
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/design/audit/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Design audit types — Generation 2.
3
+ *
4
+ * Core idea: classification → rubric → measurements → evaluation → findings.
5
+ * Each stage is a pure transformation; the orchestrator (`pipeline.ts`) wires them.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/design/audit/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Cursor + element highlight overlay.
3
+ *
4
+ * Injected into every page so screenshots and screencasts show what bad is
5
+ * doing — an animated cursor sprite that travels to click targets, a pulse
6
+ * ring on click, and a highlight box around the target element.
7
+ *
8
+ * The overlay is a thin DOM widget added via `page.addInitScript`. It exposes
9
+ * a global `__bad_overlay` object that the driver invokes from
10
+ * `page.evaluate` calls before each action. Everything happens in the page
11
+ * context — the cursor is real DOM, so it shows up in screenshots without
12
+ * any extra plumbing.
13
+ *
14
+ * Toggle via `PlaywrightDriverOptions.showCursor`.
15
+ */
16
+ /**
17
+ * The init script that runs in every page. Returned as a string so it can be
18
+ * passed to `page.addInitScript`. Pure DOM/CSS — no dependencies.
19
+ */
20
+ export declare const CURSOR_OVERLAY_INIT_SCRIPT = "\n(() => {\n if (window.__bad_overlay_installed) return;\n window.__bad_overlay_installed = true;\n\n const NS = 'http://www.w3.org/2000/svg';\n const Z = 2147483647; // max i32\n\n // \u2500\u2500 Root container (absolute, ignores pointer events) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const root = document.createElement('div');\n root.id = '__bad_overlay_root';\n Object.assign(root.style, {\n position: 'fixed',\n inset: '0',\n width: '100vw',\n height: '100vh',\n pointerEvents: 'none',\n zIndex: String(Z),\n });\n\n // \u2500\u2500 Cursor sprite (SVG arrow) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const cursor = document.createElementNS(NS, 'svg');\n cursor.setAttribute('width', '24');\n cursor.setAttribute('height', '24');\n cursor.setAttribute('viewBox', '0 0 24 24');\n cursor.setAttribute('fill', 'none');\n Object.assign(cursor.style, {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: 'translate(-100px, -100px)',\n transition: 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1)',\n pointerEvents: 'none',\n filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.4))',\n });\n cursor.innerHTML = `\n <path d=\"M3 2 L21 12 L13 14 L9 22 Z\" fill=\"#ffffff\" stroke=\"#0b1220\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n `;\n\n // \u2500\u2500 Pulse ring (animated on click) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const ring = document.createElement('div');\n Object.assign(ring.style, {\n position: 'fixed',\n left: '0',\n top: '0',\n width: '0',\n height: '0',\n borderRadius: '9999px',\n border: '3px solid rgba(79, 70, 229, 0.95)',\n transform: 'translate(-100px, -100px)',\n pointerEvents: 'none',\n opacity: '0',\n transition: 'all 500ms ease-out',\n });\n\n // \u2500\u2500 Highlight box (drawn around the target element) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const box = document.createElement('div');\n box.id = '__bad_overlay_box';\n Object.assign(box.style, {\n position: 'fixed',\n left: '0',\n top: '0',\n width: '0',\n height: '0',\n border: '2px solid rgba(34, 197, 94, 0.95)',\n borderRadius: '6px',\n background: 'rgba(34, 197, 94, 0.08)',\n boxShadow: '0 0 0 4px rgba(34, 197, 94, 0.18)',\n transform: 'translate(-100px, -100px)',\n pointerEvents: 'none',\n transition: 'all 180ms ease-out',\n opacity: '0',\n });\n\n // \u2500\u2500 Action label (shows \"click\" / \"type\" / etc next to cursor) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const label = document.createElement('div');\n Object.assign(label.style, {\n position: 'fixed',\n left: '0',\n top: '0',\n padding: '4px 10px',\n background: 'rgba(11, 18, 32, 0.92)',\n color: '#ffffff',\n fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n fontSize: '12px',\n fontWeight: '600',\n borderRadius: '6px',\n transform: 'translate(-200px, -200px)',\n pointerEvents: 'none',\n opacity: '0',\n transition: 'all 220ms cubic-bezier(0.22, 1, 0.36, 1)',\n boxShadow: '0 4px 12px rgba(0,0,0,0.3)',\n whiteSpace: 'nowrap',\n });\n\n // Append to documentElement (not body) so the overlay is not affected by\n // body's stacking context \u2014 many modern sites apply transform/will-change\n // to body which would clip a max-z-index child appended to body.\n // Bound the retry loop so a body-less doc (XML, PDF viewer, chrome://)\n // doesn't spin requestAnimationFrame forever.\n let attachAttempts = 0;\n function attach() {\n const host = document.documentElement || document.body;\n if (!host) {\n if (++attachAttempts > 60) return; // ~1 second of attempts\n requestAnimationFrame(attach);\n return;\n }\n host.appendChild(root);\n root.appendChild(box);\n root.appendChild(ring);\n root.appendChild(cursor);\n root.appendChild(label);\n }\n attach();\n\n // \u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n window.__bad_overlay = {\n /**\n * Highlight a target element by CSS selector. Returns the rect or null.\n * Used when the caller has a stable CSS selector.\n */\n highlight(selector) {\n try {\n const el = typeof selector === 'string'\n ? document.querySelector(selector)\n : null;\n if (!el) return null;\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return null;\n Object.assign(box.style, {\n width: rect.width + 'px',\n height: rect.height + 'px',\n transform: `translate(${rect.left}px, ${rect.top}px)`,\n opacity: '1',\n });\n return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, width: rect.width, height: rect.height };\n } catch { return null; }\n },\n\n /**\n * Highlight an arbitrary rect at given page coordinates. Used when the\n * caller already computed the rect via Playwright's boundingBox (i.e.,\n * the @ref selector isn't a real CSS selector).\n */\n highlightRect(x, y, width, height) {\n try {\n Object.assign(box.style, {\n width: width + 'px',\n height: height + 'px',\n transform: `translate(${x}px, ${y}px)`,\n opacity: '1',\n });\n } catch { /* never let cosmetic overlay break a run */ }\n },\n\n /**\n * Move the cursor to (x, y) and show a label.\n */\n moveTo(x, y, labelText) {\n cursor.style.transform = `translate(${x - 4}px, ${y - 4}px)`;\n if (labelText) {\n label.textContent = labelText;\n Object.assign(label.style, {\n transform: `translate(${x + 16}px, ${y + 16}px)`,\n opacity: '1',\n });\n }\n },\n\n /**\n * Pulse a click ring at (x, y).\n */\n pulseClick(x, y) {\n // reset\n Object.assign(ring.style, {\n width: '0',\n height: '0',\n transform: `translate(${x}px, ${y}px)`,\n opacity: '1',\n });\n // expand\n requestAnimationFrame(() => {\n Object.assign(ring.style, {\n width: '60px',\n height: '60px',\n transform: `translate(${x - 30}px, ${y - 30}px)`,\n opacity: '0',\n });\n });\n },\n\n /**\n * Hide all overlay elements.\n */\n hide() {\n box.style.opacity = '0';\n label.style.opacity = '0';\n ring.style.opacity = '0';\n },\n\n /**\n * Hide just the highlight box (cursor + label stay).\n */\n clearHighlight() {\n box.style.opacity = '0';\n },\n };\n})();\n";
21
+ /**
22
+ * Wait time after `moveTo` so the cursor animation finishes before the
23
+ * actual action fires. Tuned to match the CSS transition duration.
24
+ */
25
+ export declare const CURSOR_ANIMATION_MS = 240;
26
+ //# sourceMappingURL=cursor-overlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-overlay.d.ts","sourceRoot":"","sources":["../../src/drivers/cursor-overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;GAGG;AACH,eAAO,MAAM,0BAA0B,01OAiNtC,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAA"}
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Cursor + element highlight overlay.
3
+ *
4
+ * Injected into every page so screenshots and screencasts show what bad is
5
+ * doing — an animated cursor sprite that travels to click targets, a pulse
6
+ * ring on click, and a highlight box around the target element.
7
+ *
8
+ * The overlay is a thin DOM widget added via `page.addInitScript`. It exposes
9
+ * a global `__bad_overlay` object that the driver invokes from
10
+ * `page.evaluate` calls before each action. Everything happens in the page
11
+ * context — the cursor is real DOM, so it shows up in screenshots without
12
+ * any extra plumbing.
13
+ *
14
+ * Toggle via `PlaywrightDriverOptions.showCursor`.
15
+ */
16
+ /**
17
+ * The init script that runs in every page. Returned as a string so it can be
18
+ * passed to `page.addInitScript`. Pure DOM/CSS — no dependencies.
19
+ */
20
+ export const CURSOR_OVERLAY_INIT_SCRIPT = `
21
+ (() => {
22
+ if (window.__bad_overlay_installed) return;
23
+ window.__bad_overlay_installed = true;
24
+
25
+ const NS = 'http://www.w3.org/2000/svg';
26
+ const Z = 2147483647; // max i32
27
+
28
+ // ── Root container (absolute, ignores pointer events) ──────────────────
29
+ const root = document.createElement('div');
30
+ root.id = '__bad_overlay_root';
31
+ Object.assign(root.style, {
32
+ position: 'fixed',
33
+ inset: '0',
34
+ width: '100vw',
35
+ height: '100vh',
36
+ pointerEvents: 'none',
37
+ zIndex: String(Z),
38
+ });
39
+
40
+ // ── Cursor sprite (SVG arrow) ──────────────────────────────────────────
41
+ const cursor = document.createElementNS(NS, 'svg');
42
+ cursor.setAttribute('width', '24');
43
+ cursor.setAttribute('height', '24');
44
+ cursor.setAttribute('viewBox', '0 0 24 24');
45
+ cursor.setAttribute('fill', 'none');
46
+ Object.assign(cursor.style, {
47
+ position: 'fixed',
48
+ left: '0',
49
+ top: '0',
50
+ transform: 'translate(-100px, -100px)',
51
+ transition: 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1)',
52
+ pointerEvents: 'none',
53
+ filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.4))',
54
+ });
55
+ cursor.innerHTML = \`
56
+ <path d="M3 2 L21 12 L13 14 L9 22 Z" fill="#ffffff" stroke="#0b1220" stroke-width="1.5" stroke-linejoin="round"/>
57
+ \`;
58
+
59
+ // ── Pulse ring (animated on click) ─────────────────────────────────────
60
+ const ring = document.createElement('div');
61
+ Object.assign(ring.style, {
62
+ position: 'fixed',
63
+ left: '0',
64
+ top: '0',
65
+ width: '0',
66
+ height: '0',
67
+ borderRadius: '9999px',
68
+ border: '3px solid rgba(79, 70, 229, 0.95)',
69
+ transform: 'translate(-100px, -100px)',
70
+ pointerEvents: 'none',
71
+ opacity: '0',
72
+ transition: 'all 500ms ease-out',
73
+ });
74
+
75
+ // ── Highlight box (drawn around the target element) ────────────────────
76
+ const box = document.createElement('div');
77
+ box.id = '__bad_overlay_box';
78
+ Object.assign(box.style, {
79
+ position: 'fixed',
80
+ left: '0',
81
+ top: '0',
82
+ width: '0',
83
+ height: '0',
84
+ border: '2px solid rgba(34, 197, 94, 0.95)',
85
+ borderRadius: '6px',
86
+ background: 'rgba(34, 197, 94, 0.08)',
87
+ boxShadow: '0 0 0 4px rgba(34, 197, 94, 0.18)',
88
+ transform: 'translate(-100px, -100px)',
89
+ pointerEvents: 'none',
90
+ transition: 'all 180ms ease-out',
91
+ opacity: '0',
92
+ });
93
+
94
+ // ── Action label (shows "click" / "type" / etc next to cursor) ─────────
95
+ const label = document.createElement('div');
96
+ Object.assign(label.style, {
97
+ position: 'fixed',
98
+ left: '0',
99
+ top: '0',
100
+ padding: '4px 10px',
101
+ background: 'rgba(11, 18, 32, 0.92)',
102
+ color: '#ffffff',
103
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
104
+ fontSize: '12px',
105
+ fontWeight: '600',
106
+ borderRadius: '6px',
107
+ transform: 'translate(-200px, -200px)',
108
+ pointerEvents: 'none',
109
+ opacity: '0',
110
+ transition: 'all 220ms cubic-bezier(0.22, 1, 0.36, 1)',
111
+ boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
112
+ whiteSpace: 'nowrap',
113
+ });
114
+
115
+ // Append to documentElement (not body) so the overlay is not affected by
116
+ // body's stacking context — many modern sites apply transform/will-change
117
+ // to body which would clip a max-z-index child appended to body.
118
+ // Bound the retry loop so a body-less doc (XML, PDF viewer, chrome://)
119
+ // doesn't spin requestAnimationFrame forever.
120
+ let attachAttempts = 0;
121
+ function attach() {
122
+ const host = document.documentElement || document.body;
123
+ if (!host) {
124
+ if (++attachAttempts > 60) return; // ~1 second of attempts
125
+ requestAnimationFrame(attach);
126
+ return;
127
+ }
128
+ host.appendChild(root);
129
+ root.appendChild(box);
130
+ root.appendChild(ring);
131
+ root.appendChild(cursor);
132
+ root.appendChild(label);
133
+ }
134
+ attach();
135
+
136
+ // ── Public API ─────────────────────────────────────────────────────────
137
+ window.__bad_overlay = {
138
+ /**
139
+ * Highlight a target element by CSS selector. Returns the rect or null.
140
+ * Used when the caller has a stable CSS selector.
141
+ */
142
+ highlight(selector) {
143
+ try {
144
+ const el = typeof selector === 'string'
145
+ ? document.querySelector(selector)
146
+ : null;
147
+ if (!el) return null;
148
+ const rect = el.getBoundingClientRect();
149
+ if (rect.width === 0 || rect.height === 0) return null;
150
+ Object.assign(box.style, {
151
+ width: rect.width + 'px',
152
+ height: rect.height + 'px',
153
+ transform: \`translate(\${rect.left}px, \${rect.top}px)\`,
154
+ opacity: '1',
155
+ });
156
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, width: rect.width, height: rect.height };
157
+ } catch { return null; }
158
+ },
159
+
160
+ /**
161
+ * Highlight an arbitrary rect at given page coordinates. Used when the
162
+ * caller already computed the rect via Playwright's boundingBox (i.e.,
163
+ * the @ref selector isn't a real CSS selector).
164
+ */
165
+ highlightRect(x, y, width, height) {
166
+ try {
167
+ Object.assign(box.style, {
168
+ width: width + 'px',
169
+ height: height + 'px',
170
+ transform: \`translate(\${x}px, \${y}px)\`,
171
+ opacity: '1',
172
+ });
173
+ } catch { /* never let cosmetic overlay break a run */ }
174
+ },
175
+
176
+ /**
177
+ * Move the cursor to (x, y) and show a label.
178
+ */
179
+ moveTo(x, y, labelText) {
180
+ cursor.style.transform = \`translate(\${x - 4}px, \${y - 4}px)\`;
181
+ if (labelText) {
182
+ label.textContent = labelText;
183
+ Object.assign(label.style, {
184
+ transform: \`translate(\${x + 16}px, \${y + 16}px)\`,
185
+ opacity: '1',
186
+ });
187
+ }
188
+ },
189
+
190
+ /**
191
+ * Pulse a click ring at (x, y).
192
+ */
193
+ pulseClick(x, y) {
194
+ // reset
195
+ Object.assign(ring.style, {
196
+ width: '0',
197
+ height: '0',
198
+ transform: \`translate(\${x}px, \${y}px)\`,
199
+ opacity: '1',
200
+ });
201
+ // expand
202
+ requestAnimationFrame(() => {
203
+ Object.assign(ring.style, {
204
+ width: '60px',
205
+ height: '60px',
206
+ transform: \`translate(\${x - 30}px, \${y - 30}px)\`,
207
+ opacity: '0',
208
+ });
209
+ });
210
+ },
211
+
212
+ /**
213
+ * Hide all overlay elements.
214
+ */
215
+ hide() {
216
+ box.style.opacity = '0';
217
+ label.style.opacity = '0';
218
+ ring.style.opacity = '0';
219
+ },
220
+
221
+ /**
222
+ * Hide just the highlight box (cursor + label stay).
223
+ */
224
+ clearHighlight() {
225
+ box.style.opacity = '0';
226
+ },
227
+ };
228
+ })();
229
+ `;
230
+ /**
231
+ * Wait time after `moveTo` so the cursor animation finishes before the
232
+ * actual action fires. Tuned to match the CSS transition duration.
233
+ */
234
+ export const CURSOR_ANIMATION_MS = 240;
235
+ //# sourceMappingURL=cursor-overlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-overlay.js","sourceRoot":"","sources":["../../src/drivers/cursor-overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiNzC,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAA"}
@@ -43,6 +43,18 @@ export interface PlaywrightDriverOptions {
43
43
  visionStrategy?: 'always' | 'never' | 'auto';
44
44
  /** Capture a screenshot every N turns for artifact storage (0 = disabled) */
45
45
  screenshotInterval?: number;
46
+ /**
47
+ * Inject a cursor + element-highlight overlay so screenshots show what bad
48
+ * is doing. Adds an animated cursor sprite that travels to click targets,
49
+ * pulse rings on click, and highlight boxes around the target element.
50
+ *
51
+ * Default: false. Enable for demo recordings, debugging, and the session viewer.
52
+ *
53
+ * **Performance note:** each interactive action waits ~240ms for the cursor
54
+ * animation to land before executing — over a 50-turn session that's ~12s
55
+ * of added wall time. Leave off for headless CI; on for screen captures.
56
+ */
57
+ showCursor?: boolean;
46
58
  }
47
59
  export declare class PlaywrightDriver implements Driver {
48
60
  private page;
@@ -52,7 +64,27 @@ export declare class PlaywrightDriver implements Driver {
52
64
  private cdpFailed;
53
65
  private lastTiming;
54
66
  private observeCount;
67
+ /**
68
+ * Promise that resolves when the cursor overlay finishes installing.
69
+ * `animateCursorToSelector` awaits this so the first action doesn't race
70
+ * the init script. Undefined if showCursor is off.
71
+ */
72
+ private cursorInstallPromise?;
55
73
  constructor(page: Page, options?: PlaywrightDriverOptions);
74
+ /**
75
+ * Inject the cursor overlay init script into the current page and into the
76
+ * browser context, so it survives navigations and applies to popups.
77
+ */
78
+ private installCursorOverlay;
79
+ /**
80
+ * Animate the cursor to the target element + draw a highlight box, then
81
+ * pulse a click ring. No-op if showCursor is disabled.
82
+ *
83
+ * Uses Playwright's boundingBox to compute the rect (works for any locator,
84
+ * including @ref selectors that don't map to plain CSS) and drives the
85
+ * overlay via its public `highlightRect` / `moveTo` / `pulseClick` API.
86
+ */
87
+ private animateCursorToSelector;
56
88
  /** Get phase-level timing from the last observe() call */
57
89
  getLastTiming(): ObserveTiming | undefined;
58
90
  getPage(): Page;
@@ -1 +1 @@
1
- {"version":3,"file":"playwright.d.ts","sourceRoot":"","sources":["../../src/drivers/playwright.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAUrD,iDAAiD;AACjD,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,2BAA2B;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,6CAA6C;IAC7C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,qEAAqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sEAAsE;IACtE,cAAc,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;IAC7C,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,qBAAa,gBAAiB,YAAW,MAAM;IAQ3C,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,OAAO;IARjB,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,YAAY,CAAK;gBAGf,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,uBAA4B;IAG/C,0DAA0D;IAC1D,aAAa,IAAI,aAAa,GAAG,SAAS;IAI1C,OAAO,IAAI,IAAI;IAIf,MAAM,IAAI,MAAM;IAIV,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;YAS1D,SAAS;IAWvB;;;OAGG;IACG,qBAAqB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuC5E;;;OAGG;YACW,gBAAgB;IAcxB,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC;IAmDnC;;OAEG;YACW,UAAU;IA4DxB;;OAEG;YACW,iBAAiB;IAwCzB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAI7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B;;;OAGG;YACW,mBAAmB;YAUnB,aAAa;IAUrB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAkJrD"}
1
+ {"version":3,"file":"playwright.d.ts","sourceRoot":"","sources":["../../src/drivers/playwright.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAWrD,iDAAiD;AACjD,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,2BAA2B;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,6CAA6C;IAC7C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,qEAAqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sEAAsE;IACtE,cAAc,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;IAC7C,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,qBAAa,gBAAiB,YAAW,MAAM;IAc3C,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,OAAO;IAdjB,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,YAAY,CAAK;IACzB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB,CAAC,CAAgB;gBAGnC,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,uBAA4B;IAU/C;;;OAGG;YACW,oBAAoB;IAWlC;;;;;;;OAOG;YACW,uBAAuB;IA+CrC,0DAA0D;IAC1D,aAAa,IAAI,aAAa,GAAG,SAAS;IAI1C,OAAO,IAAI,IAAI;IAIf,MAAM,IAAI,MAAM;IAIV,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;YAS1D,SAAS;IAWvB;;;OAGG;IACG,qBAAqB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuC5E;;;OAGG;YACW,gBAAgB;IAcxB,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC;IAmDnC;;OAEG;YACW,UAAU;IA4DxB;;OAEG;YACW,iBAAiB;IAwCzB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAI7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B;;;OAGG;YACW,mBAAmB;YAUnB,aAAa;IAUrB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAuJrD"}
@@ -12,6 +12,7 @@ import { AriaSnapshotHelper, dismissOverlays } from './snapshot.js';
12
12
  import { ANALYTICS_PATTERNS, IMAGE_PATTERNS, MEDIA_PATTERNS } from './block-patterns.js';
13
13
  import { buildCdpSnapshot } from './cdp-snapshot.js';
14
14
  import { getPageMetadata } from './cdp-page-state.js';
15
+ import { CURSOR_OVERLAY_INIT_SCRIPT, CURSOR_ANIMATION_MS } from './cursor-overlay.js';
15
16
  function isPointerInterceptError(error) {
16
17
  return /intercepts pointer events|subtree intercepts pointer events|not receiving pointer events/i.test(error);
17
18
  }
@@ -23,9 +24,82 @@ export class PlaywrightDriver {
23
24
  cdpFailed = false;
24
25
  lastTiming;
25
26
  observeCount = 0;
27
+ /**
28
+ * Promise that resolves when the cursor overlay finishes installing.
29
+ * `animateCursorToSelector` awaits this so the first action doesn't race
30
+ * the init script. Undefined if showCursor is off.
31
+ */
32
+ cursorInstallPromise;
26
33
  constructor(page, options = {}) {
27
34
  this.page = page;
28
35
  this.options = options;
36
+ if (this.options.showCursor) {
37
+ // Install for the current page AND any future pages this context creates.
38
+ // We store the promise so callers (animateCursorToSelector) can await it
39
+ // before driving the overlay — otherwise the first action races the inject.
40
+ this.cursorInstallPromise = this.installCursorOverlay();
41
+ }
42
+ }
43
+ /**
44
+ * Inject the cursor overlay init script into the current page and into the
45
+ * browser context, so it survives navigations and applies to popups.
46
+ */
47
+ async installCursorOverlay() {
48
+ try {
49
+ // Context-level: applies to all current and future pages
50
+ await this.page.context().addInitScript({ content: CURSOR_OVERLAY_INIT_SCRIPT });
51
+ // Page-level: ensure the current page has it now (addInitScript is for new docs)
52
+ await this.page.evaluate(CURSOR_OVERLAY_INIT_SCRIPT).catch(() => { });
53
+ }
54
+ catch {
55
+ // Strict CSP can block evaluate; init script via context still works on next nav
56
+ }
57
+ }
58
+ /**
59
+ * Animate the cursor to the target element + draw a highlight box, then
60
+ * pulse a click ring. No-op if showCursor is disabled.
61
+ *
62
+ * Uses Playwright's boundingBox to compute the rect (works for any locator,
63
+ * including @ref selectors that don't map to plain CSS) and drives the
64
+ * overlay via its public `highlightRect` / `moveTo` / `pulseClick` API.
65
+ */
66
+ async animateCursorToSelector(selector, actionLabel) {
67
+ if (!this.options.showCursor)
68
+ return;
69
+ // Make sure the install promise (fired in the constructor) has resolved
70
+ // before we try to drive the overlay. Otherwise the first action races
71
+ // the script injection and the cursor never appears.
72
+ if (this.cursorInstallPromise) {
73
+ await this.cursorInstallPromise.catch(() => undefined);
74
+ }
75
+ try {
76
+ const locator = this.snapshot.resolveLocator(this.page, selector);
77
+ const box = await locator.boundingBox({ timeout: 1000 }).catch(() => null);
78
+ if (!box)
79
+ return;
80
+ const cx = box.x + box.width / 2;
81
+ const cy = box.y + box.height / 2;
82
+ await this.page.evaluate(({ x, y, w, h, label, cx: cxArg, cy: cyArg }) => {
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ const ov = window.__bad_overlay;
85
+ if (!ov)
86
+ return;
87
+ ov.highlightRect(x, y, w, h);
88
+ ov.moveTo(cxArg, cyArg, label);
89
+ }, { x: box.x, y: box.y, w: box.width, h: box.height, label: actionLabel, cx, cy }).catch(() => { });
90
+ await this.page.waitForTimeout(CURSOR_ANIMATION_MS);
91
+ if (actionLabel === 'click' || actionLabel === 'type' || actionLabel === 'press') {
92
+ await this.page.evaluate(({ x, y }) => {
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ const ov = window.__bad_overlay;
95
+ if (ov)
96
+ ov.pulseClick(x, y);
97
+ }, { x: cx, y: cy }).catch(() => { });
98
+ }
99
+ }
100
+ catch {
101
+ // Overlay is purely cosmetic — never let it break the action
102
+ }
29
103
  }
30
104
  /** Get phase-level timing from the last observe() call */
31
105
  getLastTiming() {
@@ -288,6 +362,7 @@ export class PlaywrightDriver {
288
362
  case 'click': {
289
363
  const locator = this.snapshot.resolveLocator(this.page, action.selector);
290
364
  const bounds = await this.captureBounds(locator);
365
+ await this.animateCursorToSelector(action.selector, 'click');
291
366
  // Listen for popups but don't block: collect any that fire during the click
292
367
  let popupPage = null;
293
368
  const onPopup = (page) => { popupPage = page; };
@@ -320,6 +395,7 @@ export class PlaywrightDriver {
320
395
  case 'type': {
321
396
  const locator = this.snapshot.resolveLocator(this.page, action.selector);
322
397
  const bounds = await this.captureBounds(locator);
398
+ await this.animateCursorToSelector(action.selector, 'type');
323
399
  try {
324
400
  await this.withOverlayRecovery(async () => {
325
401
  await locator.click({ timeout });
@@ -346,6 +422,7 @@ export class PlaywrightDriver {
346
422
  case 'press': {
347
423
  const locator = this.snapshot.resolveLocator(this.page, action.selector);
348
424
  const bounds = await this.captureBounds(locator);
425
+ await this.animateCursorToSelector(action.selector, 'press');
349
426
  await this.withOverlayRecovery(async () => {
350
427
  await locator.press(action.key, { timeout });
351
428
  });
@@ -354,12 +431,14 @@ export class PlaywrightDriver {
354
431
  case 'hover': {
355
432
  const locator = this.snapshot.resolveLocator(this.page, action.selector);
356
433
  const bounds = await this.captureBounds(locator);
434
+ await this.animateCursorToSelector(action.selector, 'hover');
357
435
  await locator.hover({ timeout });
358
436
  return { success: true, bounds };
359
437
  }
360
438
  case 'select': {
361
439
  const locator = this.snapshot.resolveLocator(this.page, action.selector);
362
440
  const bounds = await this.captureBounds(locator);
441
+ await this.animateCursorToSelector(action.selector, 'select');
363
442
  await locator.selectOption(action.value, { timeout });
364
443
  return { success: true, bounds };
365
444
  }