add test for getLocatorStringForElement
This commit is contained in:
@@ -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)
|
||||
})
|
||||
@@ -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
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user