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/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 { showAriaRefLabels, hideAriaRefLabels } from './aria-snapshot.js'
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
- showAriaRefLabels: (options: { page: Page; interactiveOnly?: boolean }) => Promise<{ snapshot: string; labelCount: number }>
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
- showAriaRefLabels,
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
- showAriaRefLabels,
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
- return {
955
- content: [
956
- {
957
- type: 'text',
958
- text: finalText,
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
- **showAriaRefLabels** - overlay Vimium-style visual labels on interactive elements. Useful for taking screenshots where you can see element references. Labels auto-hide after 5 seconds. Call again if page HTML changes to get fresh labels.
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
- const { snapshot, labelCount } = await showAriaRefLabels({ page });
213
- console.log(`Showing ${labelCount} labels`);
214
- await page.screenshot({ path: '/tmp/labeled-page.png' });
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: