playwriter 0.0.33 → 0.0.37

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 (65) hide show
  1. package/dist/aria-snapshot.d.ts +68 -0
  2. package/dist/aria-snapshot.d.ts.map +1 -0
  3. package/dist/aria-snapshot.js +359 -0
  4. package/dist/aria-snapshot.js.map +1 -0
  5. package/dist/cdp-relay.d.ts.map +1 -1
  6. package/dist/cdp-relay.js +95 -5
  7. package/dist/cdp-relay.js.map +1 -1
  8. package/dist/cdp-session.d.ts +24 -3
  9. package/dist/cdp-session.d.ts.map +1 -1
  10. package/dist/cdp-session.js +23 -0
  11. package/dist/cdp-session.js.map +1 -1
  12. package/dist/debugger-api.md +4 -3
  13. package/dist/debugger.d.ts +4 -3
  14. package/dist/debugger.d.ts.map +1 -1
  15. package/dist/debugger.js +3 -1
  16. package/dist/debugger.js.map +1 -1
  17. package/dist/editor-api.md +2 -2
  18. package/dist/editor.d.ts +2 -2
  19. package/dist/editor.d.ts.map +1 -1
  20. package/dist/editor.js +1 -0
  21. package/dist/editor.js.map +1 -1
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/mcp.d.ts.map +1 -1
  27. package/dist/mcp.js +151 -14
  28. package/dist/mcp.js.map +1 -1
  29. package/dist/mcp.test.js +340 -5
  30. package/dist/mcp.test.js.map +1 -1
  31. package/dist/protocol.d.ts +12 -1
  32. package/dist/protocol.d.ts.map +1 -1
  33. package/dist/react-source.d.ts +3 -3
  34. package/dist/react-source.d.ts.map +1 -1
  35. package/dist/react-source.js +3 -1
  36. package/dist/react-source.js.map +1 -1
  37. package/dist/scoped-fs.d.ts +94 -0
  38. package/dist/scoped-fs.d.ts.map +1 -0
  39. package/dist/scoped-fs.js +356 -0
  40. package/dist/scoped-fs.js.map +1 -0
  41. package/dist/styles-api.md +3 -3
  42. package/dist/styles.d.ts +3 -3
  43. package/dist/styles.d.ts.map +1 -1
  44. package/dist/styles.js +3 -1
  45. package/dist/styles.js.map +1 -1
  46. package/package.json +13 -13
  47. package/src/aria-snapshot.ts +446 -0
  48. package/src/assets/aria-labels-github-snapshot.txt +605 -0
  49. package/src/assets/aria-labels-github.png +0 -0
  50. package/src/assets/aria-labels-google-snapshot.txt +110 -0
  51. package/src/assets/aria-labels-google.png +0 -0
  52. package/src/assets/aria-labels-hacker-news-snapshot.txt +1023 -0
  53. package/src/assets/aria-labels-hacker-news.png +0 -0
  54. package/src/cdp-relay.ts +103 -5
  55. package/src/cdp-session.ts +50 -3
  56. package/src/debugger.ts +6 -4
  57. package/src/editor.ts +4 -3
  58. package/src/index.ts +8 -0
  59. package/src/mcp.test.ts +424 -5
  60. package/src/mcp.ts +242 -66
  61. package/src/prompt.md +209 -167
  62. package/src/protocol.ts +14 -1
  63. package/src/react-source.ts +5 -3
  64. package/src/scoped-fs.ts +411 -0
  65. package/src/styles.ts +5 -3
