storybook-play-functions

安装量: 75
排名: #10397

安装

npx skills add https://github.com/thebushidocollective/han --skill storybook-play-functions

Storybook - Play Functions

Write automated interaction tests within stories using play functions to verify component behavior, simulate user actions, and test edge cases.

Key Concepts Play Functions

Play functions run after a story renders, allowing you to simulate user interactions:

import { within, userEvent, expect } from '@storybook/test'; import type { Meta, StoryObj } from '@storybook/react'; import { LoginForm } from './LoginForm';

const meta = { component: LoginForm, } satisfies Meta;

export default meta; type Story = StoryObj;

export const FilledForm: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

await userEvent.type(canvas.getByLabelText('Email'), 'user@example.com');
await userEvent.type(canvas.getByLabelText('Password'), 'password123');
await userEvent.click(canvas.getByRole('button', { name: /submit/i }));

await expect(canvas.getByText('Welcome!')).toBeInTheDocument();

}, };

Testing Library Integration

Storybook integrates with Testing Library for queries and interactions:

within(canvasElement) - Scopes queries to the story userEvent - Simulates realistic user interactions expect - Jest-compatible assertions waitFor - Waits for async changes Test Execution

Play functions execute:

When viewing a story in Storybook During visual regression testing In test runners for automated testing On story hot-reload during development Best Practices 1. Use Testing Library Queries

Use semantic queries to find elements:

export const SearchFlow: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

// Good - Semantic queries
const searchInput = canvas.getByRole('searchbox');
const submitButton = canvas.getByRole('button', { name: /search/i });
const results = canvas.getByRole('list', { name: /results/i });

await userEvent.type(searchInput, 'storybook');
await userEvent.click(submitButton);

await expect(results).toBeInTheDocument();

}, };

  1. Simulate Realistic User Behavior

Use userEvent for realistic interactions:

import { userEvent } from '@storybook/test';

export const FormInteraction: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

// Type naturally with delay
await userEvent.type(canvas.getByLabelText('Name'), 'John Doe', {
  delay: 100,
});

// Tab between fields
await userEvent.tab();

// Select from dropdown
await userEvent.selectOptions(
  canvas.getByLabelText('Country'),
  'United States'
);

// Click submit
await userEvent.click(canvas.getByRole('button', { name: /submit/i }));

}, };

  1. Test Async Behavior

Use waitFor for async state changes:

import { waitFor } from '@storybook/test';

export const AsyncData: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

await userEvent.click(canvas.getByRole('button', { name: /load data/i }));

// Wait for loading state
await waitFor(() => {
  expect(canvas.getByText('Loading...')).toBeInTheDocument();
});

// Wait for data to appear
await waitFor(
  () => {
    expect(canvas.getByRole('list')).toBeInTheDocument();
    expect(canvas.getAllByRole('listitem')).toHaveLength(5);
  },
  { timeout: 3000 }
);

}, };

  1. Test Error States

Validate error handling and validation:

export const ValidationErrors: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

// Submit empty form
await userEvent.click(canvas.getByRole('button', { name: /submit/i }));

// Verify error messages
await expect(canvas.getByText('Email is required')).toBeInTheDocument();
await expect(canvas.getByText('Password is required')).toBeInTheDocument();

// Fill only email
await userEvent.type(canvas.getByLabelText('Email'), 'invalid-email');
await userEvent.click(canvas.getByRole('button', { name: /submit/i }));

// Verify email validation
await expect(canvas.getByText('Email is invalid')).toBeInTheDocument();

}, };

  1. Compose Complex Scenarios

Break complex interactions into steps:

export const CheckoutFlow: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

// Step 1: Add items to cart
await userEvent.click(canvas.getByRole('button', { name: /add to cart/i }));
await expect(canvas.getByText('1 item in cart')).toBeInTheDocument();

// Step 2: Proceed to checkout
await userEvent.click(canvas.getByRole('button', { name: /checkout/i }));
await expect(canvas.getByRole('heading', { name: /checkout/i })).toBeInTheDocument();

// Step 3: Fill shipping info
await userEvent.type(canvas.getByLabelText('Address'), '123 Main St');
await userEvent.type(canvas.getByLabelText('City'), 'New York');
await userEvent.selectOptions(canvas.getByLabelText('State'), 'NY');

// Step 4: Submit order
await userEvent.click(canvas.getByRole('button', { name: /place order/i }));
await waitFor(() => {
  expect(canvas.getByText('Order confirmed!')).toBeInTheDocument();
});

}, };

Common Patterns Modal Interactions export const OpenModal: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

// Modal not visible initially
expect(canvas.queryByRole('dialog')).not.toBeInTheDocument();

// Click trigger
await userEvent.click(canvas.getByRole('button', { name: /open/i }));

// Modal appears
const modal = canvas.getByRole('dialog');
await expect(modal).toBeInTheDocument();

// Close modal
await userEvent.click(within(modal).getByRole('button', { name: /close/i }));

