Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade Storybook to 8.x (next) #2373

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 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
9 changes: 9 additions & 0 deletions .github/workflows/chromatic-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ jobs:
# Generate snapshots for only changed stories
# See: https://www.chromatic.com/docs/turbosnap
onlyChanged: true
# Install Playwright browsers so Vitest browser mode can run story tests
- name: Install playwright dependencies
if: ${{ inputs.target == 'review' }}
run: yarn exec playwright install chromium --with-deps
- name: Run Storybook tests
if: ${{ inputs.target == 'review' }}
run: yarn test:storybook
env:
SB_URL: '${{ steps.chromatic_publish.outputs.storybookUrl }}'

# (if) Run this step on dependabot or changesets PRs.
- name: Skip Chromatic builds (dependabot or changesets PRs)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/chromatic-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
if: |
github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]' &&
!startsWith(github.head_ref, 'changeset-release/')
name: Chromatic - Build on regular PRs
name: Chromatic - Build and test on regular PRs
uses: ./.github/workflows/chromatic-build.yml
with:
target: 'review'
Expand Down
50 changes: 6 additions & 44 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import {resolve, dirname, join} from "path";
import {mergeConfig} from "vite";
import turbosnap from "vite-plugin-turbosnap";
import type {StorybookConfig} from "@storybook/react-vite";

const config: StorybookConfig = {
Expand All @@ -9,52 +6,17 @@ const config: StorybookConfig = {
"../__docs__/**/*.mdx",
],
addons: [
getAbsolutePath("@storybook/addon-essentials"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storybook upgrades always want to drop this back in. I finally searched when it's needed. Apparently in Yarn PnP situations module resolution can fail without this. Good to know we can remove this in Perseus too then.

https://storybook.js.org/docs/faq#how-do-i-fix-module-resolution-in-special-environments

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I see, thanks for the context.

getAbsolutePath("@storybook/addon-a11y"),
getAbsolutePath("@storybook/addon-designs"),
getAbsolutePath("@storybook/addon-interactions"),
getAbsolutePath("@storybook/addon-mdx-gfm"),
getAbsolutePath("storybook-addon-pseudo-states"),
"@storybook/addon-essentials",
"@storybook/addon-a11y",
"@storybook/addon-designs",
"storybook-addon-pseudo-states",
"@storybook/experimental-addon-test",
],
staticDirs: ["../static"],
core: {
disableTelemetry: true,
},
framework: getAbsolutePath("@storybook/react-vite"),
async viteFinal(config, {configType}) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I'm moving this to a separate vite.config.ts file for better integration with vitest.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

y'all are switching to vitest?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, it's just for running storybook tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct! I'm thinking that we should probably write an ADR if we want to adopt this more widely.

Just curious.... I know this is a big change, but I wonder if you have any opinions about switching from Jest to vitest for unit tests?

// Merge custom configuration into the default config
const mergedConfig = mergeConfig(config, {
resolve: {
// Allow us to detect changes from local wonder-blocks packages.
alias: [
{
find: /^@khanacademy\/wonder-blocks(-.*)$/,
replacement: resolve(
__dirname,
"../packages/wonder-blocks$1/src",
),
},
],
},
});

// Add turbosnap to production builds so we can let Chromatic take
// snapshots only to stories associated with the current PR.
if (configType === "PRODUCTION") {
config.plugins?.push(
turbosnap({rootDir: config.root || process.cwd()}),
);
}
Comment on lines -43 to -47
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Dropping this as it is no longer needed as a separate package. SB 8 now includes it internally.


return mergedConfig;
},
docs: {
autodocs: true,
},
framework: "@storybook/react-vite",
};

export default config;

function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, "package.json")));
}
42 changes: 21 additions & 21 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import wonderBlocksTheme from "./wonder-blocks-theme";

import {Decorator} from "@storybook/react";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally out of the blue and unrelated to this PR, but it looks like you don't have import/order enabled in Wonder Blocks. I (not sure about others Perseus) really love how it automatically orders imports so I don't think about it... although, maybe you don't think about it either. :D

https://github.com/Khan/perseus/blob/main/.eslintrc.js#L220..L236

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point! I'll add this rule in a separate PR. I'll probably wait until WB is in a more "quiet" state to apply the changes :)

import {color} from "@khanacademy/wonder-blocks-tokens";
import Link from "@khanacademy/wonder-blocks-link";
import {ThemeSwitcherContext} from "@khanacademy/wonder-blocks-theming";
Expand Down Expand Up @@ -95,31 +95,29 @@ const parameters = {
},
};