@@ -0,0 +1,68 @@
1
+ import type { Page, Locator, ElementHandle } from 'playwright-core';
2
+ export interface AriaRef {
3
+ role: string;
4
+ name: string;
5
+ ref: string;
6
+ }
7
+ export interface AriaSnapshotResult {
8
+ snapshot: string;
9
+ refToElement: Map<string, {
10
+ role: string;
11
+ name: string;
12
+ }>;
13
+ refHandles: Array<{
14
+ ref: string;
15
+ handle: ElementHandle;
16
+ }>;
17
+ getRefsForLocators: (locators: Array<Locator | ElementHandle>) => Promise<Array<AriaRef | null>>;
18
+ getRefForLocator: (locator: Locator | ElementHandle) => Promise<AriaRef | null>;
19
+ getRefStringForLocator: (locator: Locator | ElementHandle) => Promise<string | null>;
20
+ }
21
+ /**
22
+ * Get an accessibility snapshot with utilities to look up aria refs for elements.
23
+ * Uses Playwright's internal aria-ref selector engine.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const { snapshot, getRefsForLocators } = await getAriaSnapshot({ page })
28
+ * const refs = await getRefsForLocators([page.locator('button'), page.locator('a')])
29
+ * // refs[0].ref is e.g. "e5" - use page.locator('aria-ref=e5') to select
30
+ * ```
31
+ */
32
+ export declare function getAriaSnapshot({ page }: {
33
+ page: Page;
34
+ }): Promise<AriaSnapshotResult>;
35
+ /**
36
+ * Show Vimium-style labels on interactive elements.
37
+ * Labels are yellow badges positioned above each element showing the aria ref (e.g., "e1", "e2").
38
+ * Use with screenshots so agents can see which elements are interactive.
39
+ *
40
+ * Labels auto-hide after 5 seconds to prevent stale labels remaining on the page.
41
+ * Call this function again if the page HTML changes to get fresh labels.
42
+ *
43
+ * By default, only shows labels for truly interactive roles (button, link, textbox, etc.)
44
+ * to reduce visual clutter. Set `interactiveOnly: false` to show all elements with refs.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const { snapshot, labelCount } = await showAriaRefLabels({ page })
49
+ * await page.screenshot({ path: '/tmp/screenshot.png' })
50
+ * // Agent sees [e5] label on "Submit" button
51
+ * await page.locator('aria-ref=e5').click()
52
+ * // Labels auto-hide after 5 seconds, or call hideAriaRefLabels() manually
53
+ * ```
54
+ */
55
+ export declare function showAriaRefLabels({ page, interactiveOnly }: {
56
+ page: Page;
57
+ interactiveOnly?: boolean;
58
+ }): Promise<{
59
+ snapshot: string;
60
+ labelCount: number;
61
+ }>;
62
+ /**
63
+ * Remove all aria ref labels from the page.
64
+ */
65
+ export declare function hideAriaRefLabels({ page }: {
66
+ page: Page;
67
+ }): Promise<void>;
68
+ //# sourceMappingURL=aria-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aria-snapshot.d.ts","sourceRoot":"","sources":["../src/aria-snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEnE,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACzD,UAAU,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,aAAa,CAAA;KAAE,CAAC,CAAA;IACzD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,GAAG,aAAa,CAAC,KAAK,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAA;IAChG,gBAAgB,EAAE,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;IAC/E,sBAAsB,EAAE,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACrF;AA8ED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAwF3F;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,IAAI,EAAE,eAAsB,EAAE,EAAE;IACxE,IAAI,EAAE,IAAI,CAAA;IACV,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B,GAAG,OAAO,CAAC;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAC,CA8MD;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAc/E"}
@@ -0,0 +1,359 @@
1
+ const LABELS_CONTAINER_ID = '__playwriter_labels__';
2
+ // Roles that represent truly interactive elements (can be clicked, typed into, etc.)
3
+ const INTERACTIVE_ROLES = new Set([
4
+ 'button',
5
+ 'link',
6
+ 'textbox',
7
+ 'combobox',
8
+ 'searchbox',
9
+ 'checkbox',
10
+ 'radio',
11
+ 'slider',
12
+ 'spinbutton',
13
+ 'switch',
14
+ 'menuitem',
15
+ 'menuitemcheckbox',
16
+ 'menuitemradio',
17
+ 'option',
18
+ 'tab',
19
+ 'treeitem',
20
+ ]);
21
+ // Color categories for different role types - warm color scheme
22
+ // Format: [gradient-top, gradient-bottom, border]
23
+ const ROLE_COLORS = {
24
+ // Links - yellow (Vimium-style)
25
+ link: ['#FFF785', '#FFC542', '#E3BE23'],
26
+ // Buttons - orange
27
+ button: ['#FFE0B2', '#FFCC80', '#FFB74D'],
28
+ // Text inputs - coral/red
29
+ textbox: ['#FFCDD2', '#EF9A9A', '#E57373'],
30
+ combobox: ['#FFCDD2', '#EF9A9A', '#E57373'],
31
+ searchbox: ['#FFCDD2', '#EF9A9A', '#E57373'],
32
+ spinbutton: ['#FFCDD2', '#EF9A9A', '#E57373'],
33
+ // Checkboxes/Radios/Switches - warm pink
34
+ checkbox: ['#F8BBD0', '#F48FB1', '#EC407A'],
35
+ radio: ['#F8BBD0', '#F48FB1', '#EC407A'],
36
+ switch: ['#F8BBD0', '#F48FB1', '#EC407A'],
37
+ // Sliders - peach
38
+ slider: ['#FFCCBC', '#FFAB91', '#FF8A65'],
39
+ // Menu items - salmon
40
+ menuitem: ['#FFAB91', '#FF8A65', '#FF7043'],
41
+ menuitemcheckbox: ['#FFAB91', '#FF8A65', '#FF7043'],
42
+ menuitemradio: ['#FFAB91', '#FF8A65', '#FF7043'],
43
+ // Tabs/Options - amber
44
+ tab: ['#FFE082', '#FFD54F', '#FFC107'],
45
+ option: ['#FFE082', '#FFD54F', '#FFC107'],
46
+ treeitem: ['#FFE082', '#FFD54F', '#FFC107'],
47
+ };
48
+ // Default yellow for unknown roles
49
+ const DEFAULT_COLORS = ['#FFF785', '#FFC542', '#E3BE23'];
50
+ // Use String.raw for CSS syntax highlighting in editors
51
+ const css = String.raw;
52
+ const LABEL_STYLES = css `
53
+ .__pw_label__ {
54
+ position: absolute;
55
+ font: bold 11px Helvetica, Arial, sans-serif;
56
+ padding: 1px 4px;
57
+ border-radius: 3px;
58
+ color: black;
59
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
60
+ white-space: nowrap;
61
+ }
62
+ `;
63
+ const CONTAINER_STYLES = css `
64
+ position: absolute;
65
+ left: 0;
66
+ top: 0;
67
+ z-index: 2147483647;
68
+ pointer-events: none;
69
+ `;
70
+ /**
71
+ * Get an accessibility snapshot with utilities to look up aria refs for elements.
72
+ * Uses Playwright's internal aria-ref selector engine.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const { snapshot, getRefsForLocators } = await getAriaSnapshot({ page })
77
+ * const refs = await getRefsForLocators([page.locator('button'), page.locator('a')])
78
+ * // refs[0].ref is e.g. "e5" - use page.locator('aria-ref=e5') to select
79
+ * ```
80
+ */
81
+ export async function getAriaSnapshot({ page }) {
82
+ const snapshotMethod = page._snapshotForAI;
83
+ if (!snapshotMethod) {
84
+ throw new Error('_snapshotForAI not available. Ensure you are using Playwright.');
85
+ }
86
+ const snapshot = await snapshotMethod.call(page);
87
+ const snapshotStr = typeof snapshot === 'string' ? snapshot : (snapshot.full || JSON.stringify(snapshot, null, 2));
88
+ // Discover refs by probing aria-ref=e1, e2, e3... until 10 consecutive misses
89
+ const refToElement = new Map();
90
+ const refHandles = [];
91
+ let consecutiveMisses = 0;
92
+ let refNum = 1;
93
+ while (consecutiveMisses < 10) {
94
+ const ref = `e${refNum++}`;
95
+ try {
96
+ const locator = page.locator(`aria-ref=${ref}`);
97
+ if (await locator.count() === 1) {
98
+ consecutiveMisses = 0;
99
+ const [info, handle] = await Promise.all([
100
+ locator.evaluate((el) => ({
101
+ role: el.getAttribute('role') || {
102
+ a: el.hasAttribute('href') ? 'link' : 'generic',
103
+ button: 'button', input: { button: 'button', checkbox: 'checkbox', radio: 'radio',
104
+ text: 'textbox', search: 'searchbox', number: 'spinbutton', range: 'slider',
105
+ }[el.type] || 'textbox', select: 'combobox', textarea: 'textbox', img: 'img',
106
+ nav: 'navigation', main: 'main', header: 'banner', footer: 'contentinfo',
107
+ }[el.tagName.toLowerCase()] || 'generic',
108
+ name: el.getAttribute('aria-label') || el.textContent?.trim() || el.placeholder || '',
109
+ })),
110
+ locator.elementHandle({ timeout: 1000 }),
111
+ ]);
112
+ refToElement.set(ref, info);
113
+ if (handle) {
114
+ refHandles.push({ ref, handle });
115
+ }
116
+ }
117
+ else {
118
+ consecutiveMisses++;
119
+ }
120
+ }
121
+ catch {
122
+ consecutiveMisses++;
123
+ }
124
+ }
125
+ // Find refs for multiple locators in a single evaluate call
126
+ const getRefsForLocators = async (locators) => {
127
+ if (locators.length === 0 || refHandles.length === 0) {
128
+ return locators.map(() => null);
129
+ }
130
+ const targetHandles = await Promise.all(locators.map(async (loc) => {
131
+ try {
132
+ return 'elementHandle' in loc
133
+ ? await loc.elementHandle({ timeout: 1000 })
134
+ : loc;
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }));
140
+ const matchingRefs = await page.evaluate(({ targets, candidates }) => targets.map((target) => {
141
+ if (!target)
142
+ return null;
143
+ return candidates.find(({ element }) => element === target)?.ref ?? null;
144
+ }), { targets: targetHandles, candidates: refHandles.map(({ ref, handle }) => ({ ref, element: handle })) });
145
+ return matchingRefs.map((ref) => {
146
+ if (!ref)
147
+ return null;
148
+ const info = refToElement.get(ref);
149
+ return info ? { ...info, ref } : null;
150
+ });
151
+ };
152
+ return {
153
+ snapshot: snapshotStr,
154
+ refToElement,
155
+ refHandles,
156
+ getRefsForLocators,
157
+ getRefForLocator: async (loc) => (await getRefsForLocators([loc]))[0],
158
+ getRefStringForLocator: async (loc) => (await getRefsForLocators([loc]))[0]?.ref ?? null,
159
+ };
160
+ }
161
+ /**
162
+ * Show Vimium-style labels on interactive elements.
163
+ * Labels are yellow badges positioned above each element showing the aria ref (e.g., "e1", "e2").
164
+ * Use with screenshots so agents can see which elements are interactive.
165
+ *
166
+ * Labels auto-hide after 5 seconds to prevent stale labels remaining on the page.
167
+ * Call this function again if the page HTML changes to get fresh labels.
168
+ *
169
+ * By default, only shows labels for truly interactive roles (button, link, textbox, etc.)
170
+ * to reduce visual clutter. Set `interactiveOnly: false` to show all elements with refs.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * const { snapshot, labelCount } = await showAriaRefLabels({ page })
175
+ * await page.screenshot({ path: '/tmp/screenshot.png' })
176
+ * // Agent sees [e5] label on "Submit" button
177
+ * await page.locator('aria-ref=e5').click()
178
+ * // Labels auto-hide after 5 seconds, or call hideAriaRefLabels() manually
179
+ * ```
180
+ */
181
+ export async function showAriaRefLabels({ page, interactiveOnly = true }) {
182
+ const { snapshot, refHandles, refToElement } = await getAriaSnapshot({ page });
183
+ // Filter to only interactive elements if requested
184
+ const filteredRefs = interactiveOnly
185
+ ? refHandles.filter(({ ref }) => {
186
+ const info = refToElement.get(ref);
187
+ return info && INTERACTIVE_ROLES.has(info.role);
188
+ })
189
+ : refHandles;
190
+ // Build refs with role info for color coding
191
+ const refsWithRoles = filteredRefs.map(({ ref, handle }) => ({
192
+ ref,
193
+ element: handle,
194
+ role: refToElement.get(ref)?.role || 'generic',
195
+ }));
196
+ // Single evaluate call: create container, styles, and all labels
197
+ // ElementHandles get unwrapped to DOM elements in browser context
198
+ // Using 'any' types here since this code runs in browser context
199
+ const labelCount = await page.evaluate(
200
+ // Using 'any' for browser types since this runs in browser context
201
+ ({ refs, containerId, containerStyles, labelStyles, roleColors, defaultColors }) => {
202
+ const doc = globalThis.document;
203
+ const win = globalThis;
204
+ // Cancel any pending auto-hide timer from previous call
205
+ const timerKey = '__playwriter_labels_timer__';
206
+ if (win[timerKey]) {
207
+ win.clearTimeout(win[timerKey]);
208
+ win[timerKey] = null;
209
+ }
210
+ // Remove existing labels if present (idempotent)
211
+ doc.getElementById(containerId)?.remove();
212
+ // Create container - absolute positioned, max z-index, no pointer events
213
+ const container = doc.createElement('div');
214
+ container.id = containerId;
215
+ container.style.cssText = containerStyles;
216
+ // Inject base label CSS
217
+ const style = doc.createElement('style');
218
+ style.textContent = labelStyles;
219
+ container.appendChild(style);
220
+ // Track placed label rectangles for overlap detection
221
+ // Each rect is { left, top, right, bottom } in viewport coordinates
222
+ const placedLabels = [];
223
+ // Estimate label dimensions (11px font + padding)
224
+ const LABEL_HEIGHT = 16;
225
+ const LABEL_CHAR_WIDTH = 7; // approximate width per character
226
+ // Parse alpha from rgb/rgba color string (getComputedStyle always returns these formats)
227
+ const getColorAlpha = (color) => {
228
+ if (color === 'transparent')
229
+ return 0;
230
+ // Match rgba(r, g, b, a) or rgb(r, g, b)
231
+ const match = color.match(/rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*(?:,\s*([\d.]+)\s*)?\)/);
232
+ if (match) {
233
+ return match[1] !== undefined ? parseFloat(match[1]) : 1;
234
+ }
235
+ return 1; // Default to opaque for unrecognized formats
236
+ };
237
+ // Check if an element has an opaque background that would block elements behind it
238
+ const isOpaqueElement = (el) => {
239
+ const style = win.getComputedStyle(el);
240
+ // Check element opacity
241
+ const opacity = parseFloat(style.opacity);
242
+ if (opacity < 0.1)
243
+ return false;
244
+ // Check background-color alpha
245
+ const bgAlpha = getColorAlpha(style.backgroundColor);
246
+ if (bgAlpha > 0.1)
247
+ return true;
248
+ // Check if has background-image (usually opaque)
249
+ if (style.backgroundImage !== 'none')
250
+ return true;
251
+ return false;
252
+ };
253
+ // Check if element is visible (not covered by opaque overlay)
254
+ const isElementVisible = (element, rect) => {
255
+ const centerX = rect.left + rect.width / 2;
256
+ const centerY = rect.top + rect.height / 2;
257
+ // Get all elements at this point, from top to bottom
258
+ const stack = doc.elementsFromPoint(centerX, centerY);
259
+ // Find our target element in the stack
260
+ const targetIndex = stack.findIndex((el) => element.contains(el) || el.contains(element));
261
+ // Element not in stack at all - not visible
262
+ if (targetIndex === -1)
263
+ return false;
264
+ // Check if any opaque element is above our target
265
+ for (let i = 0; i < targetIndex; i++) {
266
+ const el = stack[i];
267
+ // Skip our own overlay container
268
+ if (el.id === containerId)
269
+ continue;
270
+ // Skip pointer-events: none elements (decorative overlays)
271
+ if (win.getComputedStyle(el).pointerEvents === 'none')
272
+ continue;
273
+ // If this element is opaque, our target is blocked
274
+ if (isOpaqueElement(el))
275
+ return false;
276
+ }
277
+ return true;
278
+ };
279
+ // Check if two rectangles overlap
280
+ const rectsOverlap = (a, b) => {
281
+ return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
282
+ };
283
+ // Create label for each interactive element
284
+ let count = 0;
285
+ for (const { ref, role, element } of refs) {
286
+ const rect = element.getBoundingClientRect();
287
+ // Skip elements with no size (hidden)
288
+ if (rect.width === 0 || rect.height === 0) {
289
+ continue;
290
+ }
291
+ // Skip elements that are covered by opaque overlays
292
+ if (!isElementVisible(element, rect)) {
293
+ continue;
294
+ }
295
+ // Calculate label position and dimensions
296
+ const labelWidth = ref.length * LABEL_CHAR_WIDTH + 8; // +8 for padding
297
+ const labelLeft = rect.left;
298
+ const labelTop = Math.max(0, rect.top - LABEL_HEIGHT);
299
+ const labelRect = {
300
+ left: labelLeft,
301
+ top: labelTop,
302
+ right: labelLeft + labelWidth,
303
+ bottom: labelTop + LABEL_HEIGHT,
304
+ };
305
+ // Skip if this label would overlap with any already-placed label
306
+ const overlaps = placedLabels.some((placed) => rectsOverlap(labelRect, placed));
307
+ if (overlaps) {
308
+ continue;
309
+ }
310
+ // Get colors for this role
311
+ const [gradTop, gradBottom, border] = roleColors[role] || defaultColors;
312
+ // Place the label
313
+ const label = doc.createElement('div');
314
+ label.className = '__pw_label__';
315
+ label.textContent = ref;
316
+ label.style.background = `linear-gradient(to bottom, ${gradTop} 0%, ${gradBottom} 100%)`;
317
+ label.style.border = `1px solid ${border}`;
318
+ // Position above element, accounting for scroll
319
+ label.style.left = `${win.scrollX + labelLeft}px`;
320
+ label.style.top = `${win.scrollY + labelTop}px`;
321
+ container.appendChild(label);
322
+ placedLabels.push(labelRect);
323
+ count++;
324
+ }
325
+ doc.documentElement.appendChild(container);
326
+ // Auto-hide labels after 5 seconds to prevent stale labels
327
+ // Store timer ID so it can be cancelled if showAriaRefLabels is called again
328
+ win[timerKey] = win.setTimeout(() => {
329
+ doc.getElementById(containerId)?.remove();
330
+ win[timerKey] = null;
331
+ }, 5000);
332
+ return count;
333
+ }, {
334
+ refs: refsWithRoles.map(({ ref, role, element }) => ({ ref, role, element })),
335
+ containerId: LABELS_CONTAINER_ID,
336
+ containerStyles: CONTAINER_STYLES,
337
+ labelStyles: LABEL_STYLES,
338
+ roleColors: ROLE_COLORS,
339
+ defaultColors: DEFAULT_COLORS,
340
+ });
341
+ return { snapshot, labelCount };
342
+ }
343
+ /**
344
+ * Remove all aria ref labels from the page.
345
+ */
346
+ export async function hideAriaRefLabels({ page }) {
347
+ await page.evaluate((id) => {
348
+ const doc = globalThis.document;
349
+ const win = globalThis;
350
+ // Cancel any pending auto-hide timer
351
+ const timerKey = '__playwriter_labels_timer__';
352
+ if (win[timerKey]) {
353
+ win.clearTimeout(win[timerKey]);
354
+ win[timerKey] = null;
355
+ }
356
+ doc.getElementById(id)?.remove();
357
+ }, LABELS_CONTAINER_ID);
358
+ }
359
+ //# sourceMappingURL=aria-snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aria-snapshot.js","sourceRoot":"","sources":["../src/aria-snapshot.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-relay.d.ts","sourceRoot":"","sources":["../src/cdp-relay.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAA0D,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAoB/G,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,IAAI,IAAI,CAAA;IACb,EAAE,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IACrF,GAAG,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;CACvF,CAAA;AAED,wBAAsB,6BAA6B,CAAC,EAAE,IAAY,EAAE,IAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;KAAE,CAAA;CAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAusBzP"}
1
+ {"version":3,"file":"cdp-relay.d.ts","sourceRoot":"","sources":["../src/cdp-relay.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAA0D,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAoB/G,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,IAAI,IAAI,CAAA;IACb,EAAE,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IACrF,GAAG,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;CACvF,CAAA;AAED,wBAAsB,6BAA6B,CAAC,EAAE,IAAY,EAAE,IAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;KAAE,CAAA;CAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAyyBzP"}
package/dist/cdp-relay.js CHANGED
@@ -11,6 +11,21 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
11
11
  let extensionWs = null;
12
12
  const extensionPendingRequests = new Map();
13
13
  let extensionMessageId = 0;
14
+ let extensionPingInterval = null;
15
+ function startExtensionPing() {
16
+ if (extensionPingInterval) {
17
+ clearInterval(extensionPingInterval);
18
+ }
19
+ extensionPingInterval = setInterval(() => {
20
+ extensionWs?.send(JSON.stringify({ method: 'ping' }));
21
+ }, 5000);
22
+ }
23
+ function stopExtensionPing() {
24
+ if (extensionPingInterval) {
25
+ clearInterval(extensionPingInterval);
26
+ extensionPingInterval = null;
27
+ }
28
+ }
14
29
  function logCdpMessage({ direction, clientId, method, sessionId, params, id, source }) {
15
30
  const noisyEvents = [
16
31
  'Network.requestWillBeSentExtraInfo',
@@ -107,6 +122,34 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
107
122
  });
108
123
  });
