fix: filter non-page targets from Playwright (issue #14)

Service workers, web workers, and iframes are now filtered out at the
server level, preventing Playwright from trying to initialize these
targets which caused timeouts and errors.
This commit is contained in:
Tommy D. Rossi
2026-01-12 18:10:54 +01:00
parent 481cb4ddc8
commit 32dca5743b
6 changed files with 925 additions and 908 deletions
+4
View File
@@ -2,6 +2,10 @@
## 0.0.44
### Bug Fixes
- **Filter non-page targets from Playwright (issue #14)**: Service workers, web workers, and other non-page targets are now filtered out at the server level. This prevents Playwright from trying to initialize these targets, which would cause timeouts waiting for `executionContextCreated` events and errors on `Target.detachFromTarget`.
### Features
- **Search context lines**: `accessibilitySnapshot`, `getCleanHTML`, and `getLatestLogs` now include 5 lines of context above and below each search match
@@ -31,7 +31,7 @@
- generic [ref=e55]:
- text: "Google offered in:"
- link [ref=e56] [cursor=pointer]:
- /url: https://www.google.com/setprefs?sig=0_-1OPkiTlOBOVFrBWG18fjvZuNsY%3D&hl=it&source=homepage&sa=X&ved=0ahUKEwjstcWBt4SSAxVe4jgGHTucPWEQ2ZgBCBU
- /url: https://www.google.com/setprefs?sig=0_naJUnYTqN6Qpp-iLC0lAP9bnktM%3D&hl=it&source=homepage&sa=X&ved=0ahUKEwiW-c_xsoaSAxX5rZUCHdnrMyMQ2ZgBCBU
- text: Italiano
- contentinfo [ref=e58]:
- generic [ref=e59]: Italy
File diff suppressed because it is too large Load Diff
+9
View File
@@ -763,6 +763,15 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
if (method === 'Target.attachedToTarget') {
const targetParams = params as Protocol.Target.AttachedToTargetEvent
const targetType = targetParams.targetInfo.type
// Filter out non-page targets (service workers, web workers, etc.)
// These targets can't be properly controlled through chrome.debugger API
// and cause issues when Playwright tries to initialize them (issue #14)
if (targetType !== 'page') {
logger?.log(chalk.gray(`[Server] Ignoring non-page target: ${targetType} (${targetParams.targetInfo.url})`))
return
}
if (!targetParams.targetInfo.url) {
logger?.error(chalk.red('[Extension] WARNING: Target.attachedToTarget received with empty URL!'), JSON.stringify({ method, params: targetParams, sessionId }))
+4 -4
View File
@@ -11,7 +11,7 @@ test('formatHtmlForPrompt', async () => {
"<!doctype html>
<html data-redirect-timezone="1">
<body>
<div data-framer-hydrate-v2="{&quot;routeId&quot;:&quot;Zw20hns9v&quot;,&quot;localeId&quot;:&quot;default&quot;,&quot;breakpoints&quot;:[{&quot;hash&quot;:&quot;2ngqvi&quot;,&quot;mediaQuery&quot;:&quot;(min-width: 1200px)&quot;},{&quot;hash&quot;:&quot;11ziuji&quot;,&quot;mediaQuery&quot;:&quot;(min-width: 810px) and (max-width: 1199.98px)&quot;},{&quot;hash&quot;:...237 more characters" data-framer-ssr-released-at="2026-01-09T18:19:52.935Z" data-framer-page-optimized-at="2026-01-09T18:42:33.421Z" data-framer-generated-page="">
<div data-framer-hydrate-v2="{&quot;routeId&quot;:&quot;Zw20hns9v&quot;,&quot;localeId&quot;:&quot;default&quot;,&quot;breakpoints&quot;:[{&quot;hash&quot;:&quot;2ngqvi&quot;,&quot;mediaQuery&quot;:&quot;(min-width: 1200px)&quot;},{&quot;hash&quot;:&quot;11ziuji&quot;,&quot;mediaQuery&quot;:&quot;(min-width: 810px) and (max-width: 1199.98px)&quot;},{&quot;hash&quot;:...237 more characters" data-framer-ssr-released-at="2026-01-12T00:23:47.512Z" data-framer-page-optimized-at="2026-01-12T09:49:28.497Z" data-framer-generated-page="">
<div data-layout-template="true" data-selection="true">
<div>
<nav data-framer-name="Desktop Nav" data-hide-scrollbars="true">
@@ -2546,7 +2546,7 @@ test('formatHtmlForPrompt', async () => {
<div data-framer-name="UI">
<div data-framer-name="Stats">
<div data-border="true" data-framer-name="Content">
<p>January 9, 2026</p>
<p>January 12, 2026</p>
<div>
<div data-framer-component-type="RichTextContainer">
<p>Pageviews</p>
@@ -2806,7 +2806,7 @@ test('formatHtmlForPrompt', async () => {
<div data-framer-name="UI">
<div data-framer-name="Stats">
<div data-border="true" data-framer-name="Content">
<p>January 9, 2026</p>
<p>January 12, 2026</p>
<div>
<div data-framer-component-type="RichTextContainer">
<p>Pageviews</p>
@@ -3066,7 +3066,7 @@ test('formatHtmlForPrompt', async () => {
<div data-framer-name="UI">
<div data-framer-name="Stats">
<div data-border="true" data-framer-name="Content">
<p>January 9, 2026</p>
<p>January 12, 2026</p>
<div>
<div data-framer-component-type="RichTextContainer">
<p>Pageviews</p>
+27 -29
View File
@@ -3541,18 +3541,21 @@ describe('Service Worker Target Tests', () => {
return testCtx.browserContext
}
it('should not disconnect when page has service worker (issue #14)', async () => {
// This test reproduces issue #14: pages with service workers cause disconnection loops.
// The problem is that Target.setAutoAttach attaches to service workers, and when
// Playwright tries to enable Network/Runtime on the SW session, the extension can't
// find a matching tab, causing errors and disconnection.
it('should not expose service worker targets to Playwright (issue #14)', async () => {
// This test verifies that service worker targets are NOT forwarded to Playwright.
// Issue #14: pages with service workers cause problems because Playwright tries to
// initialize service worker sessions with Runtime.enable/Network.enable, which
// times out waiting for executionContextCreated (service workers don't have main frames).
//
// The fix is to filter out non-page targets (service_worker, worker, etc.) in the
// server so Playwright never sees them.
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
// Discord has a service worker - navigate there
// web.dev has a service worker - navigate there
const page = await browserContext.newPage()
await page.goto('https://discord.com/login', { waitUntil: 'load' })
await page.goto('https://web.dev/', { waitUntil: 'load' })
await page.bringToFront()
// Attach extension to the page
@@ -3561,33 +3564,28 @@ describe('Service Worker Target Tests', () => {
})
await new Promise(r => setTimeout(r, 500))
// Connect via Playwright CDP - this triggers Target.setAutoAttach which
// will also attach to the service worker
// Connect via Playwright CDP - this triggers Target.setAutoAttach
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
const context = browser.contexts()[0]
// Track disconnection events
let disconnected = false
browser.on('disconnected', () => {
disconnected = true
console.log('Browser disconnected!')
})
// Get all targets/pages that Playwright knows about
const pages = context.pages()
// Wait for potential disconnection - issue says ~10s loop
// We wait 15 seconds to be sure
console.log('Waiting 15 seconds to check for disconnection loop...')
await new Promise(r => setTimeout(r, 15000))
// All pages should be actual pages, not service workers or workers
for (const p of pages) {
const url = p.url()
console.log('Page URL:', url)
// Service workers would have URLs like https://web.dev/sw.js
expect(url).not.toMatch(/sw\.js$/i)
expect(url).not.toMatch(/service.?worker/i)
}
// Should still be connected - this is what we're testing
expect(disconnected).toBe(false)
expect(browser.isConnected()).toBe(true)
// Verify we can interact with the main page
const targetPage = pages.find(p => p.url().includes('web.dev'))
expect(targetPage).toBeDefined()
// Verify we can still interact with the page
const pages = browser.contexts()[0].pages()
const discordPage = pages.find(p => p.url().includes('discord.com'))
expect(discordPage).toBeDefined()
const url = await discordPage!.evaluate(() => window.location.href)
expect(url).toContain('discord.com')
const title = await targetPage!.title()
expect(title).toBeTruthy()
await browser.close()
await page.close()