// Modal disappears
await waitFor(() => {
  expect(canvas.queryByRole('dialog')).not.toBeInTheDocument();
});

}, };

Keyboard Navigation export const KeyboardNav: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

const firstItem = canvas.getAllByRole('menuitem')[0];
firstItem.focus();

// Navigate with arrow keys
await userEvent.keyboard('{ArrowDown}');
await expect(canvas.getAllByRole('menuitem')[1]).toHaveFocus();

await userEvent.keyboard('{ArrowDown}');
await expect(canvas.getAllByRole('menuitem')[2]).toHaveFocus();

// Select with Enter
await userEvent.keyboard('{Enter}');
await expect(canvas.getByText('Item 3 selected')).toBeInTheDocument();

// Close with Escape
await userEvent.keyboard('{Escape}');
await waitFor(() => {
  expect(canvas.queryByRole('menu')).not.toBeInTheDocument();
});

}, };

Multi-Step Forms export const Wizard: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

// Step 1
await userEvent.type(canvas.getByLabelText('First Name'), 'John');
await userEvent.type(canvas.getByLabelText('Last Name'), 'Doe');
await userEvent.click(canvas.getByRole('button', { name: /next/i }));

// Step 2
await expect(canvas.getByText('Step 2 of 3')).toBeInTheDocument();
await userEvent.type(canvas.getByLabelText('Email'), 'john@example.com');
await userEvent.click(canvas.getByRole('button', { name: /next/i }));

// Step 3
await expect(canvas.getByText('Step 3 of 3')).toBeInTheDocument();
await userEvent.click(canvas.getByRole('checkbox', { name: /agree/i }));
await userEvent.click(canvas.getByRole('button', { name: /submit/i }));

// Success
await waitFor(() => {
  expect(canvas.getByText('Registration complete!')).toBeInTheDocument();
});

}, };

Drag and Drop export const DragDrop: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

const draggable = canvas.getByRole('button', { name: /drag me/i });
const dropzone = canvas.getByRole('region', { name: /drop zone/i });

// Perform drag and drop
await userEvent.pointer([
  { keys: '[MouseLeft>]', target: draggable },
  { coords: { x: 100, y: 100 } },
  { target: dropzone },
  { keys: '[/MouseLeft]' },
]);

await expect(canvas.getByText('Item dropped!')).toBeInTheDocument();

}, };

File Upload export const FileUpload: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

const file = new File(['content'], 'test.txt', { type: 'text/plain' });
const input = canvas.getByLabelText('Upload file');

await userEvent.upload(input, file);

await expect(canvas.getByText('test.txt')).toBeInTheDocument();
await expect(canvas.getByText('1 file selected')).toBeInTheDocument();

}, };

Advanced Patterns Reusable Play Functions // helpers.ts export async function login(canvas: ReturnType) { await userEvent.type(canvas.getByLabelText('Email'), 'user@example.com'); await userEvent.type(canvas.getByLabelText('Password'), 'password123'); await userEvent.click(canvas.getByRole('button', { name: /login/i })); }

// Story.stories.tsx export const AfterLogin: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await login(canvas);

// Test authenticated state
await expect(canvas.getByText('Welcome, User!')).toBeInTheDocument();

}, };

Step-Through Testing import { step } from '@storybook/test';

export const MultiStep: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);

await step('Fill in personal info', async () => {
  await userEvent.type(canvas.getByLabelText('Name'), 'John Doe');
  await userEvent.type(canvas.getByLabelText('Email'), 'john@example.com');
});

await step('Select preferences', async () => {
  await userEvent.click(canvas.getByLabelText('Subscribe to newsletter'));
  await userEvent.selectOptions(canvas.getByLabelText('Theme'), 'dark');
});

await step('Submit form', async () => {
  await userEvent.click(canvas.getByRole('button', { name: /submit/i }));
  await expect(canvas.getByText('Success!')).toBeInTheDocument();
});

}, };

Anti-Patterns ❌ Don't Use Direct DOM Manipulation // Bad export const Bad: Story = { play: async ({ canvasElement }) => { const input = canvasElement.querySelector('input'); input.value = 'text'; input.dispatchEvent(new Event('input')); }, };

// Good export const Good: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.type(canvas.getByRole('textbox'), 'text'); }, };

❌ Don't Forget Async/Await // Bad - Missing await export const Bad: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); userEvent.click(canvas.getByRole('button')); // Won't work! expect(canvas.getByText('Clicked')).toBeInTheDocument(); }, };

// Good export const Good: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByRole('button')); await expect(canvas.getByText('Clicked')).toBeInTheDocument(); }, };

❌ Don't Use Brittle Selectors // Bad - Fragile selectors export const Bad: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByText('Submit')); // Breaks if text changes }, };

// Good - Semantic selectors export const Good: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByRole('button', { name: /submit/i })); }, };

Related Skills storybook-story-writing: Creating stories to test with play functions storybook-args-controls: Using args to test different component states storybook-configuration: Setting up test runner for automated testing

返回排行榜