109
124
  }
125
+ // Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
126
+ // This allows Playwright to connect and immediately have a page to work with.
127
+ async function maybeAutoCreateInitialTab() {
128
+ if (!process.env.PLAYWRITER_AUTO_ENABLE) {
129
+ return;
130
+ }
131
+ if (!extensionWs) {
132
+ return;
133
+ }
134
+ if (connectedTargets.size > 0) {
135
+ return;
136
+ }
137
+ try {
138
+ logger?.log(chalk.blue('Auto-creating initial tab for Playwright client'));
139
+ const result = await sendToExtension({ method: 'createInitialTab', timeout: 10000 });
140
+ if (result.success && result.sessionId && result.targetInfo) {
141
+ connectedTargets.set(result.sessionId, {
142
+ sessionId: result.sessionId,
143
+ targetId: result.targetInfo.targetId,
144
+ targetInfo: result.targetInfo
145
+ });
146
+ logger?.log(chalk.blue(`Auto-created tab, now have ${connectedTargets.size} targets, url: ${result.targetInfo.url}`));
147
+ }
148
+ }
149
+ catch (e) {
150
+ logger?.error('Failed to auto-create initial tab:', e);
151
+ }
152
+ }
110
153
  async function routeCdpCommand({ method, params, sessionId }) {
111
154
  switch (method) {
112
155
  case 'Browser.getVersion': {
@@ -121,10 +164,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
121
164
  case 'Browser.setDownloadBehavior': {
122
165
  return {};
123
166
  }
167
+ // Target.setAutoAttach is a CDP command Playwright sends on first connection.
168
+ // We use it as the hook to auto-create an initial tab. If Playwright changes
169
+ // its initialization sequence in the future, this could be moved to a different command.
124
170
  case 'Target.setAutoAttach': {
125
171
  if (sessionId) {
126
172
  break;
127
173
  }
174
+ await maybeAutoCreateInitialTab();
128
175
  return {};
129
176
  }
130
177
  case 'Target.setDiscoverTargets': {
@@ -238,7 +285,25 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
238
285
  return c.json({ ok: false }, 400);
239
286
  }
240
287
  });
288
+ // Validate Origin header for WebSocket connections to prevent cross-origin attacks.
289
+ // Browsers always send Origin header for WebSocket connections, but Node.js clients don't.
290
+ // We reject browser origins (except chrome-extension://) to prevent malicious websites
291
+ // from connecting to the local WebSocket server.
292
+ function isAllowedOrigin(origin) {
293
+ if (!origin) {
294
+ return true; // Node.js clients don't send Origin
295
+ }
296
+ if (origin.startsWith('chrome-extension://')) {
297
+ return true; // Chrome extension is allowed
298
+ }
299
+ return false; // Reject browser origins (http://, https://, etc.)
300
+ }
241
301
  app.get('/cdp/:clientId?', (c, next) => {
302
+ const origin = c.req.header('origin');
303
+ if (!isAllowedOrigin(origin)) {
304
+ logger?.log(chalk.red(`Rejecting /cdp WebSocket from origin: ${origin}`));
305
+ return c.text('Forbidden', 403);
306
+ }
242
307
  if (token) {
243
308
  const url = new URL(c.req.url, 'http://localhost');
244
309
  const providedToken = url.searchParams.get('token');
@@ -250,14 +315,15 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
250
315
  }, upgradeWebSocket((c) => {
251
316
  const clientId = c.req.param('clientId') || 'default';
252
317
  return {
253
- onOpen(_event, ws) {
318
+ async onOpen(_event, ws) {
254
319
  if (playwrightClients.has(clientId)) {
255
320
  logger?.log(chalk.red(`Rejecting duplicate client ID: ${clientId}`));
256
321
  ws.close(1000, 'Client ID already connected');
257
322
  return;
258
323
  }
324
+ // Add client first so it can receive Target.attachedToTarget events
259
325
  playwrightClients.set(clientId, { id: clientId, ws });
260
- logger?.log(chalk.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total)`));
326
+ logger?.log(chalk.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total) (extension? ${!!extensionWs}) (${connectedTargets.size} pages)`));
261
327
  },
262
328
  async onMessage(event, ws) {
263
329
  let message;
@@ -302,6 +368,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
302
368
  waitingForDebugger: false
303
369
  }
304
370
  };
371
+ if (!target.targetInfo.url) {
372
+ logger?.error(chalk.red('[Server] WARNING: Target.attachedToTarget sent with empty URL!'), JSON.stringify(attachedPayload));
373
+ }
305
374
  logger?.log(chalk.magenta('[Server] Target.attachedToTarget full payload:'), JSON.stringify(attachedPayload));
306
375
  sendToPlaywright({
307
376
  message: attachedPayload,
@@ -321,6 +390,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
321
390
  }
322
391
  }
323
392
  };
393
+ if (!target.targetInfo.url) {
394
+ logger?.error(chalk.red('[Server] WARNING: Target.targetCreated sent with empty URL!'), JSON.stringify(targetCreatedPayload));
395
+ }
324
396
  logger?.log(chalk.magenta('[Server] Target.targetCreated full payload:'), JSON.stringify(targetCreatedPayload));
325
397
  sendToPlaywright({
326
398
  message: targetCreatedPayload,
@@ -344,6 +416,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
344
416
  waitingForDebugger: false
345
417
  }
346
418
  };
419
+ if (!target.targetInfo.url) {
420
+ logger?.error(chalk.red('[Server] WARNING: Target.attachedToTarget (from attachToTarget) sent with empty URL!'), JSON.stringify(attachedPayload));
421
+ }
347
422
  logger?.log(chalk.magenta('[Server] Target.attachedToTarget (from attachToTarget) payload:'), JSON.stringify(attachedPayload));
348
423
  sendToPlaywright({
349
424
  message: attachedPayload,
@@ -376,7 +451,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
376
451
  }
377
452
  };
378
453
  }));
