com um clique
storybook
Storybook stories and interaction tests. Use for story, stories, storybook, chromatic, visual test, interaction test, play function, component test
Menu
Storybook stories and interaction tests. Use for story, stories, storybook, chromatic, visual test, interaction test, play function, component test
Git workflow for branches, commits, and PRs. Use for commit, branch, pr, pull request, conventional, push, feat, fix, chore, merge, rebase
TypeScript and unicorn linting patterns. Use for typescript, array, for-of, reduce, forEach, throw, catch, modern js, es modules, string, number, error handling
Zod v4 schema validation. Use for zod, schema, validation, parse, safeParse, infer, coerce, transform, refine, z.object, z.string, z.email, z.url
Better Auth authentication. Use for auth, login, logout, session, user, signup, register, protect, middleware, password, oauth, social
Drizzle ORM + PostgreSQL database layer. Use for db, database, query, schema, table, migrate, sql, postgres, drizzle, model, relation
React error boundaries + fallback UIs. Use for error, catch, boundary, fallback, recovery, crash, ErrorBoundary, errorComponent, reset
| name | storybook |
| description | Storybook stories and interaction tests. Use for story, stories, storybook, chromatic, visual test, interaction test, play function, component test |
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { Button } from './button';
const meta = {
component: Button,
parameters: { layout: 'centered' },
tags: ['autodocs'],
title: 'Components/Button',
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { children: 'Button', variant: 'default' },
};
Prefer args for simple props:
export const Default: Story = {
args: { children: 'Button', variant: 'default' },
};
Use render for complex layouts or multiple components:
export const AllVariants: Story = {
render: () => (
<div className="flex gap-4">
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
</div>
),
};
Import from storybook/test:
import { expect, fn, userEvent } from 'storybook/test';
export const ClickInteraction: Story = {
args: { children: 'Click me', onPress: fn() },
play: async ({ args, canvas }) => {
const button = canvas.getByRole('button');
await userEvent.click(button);
await expect(args.onPress).toHaveBeenCalledTimes(1);
},
};
export const KeyboardInteraction: Story = {
args: { children: 'Press Enter', onPress: fn() },
play: async ({ args, canvas }) => {
const button = canvas.getByRole('button');
await userEvent.tab();
await expect(button).toHaveFocus();
await userEvent.keyboard('{Enter}');
await expect(args.onPress).toHaveBeenCalledTimes(1);
},
};
export const DisabledInteraction: Story = {
args: { children: 'Disabled', isDisabled: true, onPress: fn() },
play: async ({ args, canvas }) => {
const button = canvas.getByRole('button');
await expect(button).toBeDisabled();
await expect(args.onPress).not.toHaveBeenCalled();
},
};
export const IconOnlyAccessibility: Story = {
args: {
'aria-label': 'Add new item',
children: <RiAddLine data-slot="icon" />,
size: 'icon',
},
play: async ({ canvas }) => {
const button = canvas.getByRole('button', { name: 'Add new item' });
await expect(button).toBeInTheDocument();
await expect(button).toHaveAccessibleName('Add new item');
},
};
Use accessible queries (Testing Library best practices):
| Priority | Query | Use For |
|---|---|---|
| 1 | getByRole | Buttons, inputs, links |
| 2 | getByLabelText | Form fields |
| 3 | getByPlaceholderText | Inputs without labels |
| 4 | getByText | Non-interactive text |
| 5 | getByTestId | Last resort |
| Assertion | Use For |
|---|---|
toBeInTheDocument() | Element exists |
toBeVisible() | Element is visible |
toBeDisabled() | Button/input disabled |
toHaveFocus() | Element has focus |
toHaveAccessibleName() | Accessible label |
toHaveBeenCalledTimes(n) | Callback count |
not.toHaveBeenCalled() | Callback not fired |
Light/dark mode testing via @storybook/addon-themes:
// .storybook/modes.ts
export const allModes = {
dark: { theme: 'dark' },
light: { theme: 'light' },
} as const;
// .storybook/preview.ts
parameters: {
chromatic: {
modes: {
dark: allModes.dark,
light: allModes.light,
},
},
}
export const AnimatedComponent: Story = {
parameters: {
chromatic: {
delay: 300, // Wait before capturing
disableSnapshot: true, // Skip this story
pauseAnimationAtEnd: true,
diffThreshold: 0.2, // Sensitivity (0-1)
},
},
};
const meta = {
parameters: {
chromatic: { viewports: [320, 768, 1200] },
},
} satisfies Meta<typeof Component>;
export const MobileOnly: Story = {
parameters: { chromatic: { viewports: [320] } },
};
<div className="chromatic-ignore"><Clock /></div>
<video data-chromatic="ignore" src="video.mp4" />
// Or via parameters
parameters: {
chromatic: {
ignoreSelectors: ['.timestamp', '.random-avatar'],
},
}
const meta = {
argTypes: {
variant: {
control: 'select',
options: ['default', 'secondary', 'destructive', 'outline', 'ghost'],
},
size: { control: 'select', options: ['sm', 'default', 'lg', 'icon'] },
isDisabled: { control: 'boolean' },
},
component: Button,
} satisfies Meta<typeof Button>;
Reuse interactions across stories:
export const FilledForm: Story = {
play: async ({ canvas, userEvent }) => {
await userEvent.type(canvas.getByLabelText('Email'), 'test@example.com');
await userEvent.type(canvas.getByLabelText('Password'), 'password123');
},
};
export const SubmittedForm: Story = {
play: async (context) => {
const { canvas, userEvent } = context;
await FilledForm.play?.(context);
await userEvent.click(canvas.getByRole('button', { name: 'Submit' }));
},
};
Use assertions instead of fixed delays:
export const AsyncContent: Story = {
play: async ({ canvas }) => {
await expect(
await canvas.findByRole('button', { name: 'Submit' }),
).toBeVisible();
},
};
Use findBy* queries (with built-in waiting) for async elements.
In .storybook/preview.ts:
parameters: {
a11y: {
// 'todo' - show violations in test UI only
// 'error' - fail CI on violations
// 'off' - skip a11y checks
test: 'error',
},
}
| Mistake | Correct Pattern |
|---|---|
Using render for simple args | Use args object instead |
getByTestId as first choice | Use getByRole or getByLabelText |
Fixed delay for async | Use findBy* or assertions |
Missing fn() for callbacks | Add onPress: fn() for interaction |
| Hardcoded viewport in story | Use Chromatic viewports parameter |
Explore agent to find existing story patternsui-library-designer agent