feat: add media elements (img, video, audio) to aria labels
- Added img, video, audio to INTERACTIVE_ROLES for visual tasks - Light blue color scheme for media element labels - Agents can now reference images by aria-ref for visual tasks
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
import type { Page, Locator, ElementHandle } from 'playwright-core'
|
import type { Page, Locator, ElementHandle } from 'playwright-core'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import os from 'node:os'
|
||||||
|
|
||||||
export interface AriaRef {
|
export interface AriaRef {
|
||||||
role: string
|
role: string
|
||||||
@@ -6,6 +9,14 @@ export interface AriaRef {
|
|||||||
ref: string
|
ref: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScreenshotResult {
|
||||||
|
path: string
|
||||||
|
base64: string
|
||||||
|
mimeType: 'image/jpeg'
|
||||||
|
snapshot: string
|
||||||
|
labelCount: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface AriaSnapshotResult {
|
export interface AriaSnapshotResult {
|
||||||
snapshot: string
|
snapshot: string
|
||||||
refToElement: Map<string, { role: string; name: string }>
|
refToElement: Map<string, { role: string; name: string }>
|
||||||
@@ -17,7 +28,7 @@ export interface AriaSnapshotResult {
|
|||||||
|
|
||||||
const LABELS_CONTAINER_ID = '__playwriter_labels__'
|
const LABELS_CONTAINER_ID = '__playwriter_labels__'
|
||||||
|
|
||||||
// Roles that represent truly interactive elements (can be clicked, typed into, etc.)
|
// Roles that represent interactive elements (clickable, typeable) and media elements
|
||||||
const INTERACTIVE_ROLES = new Set([
|
const INTERACTIVE_ROLES = new Set([
|
||||||
'button',
|
'button',
|
||||||
'link',
|
'link',
|
||||||
@@ -35,6 +46,10 @@ const INTERACTIVE_ROLES = new Set([
|
|||||||
'option',
|
'option',
|
||||||
'tab',
|
'tab',
|
||||||
'treeitem',
|
'treeitem',
|
||||||
|
// Media elements - useful for visual tasks
|
||||||
|
'img',
|
||||||
|
'video',
|
||||||
|
'audio',
|
||||||
])
|
])
|
||||||
|
|
||||||
// Color categories for different role types - warm color scheme
|
// Color categories for different role types - warm color scheme
|
||||||
@@ -63,6 +78,10 @@ const ROLE_COLORS: Record<string, [string, string, string]> = {
|
|||||||
tab: ['#FFE082', '#FFD54F', '#FFC107'],
|
tab: ['#FFE082', '#FFD54F', '#FFC107'],
|
||||||
option: ['#FFE082', '#FFD54F', '#FFC107'],
|
option: ['#FFE082', '#FFD54F', '#FFC107'],
|
||||||
treeitem: ['#FFE082', '#FFD54F', '#FFC107'],
|
treeitem: ['#FFE082', '#FFD54F', '#FFC107'],
|
||||||
|
// Media elements - light blue
|
||||||
|
img: ['#B3E5FC', '#81D4FA', '#4FC3F7'],
|
||||||
|
video: ['#B3E5FC', '#81D4FA', '#4FC3F7'],
|
||||||
|
audio: ['#B3E5FC', '#81D4FA', '#4FC3F7'],
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default yellow for unknown roles
|
// Default yellow for unknown roles
|
||||||
@@ -471,3 +490,60 @@ export async function hideAriaRefLabels({ page }: { page: Page }): Promise<void>
|
|||||||
doc.getElementById(id)?.remove()
|
doc.getElementById(id)?.remove()
|
||||||
}, LABELS_CONTAINER_ID)
|
}, LABELS_CONTAINER_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a screenshot with accessibility labels overlaid on interactive elements.
|
||||||
|
* Shows Vimium-style labels, captures the screenshot, then removes the labels.
|
||||||
|
* The screenshot is automatically included in the MCP response.
|
||||||
|
*
|
||||||
|
* @param collector - Array to collect screenshots (passed by MCP execute tool)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* await screenshotWithAccessibilityLabels({ page })
|
||||||
|
* // Screenshot is automatically included in the MCP response
|
||||||
|
* // Use aria-ref from the snapshot to interact with elements
|
||||||
|
* await page.locator('aria-ref=e5').click()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function screenshotWithAccessibilityLabels({ page, interactiveOnly = true, collector }: {
|
||||||
|
page: Page
|
||||||
|
interactiveOnly?: boolean
|
||||||
|
collector: ScreenshotResult[]
|
||||||
|
}): Promise<void> {
|
||||||
|
// Show labels and get snapshot
|
||||||
|
const { snapshot, labelCount } = await showAriaRefLabels({ page, interactiveOnly })
|
||||||
|
|
||||||
|
// Generate unique filename with timestamp
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const random = Math.random().toString(36).slice(2, 6)
|
||||||
|
const filename = `playwriter-screenshot-${timestamp}-${random}.jpg`
|
||||||
|
|
||||||
|
// Use ./tmp folder (gitignored) instead of system temp
|
||||||
|
const tmpDir = path.join(process.cwd(), 'tmp')
|
||||||
|
if (!fs.existsSync(tmpDir)) {
|
||||||
|
fs.mkdirSync(tmpDir, { recursive: true })
|
||||||
|
}
|
||||||
|
const screenshotPath = path.join(tmpDir, filename)
|
||||||
|
|
||||||
|
// Take screenshot
|
||||||
|
const buffer = await page.screenshot({ type: 'jpeg', quality: 80 })
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
fs.writeFileSync(screenshotPath, buffer)
|
||||||
|
|
||||||
|
// Convert to base64
|
||||||
|
const base64 = buffer.toString('base64')
|
||||||
|
|
||||||
|
// Hide labels
|
||||||
|
await hideAriaRefLabels({ page })
|
||||||
|
|
||||||
|
// Add to collector array
|
||||||
|
collector.push({
|
||||||
|
path: screenshotPath,
|
||||||
|
base64,
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
snapshot,
|
||||||
|
labelCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user