playwriter 0.0.38 → 0.0.40

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.
@@ -1,4 +1,7 @@
1
1
  import type { Page, Locator, ElementHandle } from 'playwright-core'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import os from 'node:os'
2
5
 
3
6
  export interface AriaRef {
4
7
  role: string
@@ -6,6 +9,14 @@ export interface AriaRef {
6
9
  ref: string
7
10
  }
8
11
 
12
+ export interface ScreenshotResult {
13
+ path: string
14
+ base64: string
15
+ mimeType: 'image/jpeg'
16
+ snapshot: string
17
+ labelCount: number
18
+ }
19
+
9
20
  export interface AriaSnapshotResult {
10
21
  snapshot: string
11
22
  refToElement: Map<string, { role: string; name: string }>
@@ -17,7 +28,7 @@ export interface AriaSnapshotResult {
17
28
 
18
29
  const LABELS_CONTAINER_ID = '__playwriter_labels__'
19
30
 
20
- // Roles that represent truly interactive elements (can be clicked, typed into, etc.)
31
+ // Roles that represent interactive elements (clickable, typeable) and media elements
21
32
  const INTERACTIVE_ROLES = new Set([
22
33
  'button',
23
34
  'link',
@@ -35,6 +46,10 @@ const INTERACTIVE_ROLES = new Set([
35
46
  'option',
36
47
  'tab',
37
48
  'treeitem',
49
+ // Media elements - useful for visual tasks
50
+ 'img',
51
+ 'video',
52
+ 'audio',
38
53
  ])
39
54
 
40
55
  // Color categories for different role types - warm color scheme
@@ -63,6 +78,10 @@ const ROLE_COLORS: Record<string, [string, string, string]> = {
63
78
  tab: ['#FFE082', '#FFD54F', '#FFC107'],
64
79
  option: ['#FFE082', '#FFD54F', '#FFC107'],
65
80
  treeitem: ['#FFE082', '#FFD54F', '#FFC107'],
81
+ // Media elements - light blue
82
+ img: ['#B3E5FC', '#81D4FA', '#4FC3F7'],
83
+ video: ['#B3E5FC', '#81D4FA', '#4FC3F7'],
84
+ audio: ['#B3E5FC', '#81D4FA', '#4FC3F7'],
66
85
  }
67
86
 
68
87
  // Default yellow for unknown roles
@@ -197,7 +216,7 @@ export async function getAriaSnapshot({ page }: { page: Page }): Promise<AriaSna
197
216
  * Labels are yellow badges positioned above each element showing the aria ref (e.g., "e1", "e2").
198
217
  * Use with screenshots so agents can see which elements are interactive.
199
218
  *
200
- * Labels auto-hide after 5 seconds to prevent stale labels remaining on the page.
219
+ * Labels auto-hide after 30 seconds to prevent stale labels remaining on the page.
201
220
  * Call this function again if the page HTML changes to get fresh labels.
202
221
  *
203
222
  * By default, only shows labels for truly interactive roles (button, link, textbox, etc.)
@@ -209,7 +228,7 @@ export async function getAriaSnapshot({ page }: { page: Page }): Promise<AriaSna
209
228
  * await page.screenshot({ path: '/tmp/screenshot.png' })
210
229
  * // Agent sees [e5] label on "Submit" button
211
230
  * await page.locator('aria-ref=e5').click()
212
- * // Labels auto-hide after 5 seconds, or call hideAriaRefLabels() manually
231
+ * // Labels auto-hide after 30 seconds, or call hideAriaRefLabels() manually
213
232
  * ```
214
233
  */
215
234
  export async function showAriaRefLabels({ page, interactiveOnly = true }: {
@@ -238,10 +257,9 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
238
257
 
239
258
  // Single evaluate call: create container, styles, and all labels
240
259
  // ElementHandles get unwrapped to DOM elements in browser context
241
- // Using 'any' types here since this code runs in browser context
242
260
  const labelCount = await page.evaluate(
243
261
  // Using 'any' for browser types since this runs in browser context
244
- ({ refs, containerId, containerStyles, labelStyles, roleColors, defaultColors }: {
262
+ function ({ refs, containerId, containerStyles, labelStyles, roleColors, defaultColors }: {
245
263
  refs: Array<{
246
264
  ref: string
247
265
  role: string
@@ -252,7 +270,9 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
252
270
  labelStyles: string
253
271
  roleColors: Record<string, [string, string, string]>
254
272
  defaultColors: [string, string, string]
255
- }) => {
273
+ }) {
274
+ // Polyfill esbuild's __name helper which gets injected by vite-node but doesn't exist in browser
275
+ ;(globalThis as any).__name ||= (fn: any) => fn
256
276
  const doc = (globalThis as any).document
257
277
  const win = globalThis as any
258
278
 
@@ -285,8 +305,10 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
285
305
  const LABEL_CHAR_WIDTH = 7 // approximate width per character
286
306
 
287
307
  // Parse alpha from rgb/rgba color string (getComputedStyle always returns these formats)
288
- const getColorAlpha = (color: string): number => {
289
- if (color === 'transparent') return 0
308
+ function getColorAlpha(color: string): number {
309
+ if (color === 'transparent') {
310
+ return 0
311
+ }
290
312
  // Match rgba(r, g, b, a) or rgb(r, g, b)
291
313
  const match = color.match(/rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*(?:,\s*([\d.]+)\s*)?\)/)
292
314
  if (match) {
@@ -296,25 +318,31 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
296
318
  }
297
319
 
298
320
  // Check if an element has an opaque background that would block elements behind it
299
- const isOpaqueElement = (el: any): boolean => {
321
+ function isOpaqueElement(el: any): boolean {
300
322
  const style = win.getComputedStyle(el)
301
323
 
302
324
  // Check element opacity
303
325
  const opacity = parseFloat(style.opacity)
304
- if (opacity < 0.1) return false
326
+ if (opacity < 0.1) {
327
+ return false
328
+ }
305
329
 
306
330
  // Check background-color alpha
307
331
  const bgAlpha = getColorAlpha(style.backgroundColor)
308
- if (bgAlpha > 0.1) return true
332
+ if (bgAlpha > 0.1) {
333
+ return true
334
+ }
309
335
 
310
336
  // Check if has background-image (usually opaque)
311
- if (style.backgroundImage !== 'none') return true
337
+ if (style.backgroundImage !== 'none') {
338
+ return true
339
+ }
312
340
 
313
341
  return false
314
342
  }
315
343
 
316
344
  // Check if element is visible (not covered by opaque overlay)
317
- const isElementVisible = (element: any, rect: any): boolean => {
345
+ function isElementVisible(element: any, rect: any): boolean {
318
346
  const centerX = rect.left + rect.width / 2
319
347
  const centerY = rect.top + rect.height / 2
320
348
 
@@ -322,32 +350,44 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
322
350
  const stack = doc.elementsFromPoint(centerX, centerY) as any[]
323
351
 
324
352
  // Find our target element in the stack
325
- const targetIndex = stack.findIndex((el: any) =>
326
- element.contains(el) || el.contains(element)
327
- )
353
+ let targetIndex = -1
354
+ for (let i = 0; i < stack.length; i++) {
355
+ if (element.contains(stack[i]) || stack[i].contains(element)) {
356
+ targetIndex = i
357
+ break
358
+ }
359
+ }
328
360
 
329
361
  // Element not in stack at all - not visible
330
- if (targetIndex === -1) return false
362
+ if (targetIndex === -1) {
363
+ return false
364
+ }
331
365
 
332
366
  // Check if any opaque element is above our target
333
367
  for (let i = 0; i < targetIndex; i++) {
334
368
  const el = stack[i]
335
369
  // Skip our own overlay container
336
- if (el.id === containerId) continue
370
+ if (el.id === containerId) {
371
+ continue
372
+ }
337
373
  // Skip pointer-events: none elements (decorative overlays)
338
- if (win.getComputedStyle(el).pointerEvents === 'none') continue
374
+ if (win.getComputedStyle(el).pointerEvents === 'none') {
375
+ continue
376
+ }
339
377
  // If this element is opaque, our target is blocked
340
- if (isOpaqueElement(el)) return false
378
+ if (isOpaqueElement(el)) {
379
+ return false
380
+ }
341
381
  }
342
382
 
343
383
  return true
344
384
  }
345
385
 
346
386
  // Check if two rectangles overlap
347
- const rectsOverlap = (
387
+ function rectsOverlap(
348
388
  a: { left: number; top: number; right: number; bottom: number },
349
389
  b: { left: number; top: number; right: number; bottom: number }
350
- ) => {
390
+ ) {
351
391
  return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top
352
392
  }
353
393
 
@@ -378,7 +418,13 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
378
418
  }
379
419
 
380
420
  // Skip if this label would overlap with any already-placed label
381
- const overlaps = placedLabels.some((placed) => rectsOverlap(labelRect, placed))
421
+ let overlaps = false
422
+ for (const placed of placedLabels) {
423
+ if (rectsOverlap(labelRect, placed)) {
424
+ overlaps = true
425
+ break
426
+ }
427
+ }
382
428
  if (overlaps) {
383
429
  continue
384
430
  }
@@ -404,12 +450,12 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
404
450
 
405
451
  doc.documentElement.appendChild(container)
406
452
 
407
- // Auto-hide labels after 5 seconds to prevent stale labels
453
+ // Auto-hide labels after 30 seconds to prevent stale labels
408
454
  // Store timer ID so it can be cancelled if showAriaRefLabels is called again
409
- win[timerKey] = win.setTimeout(() => {
455
+ win[timerKey] = win.setTimeout(function() {
410
456
  doc.getElementById(containerId)?.remove()
411
457
  win[timerKey] = null
412
- }, 5000)
458
+ }, 30000)
413
459
 
414
460
  return count
415
461
  },
@@ -444,3 +490,60 @@ export async function hideAriaRefLabels({ page }: { page: Page }): Promise<void>
444
490
  doc.getElementById(id)?.remove()
445
491
  }, LABELS_CONTAINER_ID)
446
492
  }
493
+
494
+ /**
495
+ * Take a screenshot with accessibility labels overlaid on interactive elements.
496
+ * Shows Vimium-style labels, captures the screenshot, then removes the labels.
497
+ * The screenshot is automatically included in the MCP response.
498
+ *
499
+ * @param collector - Array to collect screenshots (passed by MCP execute tool)
500
+ *
501
+ * @example
502
+ * ```ts
503
+ * await screenshotWithAccessibilityLabels({ page })
504
+ * // Screenshot is automatically included in the MCP response
505
+ * // Use aria-ref from the snapshot to interact with elements
506
+ * await page.locator('aria-ref=e5').click()
507
+ * ```
508
+ */
509
+ export async function screenshotWithAccessibilityLabels({ page, interactiveOnly = true, collector }: {
510
+ page: Page
511
+ interactiveOnly?: boolean
512
+ collector: ScreenshotResult[]
513
+ }): Promise<void> {
514
+ // Show labels and get snapshot
515
+ const { snapshot, labelCount } = await showAriaRefLabels({ page, interactiveOnly })
516
+
517
+ // Generate unique filename with timestamp
518
+ const timestamp = Date.now()
519
+ const random = Math.random().toString(36).slice(2, 6)
520
+ const filename = `playwriter-screenshot-${timestamp}-${random}.jpg`
521
+
522
+ // Use ./tmp folder (gitignored) instead of system temp
523
+ const tmpDir = path.join(process.cwd(), 'tmp')
524
+ if (!fs.existsSync(tmpDir)) {
525
+ fs.mkdirSync(tmpDir, { recursive: true })
526
+ }
527
+ const screenshotPath = path.join(tmpDir, filename)
528
+
529
+ // Take screenshot
530
+ const buffer = await page.screenshot({ type: 'jpeg', quality: 80 })
531
+
532
+ // Save to file
533
+ fs.writeFileSync(screenshotPath, buffer)
534
+
535
+ // Convert to base64
536
+ const base64 = buffer.toString('base64')
537
+
538
+ // Hide labels
539
+ await hideAriaRefLabels({ page })
540
+
541
+ // Add to collector array
542
+ collector.push({
543
+ path: screenshotPath,
544
+ base64,
545
+ mimeType: 'image/jpeg',
546
+ snapshot,
547
+ labelCount,
548
+ })
549
+ }
Binary file
@@ -31,7 +31,7 @@
31
31
  - generic [ref=e55]:
32
32
  - text: "Google offered in:"
33
33
  - link [ref=e56] [cursor=pointer]:
34
- - /url: https://www.google.com/setprefs?sig=0_LaNB9MnwJduON19vFVmxHtMW8aY%3D&hl=it&source=homepage&sa=X&ved=0ahUKEwj22fSr2--RAxXoGTQIHVbSDAcQ2ZgBCBU
34
+ - /url: https://www.google.com/setprefs?sig=0_U_LREl4THfXa2sQ-b49g0Jd45jk%3D&hl=it&source=homepage&sa=X&ved=0ahUKEwiYgJn4hfSRAxU00xoGHUTZBMEQ2ZgBCBU
35
35
  - text: Italiano
36
36
  - contentinfo [ref=e58]:
37
37
  - generic [ref=e59]: Italy
Binary file