PT-4179: fix Storybook websocket-timeout crash for renderer stories - #2605
Conversation
Renderer app components (the startup-wizard shell/steps, plus dialogs and
overlays) import `useLocalizedStrings` from `@renderer/hooks/papi-hooks` and
otherwise reach the network service. Storybook has no PAPI backend, so the
renderer `RpcClient`'s connection attempt to `ws://localhost:<port>` never
settles and its `AsyncVariable('websocket connected')` rejects unhandled
after ~10s ("Timeout reached when waiting for websocket connected to
settle"), which the dev overlay surfaces as a crash on every startup-wizard
story. Storybook previously only stubbed `@papi/*` imports, never the
`@renderer`/`@shared` paths these renderer components actually use.
Add two Storybook-only module replacements:
- `papi-stubs/renderer-papi-hooks.ts`: re-exports the real hooks but overrides
`useLocalizedStrings` to resolve real English strings synchronously (no
connection).
- `papi-stubs/rpc-handler.factory.ts`: inert RPC handler so
`networkService.initialize()` succeeds with no socket and no timer, so no
connection is ever attempted and nothing rejects.
Both are wired via `NormalModuleReplacementPlugin`, not `resolve.alias`: the
base renderer webpack config resolves `@renderer`/`@shared` via
`TsconfigPathsPlugin`, which wins over `resolve.alias` (that is why the
existing `@papi/*` aliases work but a `@renderer/...` alias would be ignored).
Verified with `npm run storybook:build` and by rendering the first-run stories
(real English strings, zero unhandled rejections).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sonner's auto-dismiss timer fires after jsdom tears down, causing a "window is not defined" unhandled error that fails the test run even though all 1191 tests pass. Add an afterEach that calls toast.dismiss() (wrapped in act) to cancel all pending Sonner timers before the test environment is torn down. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jolierabideau
left a comment
There was a problem hiding this comment.
Looks good to me, other than the lint failures!
@jolierabideau reviewed 4 files and all commit messages, and made 2 comments.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on katherinejensen00).
.storybook/papi-stubs/renderer-papi-hooks.ts line 2 at r2 (raw file):
/** * Storybook stub for the renderer PAPI-hooks barrel (`@renderer/hooks/papi-hooks`).
NIT From Claude
renderer-papi-hooks.ts re-exports the 11 non-localization hooks from src/renderer/hooks/papi-hooks/index.ts by hand. I verified they match exactly today. But since the $-anchored replacement redirects the whole barrel to this stub, a hook added to the real barrel later would be missing here, and any story importing it would fail to resolve. It'd surface as a Storybook build error (not a runtime bug), so it's low-severity — but a one-line comment on the real barrel pointing at this stub as the sync target (or a type-level assertion that the stub covers the barrel) would make the coupling discoverable.
…nused-vars) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jolierabideau
left a comment
There was a problem hiding this comment.
@jolierabideau reviewed 2 files and all commit messages, made 1 comment, and resolved 1 discussion.
Reviewable status:complete! all files reviewed, all discussions resolved.
Summary
Renderer app components (the startup-wizard shell/steps, plus dialogs and overlays) import
useLocalizedStringsfrom@renderer/hooks/papi-hooksand otherwise reach the network service. Storybook has no PAPI backend, so the rendererRpcClient's connection attempt tows://localhost:<port>never settles and itsAsyncVariable('websocket connected')rejects unhandled after ~10s —Timeout reached when waiting for websocket connected to settle— which the dev overlay surfaces as a crash on every startup-wizard story. Storybook previously stubbed only@papi/*imports, never the@renderer/@sharedpaths these renderer components actually use.This is split out of the PT-4179 sync-progress work as a standalone fix because it is shared Storybook infra, not step-specific: it also unblocks the sibling wizard PRs (PT-4176 language, PT-4177 registration, PT-4178 sync-consent), which all render
FirstRunShelland hit the same crash. Landing it onmainlets those PRs inherit it on rebase.Why review this
Supports the startup-wizard epic: the wizard step stories currently crash in Storybook, blocking visual review of PT-4176/4177/4178/4179. This restores them (and other renderer-component stories) to a working, backend-free state.
Changes
.storybook/papi-stubs/renderer-papi-hooks.ts(new) — re-exports the real renderer hooks but overrides onlyuseLocalizedStringsto resolve real English strings synchronously (no network)..storybook/papi-stubs/rpc-handler.factory.ts(new) — inert RPC handler sonetworkService.initialize()succeeds with no socket and no timer; no connection is ever attempted, so nothing rejects..storybook/main.ts— wires both viaNormalModuleReplacementPlugin.src/renderer/components/notification-display.test.tsx— addafterEach(() => { act(() => { toast.dismiss(); }); })to cancel Sonner's pending auto-dismiss timers before jsdom tears down. Without this, a 4 s Sonner timer fires after the test environment is torn down, causingwindow is not definedto surface as an unhandled Vitest error that exits the whole test run with code 1 (all 1191 tests pass, but the process fails). This is a pre-existing race condition introduced in the PR that added Sonner (PT-4193/PT-4193: Add secondary notification action, position, and dismissible #2561); fixing it here unblocks CI on this PR.Both Storybook stub files use
NormalModuleReplacementPlugin, notresolve.alias: the base renderer webpack config resolves@renderer/@sharedviaTsconfigPathsPlugin, which wins overresolve.alias(that is why the existing@papi/*aliases work, but a@renderer/...alias would be silently ignored). The plugin rewrites the request inbeforeResolve, before TsconfigPaths runs.AI Involvement
AI-assisted. Claude diagnosed the root cause (traced the unhandled rejection from
useLocalizedStrings→dataProviderService.get→networkService.initialize→RpcClient), wrote the two stubs and themain.tswiring, and verified the result. Claude also diagnosed the CI test failure (Sonner timer leak innotification-display.test.tsx) and wrote theafterEachfix. The author reviewed the diagnosis, the stub approach, and the diffs.Testing
npm run storybook:buildpasses (the@papi-free hard gate) with both stubs.SyncProgressStep(Default, All Done) andFirstRunShell(Language) against the production build: real English strings render and zero unhandled rejections (was:Timeout reached when waiting for websocket connected to settle).notification-display.test.tsxlocally: timer leak no longer surfaces as an unhandled error.Risk Level
Low — Storybook config only (no renderer production code changed);
NormalModuleReplacementPluginrewrites only the two targeted module paths. The notification-display test fix is additive (addsafterEachcleanup only). If either stub is wrong,storybook:buildfails cleanly rather than silently.This change is