diff --git a/src/main/browser/browser-popup-adapter-compatibility.electron.test.ts b/src/main/browser/browser-popup-adapter-compatibility.electron.test.ts index e13b927abfa..2dd05b30463 100644 --- a/src/main/browser/browser-popup-adapter-compatibility.electron.test.ts +++ b/src/main/browser/browser-popup-adapter-compatibility.electron.test.ts @@ -12,6 +12,7 @@ import { snapshots, status } from './browser-popup-compatibility-cdp-fixture' +import { waitForHiddenFrameInput } from './hidden-frame-input-readiness-fixture' enablePopupAdapterProbe() setPopupFixtureEvidenceName('adapter-electron-evidence.json') @@ -334,10 +335,9 @@ async function exerciseSandboxControls( const offset = status().adapterProbe?.length ?? 0 await installSandboxFrame(page, sandbox, destination, kind) const initialReceipt = receipts.length - await page - .frameLocator('#sandbox-probe') - .locator('#open') - .click(kind === 'link' ? { modifiers: ['Shift'] } : {}) + const open = page.frameLocator('#sandbox-probe').locator('#open') + await waitForHiddenFrameInput(open) + await open.click(kind === 'link' ? { modifiers: ['Shift'] } : {}) if (path === 'adopted' && kind === 'link') { await expect .poll(() => eventsSince(offset).some((event) => event.event === 'createWindow-missing')) diff --git a/src/main/browser/browser-popup-background-cdp-probe.ts b/src/main/browser/browser-popup-background-cdp-probe.ts index 7fca368da63..27ad3a58b4f 100644 --- a/src/main/browser/browser-popup-background-cdp-probe.ts +++ b/src/main/browser/browser-popup-background-cdp-probe.ts @@ -2,6 +2,7 @@ import { mkdirSync } from 'node:fs' import { join } from 'node:path' import type { Page } from 'playwright' import { expect } from 'vitest' +import { waitForHiddenFrameInput } from './hidden-frame-input-readiness-fixture' import { artifactDir, browser, @@ -103,22 +104,8 @@ export async function clickBackgroundPopupProbe( ? page.frameLocator('#background-probe').locator('#background-open') : page.locator('#background-open') if (probe.context === 'opaque') { - // A hidden OOPIF can finish loading before browser input reaches its new frame. - await locator.evaluate((element) => { - element.removeAttribute('data-background-input-ready') - element.addEventListener('pointermove', (event) => { - if (event.isTrusted) { - element.setAttribute('data-background-input-ready', 'true') - } - }) - }) await page.locator('#background-gesture').click() - await expect - .poll(async () => { - await locator.hover() - return locator.getAttribute('data-background-input-ready') - }) - .toBe('true') + await waitForHiddenFrameInput(locator) // Opaque frame input does not reach Electron's root route gesture gate. await page.locator('#background-gesture').click() } diff --git a/src/main/browser/browser-popup-compatibility-cdp-fixture.ts b/src/main/browser/browser-popup-compatibility-cdp-fixture.ts index 8b2ee9c0013..dbfa7c10826 100644 --- a/src/main/browser/browser-popup-compatibility-cdp-fixture.ts +++ b/src/main/browser/browser-popup-compatibility-cdp-fixture.ts @@ -12,7 +12,10 @@ import { build as buildVite } from 'vite' import { waitForBrowserCdpEndpoint } from './browser-session-ua-cdp-collector' import { popupFixtureHtml } from './browser-popup-compatibility-page-fixture' import { resolveElectronProbeLaunch } from './electron-probe-display-launch' -import { startHiddenRendererFrameCapture } from './hidden-renderer-frame-capture-fixture' +import { + HIDDEN_RENDERER_FIRST_FRAME_TIMEOUT_MS, + startHiddenRendererFrameCapture +} from './hidden-renderer-frame-capture-fixture' // Hidden renderer startup and CDP events can exceed the default one-second poll on CI. vi.setConfig({ expect: { poll: { timeout: 10_000 } } }) @@ -214,7 +217,7 @@ beforeAll(async () => { stopFrameCaptures.set( page, await startHiddenRendererFrameCapture(page, (hasFrame) => - waitForFixture(hasFrame, 45_000).catch((error: unknown) => { + waitForFixture(hasFrame, HIDDEN_RENDERER_FIRST_FRAME_TIMEOUT_MS).catch((error: unknown) => { throw new Error(`No first frame for ${name}`, { cause: error }) }) ) diff --git a/src/main/browser/browser-popup-native-compatibility.electron.test.ts b/src/main/browser/browser-popup-native-compatibility.electron.test.ts index 94b00d601a8..cfe8fdb80a6 100644 --- a/src/main/browser/browser-popup-native-compatibility.electron.test.ts +++ b/src/main/browser/browser-popup-native-compatibility.electron.test.ts @@ -14,6 +14,7 @@ import { status, setPopupFixtureEvidenceName } from './browser-popup-compatibility-cdp-fixture' +import { waitForHiddenFrameInput } from './hidden-frame-input-readiness-fixture' import { MIN_POPUP_CONTENT_HEIGHT, MIN_POPUP_CONTENT_WIDTH } from './popup-window-minimum-size' setPopupFixtureEvidenceName('native-electron-evidence.json') @@ -312,14 +313,13 @@ describe('hidden Chromium native popup request compatibility', () => { }, { targetOrigin, opaque, shift } ) + const post = page.frameLocator('#frame-probe').locator('#frame-post') if (opaque) { - // Prime hidden Linux OOPIF hit testing and the route's root gesture gate. + await waitForHiddenFrameInput(post) + // Opaque frame input does not reach the route's root gesture gate. await page.locator('#frame-gesture').click() } - await page - .frameLocator('#frame-probe') - .locator('#frame-post') - .click(shift ? { modifiers: ['Shift'] } : {}) + await post.click(shift ? { modifiers: ['Shift'] } : {}) await expect .poll(() => receipts.slice(before).find((receipt) => receipt.path === '/frame-post')) .toMatchObject({ diff --git a/src/main/browser/hidden-frame-input-readiness-fixture.ts b/src/main/browser/hidden-frame-input-readiness-fixture.ts new file mode 100644 index 00000000000..378c08a9472 --- /dev/null +++ b/src/main/browser/hidden-frame-input-readiness-fixture.ts @@ -0,0 +1,28 @@ +import type { Locator } from 'playwright' +import { expect } from 'vitest' + +const READY_ATTRIBUTE = 'data-hidden-frame-input-ready' + +// A new sandboxed iframe runs in its own renderer and can finish loading before the browser routes +// pointer input to it; until then Playwright's click lands on the parent and the frame sees nothing. +export async function waitForHiddenFrameInput(target: Locator): Promise { + await target.evaluate((element, attribute) => { + element.removeAttribute(attribute) + element.addEventListener('pointermove', (event) => { + if (event.isTrusted) { + element.setAttribute(attribute, 'true') + } + }) + }, READY_ATTRIBUTE) + await expect + .poll( + async () => { + // Leave first so every hover is a fresh move the browser has to route again. + await target.page().mouse.move(0, 0) + await target.hover() + return target.getAttribute(READY_ATTRIBUTE) + }, + { timeout: 10_000 } + ) + .toBe('true') +} diff --git a/src/main/browser/hidden-renderer-frame-capture-fixture.ts b/src/main/browser/hidden-renderer-frame-capture-fixture.ts index 8019bdd8e10..1df9095027e 100644 --- a/src/main/browser/hidden-renderer-frame-capture-fixture.ts +++ b/src/main/browser/hidden-renderer-frame-capture-fixture.ts @@ -1,5 +1,8 @@ import type { Page } from 'playwright' +// Setup-only budget: a cold runner's first frame took up to 14s across 28 measured runs. +export const HIDDEN_RENDERER_FIRST_FRAME_TIMEOUT_MS = 45_000 + export async function startHiddenRendererFrameCapture( page: Page, waitForFirstFrame: (hasFrame: () => boolean) => Promise diff --git a/src/main/browser/popup-origin-chrome.electron.test.ts b/src/main/browser/popup-origin-chrome.electron.test.ts index 566d0cb7173..4f0276da65e 100644 --- a/src/main/browser/popup-origin-chrome.electron.test.ts +++ b/src/main/browser/popup-origin-chrome.electron.test.ts @@ -8,7 +8,10 @@ import { createPopupIdentityProbeRoot, launchPopupIdentityProbe } from './popup-identity-electron-probe' -import { startHiddenRendererFrameCapture } from './hidden-renderer-frame-capture-fixture' +import { + HIDDEN_RENDERER_FIRST_FRAME_TIMEOUT_MS, + startHiddenRendererFrameCapture +} from './hidden-renderer-frame-capture-fixture' type OriginProbeWindow = Window & { originProbeChild?: Window } @@ -138,9 +141,11 @@ beforeAll(async () => { } page = found stopCapture = await startHiddenRendererFrameCapture(page, (hasFrame) => - vi.waitFor(() => expect(hasFrame()).toBe(true), { timeout: 10_000 }) + vi.waitFor(() => expect(hasFrame()).toBe(true), { + timeout: HIDDEN_RENDERER_FIRST_FRAME_TIMEOUT_MS + }) ) -}, 60_000) +}, 90_000) afterAll(async () => { for (const response of held) {