Skip to content

Commit 7304611

Browse files
committed
fix(app): retry failed session metadata without reloading
1 parent ed74ec8 commit 7304611

4 files changed

Lines changed: 92 additions & 9 deletions

File tree

‎packages/app/e2e/regression/tabs.spec.ts‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import {
1414
sessionHref,
1515
} from "../utils/app"
1616
import { openFileLauncher, reviewTabs } from "../utils/side-panel"
17-
import { mockServers } from "../utils/mock-server"
17+
import { currentSession, mockServers } from "../utils/mock-server"
1818
import { fixture, mockStressTimeline } from "../utils/session-fixture"
19-
import { fileNode, mockWorkspace, openSession } from "../utils/workspace"
19+
import { fileNode, mockRemoteServer, mockWorkspace, openSession } from "../utils/workspace"
2020
import { expectSessionTitle } from "../utils/waits"
2121

2222
const a = { id: "ses_tab_a", title: "Tab A session" }
@@ -128,6 +128,72 @@ test("recently closed fills remaining slots in Home order without open or duplic
128128
await expect(items).toHaveText(["OOther session 9"])
129129
})
130130

131+
for (const server of [SERVER, REMOTE_SERVER]) {
132+
test(`session metadata failure does not follow navigation to another tab on ${server}`, async ({ page }) => {
133+
await mockWorkspace(page, {
134+
name: "Tabs",
135+
sessions: [b],
136+
seed: { tabs: [a.id, { session: b.id, server }] },
137+
})
138+
if (server === REMOTE_SERVER) {
139+
await mockRemoteServer(page, { sessions: [session({ ...b, directory: "/remote/project" })] })
140+
}
141+
await page.route(
142+
(url) => url.origin === SERVER && url.pathname === `/api/session/${a.id}`,
143+
(route) => route.fulfill({ status: 504, json: { message: "Fixture metadata timeout" } }),
144+
)
145+
await page.goto(sessionHref(a.id))
146+
await expect(page.getByRole("heading", { name: "Server request failed" })).toBeVisible()
147+
const document = await page.locator("html").elementHandle()
148+
149+
await page.locator(`a[data-titlebar-tab-link][href="${sessionHref(b.id, server)}"]`).click()
150+
151+
await expectPath(page, sessionHref(b.id, server))
152+
await expectSessionTitle(page, b.title)
153+
await expect(page.locator('[data-component="composer-editor"]')).toBeEditable()
154+
await expect(page.getByRole("heading", { name: "Server request failed" })).toHaveCount(0)
155+
expect(await document?.evaluate((element) => element.isConnected)).toBe(true)
156+
})
157+
}
158+
159+
test("session metadata failure can be retried without reloading or looping", async ({ page }, testInfo) => {
160+
await page.setViewportSize({ width: 1440, height: 900 })
161+
const workspace = await mockWorkspace(page, { name: "Tabs", sessions: [b], seed: { tabs: [a.id, b.id] } })
162+
const state = { fail: true, requests: 0 }
163+
await page.route(
164+
(url) => url.origin === SERVER && url.pathname === `/api/session/${a.id}`,
165+
(route) => {
166+
state.requests++
167+
return route.fulfill(
168+
state.fail
169+
? { status: 504, json: { message: "Fixture metadata timeout" } }
170+
: { json: { data: currentSession(session({ ...a, directory: workspace.directory })) } },
171+
)
172+
},
173+
)
174+
await page.goto(sessionHref(a.id))
175+
const retry = page.getByRole("button", { name: "Try again", exact: true })
176+
await expect(retry).toBeVisible()
177+
const document = await page.locator("html").elementHandle()
178+
const before = state.requests
179+
await retry.press("Enter")
180+
await expect(retry).toBeVisible()
181+
await expect.poll(() => state.requests).toBe(before + 1)
182+
await expect(retry).toBeInViewport()
183+
await page.screenshot({ path: testInfo.outputPath("retry-desktop.png") })
184+
expect(state.requests).toBe(before + 1)
185+
await page.setViewportSize({ width: 390, height: 720 })
186+
await expect(retry).toBeInViewport()
187+
await page.screenshot({ path: testInfo.outputPath("retry-narrow.png") })
188+
189+
state.fail = false
190+
await retry.click()
191+
await expectSessionTitle(page, a.title)
192+
await expect(page.locator('[data-component="composer-editor"]')).toBeEditable()
193+
await expect(retry).toHaveCount(0)
194+
expect(await document?.evaluate((element) => element.isConnected)).toBe(true)
195+
})
196+
131197
test("tab strip keeps draft tabs as wide as session tabs and navigates on mouse down", async ({ page }) => {
132198
const workspace = await mockWorkspace(page, {
133199
name: "Tabs",

‎packages/app/src/session/route-error.tsx‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { useTabs } from "@/shell/tabs/tabs"
88
import { isLocalSessionNotFoundError, isSessionNotFoundError } from "@/runtime/server/errors"
99
import { ServerUnavailablePanel } from "./server-unavailable-panel"
1010

11-
export function SessionErrorFallback(props: { error: unknown; sessionID?: string; serverKey?: ServerConnection.Key }) {
11+
export function SessionErrorFallback(props: {
12+
error: unknown
13+
sessionID?: string
14+
serverKey?: ServerConnection.Key
15+
onRetry?: () => void
16+
}) {
1217
const language = useLanguage()
1318
const activeServer = useServer()
1419
const server = useServers()
@@ -36,7 +41,7 @@ export function SessionErrorFallback(props: { error: unknown; sessionID?: string
3641
(isSessionNotFoundError(props.error, props.sessionID) ||
3742
isLocalSessionNotFoundError(props.error, props.sessionID))
3843
}
39-
fallback={<ErrorPage error={props.error} />}
44+
fallback={<ErrorPage error={props.error} onRetry={props.onRetry} />}
4045
>
4146
<div class="flex-1 min-h-0 overflow-hidden">
4247
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-4">

‎packages/app/src/session/route.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ function TargetSessionSettingsCommand() {
109109
function SessionRouteErrorBoundary(props: ParentProps<{ sessionID?: string; serverKey?: ServerConnection.Key }>) {
110110
return (
111111
<ErrorBoundary
112-
fallback={(error) => (
112+
fallback={(error, reset) => (
113113
<SessionStatePanel>
114-
<SessionErrorFallback error={error} sessionID={props.sessionID} serverKey={props.serverKey} />
114+
<SessionErrorFallback error={error} sessionID={props.sessionID} serverKey={props.serverKey} onRetry={reset} />
115115
</SessionStatePanel>
116116
)}
117117
>

‎packages/app/src/shell/errors/error.tsx‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ function formatError(cause: unknown, t: Translator): string {
250250

251251
interface ErrorPageProps {
252252
error: unknown
253+
onRetry?: () => void
253254
}
254255

255256
export const ErrorPage: Component<ErrorPageProps> = (props) => {
@@ -354,9 +355,20 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
354355
hideLabel
355356
/>
356357
<div class="flex flex-row items-center justify-center gap-3 flex-wrap max-w-64">
357-
<Button size="large" onClick={platform.restart}>
358-
{language.t(platform.platform === "web" ? "error.page.action.reload" : "error.page.action.restart")}
359-
</Button>
358+
<Show
359+
when={props.onRetry}
360+
fallback={
361+
<Button size="large" onClick={platform.restart}>
362+
{language.t(platform.platform === "web" ? "error.page.action.reload" : "error.page.action.restart")}
363+
</Button>
364+
}
365+
>
366+
{(retry) => (
367+
<Button size="large" onClick={() => retry()()}>
368+
{language.t("common.retry")}
369+
</Button>
370+
)}
371+
</Show>
360372
<Show when={platform.platform === "desktop" && platform.exportDebugLogs}>
361373
<Button size="large" variant="ghost" onClick={exportDebugLogs}>
362374
{language.t("error.page.action.exportLogs")}

0 commit comments

Comments
 (0)