Skip to content

Commit 730235b

Browse files
committed
fix(app): retry failed session metadata without reloading
1 parent 7d179e8 commit 730235b

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
@@ -11,9 +11,9 @@ import {
1111
session,
1212
sessionHref,
1313
} from "../utils/app"
14-
import { mockServers } from "../utils/mock-server"
14+
import { currentSession, mockServers } from "../utils/mock-server"
1515
import { fixture, mockStressTimeline } from "../utils/session-fixture"
16-
import { fileNode, mockWorkspace, openSession } from "../utils/workspace"
16+
import { fileNode, mockRemoteServer, mockWorkspace, openSession } from "../utils/workspace"
1717
import { expectSessionTitle } from "../utils/waits"
1818

1919
const a = { id: "ses_tab_a", title: "Tab A session" }
@@ -24,6 +24,72 @@ const c = { id: "ses_tab_c", title: "Tab C session" }
2424

2525
test.use({ serviceWorkers: "block" })
2626

27+
for (const server of [SERVER, REMOTE_SERVER]) {
28+
test(`session metadata failure does not follow navigation to another tab on ${server}`, async ({ page }) => {
29+
await mockWorkspace(page, {
30+
name: "Tabs",
31+
sessions: [b],
32+
seed: { tabs: [a.id, { session: b.id, server }] },
33+
})
34+
if (server === REMOTE_SERVER) {
35+
await mockRemoteServer(page, { sessions: [session({ ...b, directory: "/remote/project" })] })
36+
}
37+
await page.route(
38+
(url) => url.origin === SERVER && url.pathname === `/api/session/${a.id}`,
39+
(route) => route.fulfill({ status: 504, json: { message: "Fixture metadata timeout" } }),
40+
)
41+
await page.goto(sessionHref(a.id))
42+
await expect(page.getByRole("heading", { name: "Server request failed" })).toBeVisible()
43+
const document = await page.locator("html").elementHandle()
44+
45+
await page.locator(`a[data-titlebar-tab-link][href="${sessionHref(b.id, server)}"]`).click()
46+
47+
await expectPath(page, sessionHref(b.id, server))
48+
await expectSessionTitle(page, b.title)
49+
await expect(page.locator('[data-component="composer-editor"]')).toBeEditable()
50+
await expect(page.getByRole("heading", { name: "Server request failed" })).toHaveCount(0)
51+
expect(await document?.evaluate((element) => element.isConnected)).toBe(true)
52+
})
53+
}
54+
55+
test("session metadata failure can be retried without reloading or looping", async ({ page }, testInfo) => {
56+
await page.setViewportSize({ width: 1440, height: 900 })
57+
const workspace = await mockWorkspace(page, { name: "Tabs", sessions: [b], seed: { tabs: [a.id, b.id] } })
58+
const state = { fail: true, requests: 0 }
59+
await page.route(
60+
(url) => url.origin === SERVER && url.pathname === `/api/session/${a.id}`,
61+
(route) => {
62+
state.requests++
63+
return route.fulfill(
64+
state.fail
65+
? { status: 504, json: { message: "Fixture metadata timeout" } }
66+
: { json: { data: currentSession(session({ ...a, directory: workspace.directory })) } },
67+
)
68+
},
69+
)
70+
await page.goto(sessionHref(a.id))
71+
const retry = page.getByRole("button", { name: "Try again", exact: true })
72+
await expect(retry).toBeVisible()
73+
const document = await page.locator("html").elementHandle()
74+
const before = state.requests
75+
await retry.press("Enter")
76+
await expect(retry).toBeVisible()
77+
await expect.poll(() => state.requests).toBe(before + 1)
78+
await expect(retry).toBeInViewport()
79+
await page.screenshot({ path: testInfo.outputPath("retry-desktop.png") })
80+
expect(state.requests).toBe(before + 1)
81+
await page.setViewportSize({ width: 390, height: 720 })
82+
await expect(retry).toBeInViewport()
83+
await page.screenshot({ path: testInfo.outputPath("retry-narrow.png") })
84+
85+
state.fail = false
86+
await retry.click()
87+
await expectSessionTitle(page, a.title)
88+
await expect(page.locator('[data-component="composer-editor"]')).toBeEditable()
89+
await expect(retry).toHaveCount(0)
90+
expect(await document?.evaluate((element) => element.isConnected)).toBe(true)
91+
})
92+
2793
test("tab strip keeps draft tabs as wide as session tabs and navigates on mouse down", async ({ page }) => {
2894
const workspace = await mockWorkspace(page, {
2995
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 { IncompatibleServerPanel } from "./incompatible-server-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()
@@ -32,7 +37,7 @@ export function SessionErrorFallback(props: { error: unknown; sessionID?: string
3237
>
3338
<Show
3439
when={isCurrentSessionNotFoundError(props.error, props.sessionID)}
35-
fallback={<ErrorPage error={props.error} />}
40+
fallback={<ErrorPage error={props.error} onRetry={props.onRetry} />}
3641
>
3742
<div class="flex-1 min-h-0 overflow-hidden">
3843
<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
@@ -101,9 +101,9 @@ function TargetSessionSettingsCommand() {
101101
function SessionRouteErrorBoundary(props: ParentProps<{ sessionID?: string; serverKey?: ServerConnection.Key }>) {
102102
return (
103103
<ErrorBoundary
104-
fallback={(error) => (
104+
fallback={(error, reset) => (
105105
<SessionStatePanel>
106-
<SessionErrorFallback error={error} sessionID={props.sessionID} serverKey={props.serverKey} />
106+
<SessionErrorFallback error={error} sessionID={props.sessionID} serverKey={props.serverKey} onRetry={reset} />
107107
</SessionStatePanel>
108108
)}
109109
>

‎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)