Skip to content

test(browser): fix the opaque-frame background popup flake (the gesture window expired before the click) - #28815

Closed
nwparker wants to merge 1 commit into
mainfrom
nwparker/popup-opaque-gesture-race
Closed

nwparker wants to merge 1 commit into
mainfrom
nwparker/popup-opaque-gesture-race

Conversation

@nwparker

@nwparker nwparker commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 0 0 0 0
Prod 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​10 $\color{#cf222e}{\Huge{\mathbf{−}}}$​6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​4

ELI5

One browser test sometimes fails on CI for no reason related to the PR it runs on. The test clicks the page once to give Orca a "the user just clicked" signal, then clicks a link inside an embedded frame to open a background tab. Orca only trusts that signal for one second. On a busy CI machine, Playwright can take longer than a second to find and ready the link inside the frame. Orca then correctly refuses the tab, and the test waits forever for a request that will never come. This PR makes the test get the link ready before the first click, so the two clicks happen back to back.

What Changed

clickBackgroundPopupProbe in src/main/browser/browser-popup-background-cdp-probe.ts used to do this for the opaque context:

  1. Click #background-gesture on the root page (the gesture).
  2. Resolve #background-open inside the opaque cross-origin frame, wait for it to be actionable, then click it.

The page gesture only counts for BROWSER_GUEST_INPUT_GESTURE_WINDOW_MS (1,000 ms, browser-guest-input-gesture.ts). For opaque frames, the root click is the only gesture Orca sees, because Electron doesn't forward opaque out-of-process frame input to the route's gesture tracker. Step 2's frame resolution and actionability checks run after the gesture, inside that window.

Now the target is settled first with a trial click: actionability checks only, no action, then the gesture, then the real click. The trial click carries no modifiers or button, because Playwright presses modifiers even in a trial, and a key press would count as a gesture and let the test pass for the wrong reason.

No product code changes.

Why

Linked Issue

N/A (maintainer; CI flake found on #28761).

Visual Proof

N/A: test-only change.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • browser-popup-background-compatibility.electron.test.ts passes locally (15/15) on repeated runs.

  • pnpm tc:node and oxlint (including the low-evidence-pattern config) pass.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Test-only. If the flake recurs on opaque probes after this, the next step is to measure the gap between the gesture and the window-open in the fixture, not to widen the product's gesture window.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

clickBackgroundPopupProbe now resolves the target locator and click options before checking for an opaque frame. For opaque probes, it performs a modifier-free trial click, clicks the route gesture button, and then performs the configured target click. Other contexts perform only the configured target click.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 60330

The compatibility probe may still spend the gesture window re-resolving and checking its target, so intermittent browser-test failures can remain. The risk is limited to this test workflow.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly identifies the browser test flake, its opaque-frame context, and the expired gesture window. It accurately summarizes the primary change.
Description check Passed The description follows the required template and explains the failure, mechanism, rationale, testing, visual-proof exception, and checklist status. It provides sufficient detail for review.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f6d93772-b1c4-4834-89cb-089fa04d685f
📥 Commits

Reviewing files that changed from the base of the PR and between 872012f and 6033034.

📒 Files selected for processing: 1
  • src/main/browser/browser-popup-background-cdp-probe.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Comment on lines +108 to +115
// Electron does not forward opaque OOPIF input to the root route gesture gate, and that gate
// honors a gesture for BROWSER_GUEST_INPUT_GESTURE_WINDOW_MS (1 s). Settle the frame target
// first so only one click separates the gesture from the open on a loaded runner. The trial
// carries no modifiers: Playwright presses them even in a trial, which would fake a gesture.
await locator.click({ trial: true })
await page.locator('#background-gesture').click()
}
await locator.click(click)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

rg -n 'class Locator|async click\\(|trial|performAction' node_modules/.pnpm/playwright-core@1.64.0*/node_modules/playwright-core/lib/client/locator.js node_modules/.pnpm/playwright-core@1.64.0*/node_modules/playwright-core/lib/client/frame.js node_modules/playwright-core/lib/server/dom.js 2>/dev/null
rg -n 'playwright-core@1\\.64\\.0|playwright-core: 1\\.64\\.0' pnpm-lock.yaml

Repository: stablyai/orca

Length of output: 151


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- dependency declaration ---'
rg -n -F -- 'playwright-core' package.json pnpm-lock.yaml | head -20 || true
printf '%s\n' '--- changed helper ---'
nl -ba src/main/browser/browser-popup-background-cdp-probe.ts | sed -n '82,122p'
printf '%s\n' '--- relevant callers ---'
rg -n -F -- 'clickBackgroundPopupProbe' src/main/browser || true
printf '%s\n' '--- local Playwright implementation candidates ---'
find . -path '*playwright-core*' -type f \( -name 'locator.js' -o -name 'frame.js' -o -name 'dom.js' \) -print 2>/dev/null | head -30

Repository: stablyai/orca

Length of output: 3516


🌐 Web query:

Playwright 1.64.0 locator.click trial frame.click implementation source

💡 Result:

In Playwright **v1.64.0**, `locator.click()` is a thin wrapper: it calls `this._frame.click(this._selector, { strict: true, ...options })`. The `strict: true` is supplied by the locator; spreading `options` afterward lets caller options override it. ([github.com](https://github.com/microsoft/playwright/blob/v1.64.0/packages/playwright-core/src/client/locator.ts))

For `trial: true`, the click runs the actionability checks but skips the actual click. The server-side `frames.ts` implementation is in the tagged source; the API docs describe this behavior. ([github.com](https://github.com/microsoft/playwright/blob/v1.64.0/packages/playwright-core/src/server/frames.ts))

**Tagged implementation:** [`locator.ts`](https://github.com/microsoft/playwright/blob/v1.64.0/packages/playwright-core/src/client/locator.ts) · [`frame.ts`](https://github.com/microsoft/playwright/blob/v1.64.0/packages/playwright-core/src/client/frame.ts) · [`frames.ts`](https://github.com/microsoft/playwright/blob/v1.64.0/packages/playwright-core/src/server/frames.ts)

Citations:

- 1: https://github.com/microsoft/playwright/blob/v1.64.0/packages/playwright-core/src/client/locator.ts
- 2: https://github.com/microsoft/playwright/blob/v1.64.0/packages/playwright-core/src/server/frames.ts

🌐 Web query:

Playwright v1.64.0 frames.ts Frame.click _retryWithProgressIfNotConnected _performPointerAction trial actionability source lines

💡 Result:

In **Playwright v1.64.0**, `Frame.click` and `_retryWithProgressIfNotConnected` are in `packages/playwright-core/src/server/frames.ts`:

- **`_retryWithProgressIfNotConnected`**: lines **1104–1152**. It repeatedly resolves the target and retries if the element is detached. ([raw.githubusercontent.com](https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/frames.ts))
- **`Frame.click`**: lines **1160–1162**. It calls that retry helper, then delegates to `handle._click(...)`. ([raw.githubusercontent.com](https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/frames.ts))
- **`_performPointerAction`**: it is **not in `frames.ts`** in this version—the click path delegates to the element handle. So the pointer-action and trial/actionability implementation must be traced in the relevant DOM/element-handle source, not this file. ([raw.githubusercontent.com](https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/frames.ts))

The cited source is specifically the **v1.64.0** tag.

Citations:

- 1: https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/frames.ts
- 2: https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/frames.ts
- 3: https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/frames.ts

🌐 Web query:

raw GitHub microsoft playwright v1.64.0 packages/playwright-core/src/server/dom.ts _click _performPointerAction trial waitForEnabled

💡 Result:

In the **v1.64.0** source, `_click` configures a pointer action with `waitForEnabled: true`. `_performPointerAction` uses that flag to check for the element to be **visible, enabled, and stable** before proceeding. With `trial: true`, it still performs the actionability checks and hit-target setup, but the hit-target interceptor receives the trial flag; the mouse click itself is still called in the action path. ([raw.githubusercontent.com](https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/dom.ts))

[View `dom.ts` at v1.64.0](https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/dom.ts) ([raw.githubusercontent.com](https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/dom.ts))

Citations:

- 1: https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/dom.ts
- 2: https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/dom.ts

🏁 Script executed:

set -o pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/frames.ts -o "$tmpdir/frames.ts"
curl -fsSL https://raw.githubusercontent.com/microsoft/playwright/v1.64.0/packages/playwright-core/src/server/dom.ts -o "$tmpdir/dom.ts"
printf '%s\n' '--- frames.ts relevant methods ---'
nl -ba "$tmpdir/frames.ts" | sed -n '1100,1170p'
printf '%s\n' '--- dom.ts relevant symbols ---'
rg -n -F -e 'async _click' -e '_performPointerAction' -e 'trial' -e 'waitForEnabled' "$tmpdir/dom.ts"
printf '%s\n' '--- dom.ts relevant implementation ---'
nl -ba "$tmpdir/dom.ts" | sed -n '360,570p'

Repository: stablyai/orca

Length of output: 17723


Do not rely on the trial click to cache the final target.

locator.click(click) starts a new Playwright click after the root gesture. Playwright 1.64.0 resolves the target again and then checks visibility, enabled state, stability, scrolling, and the hit target before dispatching the click. That work can consume the one-second gesture window, so the opaque-frame flake can still occur. Use a final-click path that reuses the prepared target and does not repeat this work after the gesture.

@nwparker
nwparker marked this pull request as draft October 14, 2026 19:33
@nwparker

Copy link
Copy Markdown
Contributor Author

This fix did not work: the same probe (route/opaque, modifier click) failed on this PR's own CI run (job 116051359488, "expected 102 to be greater than 102"). Either the gesture window is not the cause, or narrowing the gap between the two clicks is not enough. Converting to draft. Not merging a fix I can't support.

What is established:

The open question for whoever owns the browser popup gate: does the window-open for an opaque OOPIF ever get a gesture from the modifier keydown or the frame click, or only from the earlier root click? Measuring the gap between the gesture and the window-open in the fixture would settle it.

@nwparker

Copy link
Copy Markdown
Contributor Author

Superseded by #28799, which runs the Electron browser probes one at a time outside the unit shards. That addresses the shared-runner load these opaque-probe failures pointed to; my click-ordering change did not fix it on its own. If the opaque probe still fails after #28799, logging when the gesture and the window-open each happen in the fixture is the next step.

@nwparker nwparker closed this Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant