mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
test(browser): route input into fresh sandboxed popup iframes before clicking them (#30966)
Fresh sandboxed iframes run in their own renderer and can finish loading before the browser routes pointer input to them, so Playwright's click landed on the parent and no popup opened. Hover-poll until the new frame sees trusted input before clicking, in one helper shared by the adapter, native and background popup probes. Share the cold first-frame budget from #30854 with popup-origin-chrome, which still waited only 10s for the same one-time GPU startup.
This commit is contained in:
@@ -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'))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<void> {
|
||||
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')
|
||||
}
|
||||
@@ -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<void>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user