fix(runtime): keep offline retry toast visible

This commit is contained in:
Brennan Benson
2026-07-31 15:21:34 -07:00
parent 622eaafe36
commit 731d30a8b0
2 changed files with 85 additions and 34 deletions
@@ -78,6 +78,7 @@ beforeEach(() => {
})
afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
})
@@ -207,7 +208,16 @@ describe('runtime-status slice', () => {
expect(toast.dismiss).toHaveBeenCalledWith('runtime-environment-disconnected:env-a')
})
it('retries from the toast and re-surfaces it when the server is still offline', async () => {
it('keeps the keyed action toast visible when an offline retry settles', async () => {
vi.useFakeTimers()
const toastId = 'runtime-environment-disconnected:env-a'
const visibleToastIds = new Set<string | number>()
vi.mocked(toast.warning).mockImplementation((_title, options) => {
if (options?.id !== undefined) {
visibleToastIds.add(options.id)
}
return options?.id ?? ''
})
const getStatus = vi.fn().mockRejectedValue(new Error('closed'))
stubRuntimeEnvironmentApi({ getStatus })
const store = createSliceStore()
@@ -215,18 +225,39 @@ describe('runtime-status slice', () => {
store.getState().setRuntimeEnvironmentStatus('env-a', { status: makeStatus(), checkedAt: 1 })
store.getState().setRuntimeEnvironmentStatus('env-a', { status: null, checkedAt: 2 })
const options = vi.mocked(toast.warning).mock.calls[0]?.[1] as unknown as {
action: { onClick: () => void }
action: { onClick: (event: { preventDefault: () => void }) => void }
}
const clickAction = (): { defaultPrevented: boolean } => {
const event = {
defaultPrevented: false,
preventDefault() {
this.defaultPrevented = true
}
}
options.action.onClick(event)
if (!event.defaultPrevented) {
setTimeout(() => visibleToastIds.delete(toastId), 200)
}
return event
}
options.action.onClick()
const firstClick = clickAction()
const secondClick = clickAction()
await vi.advanceTimersByTimeAsync(0)
await vi.waitFor(() => {
expect(getStatus).toHaveBeenCalledWith({ selector: 'env-a', timeoutMs: 10_000 })
expect(toast.warning).toHaveBeenCalledTimes(2)
})
expect(vi.mocked(toast.warning).mock.calls[1]?.[1]?.id).toBe(
'runtime-environment-disconnected:env-a'
)
expect(firstClick.defaultPrevented).toBe(true)
expect(secondClick.defaultPrevented).toBe(true)
expect(getStatus).toHaveBeenCalledWith({ selector: 'env-a', timeoutMs: 10_000 })
await vi.advanceTimersByTimeAsync(200)
expect(visibleToastIds.has(toastId)).toBe(true)
expect(getStatus).toHaveBeenCalledTimes(1)
expect(toast.warning).toHaveBeenCalledTimes(3)
expect(vi.mocked(toast.warning).mock.calls.map((call) => call[1]?.duration)).toEqual([
4_000,
Number.POSITIVE_INFINITY,
4_000
])
expect(vi.mocked(toast.warning).mock.calls.every((call) => call[1]?.id === toastId)).toBe(true)
})
it('does not report removal or explicit status clearing as a disconnect', () => {
+44 -24
View File
@@ -64,6 +64,7 @@ export type RuntimeStatusSlice = {
const connectionGenerationByEnvironment = new Map<string, number>()
const activeRuntimeDisconnectedToasts = new Map<string, symbol>()
const RUNTIME_DISCONNECTED_TOAST_DURATION_MS = 4_000
function getRuntimeDisconnectedToastId(environmentId: string): string {
return `runtime-environment-disconnected:${environmentId}`
@@ -89,30 +90,49 @@ function showRuntimeDisconnectedToast(environmentId: string, getState: () => App
activeRuntimeDisconnectedToasts.delete(toastId)
}
}
toast.warning(title, {
id: toastId,
description: translate(
'auto.store.slices.runtime.status.runtimeHostDisconnectedDescription',
'Workspaces and terminals on this server are unavailable.'
),
action: {
label: translate('browser.loadFailure.retry', 'Retry'),
onClick: () => {
void getState()
.refreshRuntimeEnvironmentStatus(environmentId)
.then((reachable) => {
const stillSaved = getState().runtimeEnvironments.some(
(entry) => entry.id === environmentId
)
if (!reachable && stillSaved) {
showRuntimeDisconnectedToast(environmentId, getState)
}
})
}
},
onDismiss: clearActiveToast,
onAutoClose: clearActiveToast
})
let retrying = false
const showToast = (duration = RUNTIME_DISCONNECTED_TOAST_DURATION_MS): void => {
toast.warning(title, {
id: toastId,
description: translate(
'auto.store.slices.runtime.status.runtimeHostDisconnectedDescription',
'Workspaces and terminals on this server are unavailable.'
),
duration,
action: {
label: translate('browser.loadFailure.retry', 'Retry'),
onClick: (event) => {
// Why: Sonner otherwise deletes the keyed toast after the action callback.
event.preventDefault()
if (retrying) {
return
}
retrying = true
showToast(Number.POSITIVE_INFINITY)
void getState()
.refreshRuntimeEnvironmentStatus(environmentId)
.then((reachable) => {
const stillSaved = getState().runtimeEnvironments.some(
(entry) => entry.id === environmentId
)
if (
!reachable &&
stillSaved &&
activeRuntimeDisconnectedToasts.get(toastId) === activation
) {
showToast()
}
})
.finally(() => {
retrying = false
})
}
},
onDismiss: clearActiveToast,
onAutoClose: clearActiveToast
})
}
showToast()
}
function dismissRuntimeDisconnectedToast(environmentId: string): void {