playwriter 0.0.39 → 0.0.41

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
@@ -74,7 +93,7 @@ const css = String.raw
74
93
  const LABEL_STYLES = css`
75
94
  .__pw_label__ {
76
95
  position: absolute;
77
- font: bold 11px Helvetica, Arial, sans-serif;
96
+ font: bold 12px Helvetica, Arial, sans-serif;
78
97
  padding: 1px 4px;
79
98
  border-radius: 3px;
80
99
  color: black;
@@ -281,8 +300,8 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
281
300
  // Each rect is { left, top, right, bottom } in viewport coordinates
282
301
  const placedLabels: Array<{ left: number; top: number; right: number; bottom: number }> = []
283
302
 
284
- // Estimate label dimensions (11px font + padding)
285
- const LABEL_HEIGHT = 16
303
+ // Estimate label dimensions (12px font + padding)
304
+ const LABEL_HEIGHT = 17
286
305
  const LABEL_CHAR_WIDTH = 7 // approximate width per character
287
306
 
288
307
  // Parse alpha from rgb/rgba color string (getComputedStyle always returns these formats)
@@ -372,6 +391,41 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
372
391
  return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top
373
392
  }
374
393
 
394
+ // Create SVG for connector lines
395
+ const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg')
396
+ svg.style.cssText = 'position:absolute;left:0;top:0;pointer-events:none;overflow:visible;'
397
+ svg.setAttribute('width', `${doc.documentElement.scrollWidth}`)
398
+ svg.setAttribute('height', `${doc.documentElement.scrollHeight}`)
399
+
400
+ // Create defs for arrow markers (one per color)
401
+ const defs = doc.createElementNS('http://www.w3.org/2000/svg', 'defs')
402
+ svg.appendChild(defs)
403
+ const markerCache: Record<string, string> = {}
404
+
405
+ function getArrowMarkerId(color: string): string {
406
+ if (markerCache[color]) {
407
+ return markerCache[color]
408
+ }
409
+ const markerId = `arrow-${color.replace('#', '')}`
410
+ const marker = doc.createElementNS('http://www.w3.org/2000/svg', 'marker')
411
+ marker.setAttribute('id', markerId)
412
+ marker.setAttribute('viewBox', '0 0 10 10')
413
+ marker.setAttribute('refX', '9')
414
+ marker.setAttribute('refY', '5')
415
+ marker.setAttribute('markerWidth', '6')
416
+ marker.setAttribute('markerHeight', '6')
417
+ marker.setAttribute('orient', 'auto-start-reverse')
418
+ const path = doc.createElementNS('http://www.w3.org/2000/svg', 'path')
419
+ path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z')
420
+ path.setAttribute('fill', color)
421
+ marker.appendChild(path)
422
+ defs.appendChild(marker)
423
+ markerCache[color] = markerId
424
+ return markerId
425
+ }
426
+
427
+ container.appendChild(svg)
428
+
375
429
  // Create label for each interactive element
376
430
  let count = 0
377
431
  for (const { ref, role, element } of refs) {
@@ -425,6 +479,22 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
425
479
  label.style.top = `${win.scrollY + labelTop}px`
426
480
 
427
481
  container.appendChild(label)
482
+
483
+ // Draw connector line from label bottom-center to element center with arrow
484
+ const line = doc.createElementNS('http://www.w3.org/2000/svg', 'line')
485
+ const labelCenterX = win.scrollX + labelLeft + labelWidth / 2
486
+ const labelBottomY = win.scrollY + labelTop + LABEL_HEIGHT
487
+ const elementCenterX = win.scrollX + rect.left + rect.width / 2
488
+ const elementCenterY = win.scrollY + rect.top + rect.height / 2
489
+ line.setAttribute('x1', `${labelCenterX}`)
490
+ line.setAttribute('y1', `${labelBottomY}`)
491
+ line.setAttribute('x2', `${elementCenterX}`)
492
+ line.setAttribute('y2', `${elementCenterY}`)
493
+ line.setAttribute('stroke', border)
494
+ line.setAttribute('stroke-width', '1.5')
495
+ line.setAttribute('marker-end', `url(#${getArrowMarkerId(border)})`)
496
+ svg.appendChild(line)
497
+
428
498
  placedLabels.push(labelRect)
429
499
  count++
430
500
  }
@@ -471,3 +541,68 @@ export async function hideAriaRefLabels({ page }: { page: Page }): Promise<void>
471
541
  doc.getElementById(id)?.remove()
472
542
  }, LABELS_CONTAINER_ID)
473
543
  }
544
+
545
+ /**
546
+ * Take a screenshot with accessibility labels overlaid on interactive elements.
547
+ * Shows Vimium-style labels, captures the screenshot, then removes the labels.
548
+ * The screenshot is automatically included in the MCP response.
549
+ *
550
+ * @param collector - Array to collect screenshots (passed by MCP execute tool)
551
+ *
552
+ * @example
553
+ * ```ts
554
+ * await screenshotWithAccessibilityLabels({ page })
555
+ * // Screenshot is automatically included in the MCP response
556
+ * // Use aria-ref from the snapshot to interact with elements
557
+ * await page.locator('aria-ref=e5').click()
558
+ * ```
559
+ */
560
+ export async function screenshotWithAccessibilityLabels({ page, interactiveOnly = true, collector }: {
561
+ page: Page
562
+ interactiveOnly?: boolean
563
+ collector: ScreenshotResult[]
564
+ }): Promise<void> {
565
+ // Show labels and get snapshot
566
+ const { snapshot, labelCount } = await showAriaRefLabels({ page, interactiveOnly })
567
+
568
+ // Generate unique filename with timestamp
569
+ const timestamp = Date.now()
570
+ const random = Math.random().toString(36).slice(2, 6)
571
+ const filename = `playwriter-screenshot-${timestamp}-${random}.jpg`
572
+
573
+ // Use ./tmp folder (gitignored) instead of system temp
574
+ const tmpDir = path.join(process.cwd(), 'tmp')
575
+ if (!fs.existsSync(tmpDir)) {
576
+ fs.mkdirSync(tmpDir, { recursive: true })
577
+ }
578
+ const screenshotPath = path.join(tmpDir, filename)
579
+
580
+ // Get actual viewport size (innerWidth/innerHeight, not outer window size)
581
+ const viewport = await page.evaluate('(() => ({ width: window.innerWidth, height: window.innerHeight }))()') as { width: number; height: number }
582
+
583
+ // Take screenshot clipped to actual viewport (excludes browser chrome)
584
+ const buffer = await page.screenshot({
585
+ type: 'jpeg',
586
+ quality: 80,
587
+ scale: 'css',
588
+ clip: { x: 0, y: 0, width: viewport.width, height: viewport.height },
589
+ })
590
+
591
+ // Save to file
592
+ fs.writeFileSync(screenshotPath, buffer)
593
+
594
+ // Convert to base64
595
+ const base64 = buffer.toString('base64')
596
+
597
+ // Hide labels
598
+ await hideAriaRefLabels({ page })
599
+
600
+ // Add to collector array
601
+ collector.push({
602
+ path: screenshotPath,
603
+ base64,
604
+ mimeType: 'image/jpeg',
605
+ snapshot,
606
+ labelCount,
607
+ })
608
+ }
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_XgFjFgidjiUAiTgTlmWfO1H5x10%3D&hl=it&source=homepage&sa=X&ved=0ahUKEwjT-5m25PaRAxUON94AHdLuNggQ2ZgBCBU
35
35
  - text: Italiano
36
36
  - contentinfo [ref=e58]:
37
37
  - generic [ref=e59]: Italy
Binary file