|
| 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"; |
1 | 20 | import CssBaseline from "@mui/material/CssBaseline";
|
2 | 21 | import {
|
3 |
| - StyledEngineProvider, |
4 |
| - ThemeProvider as MuiThemeProvider, |
| 22 | + ThemeProvider as MuiThemeProvider, |
| 23 | + StyledEngineProvider, |
5 | 24 | } from "@mui/material/styles";
|
6 |
| -import { ThemeProvider as EmotionThemeProvider } from "@emotion/react"; |
7 | 25 | 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"; |
11 | 28 | 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"; |
13 | 31 | import "theme/globalFonts";
|
14 |
| -import isChromatic from "chromatic/isChromatic"; |
| 32 | +import themes from "../src/theme"; |
15 | 33 |
|
16 | 34 | DecoratorHelpers.initializeThemeState(Object.keys(themes), "dark");
|
17 | 35 |
|
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]; |
47 | 38 |
|
| 39 | +/** @type {Preview["parameters"]} */ |
48 | 40 | 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 | + }, |
82 | 74 | };
|
83 | 75 |
|
| 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} */ |
84 | 95 | 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 | +} |
93 | 128 |
|
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"; |
110 | 134 |
|
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 | + ); |
116 | 147 | }
|
117 | 148 |
|
118 | 149 | // Try to fix storybook rendering fonts inconsistently
|
119 | 150 | // https://www.chromatic.com/docs/font-loading/#solution-c-check-fonts-have-loaded-in-a-loader
|
120 | 151 | const fontLoader = async () => ({
|
121 |
| - fonts: await document.fonts.ready, |
| 152 | + fonts: await document.fonts.ready, |
122 | 153 | });
|
123 | 154 |
|
124 | 155 | export const loaders = isChromatic() && document.fonts ? [fontLoader] : [];
|
0 commit comments