Files
orca/tests/e2e/browser-tab.spec.ts
T
OrcaWinandOrcaWin 7d6603aa17 [comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display

* [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci]

* [comment audit] Condense shared generation, path and preview comments [skip ci]

* [comment audit] Condense host, file and Git contract rationale [skip ci]

* [comment audit] Tighten Git lifecycle and provider contract comments [skip ci]

* [comment audit] Clarify shared host authority and launch isolation [skip ci]

* [comment audit] simplify local access mobile and native chat contracts [skip ci]

* [comment audit] simplify native chat presentation and state contracts [skip ci]

* [comment audit] simplify native runtime and provider contracts [skip ci]

* [comment audit] simplify migration and orchestration contracts [skip ci]

* [comment audit] simplify plugin trust and process capture rationale [skip ci]

* [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci]

* [comment audit] clarify remote recovery and launch wire rationale [skip ci]

* [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci]

* [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci]

* [comment audit] clarify sidebar, skill and SSH invariants [skip ci]

* [comment audit] condense structured chat delivery and timing rationale [skip ci]

* [comment audit] condense terminal replay and telemetry invariants [skip ci]

* [comment audit] clarify launch permission and terminal delivery rationale [skip ci]

* [comment audit] clarify workspace ownership and layout contracts [skip ci]

* [comment audit] wait for fitted and painted SSH terminal links

* [comment audit] clarify layout persistence and session ownership [skip ci]

* [comment audit] clarify host routing and workspace lifecycle contracts [skip ci]

* [comment audit] clarify startup persistence and app lifetime comments [skip ci]

* [comment audit] clarify recovery and activity boundaries [skip ci]

* [comment audit] condense activity and artifact invariants [skip ci]

* [comment audit] condense automation authority contracts [skip ci]

* [comment audit] simplify automation scheduling and history contracts [skip ci]

* [comment audit] clarify browser chrome and annotation lifecycles [skip ci]

* [comment audit] clarify browser routing and retained guest ownership [skip ci]

* [comment audit] clarify browser recovery and document ownership comments [skip ci]

* [comment audit] restore hosted runners required by native IME fixtures

* [comment audit] shorten preview, palette and dashboard invariants [skip ci]

* [comment audit] clarify terminal preview, dashboard and retention comments [skip ci]

* [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci]

* [comment audit] shorten rich-text and combined-diff ownership comments [skip ci]

* [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci]

* [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci]

* [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci]

* [comment audit] clarify rich Markdown source and selection safeguards [skip ci]

* [comment audit] clarify editor loading and emulator stream lifetimes [skip ci]

* [comment audit] trim emulator and feature tour narration [skip ci]

* [comment audit] clarify floating workspace and GitHub cache ownership [skip ci]

* [comment audit] trim provider controls and Linear filter explanations [skip ci]

* [comment audit] clarify pairing and chat resume lifetimes [skip ci]

* [comment audit] clarify chat delivery and transcript lifetimes [skip ci]

* [comment audit] clarify session options and host-owned status [skip ci]

* [comment audit] clarify draft durability and live-status precedence [skip ci]

* [comment audit] Trim chat send and model ownership explanations [skip ci]

* [comment audit] Shorten transcript ownership and send recovery comments [skip ci]

* [comment audit] Simplify native chat composer and transcript hook comments [skip ci]

* [comment audit] Shorten native chat scroll and structured session explanations [skip ci]

* [comment audit] Trim workspace picker and onboarding commentary [skip ci]

* [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci]

* [comment audit] Trim sidebar and session history explanations [skip ci]

* [comment audit] Condense session resume and review checks comments [skip ci]

* [comment audit] Shorten checks state and file explorer refresh comments [skip ci]

* [comment audit] Trim plugin bridge and source-control action rationale [skip ci]

* [comment audit] Trim source-control listing and review rationale [skip ci]

* [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci]

* [comment audit] Condense Settings account and appearance rationale [skip ci]

* [comment audit] Trim pairing and repository Settings explanations [skip ci]

* [comment audit] Condense Settings search SSH and terminal safeguards [skip ci]

* [comment audit] Shorten Settings voice and provider ownership comments [skip ci]

* [comment audit] Condense Settings draft persistence and host-state rationale [skip ci]

* [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci]

* [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci]

* [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci]

* [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci]

* [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci]

* [comment audit] Prune focus-read baseline after comment cleanup

* [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci]

* [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci]

* [comment audit] Clarify viewport geometry and skill update safeguards [skip ci]

* [comment audit] Tighten skill inventory, usage and host status comments [skip ci]

* [comment audit] Clarify usage ownership and resource kill safeguards [skip ci]

* [comment audit] Tighten tab selection, rename and search contracts [skip ci]

* [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci]

* [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci]

* [comment audit] Tighten provider loading and terminal recovery comments [skip ci]

* [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci]

* [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci]

* [comment audit] Tighten PTY startup and replay explanations [skip ci]

* [comment audit] Clarify PTY identity and delivery ownership [skip ci]

* [comment audit] Shorten remote reconnect and input replay rationale [skip ci]

* [comment audit] Condense terminal parking clipboard and file link comments [skip ci]

* [comment audit] Clarify IME ownership and trim trace history [skip ci]

* [comment audit] Shorten pane recovery and parked watcher explanations [skip ci]

* [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci]

* [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci]

* [comment audit] Trim renderer lifecycle and menu rationale [skip ci]

* [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci]

* [comment audit] Clarify renderer hook evidence and source ownership [skip ci]

* [comment audit] Tighten IPC ownership and reconnect explanations [skip ci]

* [comment audit] Trim renderer focus, paste and attention comments [skip ci]

* [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci]

* [comment audit] Trim agent launch, resume and ownership commentary [skip ci]

* [comment audit] Shorten account, browser and editor comments [skip ci]

* [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci]

* [comment audit] Condense launch recovery and editor tokenization comments [skip ci]

* [comment audit] Condense workspace selection and terminal pane rationale [skip ci]

* [comment audit] Condense terminal fitting and rendering recovery comments [skip ci]

* [comment audit] Condense terminal input and host routing explanations [skip ci]

* [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci]

* [comment audit] Condense structured launch and terminal link explanations [skip ci]

* [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci]

* [comment audit] Condense session persistence and workspace activation rationale [skip ci]

* [comment audit] Condense workspace routing and browser ownership rationale [skip ci]

* [comment audit] Condense mirror recovery and layout authority rationale [skip ci]

* [comment audit] Condense runtime transport and file safety rationale [skip ci]

* [comment audit] Condense runtime graph and structured session rationale [skip ci]

* [comment audit] Condense browser creation and remote tab recovery rationale [skip ci]

* [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci]

* [comment audit] Condense frame, startup and provider cache rationale [skip ci]

* [comment audit] Condense host twin and agent lifecycle rationale [skip ci]

* [comment audit] Condense browser authority and editor cleanup rationale [skip ci]

* [comment audit] Condense editor buffer identity and review cache rationale [skip ci]

* [comment audit] Condense provider lifetime and repository ownership rationale [skip ci]

* [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci]

* [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci]

* [comment audit] Condense cleanup ownership and listing admission rationale [skip ci]

* [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci]

* [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci]

* [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci]

* [comment audit] Condense cloud rollout and assignment safety rationale [skip ci]

* [comment audit] Condense relay session and database recovery rationale [skip ci]

* [comment audit] Condense relay placement and protocol compatibility rationale [skip ci]

* [comment audit] Condense build and packaging constraints [skip ci]

* [comment audit] Condense CI gates and validation isolation rationale [skip ci]

* [comment audit] Condense catalog identity and browser validation rationale [skip ci]

* [comment audit] Condense mobile browser and frame-budget rationale [skip ci]

* [comment audit] Shorten mobile route and browser test explanations [skip ci]

* [comment audit] Condense editor and session seam test rationale [skip ci]

* [comment audit] Trim session and terminal closure history [skip ci]

* [comment audit] Shorten terminal browser and census explanations [skip ci]

* [comment audit] Preserve root-only E2E diagnostic baseline

* [comment audit] Condense mobile seam and HTML preview test rationale [skip ci]

* [comment audit] Condense preview instrumentation and render harness rationale [skip ci]

* [comment audit] Condense bundle contracts and native-build constraints [skip ci]

* [comment audit] Shorten native packaging and CI routing rationale [skip ci]

* [comment audit] Condense release tooling and benchmark constraints [skip ci]

* [comment audit] Condense packaging and platform gate rationale [skip ci]

* [comment audit] Shorten mobile route and history rationale [skip ci]

* [comment audit] Condense mobile browser and component rationale [skip ci]

* [comment audit] Shorten drawer and workspace-form rationale [skip ci]

* [comment audit] Trim mobile review and editor-document explanations [skip ci]

* [comment audit] Condense editor lifecycle and Markdown explanations [skip ci]

* [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci]

* [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci]

* [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci]

* [comment audit] Condense bridge stream, native request and audio rationale [skip ci]

* [comment audit] Reduce bridge schema and page-client commentary [skip ci]

* [comment audit] Tighten mobile page bridge contracts [skip ci]

* [comment audit] Clarify mobile cache and session constraints [skip ci]

* [comment audit] Condense mobile route and lifecycle rationale [skip ci]

* [comment audit] Tighten mobile shell hook timing rationale [skip ci]

* [comment audit] Clarify route handoff and notification compatibility [skip ci]

* [comment audit] Clarify push registration and dictation cleanup constraints [skip ci]

* [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci]

* [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci]

* [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci]

* [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci]

* [comment audit] Tighten stream replay and queued session ownership rationale [skip ci]

* [comment audit] Condense mobile composer and reply-schema constraints [skip ci]

* [comment audit] Clarify mobile transcript recovery and session ownership [skip ci]

* [comment audit] Clarify mobile queue and source-control compatibility [skip ci]

* [comment audit] Condense storage and task launch constraints [skip ci]

* [comment audit] Clarify task schemas and terminal document ownership [skip ci]

* [comment audit] Tighten terminal input and viewport rationale [skip ci]

* [comment audit] Shorten terminal and RPC recorder contracts [skip ci]

* [comment audit] Condense recorder engine and connection contracts [skip ci]

* [comment audit] Shorten host credential and relay recovery comments [skip ci]

* [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci]

* [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci]

* [comment audit] Condense catalog and relay entry contracts [skip ci]

* [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci]

* [comment audit] Condense WSL relay and E2E regression rationale [skip ci]

* [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci]

* [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci]

* [comment audit] Shorten layout oracle and regression explanations [skip ci]

* [comment audit] Condense paired recovery and ownership regression contracts [skip ci]

* [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci]

* [comment audit] Condense terminal rendering and IME regression rationale [skip ci]

* [comment audit] Shorten terminal recovery and benchmark contracts [skip ci]

* [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci]

* [comment audit] Condense workflow routing and release rationale [skip ci]

* [comment audit] Condense mobile security and installer contracts [skip ci]

* [comment audit] Shorten native rationale and stylesheet workarounds [skip ci]

* [comment audit] Shorten Windows visibility probe rationale [skip ci]

* [comment audit] Condense embedded release and signing rationale [skip ci]

* [comment audit] Retain current main test behavior in final reconciliation [skip ci]

* [comment audit] Complete final comment review across current source inventory

* [comment audit] Handle consolidated audit reports before final CI

* [comment audit] Preserve credential storage census detail [skip ci]

* [comment audit] Scale broad changed E2E selections with verified timing shards

* [comment audit] Align E2E assertions with browser close and settled session metadata

* [comment audit] Match existing E2Es to current resource and windowing contracts

* [comment audit] Observe the shared server outage notice in reconnect coverage

* [comment audit] Make existing renderer probes measure settled tab and PTY work

* [comment audit] Align palette E2E selector with task search

* [comment audit] Refresh remaining palette search selectors

* [comment audit] Keep selector correction within selected audit specs

* [comment audit] Await emoji fixture settings inside renderer evaluation

* [comment audit] Cancel fixture fit retries between terminal tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-25 04:05:46 -07:00

697 lines
24 KiB
TypeScript

// Cover browser-tab creation and state retention across tab switches.
import { test, expect } from './helpers/orca-app'
import { createServer, type Server } from 'node:http'
import type { AddressInfo } from 'node:net'
import {
waitForSessionReady,
waitForActiveWorktree,
getActiveWorktreeId,
getActiveTabType,
getBrowserTabs,
ensureTerminalVisible
} from './helpers/store'
import { startBrowserLinkServer } from './helpers/browser-link-server'
type CreatedBrowserTab = {
id: string
pageId: string | null
}
async function createBrowserTab(
page: Parameters<typeof getActiveWorktreeId>[0],
worktreeId: string,
url?: string,
title = 'New Browser Tab'
): Promise<CreatedBrowserTab | null> {
return page.evaluate(
({ targetWorktreeId, targetUrl, targetTitle }) => {
const store = window.__store
if (!store) {
return null
}
const state = store.getState()
const tab = state.createBrowserTab(
targetWorktreeId,
targetUrl ?? state.browserDefaultUrl ?? 'about:blank',
{
title: targetTitle,
activate: true
}
)
return { id: tab.id, pageId: tab.activePageId ?? null }
},
{ targetWorktreeId: worktreeId, targetUrl: url, targetTitle: title }
)
}
async function switchToBrowserTab(
page: Parameters<typeof getActiveWorktreeId>[0],
worktreeId: string,
browserTabId: string
): Promise<void> {
await page.evaluate(
({ targetWorktreeId, targetBrowserTabId }) => {
const store = window.__store
if (!store) {
return
}
const state = store.getState()
if (
(state.browserTabsByWorktree[targetWorktreeId] ?? []).some(
(tab) => tab.id === targetBrowserTabId
)
) {
state.setActiveBrowserTab(targetBrowserTabId)
}
},
{ targetWorktreeId: worktreeId, targetBrowserTabId: browserTabId }
)
}
async function startBrowserFormServer(host = '127.0.0.1'): Promise<{
url: (label: string) => string
close: () => Promise<void>
}> {
const server = createServer((request, response) => {
const label = new URL(request.url ?? '/', 'http://127.0.0.1').pathname.slice(1)
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
response.end(`
<!doctype html>
<html>
<body>
<label>${label}<input id="q" /></label>
</body>
</html>
`)
})
await new Promise<void>((resolve) => server.listen(0, host, resolve))
const port = (server.address() as AddressInfo).port
return {
url: (label: string) => `http://${host}:${port}/${encodeURIComponent(label)}`,
close: () => closeServer(server)
}
}
async function startBrowserWindowCloseServer(): Promise<{
url: string
sourceUrl: string
close: () => Promise<void>
}> {
const server = createServer((request, response) => {
const origin = `http://127.0.0.1:${(server.address() as AddressInfo).port}`
const pathname = new URL(request.url ?? '/', origin).pathname
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
if (pathname === '/source') {
response.end(`
<!doctype html>
<html>
<head><title>Close link source</title></head>
<body><input id="q" /><a id="window-close-link" href="${origin}/window-close" target="_blank">Open close page</a></body>
</html>
`)
return
}
response.end(`
<!doctype html>
<html>
<head><title>Window close repro</title></head>
<body>
<button id="window-close">Close this tab</button>
<script>
document.getElementById('window-close').onclick = () => {
window.close = () => { document.title = 'Close override ran' }
window.close()
}
</script>
</body>
</html>
`)
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const port = (server.address() as AddressInfo).port
return {
url: `http://127.0.0.1:${port}/window-close`,
sourceUrl: `http://127.0.0.1:${port}/source`,
close: () => closeServer(server)
}
}
async function closeServer(server: Server): Promise<void> {
await new Promise<void>((resolve, reject) =>
server.close((error) => {
if (error) {
reject(error)
return
}
resolve()
})
)
}
async function clickBrowserLink(
page: Parameters<typeof getActiveWorktreeId>[0],
browserTabId: string,
selector: string,
options: {
modifiers?: ('meta' | 'control' | 'shift')[]
button?: 'left' | 'middle' | 'right'
frameSelector?: string
} = {}
): Promise<void> {
await page.evaluate(
async ({ targetBrowserTabId, targetSelector, inputModifiers, button, frameSelector }) => {
const slot = [...document.querySelectorAll('[data-browser-overlay-tab-id]')].find(
(candidate) => candidate.getAttribute('data-browser-overlay-tab-id') === targetBrowserTabId
)
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
if (!webview) {
throw new Error(`Missing webview for browser tab ${targetBrowserTabId}`)
}
const point = (await webview.executeJavaScript(`(async () => {
const deadline = Date.now() + 5000
while (Date.now() < deadline) {
const frame = ${JSON.stringify(frameSelector)}
? document.querySelector(${JSON.stringify(frameSelector)})
: null
const root = frame?.contentDocument ?? document
const linkRect = root.querySelector(${JSON.stringify(targetSelector)})?.getBoundingClientRect()
if (linkRect) {
const frameRect = frame?.getBoundingClientRect()
return {
x: (frameRect?.left ?? 0) + linkRect.left + linkRect.width / 2,
y: (frameRect?.top ?? 0) + linkRect.top + linkRect.height / 2
}
}
await new Promise((resolve) => setTimeout(resolve, 25))
}
return null
})()`)) as { x: number; y: number } | null
if (!point) {
throw new Error(`Missing browser link ${targetSelector}`)
}
const holdShift = inputModifiers.includes('shift')
if (holdShift) {
await webview.sendInputEvent({ type: 'keyDown', keyCode: 'Shift', modifiers: ['shift'] })
}
try {
await webview.sendInputEvent({ type: 'mouseMove', modifiers: inputModifiers, ...point })
await webview.sendInputEvent({
type: 'mouseDown',
button,
clickCount: 1,
modifiers: inputModifiers,
...point
})
await webview.sendInputEvent({
type: 'mouseUp',
button,
clickCount: 1,
modifiers: inputModifiers,
...point
})
} finally {
if (holdShift) {
await webview.sendInputEvent({ type: 'keyUp', keyCode: 'Shift' })
}
}
},
{
targetBrowserTabId: browserTabId,
targetSelector: selector,
inputModifiers: options.modifiers ?? [],
button: options.button ?? 'left',
frameSelector: options.frameSelector ?? null
}
)
}
async function waitForTabIdByExactTitle(
page: Parameters<typeof getActiveWorktreeId>[0],
title: string
): Promise<string> {
const resolveTabId = (): Promise<string | null> =>
page.locator('[data-tab-id]').evaluateAll((tabs, exactTitle) => {
const tab = tabs.find((candidate) => candidate.textContent?.trim() === exactTitle)
return tab?.getAttribute('data-tab-id') ?? null
}, title)
await expect.poll(resolveTabId, { timeout: 10_000 }).not.toBeNull()
return (await resolveTabId()) as string
}
async function expectBrowserTabActive(
page: Parameters<typeof getActiveWorktreeId>[0],
title: string
): Promise<void> {
const tabId = await waitForTabIdByExactTitle(page, title)
await expect(page.locator(`[data-browser-overlay-tab-id="${tabId}"]`)).toHaveCSS('opacity', '1')
}
async function expectBrowserTabOpenedInBackground(
page: Parameters<typeof getActiveWorktreeId>[0],
sourceTabId: string,
title: string
): Promise<void> {
const openedTabId = await waitForTabIdByExactTitle(page, title)
await expect(page.locator(`[data-browser-overlay-tab-id="${sourceTabId}"]`)).toHaveCSS(
'opacity',
'1'
)
await expect(page.locator(`[data-browser-overlay-tab-id="${openedTabId}"]`)).toHaveCSS(
'opacity',
'0'
)
}
async function readBrowserInputValue(
page: Parameters<typeof getActiveWorktreeId>[0],
browserTabId: string
): Promise<string | null> {
return page.evaluate(async (targetBrowserTabId) => {
const slot = [...document.querySelectorAll('[data-browser-overlay-tab-id]')].find(
(candidate) => candidate.getAttribute('data-browser-overlay-tab-id') === targetBrowserTabId
)
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
if (!webview) {
return null
}
try {
return await webview.executeJavaScript('document.querySelector("#q")?.value ?? null')
} catch {
return null
}
}, browserTabId)
}
async function writeBrowserInputValue(
page: Parameters<typeof getActiveWorktreeId>[0],
browserTabId: string,
value: string
): Promise<void> {
await expect
.poll(async () => readBrowserInputValue(page, browserTabId), { timeout: 5_000 })
.not.toBeNull()
await page.evaluate(
async ({ targetBrowserTabId, nextValue }) => {
const slot = [...document.querySelectorAll('[data-browser-overlay-tab-id]')].find(
(candidate) => candidate.getAttribute('data-browser-overlay-tab-id') === targetBrowserTabId
)
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
if (!webview) {
throw new Error(`Missing webview for browser tab ${targetBrowserTabId}`)
}
await webview.executeJavaScript(
`document.querySelector("#q").value = ${JSON.stringify(nextValue)}`
)
},
{ targetBrowserTabId: browserTabId, nextValue: value }
)
await expect
.poll(async () => readBrowserInputValue(page, browserTabId), { timeout: 5_000 })
.toBe(value)
}
test.describe('Browser Tab', () => {
test.beforeEach(async ({ orcaPage }) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
})
test('browser webview form state survives switching between browser tabs', async ({
orcaPage
}) => {
const formServer = await startBrowserFormServer()
try {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const firstTab = await createBrowserTab(
orcaPage,
worktreeId,
formServer.url('First search'),
'First Form'
)
expect(firstTab?.id).toBeTruthy()
await writeBrowserInputValue(orcaPage, firstTab!.id, 'first typed value')
const secondTab = await createBrowserTab(
orcaPage,
worktreeId,
formServer.url('Second search'),
'Second Form'
)
expect(secondTab?.id).toBeTruthy()
await writeBrowserInputValue(orcaPage, secondTab!.id, 'second typed value')
// Why: switching browser tabs used to unmount and reparent the inactive
// Electron webview, which recreated the guest document and erased form DOM.
await switchToBrowserTab(orcaPage, worktreeId, firstTab!.id)
await expect
.poll(async () => readBrowserInputValue(orcaPage, firstTab!.id), { timeout: 5_000 })
.toBe('first typed value')
await switchToBrowserTab(orcaPage, worktreeId, secondTab!.id)
await expect
.poll(async () => readBrowserInputValue(orcaPage, secondTab!.id), { timeout: 5_000 })
.toBe('second typed value')
} finally {
await formServer.close()
}
})
test('browser page reload restores the configured 100% zoom', async ({ orcaPage }) => {
const formServer = await startBrowserFormServer()
try {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const browserTab = await createBrowserTab(
orcaPage,
worktreeId,
formServer.url('Zoom reload'),
'Zoom Reload'
)
expect(browserTab?.id).toBeTruthy()
await expect
.poll(async () => readBrowserInputValue(orcaPage, browserTab!.id), { timeout: 5_000 })
.not.toBeNull()
const zoomLevels = await orcaPage.evaluate(async (browserTabId) => {
const slot = document.querySelector(`[data-browser-overlay-tab-id="${browserTabId}"]`)
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
if (!webview) {
throw new Error(`Missing webview for browser tab ${browserTabId}`)
}
const levels = [webview.getZoomLevel()]
webview.setZoomLevel(0.5)
for (let reload = 0; reload < 3; reload += 1) {
await new Promise<void>((resolve) => {
const onReady = (): void => {
webview.removeEventListener('dom-ready', onReady)
resolve()
}
webview.addEventListener('dom-ready', onReady)
if (reload === 1) {
webview.reloadIgnoringCache()
} else {
webview.reload()
}
})
levels.push(webview.getZoomLevel())
}
return levels
}, browserTab!.id)
expect(zoomLevels).toEqual([0, 0, 0, 0])
} finally {
await formServer.close()
}
})
test('Cmd/Ctrl+0 resets a zoomed browser page to 100%', async ({ orcaPage }) => {
const formServer = await startBrowserFormServer()
try {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const browserTab = await createBrowserTab(
orcaPage,
worktreeId,
formServer.url('Zoom reset'),
'Zoom Reset'
)
expect(browserTab?.id).toBeTruthy()
await expect
.poll(async () => readBrowserInputValue(orcaPage, browserTab!.id), { timeout: 5_000 })
.not.toBeNull()
const zoomModifier: 'meta' | 'control' = process.platform === 'darwin' ? 'meta' : 'control'
await orcaPage.evaluate(
async ({ browserTabId, browserPageId, modifier }) => {
const slot = document.querySelector(`[data-browser-overlay-tab-id="${browserTabId}"]`)
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
if (!webview) {
throw new Error(`Missing webview for browser tab ${browserTabId}`)
}
window.dispatchEvent(
new CustomEvent('orca:browser-page-zoom', {
detail: { browserPageId, direction: 'in' }
})
)
await webview.sendInputEvent({ type: 'keyDown', keyCode: '0', modifiers: [modifier] })
await webview.sendInputEvent({ type: 'keyUp', keyCode: '0', modifiers: [modifier] })
},
{
browserTabId: browserTab!.id,
browserPageId: browserTab!.pageId ?? browserTab!.id,
modifier: zoomModifier
}
)
await expect
.poll(() =>
orcaPage.evaluate((browserTabId) => {
const slot = document.querySelector(`[data-browser-overlay-tab-id="${browserTabId}"]`)
return (slot?.querySelector('webview') as Electron.WebviewTag | null)?.getZoomLevel()
}, browserTab!.id)
)
.toBe(0)
} finally {
await formServer.close()
}
})
test('reloading one browser tab does not adopt another tab zoom', async ({ orcaPage }) => {
const [formServerA, formServerB] = await Promise.all([
startBrowserFormServer(),
startBrowserFormServer('localhost')
])
try {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const tabA = await createBrowserTab(orcaPage, worktreeId, formServerA.url('Zoom A'), 'Zoom A')
const tabB = await createBrowserTab(orcaPage, worktreeId, formServerB.url('Zoom B'), 'Zoom B')
expect(tabA?.id).toBeTruthy()
expect(tabB?.id).toBeTruthy()
for (const tab of [tabA, tabB]) {
await expect
.poll(async () => readBrowserInputValue(orcaPage, tab!.id), { timeout: 5_000 })
.not.toBeNull()
}
const levels = await orcaPage.evaluate(
async ({ tabAId, tabBId, pageBId }) => {
const webviewFor = (id: string): Electron.WebviewTag => {
const slot = document.querySelector(`[data-browser-overlay-tab-id="${id}"]`)
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
if (!webview) {
throw new Error(`Missing webview for browser tab ${id}`)
}
return webview
}
const webviewA = webviewFor(tabAId)
const webviewB = webviewFor(tabBId)
// Zoom only tab B through the real renderer zoom path (also writes the shared setting).
for (let step = 0; step < 2; step += 1) {
window.dispatchEvent(
new CustomEvent('orca:browser-page-zoom', {
detail: { browserPageId: pageBId, direction: 'in' }
})
)
await new Promise((resolve) => setTimeout(resolve, 100))
}
const zoomedB = webviewB.getZoomLevel()
const untouchedA = webviewA.getZoomLevel()
await new Promise<void>((resolve) => {
const onReady = (): void => {
webviewA.removeEventListener('dom-ready', onReady)
resolve()
}
webviewA.addEventListener('dom-ready', onReady)
webviewA.reload()
})
return { zoomedB, untouchedA, reloadedA: webviewA.getZoomLevel() }
},
{ tabAId: tabA!.id, tabBId: tabB!.id, pageBId: tabB!.pageId ?? tabB!.id }
)
expect(levels.zoomedB).toBeGreaterThan(0)
expect(levels.untouchedA).toBe(0)
// Regression: reasserting the shared default would drag tab A to tab B's zoom.
expect(levels.reloadedA).toBe(0)
} finally {
await Promise.all([formServerA.close(), formServerB.close()])
}
})
test('new-tab link gestures follow Chrome foreground and background behavior', async ({
electronApp,
orcaPage
}) => {
const linkServer = await startBrowserLinkServer()
try {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const sourceTab = await createBrowserTab(
orcaPage,
worktreeId,
linkServer.sourceUrl,
'Source page'
)
expect(sourceTab?.id).toBeTruthy()
const baseWindowCount = await electronApp.evaluate(
({ BaseWindow }) => BaseWindow.getAllWindows().length
)
// A plain main-frame target=_blank click must not navigate the source tab away.
const sourceTabLocator = orcaPage.locator(`[data-tab-id="${sourceTab!.id}"]`)
await clickBrowserLink(orcaPage, sourceTab!.id, '#blank-link')
await expectBrowserTabActive(orcaPage, 'Blank target destination')
await expect(sourceTabLocator).toContainText('Source page')
await switchToBrowserTab(orcaPage, worktreeId, sourceTab!.id)
// Context-menu links keep the source visible until the new tab is selected.
await clickBrowserLink(orcaPage, sourceTab!.id, '#external-link', { button: 'right' })
await orcaPage
.getByRole('menuitem', { name: 'Open Link In Orca Browser', exact: true })
.click()
await expectBrowserTabOpenedInBackground(orcaPage, sourceTab!.id, 'Linked destination')
await clickBrowserLink(orcaPage, sourceTab!.id, '#frame-link', {
frameSelector: '#link-frame'
})
await expectBrowserTabActive(orcaPage, 'Frame destination')
await switchToBrowserTab(orcaPage, worktreeId, sourceTab!.id)
await clickBrowserLink(orcaPage, sourceTab!.id, '#frame-modifier-link', {
frameSelector: '#link-frame',
modifiers: process.platform === 'darwin' ? ['meta'] : ['control']
})
await expectBrowserTabOpenedInBackground(
orcaPage,
sourceTab!.id,
'Frame modifier destination'
)
await clickBrowserLink(orcaPage, sourceTab!.id, '#frame-middle-link', {
button: 'middle',
frameSelector: '#link-frame'
})
await expectBrowserTabOpenedInBackground(orcaPage, sourceTab!.id, 'Frame middle destination')
await clickBrowserLink(orcaPage, sourceTab!.id, '#modifier-link', {
modifiers: process.platform === 'darwin' ? ['meta'] : ['control']
})
await expectBrowserTabOpenedInBackground(orcaPage, sourceTab!.id, 'Modifier destination')
await clickBrowserLink(orcaPage, sourceTab!.id, '#frame-shift-middle-link', {
button: 'middle',
modifiers: ['shift'],
frameSelector: '#link-frame'
})
await expectBrowserTabActive(orcaPage, 'Frame shift middle destination')
await switchToBrowserTab(orcaPage, worktreeId, sourceTab!.id)
const tabCountBeforeCancelledClick = await orcaPage.locator('[data-tab-id]').count()
await clickBrowserLink(orcaPage, sourceTab!.id, '#cancelled-link')
await expect(
orcaPage.locator('[data-tab-id]').filter({ hasText: 'Click handled in page' })
).toBeVisible({ timeout: 10_000 })
await expect(orcaPage.locator('[data-tab-id]')).toHaveCount(tabCountBeforeCancelledClick)
await clickBrowserLink(orcaPage, sourceTab!.id, '#middle-link', { button: 'middle' })
await expectBrowserTabOpenedInBackground(orcaPage, sourceTab!.id, 'Middle-click destination')
await expect
.poll(() => electronApp.evaluate(({ BaseWindow }) => BaseWindow.getAllWindows().length), {
timeout: 5_000
})
.toBe(baseWindowCount)
} finally {
await linkServer.close()
}
})
test('window.close removes a link-created tab and keeps neighboring tabs usable', async ({
orcaPage
}) => {
const closeServer = await startBrowserWindowCloseServer()
try {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const neighboringTab = await createBrowserTab(
orcaPage,
worktreeId,
closeServer.sourceUrl,
'Neighboring tab'
)
const sourceTab = await createBrowserTab(
orcaPage,
worktreeId,
closeServer.sourceUrl,
'Close link source'
)
expect(neighboringTab?.id).toBeTruthy()
expect(sourceTab?.id).toBeTruthy()
await clickBrowserLink(orcaPage, sourceTab!.id, '#window-close-link')
let closeTabId: string | null = null
await expect
.poll(async () => {
const tabs = await getBrowserTabs(orcaPage, worktreeId)
closeTabId = tabs.find((tab) => tab.url === closeServer.url)?.id ?? null
return closeTabId
})
.not.toBeNull()
await clickBrowserLink(orcaPage, closeTabId!, '#window-close')
await expect
.poll(async () => (await getBrowserTabs(orcaPage, worktreeId)).map((tab) => tab.id))
.not.toContain(closeTabId)
expect((await getBrowserTabs(orcaPage, worktreeId)).map((tab) => tab.id)).toContain(
sourceTab!.id
)
await orcaPage.locator(`[data-tab-id="${neighboringTab!.id}"]`).click()
await expect.poll(async () => getActiveTabType(orcaPage), { timeout: 5_000 }).toBe('browser')
await expect(
orcaPage.locator(`[data-browser-overlay-tab-id="${neighboringTab!.id}"]`)
).toHaveCSS('opacity', '1')
await writeBrowserInputValue(orcaPage, neighboringTab!.id, 'still usable after close')
} finally {
await closeServer.close()
}
})
test('a single-entry browser tab honors protected window.close and preserves its neighbor', async ({
orcaPage
}) => {
const closeServer = await startBrowserWindowCloseServer()
try {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const neighboringTab = await createBrowserTab(
orcaPage,
worktreeId,
closeServer.sourceUrl,
'Neighbor'
)
expect(neighboringTab?.id).toBeTruthy()
const directTab = await createBrowserTab(
orcaPage,
worktreeId,
closeServer.url,
'Direct close tab'
)
expect(directTab?.id).toBeTruthy()
await clickBrowserLink(orcaPage, directTab!.id, '#window-close')
await expect
.poll(async () => (await getBrowserTabs(orcaPage, worktreeId)).map((tab) => tab.id))
.not.toContain(directTab!.id)
await orcaPage.locator(`[data-tab-id="${neighboringTab!.id}"]`).click()
await expect.poll(async () => getActiveTabType(orcaPage), { timeout: 5_000 }).toBe('browser')
await expect(
orcaPage.locator(`[data-browser-overlay-tab-id="${neighboringTab!.id}"]`)
).toHaveCSS('opacity', '1')
await writeBrowserInputValue(orcaPage, neighboringTab!.id, 'still usable after close')
} finally {
await closeServer.close()
}
})
})