playwriter 0.0.38 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aria-snapshot.d.ts +29 -2
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +103 -26
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +30 -13
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.js +73 -0
- package/dist/mcp.test.js.map +1 -1
- package/package.json +13 -12
- package/src/aria-snapshot.ts +129 -26
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-google-snapshot.txt +1 -1
- package/src/assets/aria-labels-google.png +0 -0
- package/src/assets/aria-labels-hacker-news-snapshot.txt +826 -811
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/mcp.test.ts +86 -0
- package/src/mcp.ts +35 -14
- package/src/prompt.md +10 -11
|
Binary file
|
package/src/mcp.test.ts
CHANGED
|
@@ -2307,6 +2307,76 @@ describe('MCP Server Tests', () => {
|
|
|
2307
2307
|
console.log(`Screenshots saved to: ${assetsDir}`)
|
|
2308
2308
|
}, 120000)
|
|
2309
2309
|
|
|
2310
|
+
it('should take screenshot with accessibility labels via MCP execute tool', async () => {
|
|
2311
|
+
const browserContext = getBrowserContext()
|
|
2312
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2313
|
+
|
|
2314
|
+
const page = await browserContext.newPage()
|
|
2315
|
+
await page.setContent(`
|
|
2316
|
+
<html>
|
|
2317
|
+
<body>
|
|
2318
|
+
<button id="submit-btn">Submit Form</button>
|
|
2319
|
+
<a href="/about">About Us</a>
|
|
2320
|
+
<input type="text" placeholder="Enter your name" />
|
|
2321
|
+
</body>
|
|
2322
|
+
</html>
|
|
2323
|
+
`)
|
|
2324
|
+
await page.bringToFront()
|
|
2325
|
+
|
|
2326
|
+
await serviceWorker.evaluate(async () => {
|
|
2327
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2328
|
+
})
|
|
2329
|
+
await new Promise(r => setTimeout(r, 400))
|
|
2330
|
+
|
|
2331
|
+
// Take screenshot with accessibility labels via MCP
|
|
2332
|
+
const result = await client.callTool({
|
|
2333
|
+
name: 'execute',
|
|
2334
|
+
arguments: {
|
|
2335
|
+
code: js`
|
|
2336
|
+
let testPage;
|
|
2337
|
+
for (const p of context.pages()) {
|
|
2338
|
+
const html = await p.content();
|
|
2339
|
+
if (html.includes('submit-btn')) { testPage = p; break; }
|
|
2340
|
+
}
|
|
2341
|
+
if (!testPage) throw new Error('Test page not found');
|
|
2342
|
+
await screenshotWithAccessibilityLabels({ page: testPage });
|
|
2343
|
+
`,
|
|
2344
|
+
timeout: 15000,
|
|
2345
|
+
},
|
|
2346
|
+
})
|
|
2347
|
+
|
|
2348
|
+
expect(result.isError).toBeFalsy()
|
|
2349
|
+
|
|
2350
|
+
// Verify response has both text and image content
|
|
2351
|
+
const content = result.content as any[]
|
|
2352
|
+
expect(content.length).toBe(2)
|
|
2353
|
+
|
|
2354
|
+
// Check text content
|
|
2355
|
+
const textContent = content.find(c => c.type === 'text')
|
|
2356
|
+
expect(textContent).toBeDefined()
|
|
2357
|
+
expect(textContent.text).toContain('Screenshot saved to:')
|
|
2358
|
+
expect(textContent.text).toContain('.jpg')
|
|
2359
|
+
expect(textContent.text).toContain('Labels shown:')
|
|
2360
|
+
expect(textContent.text).toContain('Accessibility snapshot:')
|
|
2361
|
+
expect(textContent.text).toContain('Submit Form')
|
|
2362
|
+
|
|
2363
|
+
// Check image content
|
|
2364
|
+
const imageContent = content.find(c => c.type === 'image')
|
|
2365
|
+
expect(imageContent).toBeDefined()
|
|
2366
|
+
expect(imageContent.mimeType).toBe('image/jpeg')
|
|
2367
|
+
expect(imageContent.data).toBeDefined()
|
|
2368
|
+
expect(imageContent.data.length).toBeGreaterThan(100) // base64 data should be substantial
|
|
2369
|
+
|
|
2370
|
+
// Verify the image is valid JPEG by checking base64
|
|
2371
|
+
const buffer = Buffer.from(imageContent.data, 'base64')
|
|
2372
|
+
const dimensions = imageSize(buffer)
|
|
2373
|
+
expect(dimensions.type).toBe('jpg')
|
|
2374
|
+
expect(dimensions.width).toBeGreaterThan(0)
|
|
2375
|
+
expect(dimensions.height).toBeGreaterThan(0)
|
|
2376
|
+
|
|
2377
|
+
await page.close()
|
|
2378
|
+
}, 60000)
|
|
2379
|
+
|
|
2310
2380
|
})
|
|
2311
2381
|
|
|
2312
2382
|
|
|
@@ -2648,6 +2718,11 @@ describe('CDP Session Tests', () => {
|
|
|
2648
2718
|
const browserContext = getBrowserContext()
|
|
2649
2719
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2650
2720
|
|
|
2721
|
+
// Clear any existing connected tabs from previous tests
|
|
2722
|
+
await serviceWorker.evaluate(async () => {
|
|
2723
|
+
await globalThis.disconnectEverything()
|
|
2724
|
+
})
|
|
2725
|
+
|
|
2651
2726
|
const page = await browserContext.newPage()
|
|
2652
2727
|
await page.goto('https://example.com/')
|
|
2653
2728
|
await page.bringToFront()
|
|
@@ -2692,6 +2767,11 @@ describe('CDP Session Tests', () => {
|
|
|
2692
2767
|
const browserContext = getBrowserContext()
|
|
2693
2768
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2694
2769
|
|
|
2770
|
+
// Clear any existing connected tabs from previous tests
|
|
2771
|
+
await serviceWorker.evaluate(async () => {
|
|
2772
|
+
await globalThis.disconnectEverything()
|
|
2773
|
+
})
|
|
2774
|
+
|
|
2695
2775
|
const page1 = await browserContext.newPage()
|
|
2696
2776
|
await page1.goto('https://example.com/')
|
|
2697
2777
|
await page1.bringToFront()
|
|
@@ -3267,6 +3347,12 @@ describe('Auto-enable Tests', () => {
|
|
|
3267
3347
|
const browserContext = getBrowserContext()
|
|
3268
3348
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
3269
3349
|
|
|
3350
|
+
// Ensure clean state - disconnect any tabs from previous tests or setup
|
|
3351
|
+
await serviceWorker.evaluate(async () => {
|
|
3352
|
+
await globalThis.disconnectEverything()
|
|
3353
|
+
})
|
|
3354
|
+
await new Promise(r => setTimeout(r, 100))
|
|
3355
|
+
|
|
3270
3356
|
// Verify no tabs are connected
|
|
3271
3357
|
const tabCountBefore = await serviceWorker.evaluate(() => {
|
|
3272
3358
|
const state = globalThis.getExtensionState()
|
package/src/mcp.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { Editor } from './editor.js'
|
|
|
21
21
|
import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js'
|
|
22
22
|
import { getReactSource, type ReactSourceLocation } from './react-source.js'
|
|
23
23
|
import { ScopedFS } from './scoped-fs.js'
|
|
24
|
-
import {
|
|
24
|
+
import { screenshotWithAccessibilityLabels, type ScreenshotResult } from './aria-snapshot.js'
|
|
25
25
|
const __filename = fileURLToPath(import.meta.url)
|
|
26
26
|
const __dirname = path.dirname(__filename)
|
|
27
27
|
|
|
@@ -86,8 +86,7 @@ interface VMContext {
|
|
|
86
86
|
getStylesForLocator: (options: { locator: any }) => Promise<StylesResult>
|
|
87
87
|
formatStylesAsText: (styles: StylesResult) => string
|
|
88
88
|
getReactSource: (options: { locator: any }) => Promise<ReactSourceLocation | null>
|
|
89
|
-
|
|
90
|
-
hideAriaRefLabels: (options: { page: Page }) => Promise<void>
|
|
89
|
+
screenshotWithAccessibilityLabels: (options: { page: Page; interactiveOnly?: boolean }) => Promise<void>
|
|
91
90
|
require: NodeRequire
|
|
92
91
|
import: (specifier: string) => Promise<any>
|
|
93
92
|
}
|
|
@@ -864,6 +863,13 @@ server.tool(
|
|
|
864
863
|
return getReactSource({ locator: options.locator, cdp })
|
|
865
864
|
}
|
|
866
865
|
|
|
866
|
+
// Collector for screenshots taken during this execution
|
|
867
|
+
const screenshotCollector: ScreenshotResult[] = []
|
|
868
|
+
|
|
869
|
+
const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => {
|
|
870
|
+
return screenshotWithAccessibilityLabels({ ...options, collector: screenshotCollector })
|
|
871
|
+
}
|
|
872
|
+
|
|
867
873
|
let vmContextObj: VMContextWithGlobals = {
|
|
868
874
|
page,
|
|
869
875
|
context,
|
|
@@ -880,8 +886,7 @@ server.tool(
|
|
|
880
886
|
getStylesForLocator: getStylesForLocatorFn,
|
|
881
887
|
formatStylesAsText,
|
|
882
888
|
getReactSource: getReactSourceFn,
|
|
883
|
-
|
|
884
|
-
hideAriaRefLabels,
|
|
889
|
+
screenshotWithAccessibilityLabels: screenshotWithAccessibilityLabelsFn,
|
|
885
890
|
resetPlaywright: async () => {
|
|
886
891
|
const { page: newPage, context: newContext } = await resetConnection()
|
|
887
892
|
|
|
@@ -901,8 +906,7 @@ server.tool(
|
|
|
901
906
|
getStylesForLocator: getStylesForLocatorFn,
|
|
902
907
|
formatStylesAsText,
|
|
903
908
|
getReactSource: getReactSourceFn,
|
|
904
|
-
|
|
905
|
-
hideAriaRefLabels,
|
|
909
|
+
screenshotWithAccessibilityLabels: screenshotWithAccessibilityLabelsFn,
|
|
906
910
|
resetPlaywright: vmContextObj.resetPlaywright,
|
|
907
911
|
require: sandboxedRequire,
|
|
908
912
|
// TODO --experimental-vm-modules is needed to make import work in vm
|
|
@@ -943,6 +947,13 @@ server.tool(
|
|
|
943
947
|
responseText += 'Code executed successfully (no output)'
|
|
944
948
|
}
|
|
945
949
|
|
|
950
|
+
// Add screenshot info to response text
|
|
951
|
+
for (const screenshot of screenshotCollector) {
|
|
952
|
+
responseText += `\nScreenshot saved to: ${screenshot.path}\n`
|
|
953
|
+
responseText += `Labels shown: ${screenshot.labelCount}\n\n`
|
|
954
|
+
responseText += `Accessibility snapshot:\n${screenshot.snapshot}\n`
|
|
955
|
+
}
|
|
956
|
+
|
|
946
957
|
const MAX_LENGTH = 6000
|
|
947
958
|
let finalText = responseText.trim()
|
|
948
959
|
if (finalText.length > MAX_LENGTH) {
|
|
@@ -951,14 +962,24 @@ server.tool(
|
|
|
951
962
|
`\n\n[Truncated to ${MAX_LENGTH} characters. Better manage your logs or paginate them to read the full logs]`
|
|
952
963
|
}
|
|
953
964
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
965
|
+
// Build content array with text and any collected screenshots
|
|
966
|
+
const content: Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }> = [
|
|
967
|
+
{
|
|
968
|
+
type: 'text',
|
|
969
|
+
text: finalText,
|
|
970
|
+
},
|
|
971
|
+
]
|
|
972
|
+
|
|
973
|
+
// Add all collected screenshots as images
|
|
974
|
+
for (const screenshot of screenshotCollector) {
|
|
975
|
+
content.push({
|
|
976
|
+
type: 'image',
|
|
977
|
+
data: screenshot.base64,
|
|
978
|
+
mimeType: screenshot.mimeType,
|
|
979
|
+
})
|
|
961
980
|
}
|
|
981
|
+
|
|
982
|
+
return { content }
|
|
962
983
|
} catch (error: any) {
|
|
963
984
|
const errorStack = error.stack || error.message
|
|
964
985
|
const isTimeoutError = error instanceof CodeExecutionTimeoutError || error.name === 'TimeoutError'
|
package/src/prompt.md
CHANGED
|
@@ -206,24 +206,23 @@ const matches = await editor.grep({ regex: /console\.log/ });
|
|
|
206
206
|
await editor.edit({ url: matches[0].url, oldString: 'DEBUG = false', newString: 'DEBUG = true' });
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
**
|
|
209
|
+
**screenshotWithAccessibilityLabels** - take a screenshot with Vimium-style visual labels overlaid on interactive elements. Shows labels, captures screenshot, then removes labels. The image and accessibility snapshot are automatically included in the response. Can be called multiple times to capture multiple screenshots. Use a timeout of 10 seconds at least.
|
|
210
210
|
|
|
211
211
|
```js
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Use aria-ref from snapshot to interact
|
|
212
|
+
await screenshotWithAccessibilityLabels({ page });
|
|
213
|
+
// Image and accessibility snapshot are automatically included in response
|
|
214
|
+
// Use aria-ref from snapshot to interact with elements
|
|
216
215
|
await page.locator('aria-ref=e5').click();
|
|
216
|
+
|
|
217
|
+
// Can take multiple screenshots in one execution
|
|
218
|
+
await screenshotWithAccessibilityLabels({ page });
|
|
219
|
+
await page.click('button');
|
|
220
|
+
await screenshotWithAccessibilityLabels({ page });
|
|
221
|
+
// Both images are included in the response
|
|
217
222
|
```
|
|
218
223
|
|
|
219
224
|
Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs.
|
|
220
225
|
|
|
221
|
-
**hideAriaRefLabels** - manually remove labels before the 5-second auto-hide:
|
|
222
|
-
|
|
223
|
-
```js
|
|
224
|
-
await hideAriaRefLabels({ page });
|
|
225
|
-
```
|
|
226
|
-
|
|
227
226
|
## pinned elements
|
|
228
227
|
|
|
229
228
|
Users can right-click → "Copy Playwriter Element Reference" to store elements in `globalThis.playwriterPinnedElem1` (increments for each pin). The reference is copied to clipboard:
|