playwriter 0.0.33 → 0.0.37

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.
Files changed (65) hide show
  1. package/dist/aria-snapshot.d.ts +68 -0
  2. package/dist/aria-snapshot.d.ts.map +1 -0
  3. package/dist/aria-snapshot.js +359 -0
  4. package/dist/aria-snapshot.js.map +1 -0
  5. package/dist/cdp-relay.d.ts.map +1 -1
  6. package/dist/cdp-relay.js +95 -5
  7. package/dist/cdp-relay.js.map +1 -1
  8. package/dist/cdp-session.d.ts +24 -3
  9. package/dist/cdp-session.d.ts.map +1 -1
  10. package/dist/cdp-session.js +23 -0
  11. package/dist/cdp-session.js.map +1 -1
  12. package/dist/debugger-api.md +4 -3
  13. package/dist/debugger.d.ts +4 -3
  14. package/dist/debugger.d.ts.map +1 -1
  15. package/dist/debugger.js +3 -1
  16. package/dist/debugger.js.map +1 -1
  17. package/dist/editor-api.md +2 -2
  18. package/dist/editor.d.ts +2 -2
  19. package/dist/editor.d.ts.map +1 -1
  20. package/dist/editor.js +1 -0
  21. package/dist/editor.js.map +1 -1
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/mcp.d.ts.map +1 -1
  27. package/dist/mcp.js +151 -14
  28. package/dist/mcp.js.map +1 -1
  29. package/dist/mcp.test.js +340 -5
  30. package/dist/mcp.test.js.map +1 -1
  31. package/dist/protocol.d.ts +12 -1
  32. package/dist/protocol.d.ts.map +1 -1
  33. package/dist/react-source.d.ts +3 -3
  34. package/dist/react-source.d.ts.map +1 -1
  35. package/dist/react-source.js +3 -1
  36. package/dist/react-source.js.map +1 -1
  37. package/dist/scoped-fs.d.ts +94 -0
  38. package/dist/scoped-fs.d.ts.map +1 -0
  39. package/dist/scoped-fs.js +356 -0
  40. package/dist/scoped-fs.js.map +1 -0
  41. package/dist/styles-api.md +3 -3
  42. package/dist/styles.d.ts +3 -3
  43. package/dist/styles.d.ts.map +1 -1
  44. package/dist/styles.js +3 -1
  45. package/dist/styles.js.map +1 -1
  46. package/package.json +13 -13
  47. package/src/aria-snapshot.ts +446 -0
  48. package/src/assets/aria-labels-github-snapshot.txt +605 -0
  49. package/src/assets/aria-labels-github.png +0 -0
  50. package/src/assets/aria-labels-google-snapshot.txt +110 -0
  51. package/src/assets/aria-labels-google.png +0 -0
  52. package/src/assets/aria-labels-hacker-news-snapshot.txt +1023 -0
  53. package/src/assets/aria-labels-hacker-news.png +0 -0
  54. package/src/cdp-relay.ts +103 -5
  55. package/src/cdp-session.ts +50 -3
  56. package/src/debugger.ts +6 -4
  57. package/src/editor.ts +4 -3
  58. package/src/index.ts +8 -0
  59. package/src/mcp.test.ts +424 -5
  60. package/src/mcp.ts +242 -66
  61. package/src/prompt.md +209 -167
  62. package/src/protocol.ts +14 -1
  63. package/src/react-source.ts +5 -3
  64. package/src/scoped-fs.ts +411 -0
  65. package/src/styles.ts +5 -3
package/src/mcp.test.ts CHANGED
@@ -217,8 +217,8 @@ describe('MCP Server Tests', () => {
217
217
 
218
218
  Return value:
219
219
  {
220
- \"url\": \"https://example.com/\",
221
- \"title\": \"Example Domain\"
220
+ "url": "https://example.com/",
221
+ "title": "Example Domain"
222
222
  }",
223
223
  "type": "text",
224
224
  },
@@ -686,7 +686,7 @@ describe('MCP Server Tests', () => {
686
686
 
687
687
  await browser.close()
688
688
  await page.close()
689
- })
689
+ }, 60000)
690
690
 
