mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
Preserve retained desktop browser pages during SSH conversion (#26572)
* Preserve desktop browser page placement when SSH workspaces convert * Place the browser-service-status page on the managed host explicitly Converted SSH workspaces now keep retained desktop pages local, so the spec opens its remote-placed page on the managed environment instead of relying on the retained page moving there. * test(e2e): refocus the managed browser page before retrying its Reconnect The managed host can seed a terminal for the converted workspace and the client mirrors it with focus, hiding the browser notice mid-test. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: OrcaWin <alpha-eng@stably.ai>
This commit is contained in:
@@ -35,7 +35,10 @@ it.each(['errors', 'status', 'lifecycle', 'restart-attempt'])(
|
||||
it.each([
|
||||
'src/renderer/src/lib/ssh-workspace-browser-route-eligibility.ts',
|
||||
'src/renderer/src/components/browser-pane/use-ssh-workspace-browser-route.ts',
|
||||
'src/main/browser/local-ssh-browser-route.ts'
|
||||
'src/main/browser/local-ssh-browser-route.ts',
|
||||
'src/renderer/src/store/repos/converted-ssh-browser-pages.ts',
|
||||
'src/renderer/src/store/slices/browser/browser-tab-actions.ts',
|
||||
'src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.ts'
|
||||
])('routes the managed browser routing oracle from %s', (path) => {
|
||||
expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-browser-routing.spec.ts')
|
||||
})
|
||||
|
||||
@@ -105,7 +105,7 @@ export const PR_E2E_SOURCE_ROUTES = [
|
||||
specs: ['tests/e2e/ssh-orcad-browser-routing.spec.ts'],
|
||||
matches: (file) =>
|
||||
isProductSource(file) &&
|
||||
/^src\/(?:main\/browser\/local-ssh-browser|main\/ipc\/browser\.ts$|renderer\/src\/(?:lib\/(?:ssh-workspace-browser-route-eligibility|worktree-host-connection-phase)|components\/browser-pane\/use-ssh-workspace-browser-route))/.test(
|
||||
/^src\/(?:main\/browser\/local-ssh-browser|main\/ipc\/browser\.ts$|renderer\/src\/(?:lib\/(?:ssh-workspace-browser-route-eligibility|worktree-host-connection-phase)|components\/browser-pane\/use-ssh-workspace-browser-route|store\/repos\/converted-ssh-browser-pages|store\/slices\/browser\/browser-tab-actions|hooks\/ipc-events\/ssh-managed-server-state-effects))/.test(
|
||||
file
|
||||
)
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../../runtime/runtime-compatibility-test-fixture'
|
||||
import { clearRuntimeCompatibilityCacheForTests } from '../../runtime/runtime-rpc-client'
|
||||
import { createTestStore, makeWorktree } from '../../store/slices/store-test-helpers'
|
||||
import { buildBrowserPage } from '../../store/slices/browser-page-records'
|
||||
|
||||
const store = createTestStore()
|
||||
vi.mock('../../store', () => ({ useAppStore: store }))
|
||||
@@ -102,6 +103,28 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('a host that just moved to its managed server', () => {
|
||||
it('pins existing desktop pages before refreshing their workspace ownership', async () => {
|
||||
const page = buildBrowserPage('browser', worktreeId, 'https://private.invalid/')
|
||||
store.setState({ browserPagesByWorkspace: { browser: [page] } })
|
||||
applySshManagedServerTransition('ssh-1', undefined, {
|
||||
kind: 'managed',
|
||||
environmentId: 'env-browser'
|
||||
})
|
||||
|
||||
expect(store.getState().browserPagesByWorkspace.browser?.[0]).toEqual({
|
||||
...page,
|
||||
browserRuntimeEnvironmentId: null
|
||||
})
|
||||
await vi.waitFor(() =>
|
||||
expect(store.getState().repos.map((entry) => entry.executionHostId)).toEqual([
|
||||
'runtime:env-browser'
|
||||
])
|
||||
)
|
||||
expect(
|
||||
store.getState().browserPagesByWorkspace.browser?.[0]?.browserRuntimeEnvironmentId
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('shows the server copies only, under the server name', async () => {
|
||||
applySshManagedServerTransition('ssh-1', undefined, {
|
||||
kind: 'managed',
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/ssh/ssh-managed-server-move'
|
||||
import { useAppStore } from '../../store'
|
||||
import { withoutConvertedSshHostRows } from '../../store/repos/converted-ssh-host-rows'
|
||||
import { preserveConvertedSshBrowserPages } from '../../store/repos/converted-ssh-browser-pages'
|
||||
|
||||
type ManagedServerStatus = SshConnectionState['managedServer']
|
||||
|
||||
@@ -93,6 +94,13 @@ function startManagedServerCatalogLoad(targetId: string, environmentId: string):
|
||||
async function loadManagedServerCatalogs(targetId: string, load: CatalogLoad): Promise<void> {
|
||||
const { environmentId } = load
|
||||
const isCurrent = (): boolean => catalogLoadByTarget.get(targetId) === load
|
||||
// Pin existing desktop pages before the catalogs change their workspace owner.
|
||||
useAppStore.setState((state) => {
|
||||
const browserPagesByWorkspace = preserveConvertedSshBrowserPages(state, targetId)
|
||||
return browserPagesByWorkspace === state.browserPagesByWorkspace
|
||||
? state
|
||||
: { browserPagesByWorkspace }
|
||||
})
|
||||
const store = useAppStore.getState()
|
||||
// Why: host badges read server names from this catalog, which a conversion does not refresh.
|
||||
const environments = await window.api.runtimeEnvironments.list()
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { BrowserPage } from '../../../../shared/browser-workspace-types'
|
||||
import type { WorktreeRuntimeOwnerState } from '../../lib/worktree-runtime-owner'
|
||||
import { buildBrowserPage } from '../slices/browser-page-records'
|
||||
import { preserveConvertedSshBrowserPages } from './converted-ssh-browser-pages'
|
||||
|
||||
const owners: WorktreeRuntimeOwnerState = {
|
||||
repos: [{ id: 'repo', connectionId: 'target', executionHostId: 'ssh:target' }],
|
||||
worktreesByRepo: { repo: [{ id: 'worktree', repoId: 'repo', hostId: 'ssh:target' }] },
|
||||
folderWorkspaces: [{ id: 'folder', projectGroupId: 'group' }],
|
||||
projectGroups: [{ id: 'group', connectionId: 'target' }]
|
||||
}
|
||||
|
||||
function page(worktreeId = 'worktree'): BrowserPage {
|
||||
return buildBrowserPage('browser', worktreeId, 'https://private.invalid/', 'Private')
|
||||
}
|
||||
|
||||
describe('desktop browser placement during SSH conversion', () => {
|
||||
it('pins all inherited pages to desktop without changing their saved browser state', () => {
|
||||
const pages = [page(), page()]
|
||||
const before = { ...owners, browserPagesByWorkspace: { browser: pages } }
|
||||
const after = preserveConvertedSshBrowserPages(before, 'target')
|
||||
expect(after.browser).toEqual(
|
||||
pages.map((original) => ({ ...original, browserRuntimeEnvironmentId: null }))
|
||||
)
|
||||
expect(pages.every((original) => original.browserRuntimeEnvironmentId === undefined)).toBe(true)
|
||||
expect(
|
||||
preserveConvertedSshBrowserPages({ ...before, browserPagesByWorkspace: after }, 'target')
|
||||
).toBe(after)
|
||||
})
|
||||
|
||||
it('also preserves pages in SSH-owned folder workspaces', () => {
|
||||
const original = page('folder:folder')
|
||||
const after = preserveConvertedSshBrowserPages(
|
||||
{ ...owners, browserPagesByWorkspace: { browser: [original] } },
|
||||
'target'
|
||||
)
|
||||
expect(after.browser?.[0]?.browserRuntimeEnvironmentId).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps explicit remote placement and pages on another host unchanged', () => {
|
||||
const remote = { ...page(), browserRuntimeEnvironmentId: 'env' }
|
||||
const local = { ...page(), browserRuntimeEnvironmentId: null }
|
||||
const other = page('unknown')
|
||||
const browserPagesByWorkspace = { browser: [remote, local, other] }
|
||||
expect(preserveConvertedSshBrowserPages({ ...owners, browserPagesByWorkspace }, 'target')).toBe(
|
||||
browserPagesByWorkspace
|
||||
)
|
||||
expect(preserveConvertedSshBrowserPages({ ...owners, browserPagesByWorkspace }, 'other')).toBe(
|
||||
browserPagesByWorkspace
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves local URL history across document conversion without changing document ownership', () => {
|
||||
const doc = {
|
||||
...page(),
|
||||
docLocation: {
|
||||
kind: 'workspace-doc' as const,
|
||||
worktreeId: 'worktree',
|
||||
filePath: '/notes.md'
|
||||
},
|
||||
convertedFrom: { kind: 'url' as const, url: 'https://private.invalid/' },
|
||||
convertedTo: {
|
||||
kind: 'url' as const,
|
||||
url: 'https://remote.invalid/',
|
||||
browserRuntimeEnvironmentId: 'env'
|
||||
}
|
||||
}
|
||||
const after = preserveConvertedSshBrowserPages(
|
||||
{ ...owners, browserPagesByWorkspace: { browser: [doc] } },
|
||||
'target'
|
||||
)
|
||||
expect(after.browser?.[0]).toEqual({
|
||||
...doc,
|
||||
convertedFrom: { ...doc.convertedFrom, browserRuntimeEnvironmentId: null }
|
||||
})
|
||||
expect(after.browser?.[0]?.docLocation).toBe(doc.docLocation)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { BrowserPageConversionOrigin } from '../../../../shared/browser-workspace-types'
|
||||
import { toSshExecutionHostId } from '../../../../shared/execution-host'
|
||||
import {
|
||||
getKnownExecutionHostIdForWorktree,
|
||||
type WorktreeRuntimeOwnerState
|
||||
} from '../../lib/worktree-runtime-owner'
|
||||
import type { AppState } from '../types'
|
||||
|
||||
function preserveLocalUrlOrigin(
|
||||
origin: BrowserPageConversionOrigin | null | undefined
|
||||
): BrowserPageConversionOrigin | null | undefined {
|
||||
return origin?.kind === 'url' && origin.browserRuntimeEnvironmentId === undefined
|
||||
? { ...origin, browserRuntimeEnvironmentId: null }
|
||||
: origin
|
||||
}
|
||||
|
||||
export function preserveConvertedSshBrowserPages(
|
||||
state: WorktreeRuntimeOwnerState & Pick<AppState, 'browserPagesByWorkspace'>,
|
||||
targetId: string
|
||||
): AppState['browserPagesByWorkspace'] {
|
||||
const hostId = toSshExecutionHostId(targetId)
|
||||
let result = state.browserPagesByWorkspace
|
||||
for (const [workspaceId, pages] of Object.entries(state.browserPagesByWorkspace)) {
|
||||
let changed = false
|
||||
const next = pages.map((page) => {
|
||||
if (getKnownExecutionHostIdForWorktree(state, page.worktreeId) !== hostId) {
|
||||
return page
|
||||
}
|
||||
const localPage = !page.docLocation && page.browserRuntimeEnvironmentId === undefined
|
||||
const convertedFrom = preserveLocalUrlOrigin(page.convertedFrom)
|
||||
const convertedTo = preserveLocalUrlOrigin(page.convertedTo)
|
||||
if (!localPage && convertedFrom === page.convertedFrom && convertedTo === page.convertedTo) {
|
||||
return page
|
||||
}
|
||||
changed = true
|
||||
return {
|
||||
...page,
|
||||
...(localPage ? { browserRuntimeEnvironmentId: null } : {}),
|
||||
...(convertedFrom !== page.convertedFrom ? { convertedFrom } : {}),
|
||||
...(convertedTo !== page.convertedTo ? { convertedTo } : {})
|
||||
}
|
||||
})
|
||||
if (changed) {
|
||||
if (result === state.browserPagesByWorkspace) {
|
||||
result = { ...result }
|
||||
}
|
||||
result[workspaceId] = next
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -129,6 +129,24 @@ describe('browser tab creation placement', () => {
|
||||
expect(store.getState().unifiedTabsByWorktree['repo1::/elsewhere']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('records desktop placement for both normal and profile tabs on an SSH workspace', async () => {
|
||||
seedSplit(store)
|
||||
store.setState({
|
||||
worktreesByRepo: {
|
||||
repo1: [makeWorktree({ id: WT, repoId: 'repo1', hostId: 'ssh:target' })]
|
||||
}
|
||||
})
|
||||
await store.getState().openNewBrowserTabInActiveWorkspace(G1)
|
||||
await store.getState().openBrowserProfileTabInActiveWorkspace('https://example.com', null)
|
||||
const workspaces = store.getState().browserTabsByWorktree[WT] ?? []
|
||||
expect(workspaces).toHaveLength(2)
|
||||
for (const workspace of workspaces) {
|
||||
expect(
|
||||
store.getState().browserPagesByWorkspace[workspace.id]?.[0]?.browserRuntimeEnvironmentId
|
||||
).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('places a duplicate directly after its source', () => {
|
||||
seedSplit(store)
|
||||
const workspace = store
|
||||
|
||||
@@ -225,7 +225,7 @@ export function createBrowserTabActions(
|
||||
get().createBrowserTab(worktreeId, defaultUrl, {
|
||||
title: translate('auto.store.slices.browser.d175274b6d', 'New Browser Tab'),
|
||||
focusAddressBar: true,
|
||||
...(runtimeEnvironmentId ? { browserRuntimeEnvironmentId: null } : {}),
|
||||
browserRuntimeEnvironmentId: null,
|
||||
targetGroupId: groupId
|
||||
})
|
||||
get().recordFeatureInteraction('browser-tab-created')
|
||||
@@ -268,7 +268,7 @@ export function createBrowserTabActions(
|
||||
get().createBrowserTab(worktreeId, url, {
|
||||
activate: true,
|
||||
sessionProfileId: profileId,
|
||||
...(runtimeEnvironmentId ? { browserRuntimeEnvironmentId: null } : {})
|
||||
browserRuntimeEnvironmentId: null
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'node:path'
|
||||
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { createRestartSession } from './helpers/orca-restart'
|
||||
import { waitForSessionReady } from './helpers/store'
|
||||
import { waitForSessionReady, waitForStartupWorktreeRefresh } from './helpers/store'
|
||||
import {
|
||||
cleanupDockerSshRelayTarget,
|
||||
startDockerSshRelayTarget
|
||||
@@ -36,7 +36,7 @@ async function guestMarker(page: Page, tabId: string): Promise<unknown> {
|
||||
}, tabId)
|
||||
}
|
||||
|
||||
test('new browser tabs keep SSH routing and login cookies after managed conversion', async (// oxlint-disable-next-line no-empty-pattern -- Owns app launch.
|
||||
test('retained and new browser tabs keep SSH routing and login cookies after managed conversion', async (// oxlint-disable-next-line no-empty-pattern -- Owns app launch.
|
||||
{}, testInfo) => {
|
||||
test.setTimeout(5 * 60_000)
|
||||
const target = startDockerSshRelayTarget(testInfo)
|
||||
@@ -127,6 +127,26 @@ test('new browser tabs keep SSH routing and login cookies after managed conversi
|
||||
return tab?.id ?? null
|
||||
}, remote.worktreeId)
|
||||
expect(retained).toBe(tabId)
|
||||
await expect.poll(() => guestMarker(page, tabId), { timeout: 30_000 }).toBe(cookieMarker)
|
||||
await navigateGuest(page, tabId, `${SSH_REMOTE_ONLY_ORIGIN}/echo/retained`)
|
||||
await expect.poll(() => guestMarker(page, tabId), { timeout: 30_000 }).toBe(cookieMarker)
|
||||
expect(readSshRemoteOnlyRequests(target)).toContainEqual({
|
||||
path: '/echo/retained',
|
||||
cookie: `${SSH_REMOTE_ONLY_COOKIE_NAME}=${SSH_REMOTE_ONLY_COOKIE_VALUE}`
|
||||
})
|
||||
expect(
|
||||
await page
|
||||
.locator(`[data-browser-overlay-tab-id="${tabId}"] webview`)
|
||||
.getAttribute('partition')
|
||||
).toBe(partitionBefore)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.evaluate(() => {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
})
|
||||
await expect(page.locator('[data-slot="popover-content"]')).toHaveCount(0)
|
||||
await page.screenshot({ path: testInfo.outputPath('browser-retained-after-conversion.png') })
|
||||
await page.evaluate(
|
||||
async ({ worktreeId, url }) => {
|
||||
const state = window.__store?.getState()
|
||||
@@ -190,6 +210,24 @@ test('new browser tabs keep SSH routing and login cookies after managed conversi
|
||||
path: '/echo/reconnect',
|
||||
cookie: `${SSH_REMOTE_ONLY_COOKIE_NAME}=${SSH_REMOTE_ONLY_COOKIE_VALUE}`
|
||||
})
|
||||
await page.evaluate(
|
||||
({ worktreeId, tabId }) => {
|
||||
const state = window.__store?.getState()
|
||||
const pageId = state?.browserTabsByWorktree[worktreeId]?.find(
|
||||
(tab) => tab.id === tabId
|
||||
)?.activePageId
|
||||
if (pageId) {
|
||||
state?.focusBrowserTabInWorktree(worktreeId, pageId, { surfacePane: true })
|
||||
}
|
||||
},
|
||||
{ worktreeId: remote.worktreeId, tabId }
|
||||
)
|
||||
await navigateGuest(page, tabId, `${SSH_REMOTE_ONLY_ORIGIN}/echo/retained-reconnect`)
|
||||
await expect.poll(() => guestMarker(page, tabId), { timeout: 30_000 }).toBe(cookieMarker)
|
||||
expect(readSshRemoteOnlyRequests(target)).toContainEqual({
|
||||
path: '/echo/retained-reconnect',
|
||||
cookie: `${SSH_REMOTE_ONLY_COOKIE_NAME}=${SSH_REMOTE_ONLY_COOKIE_VALUE}`
|
||||
})
|
||||
console.log(
|
||||
'[managed-browser-route]',
|
||||
JSON.stringify({
|
||||
@@ -205,7 +243,49 @@ test('new browser tabs keep SSH routing and login cookies after managed conversi
|
||||
}
|
||||
})
|
||||
await expect(page.locator('[data-slot="popover-content"]')).toHaveCount(0)
|
||||
await page.screenshot({ path: testInfo.outputPath('browser-new-tab-after.png') })
|
||||
await page.screenshot({ path: testInfo.outputPath('browser-retained-after-reconnect.png') })
|
||||
await session.close(app)
|
||||
app = null
|
||||
const restarted = await session.launch()
|
||||
app = restarted.app
|
||||
await waitForSessionReady(restarted.page)
|
||||
await waitForStartupWorktreeRefresh(restarted.page)
|
||||
console.log(
|
||||
'[retained-browser-restored]',
|
||||
JSON.stringify(
|
||||
await restarted.page.evaluate(() => {
|
||||
const state = window.__store?.getState()
|
||||
return {
|
||||
tabs: state?.browserTabsByWorktree,
|
||||
pages: state?.browserPagesByWorkspace,
|
||||
active: state?.activeWorktreeId,
|
||||
host: state?.activeWorkspaceExecutionHostId
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
await restarted.page.evaluate(
|
||||
({ worktreeId, environmentId, tabId }) => {
|
||||
const state = window.__store?.getState()
|
||||
state?.setActiveWorktree(worktreeId, `runtime:${environmentId}`)
|
||||
const pageId = state?.browserTabsByWorktree[worktreeId]?.find(
|
||||
(tab) => tab.id === tabId
|
||||
)?.activePageId
|
||||
if (pageId) {
|
||||
state?.focusBrowserTabInWorktree(worktreeId, pageId, { surfacePane: true })
|
||||
}
|
||||
},
|
||||
{ worktreeId: remote.worktreeId, environmentId: environment.id, tabId }
|
||||
)
|
||||
await expect
|
||||
.poll(() => guestMarker(restarted.page, tabId), { timeout: 60_000 })
|
||||
.toBe(cookieMarker)
|
||||
await navigateGuest(restarted.page, tabId, `${SSH_REMOTE_ONLY_ORIGIN}/echo/retained-restart`)
|
||||
expect(readSshRemoteOnlyRequests(target)).toContainEqual({
|
||||
path: '/echo/retained-restart',
|
||||
cookie: `${SSH_REMOTE_ONLY_COOKIE_NAME}=${SSH_REMOTE_ONLY_COOKIE_VALUE}`
|
||||
})
|
||||
console.log('[retained-browser-restart]', JSON.stringify(readSshRemoteOnlyRequests(target)))
|
||||
} finally {
|
||||
if (app) {
|
||||
await session.close(app)
|
||||
|
||||
@@ -113,21 +113,34 @@ test('an unavailable browser on a responding managed host does not report a serv
|
||||
{ timeout: 60_000 }
|
||||
)
|
||||
.toMatch(/^runtime:/)
|
||||
const retained = await page.evaluate((worktreeId) => {
|
||||
const state = window.__store?.getState()
|
||||
const tab = state?.browserTabsByWorktree[worktreeId]?.[0]
|
||||
const repo = state?.repos.find((repo) =>
|
||||
(state.worktreesByRepo[repo.id] ?? []).some((worktree) => worktree.id === worktreeId)
|
||||
)
|
||||
if (repo?.executionHostId) {
|
||||
state?.setActiveWorktree(worktreeId, repo.executionHostId)
|
||||
// Retained desktop pages stay local after conversion; place this one on the managed host.
|
||||
const remoteTabId = await page.evaluate(
|
||||
({ worktreeId, environmentId, url }) => {
|
||||
const state = window.__store?.getState()
|
||||
const repo = state?.repos.find((repo) =>
|
||||
(state.worktreesByRepo[repo.id] ?? []).some((worktree) => worktree.id === worktreeId)
|
||||
)
|
||||
if (!state || !repo?.executionHostId) {
|
||||
throw new Error('Missing converted workspace')
|
||||
}
|
||||
state.setActiveWorktree(worktreeId, repo.executionHostId)
|
||||
const tab = state.createBrowserTab(worktreeId, url, {
|
||||
title: 'Managed host browser',
|
||||
activate: true,
|
||||
browserRuntimeEnvironmentId: environmentId
|
||||
})
|
||||
if (tab.activePageId) {
|
||||
state.focusBrowserTabInWorktree(worktreeId, tab.activePageId, { surfacePane: true })
|
||||
}
|
||||
return tab.id
|
||||
},
|
||||
{
|
||||
worktreeId: remote.worktreeId,
|
||||
environmentId: environment.id,
|
||||
url: `${SSH_REMOTE_ONLY_ORIGIN}/login`
|
||||
}
|
||||
if (tab?.activePageId) {
|
||||
state?.focusBrowserTabInWorktree(worktreeId, tab.activePageId, { surfacePane: true })
|
||||
}
|
||||
return tab?.id ?? null
|
||||
}, remote.worktreeId)
|
||||
expect(retained).toBe(tabId)
|
||||
)
|
||||
expect(remoteTabId).not.toBe(tabId)
|
||||
const notice = page.getByTestId('remote-browser-stream-error')
|
||||
await expect(notice).toBeVisible({ timeout: 30_000 })
|
||||
await page.screenshot({ path: testInfo.outputPath('browser-service-status.png') })
|
||||
@@ -153,10 +166,26 @@ test('an unavailable browser on a responding managed host does not report a serv
|
||||
'The remote browser is unavailable. Check its setup on the server.'
|
||||
)
|
||||
await expect(notice).not.toContainText('Cannot reach the remote server.')
|
||||
await notice.getByRole('button', { name: 'Reconnect', exact: true }).click()
|
||||
await expect(notice).toContainText(
|
||||
'The remote browser is unavailable. Check its setup on the server.'
|
||||
)
|
||||
// Why: the managed host can mirror a terminal it seeds for this workspace, taking focus.
|
||||
await expect(async () => {
|
||||
await page.evaluate(
|
||||
({ worktreeId, tabId }) => {
|
||||
const state = window.__store?.getState()
|
||||
const pageId = state?.browserTabsByWorktree[worktreeId]?.find(
|
||||
(tab) => tab.id === tabId
|
||||
)?.activePageId
|
||||
if (pageId) {
|
||||
state?.focusBrowserTabInWorktree(worktreeId, pageId, { surfacePane: true })
|
||||
}
|
||||
},
|
||||
{ worktreeId: remote.worktreeId, tabId: remoteTabId }
|
||||
)
|
||||
await notice.getByRole('button', { name: 'Reconnect', exact: true }).click({ timeout: 2_000 })
|
||||
await expect(notice).toContainText(
|
||||
'The remote browser is unavailable. Check its setup on the server.',
|
||||
{ timeout: 2_000 }
|
||||
)
|
||||
}).toPass({ timeout: 30_000 })
|
||||
console.log('[browser-service-after-retry]', await notice.innerText())
|
||||
expect(JSON.parse(await serverCall(page, environment.id, 'repo.list'))).toMatchObject({
|
||||
ok: true,
|
||||
|
||||
Reference in New Issue
Block a user