379
- app.get('/extension', upgradeWebSocket(() => {
454
+ app.get('/extension', (c, next) => {
455
+ const origin = c.req.header('origin');
456
+ if (!isAllowedOrigin(origin)) {
457
+ logger?.log(chalk.red(`Rejecting /extension WebSocket from origin: ${origin}`));
458
+ return c.text('Forbidden', 403);
459
+ }
460
+ return next();
461
+ }, upgradeWebSocket(() => {
380
462
  return {
381
463
  onOpen(_event, ws) {
382
464
  if (extensionWs) {
@@ -394,6 +476,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
394
476
  playwrightClients.clear();
395
477
  }
396
478
  extensionWs = ws;
479
+ startExtensionPing();
397
480
  logger?.log('Extension connected with clean state');
398
481
  },
399
482
  async onMessage(event, ws) {
@@ -405,7 +488,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
405
488
  ws.close(1000, 'Invalid JSON');
406
489
  return;
407
490
  }
408
- if ('id' in message) {
491
+ if (message.id !== undefined) {
409
492
  const pending = extensionPendingRequests.get(message.id);
410
493
  if (!pending) {
411
494
  logger?.log('Unexpected response with id:', message.id);
@@ -419,6 +502,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
419
502
  pending.resolve(message.result);
420
503
  }
421
504
  }
505
+ else if (message.method === 'pong') {
506
+ // Keep-alive response, nothing to do
507
+ }
422
508
  else if (message.method === 'log') {
423
509
  const { level, args } = message.params;
424
510
  const logFn = logger?.[level] || logger?.log;
@@ -441,6 +527,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
441
527
  emitter.emit('cdp:event', { event: cdpEvent, sessionId });
442
528
  if (method === 'Target.attachedToTarget') {
443
529
  const targetParams = params;
530
+ if (!targetParams.targetInfo.url) {
531
+ logger?.error(chalk.red('[Extension] WARNING: Target.attachedToTarget received with empty URL!'), JSON.stringify({ method, params: targetParams, sessionId }));
532
+ }
444
533
  logger?.log(chalk.yellow('[Extension] Target.attachedToTarget full payload:'), JSON.stringify({ method, params: targetParams, sessionId }));
445
534
  // Check if we already sent this target to clients (e.g., from Target.setAutoAttach response)
446
535
  const alreadyConnected = connectedTargets.has(targetParams.sessionId);
@@ -561,7 +650,8 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
561
650
  }
562
651
  },
563
652
  onClose(event, ws) {
564
- logger?.log('Extension disconnected');
653
+ logger?.log(`Extension disconnected: code=${event.code} reason=${event.reason || 'none'}`);
654
+ stopExtensionPing();
565
655
  // If this is an old connection closing after we've already established a new one,
566
656
  // don't clear the global state
567
657
  if (extensionWs && extensionWs !== ws) {