Files
orca/tests/e2e/browser-agent-input-focus-isolation.spec.ts
Jinjing 51fffa8753 fix(browser): bind guest text input to its webContents (#40931)
* fix(browser): keep guest input bound to its webContents

* test(browser): reject unresolved guest input routes

* test(browser): keep text on the guest when the root frame probe fails

* fix(browser): insert guest text only into a confirmed focused frame

Electron's webContents.insertText crashes the guest renderer and never
settles when no local frame holds focus (a fresh tab, or focus in a
cross-site frame after a root probe timeout). Use it only when the focus
probe confirms a focused element or same-site frame, refuse otherwise,
and reject promptly if the renderer dies or stops answering.

* test(browser): explain the busy-page e2e timeout

* fix(browser): expire guest text edits at the renderer mutation boundary

* fix(browser): preserve focused local frame contexts for text edits
2026-11-12 14:48:53 -08:00

166 lines
6.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Agent browser input must reach its page without touching the user's keyboard focus (#20595).
* Runs hidden; browser-agent-input-focus-isolation-native.spec.ts repeats it in a visible window.
*/
import { test, expect } from './helpers/orca-app'
import {
evalInPage,
openBackgroundProbeTab,
readProperty,
rpc,
runAgentClicksScenario,
runAgentCrossSiteFrameScenario,
runAgentKeysScenario,
runAgentShortcutsScenario,
startProbeServer
} from './helpers/browser-agent-focus-scenarios'
import { runAgentClickDuringUserCompositionScenario } from './helpers/browser-agent-composition-scenario'
import { getActiveTabId } from './helpers/store'
test('page-bound input stays out of chat drafts across hidden tabs and stale targets', async ({
orcaPage
}) => {
const server = await startProbeServer()
try {
const first = await openBackgroundProbeTab(orcaPage, server.url)
const second = await openBackgroundProbeTab(orcaPage, server.url)
await orcaPage.evaluate(() => {
const draft = document.createElement('textarea')
draft.id = 'browser-target-chat-draft'
draft.value = 'Unsent human draft'
document.body.append(draft)
draft.focus()
})
const draft = orcaPage.locator('#browser-target-chat-draft')
await rpc(orcaPage, 'browser.click', { page: first, element: '#notes' })
await draft.focus()
await rpc(orcaPage, 'browser.tabSwitch', { page: second })
await evalInPage(orcaPage, second, 'document.getElementById("notes").focus()')
await Promise.all([
rpc(orcaPage, 'browser.keyboardInsertText', { page: first, text: 'First line\nSecond 🙏' }),
rpc(orcaPage, 'browser.keyboardInsertText', { page: second, text: 'Other\ntab' }),
draft.focus()
])
await rpc(orcaPage, 'browser.type', { page: first, input: '!' })
await rpc(orcaPage, 'browser.keypress', { page: second, key: 'End' })
await rpc(orcaPage, 'browser.type', { page: second, input: '?' })
expect(await evalInPage(orcaPage, first, 'document.getElementById("notes").value')).toBe(
'First line\nSecond 🙏!'
)
expect(await evalInPage(orcaPage, second, 'document.getElementById("notes").value')).toBe(
'Other\ntab?'
)
await expect(draft).toHaveValue('Unsent human draft')
await rpc(orcaPage, 'browser.tabClose', { page: first })
const terminalTab = await getActiveTabId(orcaPage)
for (const command of [
{ method: 'browser.keyboardInsertText', params: { page: first, text: 'Stale insert' } },
{ method: 'browser.type', params: { page: 'missing-browser-page', input: 'Missing type' } },
{ method: 'browser.click', params: { page: terminalTab, element: 'textarea' } },
{ method: 'browser.keypress', params: { page: first, key: 'Enter' } }
]) {
const response = await orcaPage.evaluate(
(request) => window.api.runtime.call(request),
command
)
expect(readProperty(response, 'ok')).toBe(false)
expect(readProperty(readProperty(response, 'error'), 'code')).toBe('browser_tab_not_found')
}
await expect(draft).toHaveValue('Unsent human draft')
expect(await evalInPage(orcaPage, second, 'document.getElementById("notes").value')).toBe(
'Other\ntab?'
)
} finally {
await server.close()
}
})
test('text insertion needs a focused page target and never crashes or wedges the tab', async ({
orcaPage
}) => {
// Waits out a deliberately busy page (6 s) on top of tab startup and agent-browser round trips.
test.setTimeout(120_000)
const server = await startProbeServer()
try {
const pageId = await openBackgroundProbeTab(orcaPage, server.url)
const insert = async (text: string): Promise<unknown> =>
orcaPage.evaluate((request) => window.api.runtime.call(request), {
method: 'browser.keyboardInsertText',
params: { page: pageId, text }
})
const expectRefused = (response: unknown): void => {
expect(readProperty(response, 'ok')).toBe(false)
expect(JSON.stringify(readProperty(response, 'error'))).toContain('no focused element')
}
// A tab nobody focused has no frame to receive native text; Electron crashes if asked anyway.
expectRefused(await insert('Fresh\ntab'))
expect(await evalInPage(orcaPage, pageId, 'document.getElementById("notes").value')).toBe('')
await expect
.poll(() => evalInPage(orcaPage, pageId, 'String(window.__frameReady)'))
.toBe('true')
await evalInPage(orcaPage, pageId, `document.getElementById('frame').focus()`)
await rpc(orcaPage, 'browser.keypress', { page: pageId, key: 'Z' })
await rpc(orcaPage, 'browser.keyboardInsertText', { page: pageId, text: 'Cross site 🙏' })
await expect
.poll(() => evalInPage(orcaPage, pageId, 'window.__frameValue'))
.toBe('ZCross site 🙏')
// A busy page cannot confirm where focus is; once it recovers, input must flow again.
await evalInPage(
orcaPage,
pageId,
'setTimeout(() => { const end = Date.now() + 6000; while (Date.now() < end) {} }, 0); "busy"'
)
expect(readProperty(await insert('Busy'), 'ok')).toBe(false)
await expect
.poll(() => evalInPage(orcaPage, pageId, '"responsive"'), { timeout: 20_000 })
.toBe('responsive')
await rpc(orcaPage, 'browser.keyboardInsertText', { page: pageId, text: ' ok' })
await expect
.poll(() => evalInPage(orcaPage, pageId, 'window.__frameValue'))
.toBe('ZCross site 🙏 ok')
} finally {
await server.close()
}
})
test('agent keys and text reach the page without leaving the user’s terminal', async ({
orcaPage
}) => {
test.setTimeout(120_000)
await runAgentKeysScenario(orcaPage)
})
test('agent clicks and presses reach the page without leaving the user’s terminal', async ({
orcaPage
}) => {
test.setTimeout(120_000)
await runAgentClicksScenario(orcaPage)
})
test('agent shortcut keys stay in the page instead of running Orca shortcuts', async ({
orcaPage
}) => {
test.setTimeout(120_000)
await runAgentShortcutsScenario(orcaPage)
})
test('agent keys and text reach a focused cross-site frame without moving the user’s focus', async ({
orcaPage
}) => {
test.setTimeout(120_000)
await runAgentCrossSiteFrameScenario(orcaPage)
})
test('an agent click is refused while the user composes with an IME in the terminal', async ({
orcaPage
}) => {
test.setTimeout(120_000)
await runAgentClickDuringUserCompositionScenario(orcaPage)
})