Bind playwright execute page to Chrome's active tab - #333
Conversation
masnwilliams
left a comment
There was a problem hiding this comment.
requesting changes on two maintainability issues: the new default relies on an undocumented page-ordering contract and the regression test duplicates a full container setup for a one-line selection policy.
| // just navigated to (e.g. a tab opened via a link click or window.open). Using | ||
| // pages[0] bound `page` to the oldest tab, so calls like page.pdf() operated on | ||
| // the wrong tab whenever more than one was open. | ||
| const page = pages.length > 0 ? pages[pages.length - 1] : await context.newPage(); |
There was a problem hiding this comment.
BrowserContext.pages() only documents that it returns all open pages; it does not guarantee creation order. This makes Playwright's current internal Set insertion order part of our public page contract, while the comment also conflates newest with current/active (open B, bring A forward, and this still selects B). Can we define the selection policy in the OpenAPI contract and either own the intended state or describe this precisely as a last-discovered-page heuristic? We should not ship the behavior change while leaving callers to infer the invariant.
There was a problem hiding this comment.
Replaced the ordering-based heuristic with a CDP lookup of the browser's real foreground tab (Target.getTargets + tabActive, mapped to a Playwright Page via Target.autoAttachRelated). This is now only a fallback, used when the underlying Chrome build doesn't expose that signal — documented both in code (see resolveActivePage) and in the OpenAPI description for /playwright/execute, which now states the policy precisely: foreground tab when available, otherwise most-recently-opened, with context.pages() as the escape hatch for explicit selection.
| require.NoError(t, err, "failed to marshal result: %v", err) | ||
| resultStr := string(resultBytes) | ||
| t.Logf("injected page url=%s", resultStr) | ||
| require.Contains(t, resultStr, "example.net", "expected injected page to be the most recently opened tab") |
There was a problem hiding this comment.
This adds 59 lines and another Docker container to verify a one-line policy change, duplicating the setup already in TestPlaywrightExecuteAPI. Can we fold this scenario into that existing warm-daemon test (or extract a shared harness if isolation matters)? While simplifying it, use deterministic data: URLs and compare JSON200.Result directly rather than adding another network dependency and JSON-marshal/substring assertion.
There was a problem hiding this comment.
Folded this into TestPlaywrightExecuteAPI, reusing its container/warm daemon connection instead of spinning up a second one. Swapped the second tab's https://example.net navigation for a data: URL so the tab-binding check no longer needs its own outbound request, and replaced the JSON-marshal/Contains check with a direct require.Equal against JSON200.Result.
|
Fixed in 59c3e11: moved |
The playwright execution daemon injected `page` as context.pages()[0], the oldest tab, regardless of which tab was active. In multi-tab sessions this bound `page` to the wrong tab, so calls like page.pdf() captured a different tab than the one just navigated to. Select the last entry in context.pages() (creation order) so `page` is the most recently opened tab, and add an e2e test covering the multi-tab case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the most-recently-opened-tab heuristic with a CDP-based lookup of the browser's actual foreground tab (Target.getTargets + tabActive + Target.autoAttachRelated), falling back to the heuristic on Chrome builds that don't expose the signal. Documents the selection policy in the OpenAPI spec and folds the tab-binding regression test into the existing warm-daemon test using a deterministic data: URL. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move newBrowserCDPSession() inside the try block so a failure there falls back to the heuristic like every other failure path, instead of rejecting and failing the whole execute request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Now that the image's Chrome is upgraded to 152.0.7977.42, the CDP tabActive signal resolveActivePage relies on is always available, so the fallback heuristic and its Page|null plumbing are dead weight. resolveActivePage now returns the foreground Page directly and throws on failure like any other daemon error, instead of silently falling back. Extends the e2e test to also verify that bringing an older tab back to the foreground flips which page gets injected -- the case the old heuristic could never have handled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
59c3e11 to
5d9bab0
Compare
| } | ||
| } | ||
|
|
||
| throw new Error('foreground tab has no matching Playwright page'); |
There was a problem hiding this comment.
Existing pages may not attach
High Severity
resolveActivePage fills relatedPageIds only from Target.attachedToTarget after Target.autoAttachRelated. CDP documents that command as monitoring related-target creation and reporting newly created related targets. For already-open tabs, the set can stay empty, so the loop never matches and execution fails with foreground tab has no matching Playwright page.
Reviewed by Cursor Bugbot for commit 5d9bab0. Configure here.
There was a problem hiding this comment.
Verified this doesn't reproduce against the image's actual Chrome (152.0.7977.42): the e2e test added in this PR (TestPlaywrightExecuteAPI, https://github.com/kernel/kernel-images/actions/runs/32164219559/job/95800815565) explicitly covers a pre-existing related target — it opens a second tab, then several execute calls later brings the original tab (created well before the autoAttachRelated call in question, not a target created in that same request) back to the foreground and asserts the daemon binds to it. That assertion passed in CI. In practice, autoAttachRelated on this target's browser session fires attachedToTarget for already-open related targets at call time, not just future creations, so relatedPageIds is populated correctly for this case. Leaving as-is; will revisit if this surfaces on a real session.
| // pages[0] bound `page` to the oldest tab regardless of which was active, so | ||
| // calls like page.pdf() operated on the wrong tab whenever more than one was | ||
| // open. | ||
| const page = pages.length > 0 ? await resolveActivePage(browserInstance, context) : await context.newPage(); |
There was a problem hiding this comment.
Active tab ignored across contexts
Medium Severity
resolveActivePage picks the browser-wide foreground tab via CDP, but only searches for a matching Playwright Page inside the single context from contexts()[0]. If that tab lives in another context, binding throws and /playwright/execute fails before user code runs, so callers cannot recover via browser or context.pages().
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f2de954. Configure here.
There was a problem hiding this comment.
Leaving this as-is: contexts()[0] is the pre-existing selection this whole endpoint has always used for the 'context' variable exposed to user code — it predates this PR and applies equally to the empty-context newPage() path a few lines up. resolveActivePage searching only within that same context is consistent with what 'context' means for this daemon, not a new limitation this PR introduces. If the true foreground tab lives in a different browser context, the old heuristic would have silently bound 'page' to the wrong context's tab; this now fails loudly instead, which is arguably safer. Multi-context support for this endpoint would be a bigger, separate change to how 'context' itself is resolved.
rgarcia
left a comment
There was a problem hiding this comment.
reviewed — the active-tab resolution direction makes sense, and the test covers both opening a new tab and refocusing an older one. two things to address:
-
server/openapi.yaml:1342— replace the currentpagedescription with: “pageis bound to an active tab reported by Chrome. In single-window sessions, this is the foreground tab. When multiple browser windows are open, Chrome reports one active tab per window and the selected window is unspecified. Usecontext.pages()to select a page explicitly.” -
server/e2e/e2e_playwright_test.go:119— this test relies oncontext.pages()[0]being the oldest page, which is the same undocumented ordering assumption this change removes. Select the known original page instead:const first = context.pages().find(candidate => candidate.url().includes('example.com') ); if (!first) throw new Error('original page not found'); await first.bringToFront(); return first.url();
- server/openapi.yaml: describe 'page' as an active tab reported by Chrome, noting that with multiple browser windows Chrome reports one active tab per window and which window is selected is unspecified. - server/e2e/e2e_playwright_test.go: select the original tab by URL instead of context.pages()[0], which relied on the same undocumented ordering assumption this change removes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Pushed 5195138 addressing both: reworded the openapi.yaml description to your exact multi-window wording, and swapped the e2e test's context.pages()[0] for a URL-based lookup (throws if the original page isn't found). |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6703f7f. Configure here.
A crashed or closing page can throw from newCDPSession/getTargetInfo, which previously aborted the whole search even when the real foreground tab was later in context.pages(). Catch per-page and continue instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Both addressed and pushed (eb7424b, rebased on the latest main-merge): openapi.yaml now uses your exact multi-window wording, and the e2e test selects the original page by URL instead of context.pages()[0]. Also fixed BugBot's low-severity finding from the latest pass (a crashed/closing page could abort the whole foreground-tab search) and replied to its medium finding (cross-context foreground tab) explaining why it's a pre-existing constraint of this endpoint's single-context design rather than something this PR introduces. All CI checks pass; BugBot is clean (no new findings) on the current commit. |
page to the most recently opened tabpage to Chrome's active tab


