add showAriaRefLabels/hideAriaRefLabels to MCP context

- Functions available in execute tool context
- Auto-hide labels after 5 seconds to prevent stale overlays
- Updated README with MCP usage example
- Example saves screenshot to /tmp
This commit is contained in:
Tommy D. Rossi
2026-01-03 15:56:31 +01:00
parent d4072d41b6
commit d889cea6b1
5 changed files with 640 additions and 631 deletions
+13 -11
View File
@@ -95,29 +95,31 @@ server.close()
### Visual Aria Ref Labels ### Visual Aria Ref Labels
Playwriter includes Vimium-style visual labels that overlay interactive elements, making it easy for AI agents to identify and click elements from screenshots. Playwriter includes Vimium-style visual labels that overlay interactive elements, making it easy for AI agents to identify and click elements from screenshots. These functions are available in the MCP context.
```typescript
import { showAriaRefLabels, hideAriaRefLabels } from 'playwriter'
```javascript
// Show labels on all visible interactive elements // Show labels on all visible interactive elements
const { snapshot, labelCount } = await showAriaRefLabels({ page }) const { snapshot, labelCount } = await showAriaRefLabels({ page })
console.log(`Showing ${labelCount} labels`)
// Take a screenshot - agent sees labeled elements // Take a screenshot with labels visible
const screenshot = await page.screenshot() await page.screenshot({ path: '/tmp/labeled-page.png' })
// Agent can click using the aria-ref selector // Use the snapshot to find elements, then interact using aria-ref selector
await page.locator('aria-ref=e5').click() await page.locator('aria-ref=e5').click()
// Remove labels when done // Labels auto-hide after 5 seconds, or remove manually:
await hideAriaRefLabels({ page }) await hideAriaRefLabels({ page })
``` ```
**Important:** Labels auto-hide after 5 seconds to prevent stale labels. If the page HTML changes (navigation, dynamic content), call `showAriaRefLabels()` again to get fresh labels matching the current DOM.
**Features:** **Features:**
- **Role filtering** - Only shows labels for interactive elements (buttons, links, inputs, etc.) - **Role filtering** - Only shows labels for interactive elements (buttons, links, inputs, etc.)
- **Visibility detection** - Skips elements covered by modals or overlays using `elementsFromPoint()` - **Visibility detection** - Skips elements covered by modals or overlays
- **Overlap prevention** - Greedy algorithm skips labels that would overlap with already-placed ones - **Overlap prevention** - Skips labels that would overlap with already-placed ones
- **Color-coded by type** - Warm color scheme helps distinguish element types at a glance - **Color-coded by type** - Warm color scheme helps distinguish element types
- **Auto-hide** - Labels disappear after 5 seconds to prevent stale overlays
**Color legend:** **Color legend:**
+11 -2
View File
@@ -197,16 +197,19 @@ export async function getAriaSnapshot({ page }: { page: Page }): Promise<AriaSna
* Labels are yellow badges positioned above each element showing the aria ref (e.g., "e1", "e2"). * Labels are yellow badges positioned above each element showing the aria ref (e.g., "e1", "e2").
* Use with screenshots so agents can see which elements are interactive. * Use with screenshots so agents can see which elements are interactive.
* *
* Labels auto-hide after 5 seconds to prevent stale labels remaining on the page.
* Call this function again if the page HTML changes to get fresh labels.
*
* By default, only shows labels for truly interactive roles (button, link, textbox, etc.) * By default, only shows labels for truly interactive roles (button, link, textbox, etc.)
* to reduce visual clutter. Set `interactiveOnly: false` to show all elements with refs. * to reduce visual clutter. Set `interactiveOnly: false` to show all elements with refs.
* *
* @example * @example
* ```ts * ```ts
* const { snapshot, labelCount } = await showAriaRefLabels({ page }) * const { snapshot, labelCount } = await showAriaRefLabels({ page })
* const screenshot = await page.screenshot() * await page.screenshot({ path: '/tmp/screenshot.png' })
* // Agent sees [e5] label on "Submit" button * // Agent sees [e5] label on "Submit" button
* await page.locator('aria-ref=e5').click() * await page.locator('aria-ref=e5').click()
* await hideAriaRefLabels({ page }) * // Labels auto-hide after 5 seconds, or call hideAriaRefLabels() manually
* ``` * ```
*/ */
export async function showAriaRefLabels({ page, interactiveOnly = true }: { export async function showAriaRefLabels({ page, interactiveOnly = true }: {
@@ -393,6 +396,12 @@ export async function showAriaRefLabels({ page, interactiveOnly = true }: {
} }
doc.documentElement.appendChild(container) doc.documentElement.appendChild(container)
// Auto-hide labels after 5 seconds to prevent stale labels
win.setTimeout(() => {
doc.getElementById(containerId)?.remove()
}, 5000)
return count return count
}, },
{ {
@@ -31,7 +31,7 @@
- generic [ref=e55]: - generic [ref=e55]:
- text: "Google offered in:" - text: "Google offered in:"
- link [ref=e56] [cursor=pointer]: - link [ref=e56] [cursor=pointer]:
- /url: https://www.google.com/setprefs?sig=0_ONtbUSl6RHgXjr8-VtrSj8Fbgus%3D&hl=it&source=homepage&sa=X&ved=0ahUKEwia5cnuzu-RAxVO78kDHZbbBV8Q2ZgBCBU - /url: https://www.google.com/setprefs?sig=0_rBKrQWz6eh9LiHviDdhtYH9CsEc%3D&hl=it&source=homepage&sa=X&ved=0ahUKEwiR9Py10O-RAxWpGDQIHWbZLRoQ2ZgBCBU
- text: Italiano - text: Italiano
- contentinfo [ref=e58]: - contentinfo [ref=e58]:
- generic [ref=e59]: Italy - generic [ref=e59]: Italy
File diff suppressed because it is too large Load Diff
+7
View File
@@ -21,6 +21,7 @@ import { Editor } from './editor.js'
import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js' import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js'
import { getReactSource, type ReactSourceLocation } from './react-source.js' import { getReactSource, type ReactSourceLocation } from './react-source.js'
import { ScopedFS } from './scoped-fs.js' import { ScopedFS } from './scoped-fs.js'
import { showAriaRefLabels, hideAriaRefLabels } from './aria-snapshot.js'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
@@ -85,6 +86,8 @@ interface VMContext {
getStylesForLocator: (options: { locator: any }) => Promise<StylesResult> getStylesForLocator: (options: { locator: any }) => Promise<StylesResult>
formatStylesAsText: (styles: StylesResult) => string formatStylesAsText: (styles: StylesResult) => string
getReactSource: (options: { locator: any }) => Promise<ReactSourceLocation | null> getReactSource: (options: { locator: any }) => Promise<ReactSourceLocation | null>
showAriaRefLabels: (options: { page: Page; interactiveOnly?: boolean }) => Promise<{ snapshot: string; labelCount: number }>
hideAriaRefLabels: (options: { page: Page }) => Promise<void>
require: NodeRequire require: NodeRequire
import: (specifier: string) => Promise<any> import: (specifier: string) => Promise<any>
} }
@@ -877,6 +880,8 @@ server.tool(
getStylesForLocator: getStylesForLocatorFn, getStylesForLocator: getStylesForLocatorFn,
formatStylesAsText, formatStylesAsText,
getReactSource: getReactSourceFn, getReactSource: getReactSourceFn,
showAriaRefLabels,
hideAriaRefLabels,
resetPlaywright: async () => { resetPlaywright: async () => {
const { page: newPage, context: newContext } = await resetConnection() const { page: newPage, context: newContext } = await resetConnection()
@@ -896,6 +901,8 @@ server.tool(
getStylesForLocator: getStylesForLocatorFn, getStylesForLocator: getStylesForLocatorFn,
formatStylesAsText, formatStylesAsText,
getReactSource: getReactSourceFn, getReactSource: getReactSourceFn,
showAriaRefLabels,
hideAriaRefLabels,
resetPlaywright: vmContextObj.resetPlaywright, resetPlaywright: vmContextObj.resetPlaywright,
require: sandboxedRequire, require: sandboxedRequire,
// TODO --experimental-vm-modules is needed to make import work in vm // TODO --experimental-vm-modules is needed to make import work in vm