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:
@@ -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:**
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user