Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions apps/docs/src/lib/storybook/react-router-stub.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import type { Decorator } from '@storybook/react-vite';
import type { ComponentType } from 'react';
import {
Expand Down Expand Up @@ -30,27 +31,40 @@ interface RemixStubOptions {
export const withReactRouterStubDecorator = (options: RemixStubOptions): Decorator => {
const { routes, initialPath = '/' } = options;

// We define the Stub component outside the return function to ensure it's not recreated
// on every render of the Story component itself.
const CachedStub: ComponentType<{ initialEntries?: string[] }> | null = null;
const lastMappedRoutes: StubRouteObject[] | null = null;

return (Story, context) => {
// Map routes to include the Story component if no Component is provided
const mappedRoutes = routes.map((route) => ({
...route,
Component: route.Component ?? (() => <Story {...context.args} />),
}));
const mappedRoutes = useMemo(
() =>
routes.map((route) => ({
...route,
Component: route.Component ?? Story,
})),
[Story],
);

// Get the base path (without existing query params from options)
const basePath = initialPath.split('?')[0];
const basePath = useMemo(() => initialPath.split('?')[0], []);

// Get the current search string from the actual browser window, if available
// If not available, use a default search string with parameters needed for the data table
const currentWindowSearch = typeof window !== 'undefined' ? window.location.search : '?page=0&pageSize=10';

// Combine them for the initial entry
const actualInitialPath = `${basePath}${currentWindowSearch}`;
const actualInitialPath = useMemo(() => `${basePath}${currentWindowSearch}`, [basePath, currentWindowSearch]);

// Use React Router's official createRoutesStub
const Stub = createRoutesStub(mappedRoutes);
// We memoize the Stub component to prevent unnecessary remounts of the entire story
// when the decorator re-renders.
const Stub = useMemo(() => createRoutesStub(mappedRoutes), [mappedRoutes]);

const initialEntries = useMemo(() => [actualInitialPath], [actualInitialPath]);

return <Stub initialEntries={[actualInitialPath]} />;
return <Stub initialEntries={initialEntries} />;
};
};

Expand Down
49 changes: 49 additions & 0 deletions apps/docs/src/lib/storybook/test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { userEvent, within, screen, waitFor } from '@storybook/test';

/**
* A robust helper to select an option from a Radix-based Select/Combobox.
* Handles portals, animations, and pointer-event blockers.
*/
export async function selectRadixOption(
canvasElement: HTMLElement,
options: {
triggerRole?: 'combobox' | 'button';
triggerName: string | RegExp;
optionName: string | RegExp;
optionTestId?: string;
},
) {
const canvas = within(canvasElement);
const { triggerRole = 'combobox', triggerName, optionName, optionTestId } = options;

// 1. Find and click the trigger within the component canvas
const trigger = await canvas.findByRole(triggerRole, { name: triggerName });
if (!trigger) throw new Error(`Trigger with role ${triggerRole} and name ${triggerName} not found`);

await userEvent.click(trigger);

// 2. Wait for the listbox to appear in the document body (Portal)
// We use a slightly longer timeout for CI stability.
const listbox = await screen.findByRole('listbox', {}, { timeout: 3000 });
if (!listbox) throw new Error('Radix listbox (portal) not found after clicking trigger');

// 3. Find the option specifically WITHIN the listbox
let option: HTMLElement | null = null;
if (optionTestId) {
option = await within(listbox).findByTestId(optionTestId);
} else {
option = await within(listbox).findByRole('option', { name: optionName });
}

if (!option) throw new Error(`Option ${optionName || optionTestId} not found in listbox`);

// 4. Click the option
// pointerEventsCheck: 0 is used to bypass Radix's temporary pointer-event locks during animations
await userEvent.click(option, { pointerEventsCheck: 0 });

// 5. Verify the dropdown closed (optional but ensures stability)
await waitFor(() => {
const listbox = screen.queryByRole('listbox');
if (listbox) throw new Error('Listbox still visible');
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const ControlledCalendarWithFormExample = () => {
});

const [dropdown, setDropdown] = React.useState<'dropdown' | 'dropdown-months' | 'dropdown-years'>('dropdown');
const [date, setDate] = React.useState<Date | undefined>();
const [date, setDate] = React.useState<Date | undefined>(new Date(2025, 5, 12));

const dropdownOptions = [
{ label: 'Month and Year', value: 'dropdown' },
Expand Down
Loading