From d79669b6c9c11a247cbe8cb55df8692a91e71ade Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:51:50 -0700 Subject: [PATCH] fix(renderer): clear the stalled-restart notice on a resetKey reset and pin the boundary's relaunch-timer teardown A resetKey reset cleared the error but kept relaunchStalled, so a later unrelated error's fallback showed "Restarting didn't complete. Try again" with no Restart button to try. Also pins the settle-timer unmount teardown (clear on unmount, no scheduling into a torn-down instance), which no test covered. --- ...eRenderErrorBoundary.restart-orca.test.tsx | 72 +++++++++++++++++++ .../RecoverableRenderErrorBoundary.tsx | 4 +- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.restart-orca.test.tsx b/src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.restart-orca.test.tsx index 2855822e788..da7c6c6c89f 100644 --- a/src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.restart-orca.test.tsx +++ b/src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.restart-orca.test.tsx @@ -172,6 +172,78 @@ describe('RecoverableRenderErrorBoundary Restart Orca button', () => { expect(container!.textContent).not.toContain("Restarting didn't complete.") }) + it('does not leak the stalled-restart notice into a later error after a resetKey reset', async () => { + vi.useFakeTimers() + const relaunch = vi.fn<() => Promise>().mockResolvedValue(undefined) + ;(window as unknown as { api: unknown }).api = { app: { relaunch } } + + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + const renderWith = (resetKey: string, error: Error): void => { + act(() => { + root?.render( + + + + ) + }) + } + + renderWith('doc-a', new LazyChunkLoadError(new SyntaxError("Unexpected token '}'"))) + act(() => findRestartButton(container!)?.click()) + await act(async () => { + await vi.advanceTimersByTimeAsync(RELAUNCH_SETTLE_GRACE_MS + 1) + }) + expect(container!.textContent).toContain("Restarting didn't complete.") + + // A reset abandons that fallback; the next, unrelated error must not carry + // a stalled-restart notice whose "Try again" has no Restart button. + renderWith('doc-b', new Error('ordinary render failure')) + expect(container!.querySelector('[role="alert"]')).not.toBeNull() + expect(findRestartButton(container!)).toBeNull() + expect(container!.textContent).not.toContain("Restarting didn't complete.") + }) + + it('drops the settle timer when the boundary unmounts around the relaunch', async () => { + vi.useFakeTimers() + let resolveRelaunch: () => void = () => undefined + const relaunch = vi.fn<() => Promise>().mockReturnValue( + new Promise((resolve) => { + resolveRelaunch = resolve + }) + ) + ;(window as unknown as { api: unknown }).api = { app: { relaunch } } + + // Unmount while the invoke is pending: the resolve must not schedule a timer + // into a torn-down instance. + renderBoundaryWith(new LazyChunkLoadError(new SyntaxError("Unexpected token '}'"))) + act(() => findRestartButton(container!)?.click()) + act(() => root?.unmount()) + root = null + await act(async () => { + resolveRelaunch() + await Promise.resolve() + }) + expect(vi.getTimerCount()).toBe(0) + + // Unmount after the invoke resolved: the already-armed timer must be cleared. + container?.remove() + renderBoundaryWith(new LazyChunkLoadError(new SyntaxError("Unexpected token '}'"))) + act(() => findRestartButton(container!)?.click()) + await act(async () => { + await Promise.resolve() + }) + expect(vi.getTimerCount()).toBe(1) + act(() => root?.unmount()) + root = null + expect(vi.getTimerCount()).toBe(0) + }) + it('re-enables the button when the pre-relaunch checkpoint refuses', async () => { let rejectRelaunch: (error: Error) => void = () => undefined const relaunch = vi.fn<() => Promise>().mockReturnValue( diff --git a/src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.tsx b/src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.tsx index 0e8f7e81ef5..23191b522e6 100644 --- a/src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.tsx +++ b/src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.tsx @@ -54,7 +54,9 @@ export class RecoverableRenderErrorBoundary extends React.Component | null { if (props.resetKey !== state.resetKey) { - return { error: null, resetKey: props.resetKey } + // A reset abandons the old fallback, so a later error must not inherit its + // stalled-restart notice ("Try again" would name a button that isn't there). + return { error: null, resetKey: props.resetKey, relaunchStalled: false } } return null }