From fd63ccf21372bdbbb46c22997ad3d929db010d80 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 13 Sep 2024 16:36:05 +0000 Subject: [PATCH 1/3] fix: add type-safety to Storybook preview.jsx file --- site/.storybook/preview.jsx | 223 ++++++++++++++++++++---------------- 1 file changed, 123 insertions(+), 100 deletions(-) diff --git a/site/.storybook/preview.jsx b/site/.storybook/preview.jsx index d4eeafec72737..048638cc1ae37 100644 --- a/site/.storybook/preview.jsx +++ b/site/.storybook/preview.jsx @@ -1,124 +1,147 @@ +// @ts-check +/** + * @typedef {import("react").ReactElement} ReactElement + * @typedef {import("react").PropsWithChildren} PropsWithChildren + * @typedef {import("react").FC} FC + * @typedef {import("@storybook/react").StoryContext} StoryContext + * + * @typedef {(Story: FC, Context: StoryContext) => React.JSX.Element} Decorator A + * Storybook decorator function used to inject baseline data dependencies into + * our React components during testing. + */ +import { ThemeProvider as EmotionThemeProvider } from "@emotion/react"; import CssBaseline from "@mui/material/CssBaseline"; import { - StyledEngineProvider, - ThemeProvider as MuiThemeProvider, + ThemeProvider as MuiThemeProvider, + StyledEngineProvider, } from "@mui/material/styles"; -import { ThemeProvider as EmotionThemeProvider } from "@emotion/react"; import { DecoratorHelpers } from "@storybook/addon-themes"; -import { withRouter } from "storybook-addon-remix-react-router"; -import { StrictMode } from "react"; -import { parseQueryArgs, QueryClient, QueryClientProvider } from "react-query"; +import isChromatic from "chromatic/isChromatic"; +import React, { StrictMode } from "react"; import { HelmetProvider } from "react-helmet-async"; -import themes from "theme"; +import { parseQueryArgs, QueryClient, QueryClientProvider } from "react-query"; +import { withRouter } from "storybook-addon-remix-react-router"; import "theme/globalFonts"; -import isChromatic from "chromatic/isChromatic"; +import themes from "../src/theme"; DecoratorHelpers.initializeThemeState(Object.keys(themes), "dark"); -export const decorators = [ - withRouter, - withQuery, - (Story) => { - return ( - - - - ); - }, - (Story, context) => { - const selectedTheme = DecoratorHelpers.pluckThemeFromContext(context); - const { themeOverride } = DecoratorHelpers.useThemeParameters(); - const selected = themeOverride || selectedTheme || "dark"; - - return ( - - - - - - - - - - - ); - }, -]; +/**@type {readonly Decorator[]} */ +export const decorators = [withRouter, withQuery, withHelmet, withTheme]; export const parameters = { - options: { - storySort: { - method: "alphabetical", - order: ["design", "pages", "modules", "components"], - locales: "en-US", - }, - }, - controls: { - expanded: true, - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, - viewport: { - viewports: { - ipad: { - name: "iPad Mini", - styles: { - height: "1024px", - width: "768px", - }, - type: "tablet", - }, - terminal: { - name: "Terminal", - styles: { - height: "400", - width: "400", - }, - }, - }, - }, + options: { + storySort: { + method: "alphabetical", + order: ["design", "pages", "modules", "components"], + locales: "en-US", + }, + }, + controls: { + expanded: true, + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + viewport: { + viewports: { + ipad: { + name: "iPad Mini", + styles: { + height: "1024px", + width: "768px", + }, + type: "tablet", + }, + terminal: { + name: "Terminal", + styles: { + height: "400", + width: "400", + }, + }, + }, + }, }; +/** + * There's a mismatch for the React Helmet types that causes issues when + * mounting the component in JS files only. Have to do type assertion, which is + * especially ugly in JSDoc + */ +const SafeHelmetProvider = /** @type {FC} */ ( + /** @type {unknown} */ (HelmetProvider) +); + +/** @type {Decorator} */ +function withHelmet(Story) { + return ( + + + + ); +} + +/** @type {Decorator} */ function withQuery(Story, { parameters }) { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: Infinity, - retry: false, - }, - }, - }); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: Number.POSITIVE_INFINITY, + retry: false, + }, + }, + }); + + if (parameters.queries) { + for (const query of parameters.queries) { + if (query.data instanceof Error) { + // This is copied from setQueryData() but sets the error. + const cache = queryClient.getQueryCache(); + const parsedOptions = parseQueryArgs(query.key); + const defaultedOptions = queryClient.defaultQueryOptions(parsedOptions); + const cachedQuery = cache.build(queryClient, defaultedOptions); + // Set manual data so react-query will not try to refetch. + cachedQuery.setData(undefined, { manual: true }); + cachedQuery.setState({ error: query.data }); + } else { + queryClient.setQueryData(query.key, query.data); + } + } + } + + return ( + + + + ); +} - if (parameters.queries) { - parameters.queries.forEach((query) => { - if (query.data instanceof Error) { - // This is copied from setQueryData() but sets the error. - const cache = queryClient.getQueryCache(); - const parsedOptions = parseQueryArgs(query.key) - const defaultedOptions = queryClient.defaultQueryOptions(parsedOptions) - const cachedQuery = cache.build(queryClient, defaultedOptions); - // Set manual data so react-query will not try to refetch. - cachedQuery.setData(undefined, { manual: true }); - cachedQuery.setState({ error: query.data }); - } else { - queryClient.setQueryData(query.key, query.data); - } - }); - } +/** @type {Decorator} */ +function withTheme(Story, context) { + const selectedTheme = DecoratorHelpers.pluckThemeFromContext(context); + const { themeOverride } = DecoratorHelpers.useThemeParameters(); + const selected = themeOverride || selectedTheme || "dark"; - return ( - - - - ); + return ( + + + + + + + + + + + ); } // Try to fix storybook rendering fonts inconsistently // https://www.chromatic.com/docs/font-loading/#solution-c-check-fonts-have-loaded-in-a-loader const fontLoader = async () => ({ - fonts: await document.fonts.ready, + fonts: await document.fonts.ready, }); export const loaders = isChromatic() && document.fonts ? [fontLoader] : []; From e5d32ea5d7cba04002731467c0a534d9727827c1 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 13 Sep 2024 16:39:42 +0000 Subject: [PATCH 2/3] fix: add clarifying comments --- site/.storybook/preview.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/site/.storybook/preview.jsx b/site/.storybook/preview.jsx index 048638cc1ae37..7898b7a7a1d4c 100644 --- a/site/.storybook/preview.jsx +++ b/site/.storybook/preview.jsx @@ -1,5 +1,10 @@ // @ts-check /** + * @file Defines the main configuration file for all of our Storybook tests. + * This file must be a JSX/JS file, but we can at least add some type safety via + * the ts-check directive. + * @see {@link https://storybook.js.org/docs/configure#configure-story-rendering} + * * @typedef {import("react").ReactElement} ReactElement * @typedef {import("react").PropsWithChildren} PropsWithChildren * @typedef {import("react").FC} FC @@ -66,7 +71,7 @@ export const parameters = { }; /** - * There's a mismatch for the React Helmet types that causes issues when + * There's a mismatch on the React Helmet return type that causes issues when * mounting the component in JS files only. Have to do type assertion, which is * especially ugly in JSDoc */ From b926775b59626e25dca3b5a5c6b49b4bd1388bbe Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 13 Sep 2024 17:06:03 +0000 Subject: [PATCH 3/3] fix: add type-safety to preview config --- site/.storybook/preview.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/site/.storybook/preview.jsx b/site/.storybook/preview.jsx index 7898b7a7a1d4c..99d2bb5b577ce 100644 --- a/site/.storybook/preview.jsx +++ b/site/.storybook/preview.jsx @@ -8,7 +8,9 @@ * @typedef {import("react").ReactElement} ReactElement * @typedef {import("react").PropsWithChildren} PropsWithChildren * @typedef {import("react").FC} FC + * * @typedef {import("@storybook/react").StoryContext} StoryContext + * @typedef {import("@storybook/react").Preview} Preview * * @typedef {(Story: FC, Context: StoryContext) => React.JSX.Element} Decorator A * Storybook decorator function used to inject baseline data dependencies into @@ -31,9 +33,10 @@ import themes from "../src/theme"; DecoratorHelpers.initializeThemeState(Object.keys(themes), "dark"); -/**@type {readonly Decorator[]} */ +/** @type {readonly Decorator[]} */ export const decorators = [withRouter, withQuery, withHelmet, withTheme]; +/** @type {Preview["parameters"]} */ export const parameters = { options: { storySort: {