add test for getLocatorStringForElement

This commit is contained in:
Tommy D. Rossi
2025-12-17 18:45:30 +01:00
parent 3b16d43047
commit db4f7ef5a5
5 changed files with 170 additions and 21 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
"types": "dist/index.d.ts",
"repository": "https://github.com/remorses/playwriter",
"scripts": {
"build": "rm -rf dist *.tsbuildinfo && mkdir dist && cp src/resource.md src/prompt.md dist/ && tsc",
"build": "rm -rf dist *.tsbuildinfo && mkdir dist && cp src/resource.md src/prompt.md dist/ && vite-node scripts/download-selector-generator.ts && tsc",
"prepublishOnly": "pnpm build",
"watch": "tsc -w",
"typecheck": "tsc --noEmit",
@@ -0,0 +1,55 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const distDir = path.join(__dirname, '..', 'dist')
const SELECTOR_GENERATOR_URL = 'https://esm.sh/@mizchi/selector-generator@1.50.0-next/es2022/selector-generator.bundle.mjs'
async function main() {
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true })
}
console.log('Downloading selector-generator from esm.sh...')
const response = await fetch(SELECTOR_GENERATOR_URL)
if (!response.ok) {
throw new Error(`Failed to download: ${response.status} ${response.statusText}`)
}
let esmScript = await response.text()
const exportMatch = esmScript.match(/export\s*\{([^}]+)\}\s*;?\s*(\/\/.*)?$/)
if (!exportMatch) {
throw new Error('Could not find export statement in bundle')
}
const exports = exportMatch[1].split(',').map((e) => {
const parts = e.trim().split(/\s+as\s+/)
return { local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() }
})
const createSelectorGenerator = exports.find((e) => e.exported === 'createSelectorGenerator')
const toLocator = exports.find((e) => e.exported === 'toLocator')
if (!createSelectorGenerator || !toLocator) {
throw new Error('Could not find createSelectorGenerator or toLocator exports')
}
esmScript = esmScript.replace(/export\s*\{[^}]+\}\s*;?\s*(\/\/.*)?$/, '')
const wrappedScript = `(function() {
${esmScript}
globalThis.__selectorGenerator = { createSelectorGenerator: ${createSelectorGenerator.local}, toLocator: ${toLocator.local} };
})();`
const outputPath = path.join(distDir, 'selector-generator.js')
fs.writeFileSync(outputPath, wrappedScript)
console.log(`Saved to ${outputPath}`)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
+86 -1
View File
@@ -175,6 +175,37 @@ describe('MCP Server Tests', () => {
return testCtx.browserContext
}
it('should inject script via addScriptTag through CDP relay', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const page = await browserContext.newPage()
await page.setContent('<html><body><button id="btn">Click</button></body></html>')
await page.bringToFront()
await serviceWorker.evaluate(async () => {
await globalThis.toggleExtensionForActiveTab()
})
await new Promise(r => setTimeout(r, 500))
const browser = await chromium.connectOverCDP(getCdpUrl())
const cdpPage = browser.contexts()[0].pages().find(p => {
return p.url().startsWith('about:')
})
expect(cdpPage).toBeDefined()
const hasGlobalBefore = await cdpPage!.evaluate(() => !!(globalThis as any).__testGlobal)
expect(hasGlobalBefore).toBe(false)
await cdpPage!.addScriptTag({ content: 'globalThis.__testGlobal = { foo: "bar" };' })
const hasGlobalAfter = await cdpPage!.evaluate(() => (globalThis as any).__testGlobal)
expect(hasGlobalAfter).toEqual({ foo: 'bar' })
await browser.close()
await page.close()
}, 60000)
it('should execute code and capture console output', async () => {
await client.callTool({
name: 'execute',
@@ -1331,6 +1362,60 @@ describe('MCP Server Tests', () => {
await page.close()
}, 60000)
it('should get locator string for element using getLocatorStringForElement', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const page = await browserContext.newPage()
await page.setContent(`
<html>
<body>
<button id="test-btn">Click Me</button>
<input type="text" placeholder="Enter name" />
</body>
</html>
`)
await page.bringToFront()
await serviceWorker.evaluate(async () => {
await globalThis.toggleExtensionForActiveTab()
})
await new Promise(r => setTimeout(r, 500))
const result = await client.callTool({
name: 'execute',
arguments: {
code: js`
let testPage;
for (const p of context.pages()) {
const html = await p.content();
if (html.includes('test-btn')) { testPage = p; break; }
}
if (!testPage) throw new Error('Test page not found');
const btn = testPage.locator('#test-btn');
const locatorString = await getLocatorStringForElement(btn);
console.log('Locator string:', locatorString);
const locatorFromString = eval('testPage.' + locatorString);
const count = await locatorFromString.count();
console.log('Locator count:', count);
const text = await locatorFromString.textContent();
console.log('Locator text:', text);
`,
timeout: 30000,
},
})
expect(result.isError).toBeFalsy()
const text = (result.content as any)[0]?.text || ''
expect(text).toContain('Locator string:')
expect(text).toContain("getByRole('button', { name: 'Click Me' })")
expect(text).toContain('Locator count: 1')
expect(text).toContain('Locator text: Click Me')
await page.close()
}, 60000)
it('should return correct layout metrics via CDP', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
@@ -1711,7 +1796,7 @@ describe('CDP Session Tests', () => {
sampleFunctionNames: functionNames,
}).toMatchInlineSnapshot(`
{
"durationMicroseconds": 6486,
"durationMicroseconds": 6205,
"hasNodes": true,
"nodeCount": 3,
"sampleFunctionNames": [
+13 -14
View File
@@ -486,20 +486,19 @@ server.tool(
throw new Error('getLocatorStringForElement: argument must be a Playwright Locator or ElementHandle')
}
return await element.evaluate(async (el: any) => {
const WIN = globalThis as any
if (!WIN.__selectorGenerator) {
const module: SelectorGenerator = await import(
// @ts-ignore
'https://unpkg.com/@mizchi/selector-generator@1.50.0-next/dist/index.js'
)
WIN.__selectorGenerator = {
createSelectorGenerator: module.createSelectorGenerator,
toLocator: module.toLocator,
}
}
const { createSelectorGenerator, toLocator } = WIN.__selectorGenerator as SelectorGenerator
const generator = createSelectorGenerator(WIN)
const elementPage = element.page ? element.page() : page
const hasGenerator = await elementPage.evaluate(() => !!(globalThis as any).__selectorGenerator)
if (!hasGenerator) {
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const scriptPath = path.join(currentDir, '..', 'dist', 'selector-generator.js')
const scriptContent = fs.readFileSync(scriptPath, 'utf-8')
await elementPage.addScriptTag({ content: scriptContent })
}
return await element.evaluate((el: any) => {
const { createSelectorGenerator, toLocator } = (globalThis as any).__selectorGenerator
const generator = createSelectorGenerator(globalThis)
const result = generator(el)
return toLocator(result.selector, 'javascript')
})
+15 -5
View File
@@ -126,14 +126,24 @@ Then you can use `page.locator(`aria-ref=${ref}`)` to get an element with a spec
IMPORTANT: notice that we do not add any quotes in `aria-ref`! it MUST be called without quotes
## getting selector for a locator identified by snapshot aria-ref
## getting a stable selector for an element (getLocatorStringForElement)
in some cases you want to get a selector for a locator you just identified using `const element = page.locator('aria-ref=${ref}')`. To do so you can use `await getLocatorStringForElement(element)`. This is useful if you need to find other elements of the same type in a list for example. If you know the selector you can usually change a bit the selector to find the other elements of the same type in the list or table
The `aria-ref` values from accessibility snapshots are ephemeral - they change on page reload and when components remount. Use `getLocatorStringForElement(element)` to get a stable Playwright locator string that you can reuse programmatically.
This is useful for:
- Getting a selector you can store and reuse across page reloads
- Finding similar elements in a list (modify the selector pattern)
- Debugging which selector Playwright would use for an element
```js
const loc = page.locator('aria-ref=123');
console.log(await getLocatorStringForElement(loc));
// => "getByRole('button', { name: 'Save' })" or similar
const loc = page.locator('aria-ref=e14');
const selector = await getLocatorStringForElement(loc);
console.log(selector);
// => "getByRole('button', { name: 'Save' })"
// use the selector programmatically with eval:
const stableLocator = page.getByRole('button', { name: 'Save' })
await stableLocator.click();
```
## finding specific elements with snapshot