691
691
  it('should be able to reconnect after disconnecting everything', async () => {
692
692
  const browserContext = getBrowserContext()
@@ -824,6 +824,83 @@ describe('MCP Server Tests', () => {
824
824
  await page.goto('about:blank')
825
825
  })
826
826
 
827
+ it('should auto-reconnect MCP after extension WebSocket reconnects', async () => {
828
+ // This test verifies that the MCP automatically reconnects when the browser
829
+ // disconnects (e.g., when the extension WebSocket reconnects and the relay
830
+ // server closes all playwright clients). The fix adds browser.on('disconnected')
831
+ // handler that clears state.isConnected, so ensureConnection() creates a new connection.
832
+
833
+ const serviceWorker = await getExtensionServiceWorker(testCtx!.browserContext)
834
+
835
+ // 1. Create a test page and enable extension
836
+ const page = await testCtx!.browserContext.newPage()
837
+ await page.goto('https://example.com/auto-reconnect-test')
838
+ await page.waitForLoadState('domcontentloaded')
839
+ await page.bringToFront()
840
+
841
+ const initialEnable = await serviceWorker.evaluate(async () => {
842
+ return await globalThis.toggleExtensionForActiveTab()
843
+ })
844
+ expect(initialEnable.isConnected).toBe(true)
845
+ await new Promise(resolve => setTimeout(resolve, 100))
846
+
847
+ // 2. Verify MCP can execute commands
848
+ const beforeResult = await client.callTool({
849
+ name: 'execute',
850
+ arguments: {
851
+ code: js`
852
+ const pages = context.pages();
853
+ const testPage = pages.find(p => p.url().includes('auto-reconnect-test'));
854
+ return { pagesCount: pages.length, foundTestPage: !!testPage };
855
+ `,
856
+ },
857
+ })
858
+ const beforeOutput = (beforeResult as any).content[0].text
859
+ expect(beforeOutput).toContain('foundTestPage')
860
+ expect(beforeOutput).toContain('true')
861
+
862
+ // 3. Simulate extension WebSocket reconnection
863
+ // This causes relay server to close all playwright client WebSockets
864
+ await serviceWorker.evaluate(async () => {
865
+ await globalThis.disconnectEverything()
866
+ })
867
+ await new Promise(resolve => setTimeout(resolve, 100))
868
+
869
+ // Re-enable extension (simulates extension reconnecting)
870
+ await page.bringToFront()
871
+ const reconnectResult = await serviceWorker.evaluate(async () => {
872
+ return await globalThis.toggleExtensionForActiveTab()
873
+ })
874
+ expect(reconnectResult.isConnected).toBe(true)
875
+ await new Promise(resolve => setTimeout(resolve, 100))
876
+
877
+ // 4. Execute command WITHOUT calling resetPlaywright()
878
+ // The browser.on('disconnected') handler should have cleared state.isConnected,
879
+ // causing ensureConnection() to automatically create a new connection
880
+ const afterResult = await client.callTool({
881
+ name: 'execute',
882
+ arguments: {
883
+ code: js`
884
+ const pages = context.pages();
885
+ const testPage = pages.find(p => p.url().includes('auto-reconnect-test'));
886
+ return { pagesCount: pages.length, foundTestPage: !!testPage, url: testPage?.url() };
887
+ `,
888
+ },
889
+ })
890
+
891
+ const afterOutput = (afterResult as any).content[0].text
892
+ // The command should succeed and find our test page
893
+ expect(afterOutput).toContain('foundTestPage')
894
+ expect(afterOutput).toContain('true')
895
+ expect(afterOutput).toContain('auto-reconnect-test')
896
+ // Should NOT contain error about extension not connected
897
+ expect(afterOutput).not.toContain('Extension not connected')
898
+ expect((afterResult as any).isError).not.toBe(true)
899
+
900
+ // Clean up
901
+ await page.goto('about:blank')
902
+ })
903
+
827
904
  it('should capture browser console logs with getLatestLogs', async () => {
828
905
  // Ensure clean state and clear any existing logs
829
906
  const resetResult = await client.callTool({
@@ -1260,6 +1337,92 @@ describe('MCP Server Tests', () => {
1260
1337
  await page.close()
1261
1338
  }, 30000)
1262
1339
 
1340
+ it('should be usable after toggle with valid URL', async () => {
1341
+ // This test validates the extension properly waits for valid URLs before
1342
+ // sending Target.attachedToTarget. Uses Discord - a heavy React SPA.
1343
+ //
1344
+ // We use waitForEvent('page') to wait for Playwright to process the event.
1345
+ // The KEY assertion is that when the event fires, the URL is VALID (not empty).
1346
+ // Before the fix: event fired with empty URL -> page broken forever
1347
+ // After the fix: event fires with valid URL -> page works immediately
1348
+
1349
+ const _browserContext = getBrowserContext()
1350
+ const serviceWorker = await getExtensionServiceWorker(_browserContext)
1351
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1352
+ const context = browser.contexts()[0]
1353
+
1354
+ const page = await _browserContext.newPage()
1355
+ await page.goto('https://discord.com/login')
1356
+ await page.bringToFront()
1357
+
1358
+ // Set up listener BEFORE toggle
1359
+ const pagePromise = context.waitForEvent('page', { timeout: 10000 })
1360
+
1361
+ // Toggle extension - extension waits for valid URL before sending event
1362
+ await serviceWorker.evaluate(async () => {
1363
+ await globalThis.toggleExtensionForActiveTab()
1364
+ })
1365
+
1366
+ // Wait for page event
1367
+ const targetPage = await pagePromise
1368
+ console.log('Page URL when event fired:', targetPage.url())
1369
+
1370
+ // KEY ASSERTION: URL must NOT be empty - this is what the extension fix guarantees
1371
+ expect(targetPage.url()).not.toBe('')
1372
+ expect(targetPage.url()).not.toBe(':')
1373
+ expect(targetPage.url()).toContain('discord.com')
1374
+
1375
+ // evaluate() works immediately - no waiting needed
1376
+ const result = await targetPage.evaluate(() => window.location.href)
1377
+ expect(result).toContain('discord.com')
1378
+
1379
+ await browser.close()
1380
+ await page.close()
1381
+ }, 60000)
1382
+
1383
+ it('should have non-empty URLs when connecting to already-loaded pages', async () => {
1384
+ // This test validates that when we connect to a browser with already-loaded pages,
1385
+ // all pages have non-empty URLs. Empty URLs break Playwright permanently.
1386
+
1387
+ const _browserContext = getBrowserContext()
1388
+ const serviceWorker = await getExtensionServiceWorker(_browserContext)
1389
+
1390
+ // Create and fully load a heavy page BEFORE connecting
1391
+ const page = await _browserContext.newPage()
1392
+ await page.goto('https://discord.com/login', { waitUntil: 'load' })
1393
+ await page.bringToFront()
1394
+
1395
+ // Toggle extension to attach to the loaded page
1396
+ await serviceWorker.evaluate(async () => {
1397
+ await globalThis.toggleExtensionForActiveTab()
1398
+ })
1399
+
1400
+ // NOW connect via CDP - page should already be attached
1401
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1402
+ const context = browser.contexts()[0]
1403
+
1404
+ // Get all pages and verify NONE have empty URLs
1405
+ const pages = context.pages()
1406
+ console.log('All page URLs:', pages.map(p => p.url()))
1407
+
1408
+ expect(pages.length).toBeGreaterThan(0)
1409
+ for (const p of pages) {
1410
+ expect(p.url()).not.toBe('')
1411
+ expect(p.url()).not.toBe(':')
1412
+ expect(p.url()).not.toBeUndefined()
1413
+ }
1414
+
1415
+ // Find Discord page and verify it works
1416
+ const discordPage = pages.find(p => p.url().includes('discord.com'))
1417
+ expect(discordPage).toBeDefined()
1418
+
1419
+ const result = await discordPage!.evaluate(() => window.location.href)
1420
+ expect(result).toContain('discord.com')
1421
+
1422
+ await browser.close()
1423
+ await page.close()
1424
+ }, 60000)
1425
+
1263
1426
  it('should maintain correct page.url() with iframe-heavy pages', async () => {
1264
1427
  const browserContext = getBrowserContext()
1265
1428
  const serviceWorker = await getExtensionServiceWorker(browserContext)
@@ -1943,8 +2106,8 @@ describe('MCP Server Tests', () => {
1943
2106
  {
1944
2107
  "text": "Return value:
1945
2108
  {
1946
- "error": "Page not found",
1947
- "urls": []
2109
+ \"matchesDark\": false,
2110
+ \"matchesLight\": true
1948
2111
  }",
1949
2112
  "type": "text",
1950
2113
  },
@@ -1954,6 +2117,196 @@ describe('MCP Server Tests', () => {
1954
2117
  await page.close()
1955
2118
  }, 60000)
1956
2119
 
2120
+ it('should get aria ref for locator using getAriaSnapshot', async () => {
2121
+ const browserContext = getBrowserContext()
2122
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2123
+
2124
+ const page = await browserContext.newPage()
2125
+ await page.setContent(`
2126
+ <html>
2127
+ <body>
2128
+ <button id="submit-btn">Submit Form</button>
2129
+ <a href="/about">About Us</a>
2130
+ <input type="text" placeholder="Enter your name" />
2131
+ </body>
2132
+ </html>
2133
+ `)
2134
+ await page.bringToFront()
2135
+
2136
+ await serviceWorker.evaluate(async () => {
2137
+ await globalThis.toggleExtensionForActiveTab()
2138
+ })
2139
+ await new Promise(r => setTimeout(r, 400))
2140
+
2141
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2142
+ let cdpPage
2143
+ for (const p of browser.contexts()[0].pages()) {
2144
+ const html = await p.content()
2145
+ if (html.includes('submit-btn')) {
2146
+ cdpPage = p
2147
+ break
2148
+ }
2149
+ }
2150
+ expect(cdpPage).toBeDefined()
2151
+
2152
+ const { getAriaSnapshot } = await import('./aria-snapshot.js')
2153
+
2154
+ // Get aria snapshot and verify we can get refs
2155
+ const ariaResult = await getAriaSnapshot({ page: cdpPage! })
2156
+
2157
+ expect(ariaResult.snapshot).toBeDefined()
2158
+ expect(ariaResult.snapshot.length).toBeGreaterThan(0)
2159
+ expect(ariaResult.snapshot).toContain('Submit Form')
2160
+
2161
+ // Verify refToElement map is populated
2162
+ expect(ariaResult.refToElement.size).toBeGreaterThan(0)
2163
+ console.log('RefToElement map size:', ariaResult.refToElement.size)
2164
+ console.log('RefToElement entries:', [...ariaResult.refToElement.entries()])
2165
+
2166
+ // Verify we can select elements using aria-ref selectors
2167
+ const btnViaAriaRef = cdpPage!.locator('aria-ref=e2')
2168
+ const btnTextViaRef = await btnViaAriaRef.textContent()
2169
+ console.log('Button text via aria-ref=e2:', btnTextViaRef)
2170
+ expect(btnTextViaRef).toBe('Submit Form')
2171
+
2172
+ // Get ref for the submit button using getRefForLocator
2173
+ const submitBtn = cdpPage!.locator('#submit-btn')
2174
+ const btnAriaRef = await ariaResult.getRefForLocator(submitBtn)
2175
+ console.log('Button ariaRef:', btnAriaRef)
2176
+ expect(btnAriaRef).toBeDefined()
2177
+ expect(btnAriaRef?.role).toBe('button')
2178
+ expect(btnAriaRef?.name).toBe('Submit Form')
2179
+ expect(btnAriaRef?.ref).toMatch(/^e\d+$/)
2180
+
2181
+ // Verify the ref matches what we can use to select
2182
+ const btnFromRef = cdpPage!.locator(`aria-ref=${btnAriaRef?.ref}`)
2183
+ const btnText = await btnFromRef.textContent()
2184
+ expect(btnText).toBe('Submit Form')
2185
+
2186
+ // Test getRefStringForLocator
2187
+ const btnRefStr = await ariaResult.getRefStringForLocator(submitBtn)
2188
+ console.log('Button ref string:', btnRefStr)
2189
+ expect(btnRefStr).toBe(btnAriaRef?.ref)
2190
+
2191
+ // Test link
2192
+ const aboutLink = cdpPage!.locator('a')
2193
+ const linkAriaRef = await ariaResult.getRefForLocator(aboutLink)
2194
+ console.log('Link ariaRef:', linkAriaRef)
2195
+ expect(linkAriaRef).toBeDefined()
2196
+ expect(linkAriaRef?.role).toBe('link')
2197
+ expect(linkAriaRef?.name).toBe('About Us')
2198
+
2199
+ // Verify the link ref works
2200
+ const linkFromRef = cdpPage!.locator(`aria-ref=${linkAriaRef?.ref}`)
2201
+ const linkText = await linkFromRef.textContent()
2202
+ expect(linkText).toBe('About Us')
2203
+
2204
+ // Test input field
2205
+ const inputField = cdpPage!.locator('input')
2206
+ const inputAriaRef = await ariaResult.getRefForLocator(inputField)
2207
+ console.log('Input ariaRef:', inputAriaRef)
2208
+ expect(inputAriaRef).toBeDefined()
2209
+ expect(inputAriaRef?.role).toBe('textbox')
2210
+
2211
+ // Test batch getRefsForLocators - single evaluate call for multiple elements
2212
+ const batchRefs = await ariaResult.getRefsForLocators([submitBtn, aboutLink, inputField])
2213
+ console.log('Batch refs:', batchRefs)
2214
+ expect(batchRefs).toHaveLength(3)
2215
+ expect(batchRefs[0]?.ref).toBe(btnAriaRef?.ref)
2216
+ expect(batchRefs[1]?.ref).toBe(linkAriaRef?.ref)
2217
+ expect(batchRefs[2]?.ref).toBe(inputAriaRef?.ref)
2218
+
2219
+ await browser.close()
2220
+ await page.close()
2221
+ }, 60000)
2222
+
2223
+ it('should show aria ref labels on real pages and save screenshots', async () => {
2224
+ const browserContext = getBrowserContext()
2225
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2226
+
2227
+ const { showAriaRefLabels, hideAriaRefLabels } = await import('./aria-snapshot.js')
2228
+ const fs = await import('node:fs')
2229
+ const path = await import('node:path')
2230
+
2231
+ // Create assets folder for screenshots
2232
+ const assetsDir = path.join(path.dirname(new URL(import.meta.url).pathname), 'assets')
2233
+ if (!fs.existsSync(assetsDir)) {
2234
+ fs.mkdirSync(assetsDir, { recursive: true })
2235
+ }
2236
+
2237
+ const testPages = [
2238
+ { name: 'hacker-news', url: 'https://news.ycombinator.com/' },
2239
+ { name: 'google', url: 'https://www.google.com/' },
2240
+ { name: 'github', url: 'https://github.com/' },
2241
+ ]
2242
+
2243
+ // Create all pages and enable extension for each
2244
+ const pages = await Promise.all(
2245
+ testPages.map(async ({ name, url }) => {
2246
+ const page = await browserContext.newPage()
2247
+ await page.goto(url, { waitUntil: 'domcontentloaded' })
2248
+ return { name, url, page }
2249
+ })
2250
+ )
2251
+
2252
+ // Enable extension for each tab (must be done sequentially as it uses active tab)
2253
+ for (const { page } of pages) {
2254
+ await page.bringToFront()
2255
+ await serviceWorker.evaluate(async () => {
2256
+ await globalThis.toggleExtensionForActiveTab()
2257
+ })
2258
+ }
2259
+
2260
+ // Connect CDP and process all pages concurrently
2261
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2262
+
2263
+ await Promise.all(
2264
+ pages.map(async ({ name, url, page }) => {
2265
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes(new URL(url).hostname))
2266
+
2267
+ if (!cdpPage) {
2268
+ console.log(`Could not find CDP page for ${name}, skipping...`)
2269
+ return
2270
+ }
2271
+
2272
+ // Show aria ref labels
2273
+ const { snapshot, labelCount } = await showAriaRefLabels({ page: cdpPage })
2274
+ console.log(`${name}: ${labelCount} labels shown`)
2275
+ expect(labelCount).toBeGreaterThan(0)
2276
+
2277
+ // Take screenshot with labels visible
2278
+ const screenshot = await cdpPage.screenshot({ type: 'png', fullPage: false })
2279
+ const screenshotPath = path.join(assetsDir, `aria-labels-${name}.png`)
2280
+ fs.writeFileSync(screenshotPath, screenshot)
2281
+ console.log(`Screenshot saved: ${screenshotPath}`)
2282
+
2283
+ // Save snapshot text for reference
2284
+ const snapshotPath = path.join(assetsDir, `aria-labels-${name}-snapshot.txt`)
2285
+ fs.writeFileSync(snapshotPath, snapshot)
2286
+
2287
+ // Verify labels are in DOM
2288
+ const labelElements = await cdpPage.evaluate(() =>
2289
+ document.querySelectorAll('.__pw_label__').length
2290
+ )
2291
+ expect(labelElements).toBe(labelCount)
2292
+
2293
+ // Cleanup
2294
+ await hideAriaRefLabels({ page: cdpPage })
2295
+
2296
+ // Verify labels removed
2297
+ const labelsAfterHide = await cdpPage.evaluate(() =>
2298
+ document.getElementById('__playwriter_labels__')
2299
+ )
2300
+ expect(labelsAfterHide).toBeNull()
2301
+
2302
+ await page.close()
2303
+ })
2304
+ )
2305
+
2306
+ await browser.close()
2307
+ console.log(`Screenshots saved to: ${assetsDir}`)
2308
+ }, 120000)
2309
+
1957
2310
  })
1958
2311
 
1959
2312
 
@@ -2881,3 +3234,69 @@ describe('CDP Session Tests', () => {
2881
3234
  await page.close()
2882
3235
  }, 60000)
2883
3236
  })
3237
+
3238
+ describe('Auto-enable Tests', () => {
3239
+ let testCtx: TestContext | null = null
3240
+
3241
+ // Set env var before any setup runs
3242
+ process.env.PLAYWRITER_AUTO_ENABLE = '1'
3243
+
3244
+ beforeAll(async () => {
3245
+ testCtx = await setupTestContext({ tempDirPrefix: 'pw-auto-test-' })
3246
+
3247
+ // Disconnect all tabs to start with a clean state
3248
+ const serviceWorker = await getExtensionServiceWorker(testCtx.browserContext)
3249
+ await serviceWorker.evaluate(async () => {
3250
+ await globalThis.disconnectEverything()
3251
+ })
3252
+ await new Promise(r => setTimeout(r, 100))
3253
+ }, 600000)
3254
+
3255
+ afterAll(async () => {
3256
+ delete process.env.PLAYWRITER_AUTO_ENABLE
3257
+ await cleanupTestContext(testCtx)
3258
+ testCtx = null
3259
+ })
3260
+
3261
+ const getBrowserContext = () => {
3262
+ if (!testCtx?.browserContext) throw new Error('Browser not initialized')
3263
+ return testCtx.browserContext
3264
+ }
3265
+
3266
+ it('should auto-create a tab when Playwright connects and no tabs exist', async () => {
3267
+ const browserContext = getBrowserContext()
3268
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
3269
+
3270
+ // Verify no tabs are connected
3271
+ const tabCountBefore = await serviceWorker.evaluate(() => {
3272
+ const state = globalThis.getExtensionState()
3273
+ return state.tabs.size
3274
+ })
3275
+ expect(tabCountBefore).toBe(0)
3276
+
3277
+ // Connect Playwright - this should trigger auto-create
3278
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
3279
+
3280
+ // Verify a page was auto-created
3281
+ const pages = browser.contexts()[0].pages()
3282
+ expect(pages.length).toBeGreaterThan(0)
3283
+ expect(pages.length).toBe(1)
3284
+
3285
+ const autoCreatedPage = pages[0]
3286
+ expect(autoCreatedPage.url()).toBe('about:blank')
3287
+
3288
+ // Verify extension state shows the tab as connected
3289
+ const tabCountAfter = await serviceWorker.evaluate(() => {
3290
+ const state = globalThis.getExtensionState()
3291
+ return state.tabs.size
3292
+ })
3293
+ expect(tabCountAfter).toBe(1)
3294
+
3295
+ // Verify we can interact with the auto-created page
3296
+ await autoCreatedPage.setContent('<h1>Auto-created page</h1>')
3297
+ const title = await autoCreatedPage.locator('h1').textContent()
3298
+ expect(title).toBe('Auto-created page')
3299
+
3300
+ await browser.close()
3301
+ }, 60000)
3302
+ })