export const decorators = [
(Story, context) => {
const theme = context.globals.theme;
const enableRenderStateRootDecorator =
context.parameters.enableRenderStateRootDecorator;

if (enableRenderStateRootDecorator) {
return (
<RenderStateRoot>
<ThemeSwitcherContext.Provider value={theme}>
<Story />
</ThemeSwitcherContext.Provider>
</RenderStateRoot>
);
}
const withThemeSwitcher: Decorator = (
Story,
{globals: {theme}, parameters: {enableRenderStateRootDecorator}},
) => {
if (enableRenderStateRootDecorator) {
return (
<ThemeSwitcherContext.Provider value={theme}>
<Story />
</ThemeSwitcherContext.Provider>
<RenderStateRoot>
<ThemeSwitcherContext.Provider value={theme}>
<Story />
</ThemeSwitcherContext.Provider>
</RenderStateRoot>
);
},
];
}
return (
<ThemeSwitcherContext.Provider value={theme}>
<Story />
</ThemeSwitcherContext.Provider>
);
};

const preview: Preview = {
parameters,
decorators: [withThemeSwitcher],
globalTypes: {
// Allow the user to select a theme from the toolbar.
theme: {
Expand All @@ -146,6 +144,8 @@ const preview: Preview = {
},
},
},

tags: ["autodocs"],
};

export default preview;
9 changes: 9 additions & 0 deletions .storybook/vitest.setup.ts
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: This file was generated when I added the experimental addon. More info here: https://storybook.js.org/docs/writing-tests/test-addon#automatic-setup

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { beforeAll } from 'vitest';
import { setProjectAnnotations } from '@storybook/react';
import * as projectAnnotations from './preview';

// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
const project = setProjectAnnotations([projectAnnotations]);

beforeAll(project.beforeAll);
2 changes: 1 addition & 1 deletion __docs__/wonder-blocks-dropdown/combobox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ export const ControlledMultilpleCombobox: Story = {
],

