Skip to content

Commit 7ea8a22

Browse files
authored
fix: add type-safety for Storybook preview.jsx config file (#14671)
* fix: add type-safety to Storybook preview.jsx file * fix: add clarifying comments * fix: add type-safety to preview config
1 parent c6bc741 commit 7ea8a22

File tree

1 file changed

+131
-100
lines changed

1 file changed

+131
-100
lines changed

site/.storybook/preview.jsx

Lines changed: 131 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,155 @@
1+
// @ts-check
2+
/**
3+
* @file Defines the main configuration file for all of our Storybook tests.
4+
* This file must be a JSX/JS file, but we can at least add some type safety via
5+
* the ts-check directive.
6+
* @see {@link https://storybook.js.org/docs/configure#configure-story-rendering}
7+
*
8+
* @typedef {import("react").ReactElement} ReactElement
9+
* @typedef {import("react").PropsWithChildren} PropsWithChildren
10+
* @typedef {import("react").FC<PropsWithChildren>} FC
11+
*
12+
* @typedef {import("@storybook/react").StoryContext} StoryContext
13+
* @typedef {import("@storybook/react").Preview} Preview
14+
*
15+
* @typedef {(Story: FC, Context: StoryContext) => React.JSX.Element} Decorator A
16+
* Storybook decorator function used to inject baseline data dependencies into
17+
* our React components during testing.
18+
*/
19+
import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
120
import CssBaseline from "@mui/material/CssBaseline";
221
import {
3-
StyledEngineProvider,
4-
ThemeProvider as MuiThemeProvider,
22+
ThemeProvider as MuiThemeProvider,
23+
StyledEngineProvider,
524
} from "@mui/material/styles";
6-
import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
725
import { DecoratorHelpers } from "@storybook/addon-themes";
8-
import { withRouter } from "storybook-addon-remix-react-router";
9-
import { StrictMode } from "react";
10-
import { parseQueryArgs, QueryClient, QueryClientProvider } from "react-query";
26+
import isChromatic from "chromatic/isChromatic";
27+
import React, { StrictMode } from "react";
1128
import { HelmetProvider } from "react-helmet-async";
12-
import themes from "theme";
29+
import { parseQueryArgs, QueryClient, QueryClientProvider } from "react-query";
30+
import { withRouter } from "storybook-addon-remix-react-router";
1331
import "theme/globalFonts";
14-
import isChromatic from "chromatic/isChromatic";
32+
import themes from "../src/theme";
1533

1634
DecoratorHelpers.initializeThemeState(Object.keys(themes), "dark");
1735

18-
export const decorators = [
19-
withRouter,
20-
withQuery,
21-
(Story) => {
22-
return (
23-
<HelmetProvider>
24-
<Story />
25-
</HelmetProvider>
26-
);
27-
},
28-
(Story, context) => {
29-
const selectedTheme = DecoratorHelpers.pluckThemeFromContext(context);
30-
const { themeOverride } = DecoratorHelpers.useThemeParameters();
31-
const selected = themeOverride || selectedTheme || "dark";
32-
33-
return (
34-
<StrictMode>
35-
<StyledEngineProvider injectFirst>
36-
<MuiThemeProvider theme={themes[selected]}>
37-
<EmotionThemeProvider theme={themes[selected]}>
38-
<CssBaseline />
39-
<Story />
40-
</EmotionThemeProvider>
41-
</MuiThemeProvider>
42-
</StyledEngineProvider>
43-
</StrictMode>
44-
);
45-
},
46-
];
36+
/** @type {readonly Decorator[]} */
37+
export const decorators = [withRouter, withQuery, withHelmet, withTheme];
4738

39+
/** @type {Preview["parameters"]} */
4840
export const parameters = {
49-
options: {
50-
storySort: {
51-
method: "alphabetical",
52-
order: ["design", "pages", "modules", "components"],
53-
locales: "en-US",
54-
},
55-
},
56-
controls: {
57-
expanded: true,
58-
matchers: {
59-
color: /(background|color)$/i,
60-
date: /Date$/,
61-
},
62-
},
63-
viewport: {
64-
viewports: {
65-
ipad: {
66-
name: "iPad Mini",
67-
styles: {
68-
height: "1024px",
69-
width: "768px",
70-
},
71-
type: "tablet",
72-
},
73-
terminal: {
74-
name: "Terminal",
75-
styles: {
76-
height: "400",
77-
width: "400",
78-
},
79-
},
80-
},
81-
},
41+
options: {
42+
storySort: {
43+
method: "alphabetical",
44+
order: ["design", "pages", "modules", "components"],
45+
locales: "en-US",
46+
},
47+
},
48+
controls: {
49+
expanded: true,
50+
matchers: {
51+
color: /(background|color)$/i,
52+
date: /Date$/,
53+
},
54+
},
55+
viewport: {
56+
viewports: {
57+
ipad: {
58+
name: "iPad Mini",
59+
styles: {
60+
height: "1024px",
61+
width: "768px",
62+
},
63+
type: "tablet",
64+
},
65+
terminal: {
66+
name: "Terminal",
67+
styles: {
68+
height: "400",
69+
width: "400",
70+
},
71+
},
72+
},
73+
},
8274
};
8375

76+
/**
77+
* There's a mismatch on the React Helmet return type that causes issues when
78+
* mounting the component in JS files only. Have to do type assertion, which is
79+
* especially ugly in JSDoc
80+
*/
81+
const SafeHelmetProvider = /** @type {FC} */ (
82+
/** @type {unknown} */ (HelmetProvider)
83+
);
84+
85+
/** @type {Decorator} */
86+
function withHelmet(Story) {
87+
return (
88+
<SafeHelmetProvider>
89+
<Story />
90+
</SafeHelmetProvider>
91+
);
92+
}
93+
94+
/** @type {Decorator} */
8495
function withQuery(Story, { parameters }) {
85-
const queryClient = new QueryClient({
86-
defaultOptions: {
87-
queries: {
88-
staleTime: Infinity,
89-
retry: false,
90-
},
91-
},
92-
});
96+
const queryClient = new QueryClient({
97+
defaultOptions: {
98+
queries: {
99+
staleTime: Number.POSITIVE_INFINITY,
100+
retry: false,
101+
},
102+
},
103+
});
104+
105+
if (parameters.queries) {
106+
for (const query of parameters.queries) {
107+
if (query.data instanceof Error) {
108+
// This is copied from setQueryData() but sets the error.
109+
const cache = queryClient.getQueryCache();
110+
const parsedOptions = parseQueryArgs(query.key);
111+
const defaultedOptions = queryClient.defaultQueryOptions(parsedOptions);
112+
const cachedQuery = cache.build(queryClient, defaultedOptions);
113+
// Set manual data so react-query will not try to refetch.
114+
cachedQuery.setData(undefined, { manual: true });
115+
cachedQuery.setState({ error: query.data });
116+
} else {
117+
queryClient.setQueryData(query.key, query.data);
118+
}
119+
}
120+
}
121+
122+
return (
123+
<QueryClientProvider client={queryClient}>
124+
<Story />
125+
</QueryClientProvider>
126+
);
127+
}
93128

94-
if (parameters.queries) {
95-
parameters.queries.forEach((query) => {
96-
if (query.data instanceof Error) {
97-
// This is copied from setQueryData() but sets the error.
98-
const cache = queryClient.getQueryCache();
99-
const parsedOptions = parseQueryArgs(query.key)
100-
const defaultedOptions = queryClient.defaultQueryOptions(parsedOptions)
101-
const cachedQuery = cache.build(queryClient, defaultedOptions);
102-
// Set manual data so react-query will not try to refetch.
103-
cachedQuery.setData(undefined, { manual: true });
104-
cachedQuery.setState({ error: query.data });
105-
} else {
106-
queryClient.setQueryData(query.key, query.data);
107-
}
108-
});
109-
}
129+
/** @type {Decorator} */
130+
function withTheme(Story, context) {
131+
const selectedTheme = DecoratorHelpers.pluckThemeFromContext(context);
132+
const { themeOverride } = DecoratorHelpers.useThemeParameters();
133+
const selected = themeOverride || selectedTheme || "dark";
110134

111-
return (
112-
<QueryClientProvider client={queryClient}>
113-
<Story />
114-
</QueryClientProvider>
115-
);
135+
return (
136+
<StrictMode>
137+
<StyledEngineProvider injectFirst>
138+
<MuiThemeProvider theme={themes[selected]}>
139+
<EmotionThemeProvider theme={themes[selected]}>
140+
<CssBaseline />
141+
<Story />
142+
</EmotionThemeProvider>
143+
</MuiThemeProvider>
144+
</StyledEngineProvider>
145+
</StrictMode>
146+
);
116147
}
117148

118149
// Try to fix storybook rendering fonts inconsistently
119150
// https://www.chromatic.com/docs/font-loading/#solution-c-check-fonts-have-loaded-in-a-loader
120151
const fontLoader = async () => ({
121-
fonts: await document.fonts.ready,
152+
fonts: await document.fonts.ready,
122153
});
123154

124155
export const loaders = isChromatic() && document.fonts ? [fontLoader] : [];

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy