Skip to content

Commit 99d17b7

Browse files
authored
🤖 tests: stabilize Storybook left sidebar state (#2937)
## Summary Stabilize Storybook app stories that were flaking because the left sidebar could inherit the previous story's persisted open/closed state. ## Background Stories such as "All Reviews Checked" could render with the wrong left-sidebar visibility when Storybook reused localStorage and an existing AppLoader instance across captures. That made visual snapshots depend on whichever story ran before them. ## Implementation - reset `LEFT_SIDEBAR_COLLAPSED_KEY` before each Storybook render using the same mobile/desktop default that the app uses - key `AppWithMocks` by story id plus a viewport bucket so crossing the mobile breakpoint forces a fresh `AppLoader` mount - keep per-story overrides intact so stories that intentionally open or collapse the left sidebar still control their own state ## Validation - `make static-check` - `make storybook-build` ## Risks Low risk. The change is isolated to Storybook wiring and only affects how persisted sidebar state is seeded for stories. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$2.50`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=2.50 -->
1 parent 640a087 commit 99d17b7

File tree

2 files changed

+26
-10
lines changed

2 files changed

+26
-10
lines changed

.storybook/preview.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "../src/browser/styles/globals.css";
44
import {
55
TUTORIAL_STATE_KEY,
66
RIGHT_SIDEBAR_COLLAPSED_KEY,
7+
LEFT_SIDEBAR_COLLAPSED_KEY,
78
EXPANDED_PROJECTS_KEY,
89
WORKSPACE_DRAFTS_BY_PROJECT_KEY,
910
type TutorialState,
@@ -68,6 +69,14 @@ function collapseRightSidebar() {
6869
localStorage.setItem(RIGHT_SIDEBAR_COLLAPSED_KEY, JSON.stringify(true));
6970
}
7071
}
72+
// Reset the left sidebar to the app's viewport default before each story render.
73+
// This prevents stories from inheriting whichever open/closed state a previous
74+
// story left behind, while still allowing individual stories to override it.
75+
function resetLeftSidebar() {
76+
if (typeof localStorage !== "undefined") {
77+
localStorage.setItem(LEFT_SIDEBAR_COLLAPSED_KEY, JSON.stringify(window.innerWidth <= 768));
78+
}
79+
}
7180
// Collapse projects by default to ensure deterministic snapshots.
7281
// Some stories explicitly expand projects via expandProjects() in their setup.
7382
function collapseProjects() {
@@ -128,6 +137,10 @@ const preview: Preview = {
128137
disableTutorials();
129138
}
130139

140+
// Reset the left sidebar to the app's viewport-dependent default.
141+
// Stories that need a specific open/closed state can override it in setup.
142+
resetLeftSidebar();
143+
131144
// Collapse right sidebar by default for deterministic snapshots
132145
// Stories can expand via expandRightSidebar() in setup after this runs
133146
collapseRightSidebar();

src/browser/stories/meta.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,31 +76,34 @@ function resetStorybookPersistedStateForStory(): void {
7676
localStorage.setItem(UI_THEME_KEY, JSON.stringify("dark"));
7777
}
7878
}
79-
function getStorybookStoryId(): string | null {
79+
function getStorybookRenderKey(): string | null {
8080
if (typeof window === "undefined") {
8181
return null;
8282
}
8383

8484
const params = new URLSearchParams(window.location.search);
85-
return params.get("id") ?? params.get("path");
85+
const storyId = params.get("id") ?? params.get("path");
86+
const viewportBucket = window.innerWidth <= 768 ? "narrow" : "wide";
87+
return storyId ? `${storyId}:${viewportBucket}` : viewportBucket;
8688
}
8789

8890
export const AppWithMocks: FC<AppWithMocksProps> = ({ setup }) => {
89-
const lastStoryIdRef = useRef<string | null>(null);
91+
const lastRenderKeyRef = useRef<string | null>(null);
9092
const clientRef = useRef<APIClient | null>(null);
9193

92-
const storyId = getStorybookStoryId();
93-
const shouldReset = clientRef.current === null || lastStoryIdRef.current !== storyId;
94+
const renderKey = getStorybookRenderKey();
95+
const shouldReset = clientRef.current === null || lastRenderKeyRef.current !== renderKey;
9496
if (shouldReset) {
9597
resetStorybookPersistedStateForStory();
96-
lastStoryIdRef.current = storyId;
98+
lastRenderKeyRef.current = renderKey;
9799
clientRef.current = null;
98100
}
99101

100102
clientRef.current ??= setup();
101103

102-
// Key by storyId to force full remount between stories.
103-
// Without this, RouterProvider keeps its initial route and APIProvider
104-
// doesn't re-initialize, causing flaky "loading page vs left screen" states.
105-
return <AppLoader key={storyId} client={clientRef.current} />;
104+
// Key by story + viewport bucket so Storybook fully remounts when switching
105+
// stories or crossing the mobile breakpoint that changes the left sidebar default.
106+
// Without this, RouterProvider keeps its initial route and APIProvider doesn't
107+
// re-initialize, causing flaky "loading page vs left screen" states.
108+
return <AppLoader key={renderKey} client={clientRef.current} />;
106109
};

0 commit comments

Comments
 (0)