mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* perf(ci): reduce test overhead and reuse expensive build work * test(ci): keep worker startup outside failure-case deadlines * test: isolate push fixture advisory locks * test(ci): preserve fixed drain retry allowance * Align CI contracts with current triggers and guide content * Stabilize consolidated reveal scenarios and retain behavioral CI contracts * Fix upstream lint blockers while retaining behavior and assertions * Classify global Jira preferences in the WSL migration inventory * Align Jira test fixtures with startup and build safety checks * Include package-test routing dependency in CI checkouts and fixtures * Keep native stream classes intact in transcript dependency tests
716 lines
25 KiB
TypeScript
716 lines
25 KiB
TypeScript
/**
|
|
* E2E tests for the browser tab: creating browser tabs and state retention.
|
|
*
|
|
* User Prompt:
|
|
* - Browser works and also retains state when switching tabs etc.
|
|
*/
|
|
|
|
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><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>
|
|
<p id="s">Attempting close…</p>
|
|
<script>
|
|
window.close()
|
|
setTimeout(() => {
|
|
document.getElementById('s').textContent =
|
|
'window.close() was blocked (expected).'
|
|
}, 200)
|
|
</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 readBrowserWindowCloseStatus(
|
|
page: Parameters<typeof getActiveWorktreeId>[0],
|
|
browserTabId: string
|
|
): Promise<string> {
|
|
return page.evaluate(async (targetBrowserTabId) => {
|
|
const slot = document.querySelector(`[data-browser-overlay-tab-id="${targetBrowserTabId}"]`)
|
|
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
|
|
if (!webview) {
|
|
return 'webview missing'
|
|
}
|
|
try {
|
|
return (await webview.executeJavaScript(
|
|
'document.querySelector("#s")?.textContent ?? "status missing"'
|
|
)) as string
|
|
} catch {
|
|
return 'guest lost'
|
|
}
|
|
}, browserTabId)
|
|
}
|
|
|
|
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) => {
|
|
webview.addEventListener('dom-ready', () => resolve(), { once: true })
|
|
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()
|
|
|
|
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: process.platform === 'darwin' ? 'meta' : 'control'
|
|
}
|
|
)
|
|
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) => {
|
|
webviewA.addEventListener('dom-ready', () => resolve(), { once: true })
|
|
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('blocked window.close in a link-created tab does not break tab switching', async ({
|
|
orcaPage
|
|
}) => {
|
|
const closeServer = await startBrowserWindowCloseServer()
|
|
try {
|
|
const worktreeId = (await getActiveWorktreeId(orcaPage))!
|
|
const neighboringTab = await createBrowserTab(
|
|
orcaPage,
|
|
worktreeId,
|
|
'about:blank',
|
|
'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 orcaPage.locator(`[data-tab-id="${neighboringTab!.id}"]`).click()
|
|
await expect.poll(async () => getActiveTabType(orcaPage), { timeout: 5_000 }).toBe('browser')
|
|
await expect
|
|
.poll(() => readBrowserWindowCloseStatus(orcaPage, closeTabId!), { timeout: 5_000 })
|
|
.toContain('window.close() was blocked')
|
|
} finally {
|
|
await closeServer.close()
|
|
}
|
|
})
|
|
|
|
test('directly created browser tabs block window.close and remain usable', async ({
|
|
orcaPage
|
|
}) => {
|
|
const closeServer = await startBrowserWindowCloseServer()
|
|
try {
|
|
const worktreeId = (await getActiveWorktreeId(orcaPage))!
|
|
const directTab = await createBrowserTab(
|
|
orcaPage,
|
|
worktreeId,
|
|
closeServer.url,
|
|
'Direct close tab'
|
|
)
|
|
expect(directTab?.id).toBeTruthy()
|
|
|
|
await expect
|
|
.poll(() => readBrowserWindowCloseStatus(orcaPage, directTab!.id), { timeout: 5_000 })
|
|
.toContain('window.close() was blocked')
|
|
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
orcaPage.evaluate(async (targetBrowserTabId) => {
|
|
const slot = document.querySelector(
|
|
`[data-browser-overlay-tab-id="${targetBrowserTabId}"]`
|
|
)
|
|
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
|
|
if (!webview) {
|
|
return 'webview missing'
|
|
}
|
|
try {
|
|
return (await webview.executeJavaScript(`(() => {
|
|
window.close = () => 'replacement-called'
|
|
return window.close() === 'replacement-called' ? 'replacement-called' : 'blocked'
|
|
})()`)) as string
|
|
} catch {
|
|
return 'guest unavailable'
|
|
}
|
|
}, directTab!.id),
|
|
{ timeout: 5_000 }
|
|
)
|
|
.toBe('blocked')
|
|
} finally {
|
|
await closeServer.close()
|
|
}
|
|
})
|
|
})
|