Skip to content

Commit e2f68eb

Browse files
🤖 fix: use ownerDocument.body for portal queries in settings stories (#1107)
Fix settings modal stories to properly query portal-rendered content. **Changes:** - Use `canvasElement.ownerDocument.body` instead of `document.body` to scope queries to the Storybook iframe (not the parent UI) - Use `findByRole` with explicit names instead of finding all elements by role and filtering - Pre-set experiment state in localStorage for `ExperimentsToggleOn` story instead of clicking the toggle (more deterministic, avoids timing issues with `localStorage → useSyncExternalStore → re-render` in Chromatic) _Generated with `mux`_
1 parent 9e6699e commit e2f68eb

File tree

1 file changed

+33
-48
lines changed

1 file changed

+33
-48
lines changed

‎src/browser/stories/App.settings.stories.tsx‎

Lines changed: 33 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { appMeta, AppWithMocks, type AppStory } from "./meta.js";
1414
import { createWorkspace, groupWorkspacesByProject } from "./mockFactory";
1515
import { selectWorkspace } from "./storyHelpers";
1616
import { createMockORPCClient } from "../../../.storybook/mocks/orpc";
17-
import { within, waitFor, userEvent } from "@storybook/test";
17+
import { within, userEvent } from "@storybook/test";
18+
import { getExperimentKey, EXPERIMENT_IDS } from "@/common/constants/experiments";
1819

1920
export default {
2021
...appMeta,
@@ -29,11 +30,21 @@ export default {
2930
function setupSettingsStory(options: {
3031
providersConfig?: Record<string, { apiKeySet: boolean; baseUrl?: string; models?: string[] }>;
3132
providersList?: string[];
33+
/** Pre-set experiment states in localStorage before render */
34+
experiments?: Partial<Record<string, boolean>>;
3235
}): APIClient {
3336
const workspaces = [createWorkspace({ id: "ws-1", name: "main", projectName: "my-app" })];
3437

3538
selectWorkspace(workspaces[0]);
3639

40+
// Pre-set experiment states if provided
41+
if (options.experiments) {
42+
for (const [experimentId, enabled] of Object.entries(options.experiments)) {
43+
const key = getExperimentKey(experimentId as typeof EXPERIMENT_IDS.POST_COMPACTION_CONTEXT);
44+
window.localStorage.setItem(key, JSON.stringify(enabled));
45+
}
46+
}
47+
3748
return createMockORPCClient({
3849
projects: groupWorkspacesByProject(workspaces),
3950
workspaces,
@@ -45,33 +56,23 @@ function setupSettingsStory(options: {
4556
/** Open settings modal and optionally navigate to a section */
4657
async function openSettingsToSection(canvasElement: HTMLElement, section?: string): Promise<void> {
4758
const canvas = within(canvasElement);
59+
// Use ownerDocument.body to scope to iframe, not parent Storybook UI
60+
const body = within(canvasElement.ownerDocument.body);
4861

4962
// Wait for app to fully load (sidebar with settings button should appear)
50-
// Use longer timeout since app initialization can take time
5163
const settingsButton = await canvas.findByTestId("settings-button", {}, { timeout: 10000 });
5264
await userEvent.click(settingsButton);
5365

54-
// Wait for modal to appear - Radix Dialog uses a portal so we need to search the entire document
55-
const body = within(document.body);
56-
await waitFor(
57-
() => {
58-
const modal = body.getByRole("dialog");
59-
if (!modal) throw new Error("Settings modal not found");
60-
},
61-
{ timeout: 5000 }
62-
);
66+
// Wait for dialog to appear (portal renders outside canvasElement but inside iframe body)
67+
await body.findByRole("dialog");
6368

6469
// Navigate to specific section if requested
65-
// The sidebar nav has buttons with exact section names
6670
if (section && section !== "general") {
67-
const modal = body.getByRole("dialog");
68-
const modalCanvas = within(modal);
69-
// Find the nav section button (exact text match)
70-
const navButtons = await modalCanvas.findAllByRole("button");
71-
const sectionButton = navButtons.find(
72-
(btn) => btn.textContent?.toLowerCase().trim() === section.toLowerCase()
73-
);
74-
if (!sectionButton) throw new Error(`Section button "${section}" not found`);
71+
// Capitalize first letter to match the button text (e.g., "experiments" -> "Experiments")
72+
const sectionLabel = section.charAt(0).toUpperCase() + section.slice(1);
73+
const sectionButton = await body.findByRole("button", {
74+
name: new RegExp(sectionLabel, "i"),
75+
});
7576
await userEvent.click(sectionButton);
7677
}
7778
}
@@ -175,43 +176,27 @@ export const Experiments: AppStory = {
175176
},
176177
};
177178

178-
/** Experiments section - toggle experiment on */
179+
/** Experiments section - shows experiment in ON state (pre-enabled via localStorage) */
179180
export const ExperimentsToggleOn: AppStory = {
180-
render: () => <AppWithMocks setup={() => setupSettingsStory({})} />,
181+
render: () => (
182+
<AppWithMocks
183+
setup={() =>
184+
setupSettingsStory({
185+
experiments: { [EXPERIMENT_IDS.POST_COMPACTION_CONTEXT]: true },
186+
})
187+
}
188+
/>
189+
),
181190
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
182191
await openSettingsToSection(canvasElement, "experiments");
183-
184-
// Find and click the switch to toggle it on
185-
const body = within(document.body);
186-
const modal = body.getByRole("dialog");
187-
const modalCanvas = within(modal);
188-
189-
// Find the switch by its role - experiments use role="switch"
190-
const switches = await modalCanvas.findAllByRole("switch");
191-
if (switches.length > 0) {
192-
// Toggle the first experiment on
193-
await userEvent.click(switches[0]);
194-
}
195192
},
196193
};
197194

198-
/** Experiments section - toggle experiment off (starts enabled, then toggles off) */
195+
/** Experiments section - shows experiment in OFF state (default) */
199196
export const ExperimentsToggleOff: AppStory = {
200197
render: () => <AppWithMocks setup={() => setupSettingsStory({})} />,
201198
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
202199
await openSettingsToSection(canvasElement, "experiments");
203-
204-
const body = within(document.body);
205-
const modal = body.getByRole("dialog");
206-
const modalCanvas = within(modal);
207-
208-
// Find the switch
209-
const switches = await modalCanvas.findAllByRole("switch");
210-
if (switches.length > 0) {
211-
// Toggle on first
212-
await userEvent.click(switches[0]);
213-
// Then toggle off
214-
await userEvent.click(switches[0]);
215-
}
200+
// Default state is OFF - no clicks needed
216201
},
217202
};

0 commit comments

Comments
 (0)