4.3 KiB
Page Ready Investigation Summary
Goal
Convert undeterministic sleep() calls into deterministic event waits when toggling the extension.
Problem
After toggleExtensionForActiveTab() returns, the page is not immediately available in Playwright's context.pages(). Tests currently use arbitrary sleeps (100-400ms) to work around this.
Key Events Required
For a page to appear in context.pages(), Playwright needs:
Target.attachedToTarget- Tells Playwright a new target exists with its URLRuntime.executionContextCreated- Tells Playwright the JS context is ready
The page appears in context.pages() only after BOTH events are processed.
Current Flow (Timeline from test)
63ms ← EVT Target.attachedToTarget # Extension sends to relay
431ms ← EVT Runtime.executionContextCreated # Relay's proactive Runtime.enable
437ms → CMD Runtime.enable # Playwright sends its own
438ms Toggle completes (pageReady received)
467ms ← EVT Runtime.executionContextCreated # Response to Playwright's Runtime.enable
728ms Page appears in context.pages()
Root Cause
The ~300ms delay between toggle completing and page appearing is caused by:
-
Extension's
Runtime.enablehandler (background.ts:256-272):- Always calls
Runtime.disablethenRuntime.enable - Has a 50ms
sleep()between them - This is needed to force Chrome to re-send
executionContextCreatedevents for multiple clients
- Always calls
-
Double Runtime.enable cycle:
- Relay proactively enables Runtime → events at ~430ms
- Playwright receives
Target.attachedToTarget, sends its ownRuntime.enable - Extension does disable+enable again → events at ~470ms
- Playwright waits for ITS events before adding page to pages()
-
Playwright's internal processing:
- Even after receiving events, Playwright takes time to create Page objects
- This is async and cannot be controlled from our side
What We Tried
-
Proactive Runtime.enable in relay - Enable Runtime before forwarding
Target.attachedToTarget- Helps get events faster, but Playwright still calls Runtime.enable itself
-
Skip disable cycle if recently enabled - Track recent enables in extension
- Breaks because relay's Runtime.enable handler waits for events that won't come
-
pageReady handshake - Extension waits for relay confirmation before returning from attachTab
- Toggle now waits for executionContextCreated
- But Playwright STILL calls Runtime.enable after, causing another cycle
The Core Issue
Playwright always calls Runtime.enable after receiving Target.attachedToTarget, regardless of whether we pre-enabled it. The extension's disable+enable cycle adds ~200ms, and we cannot skip it without breaking the multi-client case.
Possible Solutions
Option 1: Accept the delay, use proper waiting
Instead of sleep, use context.waitForEvent('page') with a predicate:
await serviceWorker.evaluate(() => globalThis.toggleExtensionForActiveTab())
const page = await context.waitForEvent('page', {
predicate: p => p.url().includes('target-url')
})
Option 2: Expose a "page ready" promise from the relay
Add an endpoint or event that resolves when the page is fully ready in Playwright:
await serviceWorker.evaluate(() => globalThis.toggleExtensionForActiveTab())
await relay.waitForPageReady(sessionId) // Waits for Playwright to process everything
Option 3: Have extension track Runtime state per session
Skip the disable+enable if:
- This is the SAME Playwright client that just received
Target.attachedToTarget - The session was JUST created (within last 100ms)
This requires tracking which client enabled Runtime and when.
Recommendation
Option 1 is the simplest and most reliable. The delay is inherent to how Playwright processes CDP events. We cannot eliminate it, but we can wait for it properly using context.waitForEvent('page') instead of arbitrary sleeps.
The test should be:
const pagePromise = context.waitForEvent('page', {
predicate: p => p.url().includes('discord.com'),
timeout: 5000
})
await serviceWorker.evaluate(() => globalThis.toggleExtensionForActiveTab())
const page = await pagePromise
// Page is now guaranteed to be ready