add showAriaRefLabels/hideAriaRefLabels for Vimium-style visual labels

- getAriaSnapshot() discovers aria refs and caches ElementHandles
- showAriaRefLabels() overlays yellow badges on interactive elements
- hideAriaRefLabels() removes labels from page
- test captures screenshots from HN, Google, GitHub
This commit is contained in:
Tommy D. Rossi
2026-01-03 15:17:47 +01:00
parent 22a255e1dd
commit 802e6768e2
10 changed files with 2520 additions and 3 deletions
+191 -3
View File
@@ -686,7 +686,7 @@ describe('MCP Server Tests', () => {
await browser.close()
await page.close()
})
}, 60000)
it('should be able to reconnect after disconnecting everything', async () => {
const browserContext = getBrowserContext()
@@ -2106,8 +2106,8 @@ describe('MCP Server Tests', () => {
{
"text": "Return value:
{
"matchesDark": false,
"matchesLight": true
\"matchesDark\": false,
\"matchesLight\": true
}",
"type": "text",
},
@@ -2117,7 +2117,195 @@ describe('MCP Server Tests', () => {
await page.close()
}, 60000)
it('should get aria ref for locator using getAriaSnapshot', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const page = await browserContext.newPage()
await page.setContent(`
<html>
<body>
<button id="submit-btn">Submit Form</button>
<a href="/about">About Us</a>
<input type="text" placeholder="Enter your name" />
</body>
</html>
`)
await page.bringToFront()
await serviceWorker.evaluate(async () => {
await globalThis.toggleExtensionForActiveTab()
})
await new Promise(r => setTimeout(r, 400))
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
let cdpPage
for (const p of browser.contexts()[0].pages()) {
const html = await p.content()
if (html.includes('submit-btn')) {
cdpPage = p
break
}
}
expect(cdpPage).toBeDefined()
const { getAriaSnapshot } = await import('./aria-snapshot.js')
// Get aria snapshot and verify we can get refs
const ariaResult = await getAriaSnapshot({ page: cdpPage! })
expect(ariaResult.snapshot).toBeDefined()
expect(ariaResult.snapshot.length).toBeGreaterThan(0)
expect(ariaResult.snapshot).toContain('Submit Form')
// Verify refToElement map is populated
expect(ariaResult.refToElement.size).toBeGreaterThan(0)
console.log('RefToElement map size:', ariaResult.refToElement.size)
console.log('RefToElement entries:', [...ariaResult.refToElement.entries()])
// Verify we can select elements using aria-ref selectors
const btnViaAriaRef = cdpPage!.locator('aria-ref=e2')
const btnTextViaRef = await btnViaAriaRef.textContent()
console.log('Button text via aria-ref=e2:', btnTextViaRef)
expect(btnTextViaRef).toBe('Submit Form')
// Get ref for the submit button using getRefForLocator
const submitBtn = cdpPage!.locator('#submit-btn')
const btnAriaRef = await ariaResult.getRefForLocator(submitBtn)
console.log('Button ariaRef:', btnAriaRef)
expect(btnAriaRef).toBeDefined()
expect(btnAriaRef?.role).toBe('button')
expect(btnAriaRef?.name).toBe('Submit Form')
expect(btnAriaRef?.ref).toMatch(/^e\d+$/)
// Verify the ref matches what we can use to select
const btnFromRef = cdpPage!.locator(`aria-ref=${btnAriaRef?.ref}`)
const btnText = await btnFromRef.textContent()
expect(btnText).toBe('Submit Form')
// Test getRefStringForLocator
const btnRefStr = await ariaResult.getRefStringForLocator(submitBtn)
console.log('Button ref string:', btnRefStr)
expect(btnRefStr).toBe(btnAriaRef?.ref)
// Test link
const aboutLink = cdpPage!.locator('a')
const linkAriaRef = await ariaResult.getRefForLocator(aboutLink)
console.log('Link ariaRef:', linkAriaRef)
expect(linkAriaRef).toBeDefined()
expect(linkAriaRef?.role).toBe('link')
expect(linkAriaRef?.name).toBe('About Us')
// Verify the link ref works
const linkFromRef = cdpPage!.locator(`aria-ref=${linkAriaRef?.ref}`)
const linkText = await linkFromRef.textContent()
expect(linkText).toBe('About Us')
// Test input field
const inputField = cdpPage!.locator('input')
const inputAriaRef = await ariaResult.getRefForLocator(inputField)
console.log('Input ariaRef:', inputAriaRef)
expect(inputAriaRef).toBeDefined()
expect(inputAriaRef?.role).toBe('textbox')
// Test batch getRefsForLocators - single evaluate call for multiple elements
const batchRefs = await ariaResult.getRefsForLocators([submitBtn, aboutLink, inputField])
console.log('Batch refs:', batchRefs)
expect(batchRefs).toHaveLength(3)
expect(batchRefs[0]?.ref).toBe(btnAriaRef?.ref)
expect(batchRefs[1]?.ref).toBe(linkAriaRef?.ref)
expect(batchRefs[2]?.ref).toBe(inputAriaRef?.ref)
await browser.close()
await page.close()
}, 60000)
it('should show aria ref labels on real pages and save screenshots', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const { showAriaRefLabels, hideAriaRefLabels } = await import('./aria-snapshot.js')
const fs = await import('node:fs')
const path = await import('node:path')
// Create assets folder for screenshots
const assetsDir = path.join(path.dirname(new URL(import.meta.url).pathname), 'assets')
if (!fs.existsSync(assetsDir)) {
fs.mkdirSync(assetsDir, { recursive: true })
}
const testPages = [
{ name: 'hacker-news', url: 'https://news.ycombinator.com/' },
{ name: 'google', url: 'https://www.google.com/' },
{ name: 'github', url: 'https://github.com/' },
]
// Create all pages and enable extension for each
const pages = await Promise.all(
testPages.map(async ({ name, url }) => {
const page = await browserContext.newPage()
await page.goto(url, { waitUntil: 'domcontentloaded' })
return { name, url, page }
})
)
// Enable extension for each tab (must be done sequentially as it uses active tab)
for (const { page } of pages) {
await page.bringToFront()
await serviceWorker.evaluate(async () => {
await globalThis.toggleExtensionForActiveTab()
})
}
// Connect CDP and process all pages concurrently
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
await Promise.all(
pages.map(async ({ name, url, page }) => {
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes(new URL(url).hostname))
if (!cdpPage) {
console.log(`Could not find CDP page for ${name}, skipping...`)
return
}
// Show aria ref labels
const { snapshot, labelCount } = await showAriaRefLabels({ page: cdpPage })
console.log(`${name}: ${labelCount} labels shown`)
expect(labelCount).toBeGreaterThan(0)
// Take screenshot with labels visible
const screenshot = await cdpPage.screenshot({ type: 'png', fullPage: false })
const screenshotPath = path.join(assetsDir, `aria-labels-${name}.png`)
fs.writeFileSync(screenshotPath, screenshot)
console.log(`Screenshot saved: ${screenshotPath}`)
// Save snapshot text for reference
const snapshotPath = path.join(assetsDir, `aria-labels-${name}-snapshot.txt`)
fs.writeFileSync(snapshotPath, snapshot)
// Verify labels are in DOM
const labelElements = await cdpPage.evaluate(() =>
document.querySelectorAll('.__pw_label__').length
)
expect(labelElements).toBe(labelCount)
// Cleanup
await hideAriaRefLabels({ page: cdpPage })
// Verify labels removed
const labelsAfterHide = await cdpPage.evaluate(() =>
document.getElementById('__playwriter_labels__')
)
expect(labelsAfterHide).toBeNull()
await page.close()
})
)
await browser.close()
console.log(`Screenshots saved to: ${assetsDir}`)
}, 120000)
})