Summary
browsers.playwright.executeinjectedpageascontext.pages()[0]— the oldest tab in the context — regardless of which tab was active. In any multi-tab session this boundpageto the wrong tab, so calls likepage.pdf(),page.title(), etc. operated on a different tab than the one the automation had just navigated to.Change
server/runtime/playwright-daemon.ts: addedresolveActivePage, which resolves the browser's actual active tab over CDP —Target.getTargetsfiltered totabtargets, matched onTargetInfo.embedderData.tabActive(exposed by the Chrome 152.0.7977.42 build this image now ships, per Upgrade Chromium to 152.0.7977.42 #340), then mapped to a PlaywrightPageviaTarget.autoAttachRelated. Every CDP session it opens is temporary and detached before returning, so the daemon stays stateless across requests.pageis bound to that resolved tab directly; resolution failures surface like any other daemon error, with no ordering heuristic or fallback.context.pages()for the matching tab, a page whose CDP session setup orTarget.getTargetInfocall fails is skipped rather than aborting the whole search, so one crashed or closing tab can't block resolution of the real active tab.server/openapi.yaml: documents the selection policy on/playwright/execute—pageis bound to an active tab reported by Chrome. In single-window sessions that's the foreground tab; with multiple browser windows open, Chrome reports one active tab per window and the selected window is unspecified.context.pages()remains the explicit escape hatch.server/e2e/e2e_playwright_test.go: extends the existingTestPlaywrightExecuteAPI(same container/warm daemon connection, no second container) to open a second tab via adata:URL and assertpagebinds to it, then bring the original tab — selected by URL rather thancontext.pages()[0]— back to the foreground and assert binding follows focus, not tab-creation order.Scope
Depends on this image's Chrome 152.0.7977.42 exposing
TargetInfo.embedderData.tabActive; single-tab sessions are unaffected. Cross-context foreground-tab resolution is out of scope —resolveActivePagesearches the samecontext(contexts()[0]) already exposed to user code, matching this endpoint's existing single-context design rather than introducing a new limitation.Testing
go vet ./e2e/...passes.esbuildbundles the daemon cleanly.TestPlaywrightExecuteAPI(extended) passes in CI against the image's real Chrome 152 build, covering both the new-tab and refocus cases; requires docker, skips otherwise.