Skip to content

Commit

Permalink
test(storybook): support running visual tests in chromatic
Browse files Browse the repository at this point in the history
To better support visual tests, Modals are now opened automatically
(this does not happen in docs mode, so it doesn't affect the docs pages)

For now, Chromatic is not integrated into CI.
To run visual tests, add (or edit) the file `.env` at the root of the repo.
This file is in .gitignore and will not be committed.

Add the following to `.env`:
```
CHROMATIC_PROJECT_TOKEN=<token>
```

Replace `<token>` with the token found [here](https://www.chromatic.com/manage?appId=66fe736b9d639fe6801bf130&setup=true), under "Setup Chromatic with this project token".

Then run these commands:
```
yarn build:storybook
yarn chromatic
```

You can also replace the last command with e.g.
```
yarn chromatic:all --onlyStoryNames "Komponenter/Modal/*"
```
...to only run tests for the Modal components
  • Loading branch information
unekinn committed Oct 4, 2024
1 parent 5fe5050 commit c8a4097
Show file tree
Hide file tree
Showing 15 changed files with 245 additions and 16 deletions.
10 changes: 9 additions & 1 deletion apps/storybook/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import type { Preview } from '@storybook/react';
import type { LinkProps } from '@digdir/designsystemet-react';
import { Link, List, Paragraph, Table } from '@digdir/designsystemet-react';

import { customStylesDecorator } from '../story-utils/customStylesDecorator';
import { allModes, viewportWidths } from '../story-utils/modes';
import customTheme from './customTheme';

const viewports: Record<string, object> = {};
const viewportWidths = [320, 375, 576, 768, 992, 1200, 1440];

for (const width of viewportWidths) {
viewports[`${width}px`] = {
Expand Down Expand Up @@ -143,10 +144,17 @@ const preview: Preview = {
viewport: {
viewports,
},
chromatic: {
modes: {
mobile: allModes[320],
desktop: allModes[1440],
},
},
backgrounds: {
disable: true,
},
},
decorators: [customStylesDecorator],
};

/* Add this back when https://github.com/storybookjs/storybook/issues/29189 is fixed */
Expand Down
54 changes: 54 additions & 0 deletions apps/storybook/story-utils/customStylesDecorator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Decorator } from '@storybook/react';

/**
* This decorator is used to customize the style of the root story element.
* It is useful for customizing the layout, or when you need to account
* for elements that would otherwise not be visible in Chromatic's visual snapshots.
*
* The decorator is added globally, and can be configured through `parameters.customStyles`
* at the meta or story level. E.g.
* ```ts
* parameters: {
* customStyles: {
* // These apply both in docs mode and story mode
* display: 'flex',
* gap: '8px',
* docs: {
* // These apply only when the story renders in a docs page
* height: '200px'
* },
* story: {
* // These apply only when the story is viewed individually
* height: '100vh'
* }
* }
* }
* ```
*
* By default, the decorator sets `overflow: hidden` so you can see in Storybook exactly
* what Chromatic's snapshot will be, and `padding: 1rem` to account for most overflowing
* elements like focus styles, badges etc.
*
* From Chromatic's documentation:
* > Snapshots can sometimes exclude outline and other focus styles because Chromatic
* > trims each snapshot to the dimensions of the root node of the story. To capture
* > those styles, wrap the story in a decorator that adds slight padding.
*/
export const customStylesDecorator: Decorator = (Story, ctx) => {
const { docs, story, ...style } = ctx.parameters.customStyles ?? {};

return (
<div
className='storybook-decorator'
style={{
overflow: 'hidden',
padding: '1rem',
...style,
...(ctx.viewMode === 'docs' && docs),
...(ctx.viewMode === 'story' && story),
}}
>
<Story />
</div>
);
};
6 changes: 6 additions & 0 deletions apps/storybook/story-utils/modes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { fromPairs } from 'ramda';
export const viewportWidths = [320, 375, 576, 768, 992, 1200, 1440] as const;

export const allModes = fromPairs(
viewportWidths.map((width) => [width, { viewport: { width } }]),
);
81 changes: 81 additions & 0 deletions apps/storybook/story-utils/type-extensions.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type {} from '@storybook/types';
import type { CSSProperties } from 'react';

type ChromaticViewport = {
width?: number | `${string}px`;
height?: number | `${string}px`;
};

declare module '@storybook/types' {
interface Parameters {
/**
* Set custom styling for the story's root element. The default styling is:
* ```css
* { overflow: hidden; padding: 1rem; }
* ```
*
* This is a custom parameter, implemented by `customStylesDecorator.ts`.
* */
customStyles?: CSSProperties & {
/** Styles that only apply when viewing a docs page */
docs?: CSSProperties;
/** Styles that only apply when viewing an individual story */
story?: CSSProperties;
};

/**
* Set the story layout.
*
* This is a standard Storybook parameter,
* [see the docs](https://storybook.js.org/docs/configure/story-layout)
*/
layout?: 'centered' | 'fullscreen' | 'padded';

/**
* Configure Chromatic. See [the documentation](https://www.chromatic.com/docs/config-with-story-params/).
*/
chromatic?: {
/** Disable visual snapshots at the component or story level */
disableSnapshot?: true;
/**
* By default, CSS animations are paused at the end of their animation cycle
* when tests are run in Chromatic. Setting this to false will pause animations
* at the first frame instead.
*/
pauseAnimationAtEnd?: false;
/** Delay in ms before running tests in Chromatic */
delay?: number;
/**
* Allows you to fine-tune the threshold for visual change between snapshots before
* Chromatic flags them. Must be a number from 0 to 1. 0 is the most accurate, while
* 1 is the least accurate.
*
* @default 0.063
*/
diffThreshold?: number;
/**
* Modes allow separate snapshots and baselines for a collection
* of parameters like viewport size, theme etc.
*/
modes?: Record<
string,
{
/**
* Disable a mode that has been enabled at a higher level.
* E.g. disable a global mode for a specific story.
**/
disable?: true;
/**
* The viewport to use.
*
* This parameter can either be an object with height and/or width (in px), or
* the name of one of the viewports configured in `parameters.viewports` in `.storybook/preview.tsx`
*/
viewport?: ChromaticViewport | string;
// ...any other globals from Storybook, addons or decorators which we want
// to use in modes can also be added here
}
>;
};
}
}
8 changes: 8 additions & 0 deletions chromatic.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://www.chromatic.com/config-file.schema.json",
"onlyChanged": true,
"projectId": "Project:66fe736b9d639fe6801bf130",
"storybookBaseDir": "apps/storybook",
"storybookBuildDir": "apps/storybook/dist",
"zip": true
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
"types:react": "yarn workspace @digdir/designsystemet-react types",
"types:storefront": "yarn workspace storefront types",
"version-packages": "changeset version",
"publish": "yarn build && changeset publish"
"publish": "yarn build && changeset publish",
"chromatic": "npx chromatic",
"chromatic:all": "npx chromatic --no-only-changed"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
Expand All @@ -46,6 +48,7 @@
"@vitejs/plugin-react-swc": "^3.7.0",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/expect": "^2.0.5",
"chromatic": "^11.11.0",
"copyfiles": "^2.4.1",
"prettier": "^3.3.3",
"stylelint": "^16.8.1",
Expand Down
10 changes: 6 additions & 4 deletions packages/react/src/components/Card/Card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ type Story = StoryFn<typeof Card>;
export default {
title: 'Komponenter/Card',
component: Card,
parameters: {
layout: 'fullscreen',
},
decorators: [
(Story) => (
<div
style={{
width: '90vw',
width: '100%',
maxWidth: 800,
alignItems: 'center',
display: 'grid',
gap: 'var(--ds-spacing-4)',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px , 1fr))',
justifyItems: 'center',
gridTemplateColumns: 'repeat(auto-fit, minmax(280px , 1fr))',
}}
>
<Story />
Expand All @@ -38,7 +40,7 @@ export default {
} as Meta;

export const Preview: Story = (args) => (
<Card {...args} style={{ width: 320 }}>
<Card {...args} style={{ maxWidth: '320px' }}>
<Heading size='sm' level={2}>
Card Neutral
</Heading>
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/Link/Link.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default {
title: 'Komponenter/Link',
component: Link,
parameters: {
customStyles: { padding: '2px' },
status: {
type: 'beta',
url: 'http://www.url.com/status',
Expand Down
47 changes: 38 additions & 9 deletions packages/react/src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,51 @@
import type { Meta, StoryFn } from '@storybook/react';
import { expect, within } from '@storybook/test';
import { useRef, useState } from 'react';

import { Button, Combobox, Heading, Paragraph, Textfield } from '..';

import userEvent from '@testing-library/user-event';
import { Modal } from '.';

const decorators = [
(Story: StoryFn) => (
<div style={{ margin: '2rem' }}>
<Story />
</div>
),
];

export default {
title: 'Komponenter/Modal',
component: Modal,
decorators,
parameters: {
layout: 'fullscreen',
customStyles: {
story: {
boxSizing: 'border-box',
height: '100cqh',
width: '100cqw',
},
},
chromatic: {
modes: {
mobile: {
viewport: { height: 600 },
},
desktop: {
viewport: { height: 1080 },
},
},
},
},
play: async (ctx) => {
// When not in Docs mode, automatically open the modal
const canvas = within(ctx.canvasElement);
const button = canvas.getByRole('button');
await userEvent.click(button);
// Wait for modal to fade in before running tests
const dialog = canvas.getByRole('dialog');
await new Promise<void>((resolve) => {
dialog.addEventListener('animationend', () => {
resolve();
});
});

await expect(dialog).toBeInTheDocument();
await expect(dialog).toHaveAttribute('open');
},
} as Meta;

export const Preview: StoryFn<typeof Modal> = (args) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useDebounceCallback } from './useDebounceCallback';

const meta: Meta = {
title: 'Utilities/useDebounceCallback',
parameters: { chromatic: { disableSnapshot: true } },
};

export default meta;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useMediaQuery } from './useMediaQuery';

const meta: Meta = {
title: 'Utilities/useMediaQuery',
parameters: { chromatic: { disableSnapshot: true } },
};

export default meta;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const decorators = [

const meta: Meta = {
title: 'Utilities/useSynchronizedAnimation',
parameters: { chromatic: { disableSnapshot: true } },
decorators,
};

Expand Down
9 changes: 9 additions & 0 deletions packages/react/stories/showcase.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ import classes from './showcase.module.css';

export default {
title: 'Showcase',
parameters: {
chromatic: {
modes: {
mobile: {
disable: true,
},
},
},
},
} as Meta;

export const Showcase: StoryFn = () => {
Expand Down
7 changes: 6 additions & 1 deletion packages/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"noEmit": false,
"incremental": true
},
"include": ["./src", "./stories", "declarations.d.ts"],
"include": [
"./src",
"./stories",
"declarations.d.ts",
"../../apps/storybook/story-utils/type-extensions.d.ts"
],
"rootDir": "./src"
}
20 changes: 20 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6725,6 +6725,25 @@ __metadata:
languageName: node
linkType: hard

"chromatic@npm:^11.11.0":
version: 11.11.0
resolution: "chromatic@npm:11.11.0"
peerDependencies:
"@chromatic-com/cypress": ^0.*.* || ^1.0.0
"@chromatic-com/playwright": ^0.*.* || ^1.0.0
peerDependenciesMeta:
"@chromatic-com/cypress":
optional: true
"@chromatic-com/playwright":
optional: true
bin:
chroma: dist/bin.js
chromatic: dist/bin.js
chromatic-cli: dist/bin.js
checksum: 10/5b1fd78af5b0c68b4a3d85f0886326c8bb790e3da7b69c8375a829cc9fa697c7d701d2ef2891109d0b9024102d402b163e5653b3f597d40d01baa74393ed4599
languageName: node
linkType: hard

"chromatic@npm:^11.4.0":
version: 11.5.4
resolution: "chromatic@npm:11.5.4"
Expand Down Expand Up @@ -15580,6 +15599,7 @@ __metadata:
"@vitejs/plugin-react-swc": "npm:^3.7.0"
"@vitest/coverage-v8": "npm:^2.0.5"
"@vitest/expect": "npm:^2.0.5"
chromatic: "npm:^11.11.0"
copyfiles: "npm:^2.4.1"
prettier: "npm:^3.3.3"
stylelint: "npm:^16.8.1"
Expand Down

0 comments on commit c8a4097

Please sign in to comment.