play: async ({canvasElement}) => {
const canvas = within(canvasElement);
const canvas = within(canvasElement.ownerDocument.body);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, this differs from the examples for these play() functions. :( They always show how you originally had it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yeah, this is useful for when we are using portals. I should add a comment here stating that.


// Move to second option item
await userEvent.keyboard("{ArrowDown}");
Expand Down
48 changes: 16 additions & 32 deletions __docs__/wonder-blocks-link/link.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,8 @@ Primary.play = async ({canvasElement}) => {

// Focus style with keyboard navigation
await userEvent.tab();
const computedStyle = getComputedStyle(link, ":focus-visible");
// rgb(24, 101, 242) is the same as Color.blue. `toBe` doesn't seem to
// compare different color formats, so hex was converted to RGB.
await expect(computedStyle.outline).toBe("rgb(24, 101, 242) solid 1px");
// rgb(24, 101, 242) is the same as Color.blue
await expect(link).toHaveStyle("outline: rgb(24, 101, 242) solid 1px");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: This and other similar assertions where changed b/c getComputedStyle doesn't work with the new testing approach.

I got these errors the first time I integrated the new SB test CLI in CI, and made these changes to fix the failures.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we think about tests that check for specific styles? I had started removing jest tests that were asserting styles since it would be visually covered by Chromatic (especially with the "All Variants" stories + pseudostates add on!). Is it helpful to also have these component test checks for styles?

Kinda related - In general, when would it be helpful for us to reach for component tests in Storybook? I'm thinking if there's functionality we want to test for that relies on browser behaviour! Curious to see what other think!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% agree with you. We should move this to "All variants" but the issue is that Link (and Button) still rely on Clickable for setting pseudo states styles. As you know, I'd love to get us to a point where we don't use Clickable for those styles, and we let CSS handle that for us, but that's a tricky change that would be definitively nice to address.

Once we migrate to CSS pseudoclasses, I'm totally in favor of moving this to visual regression tests instead of interaction tests.


// Mousedown style
await fireEvent.mouseDown(link);
Expand Down Expand Up @@ -130,10 +128,8 @@ Secondary.play = async ({canvasElement}) => {

// Focus style with keyboard navigation
await userEvent.tab();
const computedStyle = getComputedStyle(link, ":focus-visible");
// rgb(24, 101, 242) is the same as Color.blue. `toBe` doesn't seem to
// compare different color formats, so hex was converted to RGB.
await expect(computedStyle.outline).toBe("rgb(24, 101, 242) solid 1px");
// rgb(24, 101, 242) is the same as Color.blue.
await expect(link).toHaveStyle("outline: rgb(24, 101, 242) solid 1px");

// Mousedown style
await fireEvent.mouseDown(link);
Expand Down Expand Up @@ -198,10 +194,8 @@ LightPrimary.play = async ({canvasElement}) => {

// Focus style with keyboard navigation
await userEvent.tab();
const computedStyle = getComputedStyle(link, ":focus-visible");
// rgb(255, 255, 255) is the same as Color.white. `toBe` doesn't seem to
// compare different color formats, so hex was converted to RGB.
await expect(computedStyle.outline).toBe("rgb(255, 255, 255) solid 1px");
// rgb(255, 255, 255) is the same as Color.white.
await expect(link).toHaveStyle("outline: rgb(255, 255, 255) solid 1px");

// Mousedown style
await fireEvent.mouseDown(link);
Expand Down Expand Up @@ -492,14 +486,9 @@ Inline.play = async ({canvasElement}) => {

// Focus style with keyboard navigation
await userEvent.tab();
const primaryComputedStyle = getComputedStyle(
primaryLink,
":focus-visible",
);
// rgb(24, 101, 242) is the same as Color.blue. `toBe` doesn't seem to
// compare different color formats, so hex was converted to RGB.
await expect(primaryComputedStyle.outline).toBe(
"rgb(24, 101, 242) solid 1px",
// rgb(24, 101, 242) is the same as Color.blue.
await expect(primaryLink).toHaveStyle(
"outline: rgb(24, 101, 242) solid 1px",
);

// Mousedown style
Expand All @@ -526,14 +515,9 @@ Inline.play = async ({canvasElement}) => {
// Focus style with keyboard navigation
await userEvent.tab();
await userEvent.tab();
const secondaryComputedStyle = getComputedStyle(
secondaryLink,
":focus-visible",
);
// rgb(24, 101, 242) is the same as Color.blue. `toBe` doesn't seem to
// compare different color formats, so hex was converted to RGB.
await expect(secondaryComputedStyle.outline).toBe(
"rgb(24, 101, 242) solid 1px",
// rgb(24, 101, 242) is the same as Color.blue.
await expect(secondaryLink).toHaveStyle(
"outline: rgb(24, 101, 242) solid 1px",
);

// Mousedown style
Expand Down Expand Up @@ -611,10 +595,10 @@ InlineLight.play = async ({canvasElement}) => {

// Focus style with keyboard navigation
await userEvent.tab();
const computedStyle = getComputedStyle(primaryLink, ":focus-visible");
// rgb(255, 255, 255) is the same as Color.white. `toBe` doesn't seem to
// compare different color formats, so hex was converted to RGB.
await expect(computedStyle.outline).toBe("rgb(255, 255, 255) solid 1px");
// rgb(255, 255, 255) is the same as Color.white.
await expect(primaryLink).toHaveStyle(
"outline: rgb(255, 255, 255) solid 1px",
);

// Mousedown style
await fireEvent.mouseDown(primaryLink);
Expand Down
30 changes: 18 additions & 12 deletions __docs__/wonder-blocks-pill/pill.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,28 +324,34 @@ Variants.play = async ({canvasElement}) => {

// Test clickable pill styles
await neutralMediumClickable.focus();
let computedStyle = getComputedStyle(neutralMediumClickable, ":hover");
await expect(computedStyle.outline).toBe("rgb(24, 101, 242) solid 2px");
await expect(neutralMediumClickable).toHaveStyle(
"outline: rgb(24, 101, 242) solid 2px",
);

await userEvent.tab();
computedStyle = getComputedStyle(accentMediumClickable, ":hover");
await expect(computedStyle.outline).toBe("rgb(24, 101, 242) solid 2px");
await expect(accentMediumClickable).toHaveStyle(
"outline: rgb(24, 101, 242) solid 2px",
);

await userEvent.tab();
computedStyle = getComputedStyle(infoMediumClickable, ":hover");
await expect(computedStyle.outline).toBe("rgb(24, 101, 242) solid 2px");
await expect(infoMediumClickable).toHaveStyle(
"outline: rgb(24, 101, 242) solid 2px",
);

await userEvent.tab();
computedStyle = getComputedStyle(successMediumClickable, ":hover");
await expect(computedStyle.outline).toBe("rgb(24, 101, 242) solid 2px");
await expect(successMediumClickable).toHaveStyle(
"outline: rgb(24, 101, 242) solid 2px",
);

await userEvent.tab();
computedStyle = getComputedStyle(warningMediumClickable, ":hover");
await expect(computedStyle.outline).toBe("rgb(24, 101, 242) solid 2px");
await expect(warningMediumClickable).toHaveStyle(
"outline: rgb(24, 101, 242) solid 2px",
);

await userEvent.tab();
computedStyle = getComputedStyle(criticalMediumClickable, ":hover");
await expect(computedStyle.outline).toBe("rgb(217, 41, 22) solid 2px");
await expect(criticalMediumClickable).toHaveStyle(
"outline: rgb(217, 41, 22) solid 2px",
);
};

export const WithTypography: StoryComponentType = () => (
Expand Down
72 changes: 38 additions & 34 deletions __docs__/wonder-blocks-switch/switch.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,46 +82,50 @@ export const Default: StoryComponentType = {
* The `onChange` prop is optional in case the toggle will
* be wrapped in a larger clickable component.
*/
export const Controlled: StoryComponentType = () => {
const [checkedOne, setCheckedOne] = React.useState(false);
const [checkedTwo, setCheckedTwo] = React.useState(false);
export const Controlled: StoryComponentType = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: This component doesn't have any big changes, just moved the code around to support CSF3.

render: function Render() {
const [checkedOne, setCheckedOne] = React.useState(false);
const [checkedTwo, setCheckedTwo] = React.useState(false);

return (
<View style={styles.column}>
<Switch checked={checkedOne} onChange={setCheckedOne} />

<Switch
testId="test-switch"
aria-label="test switch"
checked={checkedTwo}
onChange={setCheckedTwo}
icon={<PhosphorIcon icon={magnifyingGlassIcon} />}
/>
</View>
);
};

Controlled.play = async ({canvasElement}) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In previous versions of Storybook you had to manually navigate to each story and press "play" to run the tests, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! and that made it hard to determine when we introduced a regression with these interaction tests. I also added the CI step to ensure that we don't introduce issues in the future.

NOTE: Storybook uses the test-runner for this, but it will be replaced by this new test addon.

The previous runner used Jest + Playwright, and they wanted to switch to Vitest specially because they can run these tests in browsers as well as in headless mode. This is a new vitest feature that is also being refined in collaboration between the Storybook and vitest teams: https://vitest.dev/guide/browser/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const canvas = within(canvasElement);
return (
<View style={styles.column}>
<Switch checked={checkedOne} onChange={setCheckedOne} />
<Switch
testId="test-switch"
aria-label="test switch"
checked={checkedTwo}
onChange={setCheckedTwo}
icon={<PhosphorIcon icon={magnifyingGlassIcon} />}
/>
</View>
);
},
play: async ({canvasElement}) => {
const canvas = within(canvasElement);

const switchWithIcon = canvas.getByTestId("test-switch");
const switchInput = canvas.getByRole("switch", {name: "test switch"});
const switchWithIcon = canvas.getByTestId("test-switch");
const switchInput = canvas.getByRole("switch", {name: "test switch"});

await userEvent.tab();
await userEvent.tab();
await userEvent.tab();
await userEvent.tab();

expect(switchWithIcon).toHaveStyle(
"background-color: rgba(33, 36, 44, 0.5)",
);
expect(switchWithIcon).toHaveStyle("outline: 2px solid rgb(24, 101, 242)");
expect(switchInput).toHaveProperty("checked", false);
expect(switchWithIcon).toHaveStyle(
"background-color: rgba(33, 36, 44, 0.5)",
);
expect(switchWithIcon).toHaveStyle(
"outline: 2px solid rgb(24, 101, 242)",
);
expect(switchInput).toHaveProperty("checked", false);

await userEvent.click(switchWithIcon);
// Wait for animations to finish
await new Promise((resolve) => setTimeout(resolve, 150));
await userEvent.click(switchWithIcon);
// Wait for animations to finish
await new Promise((resolve) => setTimeout(resolve, 150));

expect(switchInput).toHaveProperty("checked", true);
expect(switchWithIcon).toHaveStyle("background-color: rgb(24, 101, 242)");
expect(switchInput).toHaveProperty("checked", true);
expect(switchWithIcon).toHaveStyle(
"background-color: rgb(24, 101, 242)",
);
},
};

/**
Expand Down
Loading