@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.
- package/README.md +81 -0
- package/dist/brain/index.d.ts.map +1 -1
- package/dist/brain/index.js +88 -6
- package/dist/brain/index.js.map +1 -1
- package/dist/cli-design-audit.d.ts +2 -0
- package/dist/cli-design-audit.d.ts.map +1 -1
- package/dist/cli-design-audit.js +111 -303
- package/dist/cli-design-audit.js.map +1 -1
- package/dist/cli-view.d.ts +83 -0
- package/dist/cli-view.d.ts.map +1 -0
- package/dist/cli-view.js +397 -0
- package/dist/cli-view.js.map +1 -0
- package/dist/cli.js +32 -1
- package/dist/cli.js.map +1 -1
- package/dist/design/audit/classify.d.ts +20 -0
- package/dist/design/audit/classify.d.ts.map +1 -0
- package/dist/design/audit/classify.js +148 -0
- package/dist/design/audit/classify.js.map +1 -0
- package/dist/design/audit/evaluate.d.ts +49 -0
- package/dist/design/audit/evaluate.d.ts.map +1 -0
- package/dist/design/audit/evaluate.js +355 -0
- package/dist/design/audit/evaluate.js.map +1 -0
- package/dist/design/audit/measure/a11y.d.ts +25 -0
- package/dist/design/audit/measure/a11y.d.ts.map +1 -0
- package/dist/design/audit/measure/a11y.js +144 -0
- package/dist/design/audit/measure/a11y.js.map +1 -0
- package/dist/design/audit/measure/contrast.d.ts +18 -0
- package/dist/design/audit/measure/contrast.d.ts.map +1 -0
- package/dist/design/audit/measure/contrast.js +201 -0
- package/dist/design/audit/measure/contrast.js.map +1 -0
- package/dist/design/audit/measure/index.d.ts +14 -0
- package/dist/design/audit/measure/index.d.ts.map +1 -0
- package/dist/design/audit/measure/index.js +38 -0
- package/dist/design/audit/measure/index.js.map +1 -0
- package/dist/design/audit/pipeline.d.ts +33 -0
- package/dist/design/audit/pipeline.d.ts.map +1 -0
- package/dist/design/audit/pipeline.js +109 -0
- package/dist/design/audit/pipeline.js.map +1 -0
- package/dist/design/audit/roi.d.ts +49 -0
- package/dist/design/audit/roi.d.ts.map +1 -0
- package/dist/design/audit/roi.js +157 -0
- package/dist/design/audit/roi.js.map +1 -0
- package/dist/design/audit/rubric/fragments/domain-ai.md +19 -0
- package/dist/design/audit/rubric/fragments/domain-crypto.md +24 -0
- package/dist/design/audit/rubric/fragments/domain-devtools.md +21 -0
- package/dist/design/audit/rubric/fragments/domain-fintech.md +19 -0
- package/dist/design/audit/rubric/fragments/maturity-prototype.md +36 -0
- package/dist/design/audit/rubric/fragments/type-blog.md +22 -0
- package/dist/design/audit/rubric/fragments/type-docs.md +23 -0
- package/dist/design/audit/rubric/fragments/type-ecommerce.md +23 -0
- package/dist/design/audit/rubric/fragments/type-marketing.md +22 -0
- package/dist/design/audit/rubric/fragments/type-saas-app.md +20 -0
- package/dist/design/audit/rubric/fragments/universal-calibration.md +16 -0
- package/dist/design/audit/rubric/fragments/universal-effort-anchor.md +46 -0
- package/dist/design/audit/rubric/fragments/universal-foundation.md +49 -0
- package/dist/design/audit/rubric/loader.d.ts +49 -0
- package/dist/design/audit/rubric/loader.d.ts.map +1 -0
- package/dist/design/audit/rubric/loader.js +216 -0
- package/dist/design/audit/rubric/loader.js.map +1 -0
- package/dist/design/audit/types.d.ts +166 -0
- package/dist/design/audit/types.d.ts.map +1 -0
- package/dist/design/audit/types.js +8 -0
- package/dist/design/audit/types.js.map +1 -0
- package/dist/drivers/cursor-overlay.d.ts +26 -0
- package/dist/drivers/cursor-overlay.d.ts.map +1 -0
- package/dist/drivers/cursor-overlay.js +235 -0
- package/dist/drivers/cursor-overlay.js.map +1 -0
- package/dist/drivers/playwright.d.ts +32 -0
- package/dist/drivers/playwright.d.ts.map +1 -1
- package/dist/drivers/playwright.js +79 -0
- package/dist/drivers/playwright.js.map +1 -1
- package/dist/drivers/steel.d.ts +135 -0
- package/dist/drivers/steel.d.ts.map +1 -0
- package/dist/drivers/steel.js +236 -0
- package/dist/drivers/steel.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/provider-defaults.d.ts +19 -1
- package/dist/provider-defaults.d.ts.map +1 -1
- package/dist/provider-defaults.js +34 -0
- package/dist/provider-defaults.js.map +1 -1
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +51 -14
- package/dist/test-runner.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/viewer/viewer.html +940 -0
- 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 @@
|
|
|
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;
|
|
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
|
}
|