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.
- package/dist/aria-snapshot.d.ts +27 -0
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +111 -4
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +34 -15
- package/dist/cdp-relay.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +30 -13
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.js +130 -0
- package/dist/mcp.test.js.map +1 -1
- package/package.json +1 -1
- package/src/aria-snapshot.ts +139 -4
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-google-snapshot.txt +1 -1
- package/src/assets/aria-labels-google.png +0 -0
- package/src/assets/aria-labels-hacker-news-snapshot.txt +826 -814
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/cdp-relay.ts +39 -16
- package/src/mcp.test.ts +145 -0
- package/src/mcp.ts +35 -14
- package/src/prompt.md +33 -11
package/src/aria-snapshot.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 (
|
|
285
|
-
const LABEL_HEIGHT =
|
|
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=
|
|
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
|