Kicking Ass With Playwright
By the end of this workshop, you'll be able to:
- Write stories in Component Story Format
- Test components with Storybook interaction tests
- Use Storybook in development workflows
Writing stories in Component Story Format (CSF)
Storybook Cascade
.storybook/preview.js
src/some-component.stories.tsx - Meta
src/some-component.stories.tsx - Story
Every story
Groups of stories
One story
Component Story Format
import { Meta, StoryObj } from "@storybook/react"
import { MyComponent } from "./MyComponent"
// Create Meta
const meta: Meta<typeof MyComponent> = {
title: 'Path/To/MyComponent',
component: MyComponent,
};
export default meta;
// Create a story type
type Story = StoryObj<typeof MyComponent>;
// Write stories
export const SomeName: Story = {
name: "Some Name",
args: {
someProp: "Some value",
},
};
1. Create Meta
2. Create Story type
3. Write stories
// Don't import Story
import { Meta, Story } from "@storybook/react"
import { MyComponent } from "./MyComponent"
// `as` loses the type check
export default {
title: 'Path/To/MyComponent',
component: MyComponent,
} as Meta;
// Use types and render methods instead of templates
const Template: Story<MyComponentProps> = (...args) => (
<MyComponent {...args} />
)
// No need to bind anymore
const SomeName = Template.bind({})
SomeName.name = "Some Name"
SomeName.args = {
someProp: "Some value",
}
// Story exports should go inline
export { Default }
import { Meta, StoryObj } from "@storybook/react"
import { MyComponent } from "./MyComponent"
// Create Meta
const meta: Meta<typeof MyComponent> = {
title: 'Path/To/MyComponent',
component: MyComponent,
};
export default meta;
// Create a story type
type Story = StoryObj<typeof MyComponent>;
// Write stories
export const SomeName: Story = {
name: "Some Name",
args: {
someProp: "Some value",
},
};
Old Way
New Way
Before and After
Title Hierarchy
const meta: Meta<typeof MyComponent> = {
title: 'Components/ContactCardCheckout/States',
component: MyComponent,
};
- States
- Actions
- Other
- Components
- Organisms
- Pages
Component Name
Sharing Attributes
export const Default: Story = {
args: {
heading: "Some Heading",
},
}
export const Valid: Story = {
args: {
...Default.args,
isValid: true,
},
}
export const Invalid: Story = {
args: {
...Default.args,
isValid: false,
},
}
Just spread!
Template
`render()`
export const SomeName: Story = {
name: "Some Name",
render: (args) => {
const [someState, setSomeState] = useState("")
return (
<MyComponent
{...args}
someState={someState}
setSomeState={setSomeState}
/>
)
},
args: {
someProp: "Some value",
},
};
Any JavaScripty stuff has to happen here!
Testing components in Storybook
Interaction Tests
- Go in `.play()` functions in stories
- Uses Jest (Vitest) and RTL helpers/assertions
- Run in your browser locally, run in Playwright in CI
- Basic rendering tests are free
States vs. Interactions
- One per interesting state
- Static
- Different props combinations
- Starting point for tests & experiments
- Play functions usually not needed
- One per interesting failure
- Reuse states
- Things that change over time
- Assert at boundary
Testing Syntax
export const SomeInteraction = {
...SomeState
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await click(canvas.getByRole("button", { name: /Add/i }));
expect(args.someCallback).toHaveBeenCalledWith("Hello, world!")
},
}
Arrange
Assert
Act
- `canvas` is your screen
- Don't always need an `expect`
Test Helpers
// Use these instead of the Jest/RTL/User-Event versions
import { expect, within, fn, userEvent, waitFor } from "@storybook/test";
// These are interaction helpers, use these instead of DOM events
const { hover, click, dblClick, type, keyboard } = userEvent;
Steps
import { clickAdd } from "../steps";
export const SomeStory = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
// Inline
await step("Click add", () => {
await click(await canvas.findByRole("button", { name: /Add/i }));
});
// Reuse a step
await step("Click add", clickAdd(canvas));
},
}
Steps are a way to make interaction tests more semantic and composeable
Fixtures
const fixtures: Record<string, Contact> = {
primaryHomeOwner: {
contactId: "1",
customerFirstName: "Primary",
customerLastName: "Homeowner",
customerPrimaryPhone: "(123) 456-7890",
customerEmail: "primary.homeowner@email.com",
primary: true,
roles: ["Homeowner"],
derivedRoles: ["Homeowner"],
},
// Others
};
export const createFixture = (key: keyof typeof fixtures) =>
structuredClone(fixtures[key]);
Fixtures make data more semantic and reusable
Using Storybook
Addons Panel
- Test Runner
- a11y Checks
- Controls
Typical Storybook Workflow
- Build the non-interactive UI
- Stories for each interesting state
- Build the interactive elements
- Test is: What would you do to verify?
- Refactor code and tests
- States, steps, fixtures
playwright-testing
By Kyle Coberly
playwright-testing
- 92