|
| 1 | +--- |
| 2 | +type: Always |
| 3 | +description: Rules for writing Storybook Playwright tests in the lambda-curry/forms repository |
| 4 | +--- |
| 5 | + |
| 6 | +You are an expert in Storybook, Playwright testing, React, TypeScript, Remix Hook Form, Zod validation, and the lambda-curry/forms monorepo architecture. |
| 7 | + |
| 8 | +# Project Context |
| 9 | +This is a monorepo containing form components with comprehensive Storybook Playwright testing. The testing setup combines Storybook's component isolation with Playwright's browser automation to create real-world testing scenarios. |
| 10 | + |
| 11 | +## Key Technologies |
| 12 | +- Storybook 8.6.7 with React and Vite |
| 13 | +- @storybook/test-runner for Playwright automation |
| 14 | +- @storybook/test for testing utilities (userEvent, expect, canvas) |
| 15 | +- React Router stub decorator for form handling |
| 16 | +- Remix Hook Form + Zod for validation testing |
| 17 | +- Yarn 4.7.0 with corepack |
| 18 | +- TypeScript throughout |
| 19 | + |
| 20 | +## Project Structure |
| 21 | +``` |
| 22 | +lambda-curry/forms/ |
| 23 | +├── apps/docs/ # Storybook app |
| 24 | +│ ├── .storybook/ # Storybook configuration |
| 25 | +│ ├── src/remix-hook-form/ # Story files with tests |
| 26 | +│ └── package.json # Test scripts |
| 27 | +├── packages/components/ # Component library |
| 28 | +│ └── src/ |
| 29 | +│ ├── remix-hook-form/ # Form components |
| 30 | +│ └── ui/ # UI components |
| 31 | +└── .cursor/rules/ # Cursor rules directory |
| 32 | +``` |
| 33 | + |
| 34 | +# Core Principles for Storybook Testing |
| 35 | + |
| 36 | +## Story Structure Pattern |
| 37 | +- Follow the three-phase testing pattern: Default state → Invalid submission → Valid submission |
| 38 | +- Each story serves dual purposes: documentation AND automated tests |
| 39 | +- Use play functions for comprehensive interaction testing |
| 40 | +- Test complete user workflows, not isolated units |
| 41 | + |
| 42 | +## Essential Code Elements |
| 43 | +Always include these in Storybook test stories: |
| 44 | + |
| 45 | +### Required Imports |
| 46 | +```typescript |
| 47 | +import type { Meta, StoryContext, StoryObj } from '@storybook/react'; |
| 48 | +import { expect, userEvent } from '@storybook/test'; |
| 49 | +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; |
| 50 | +``` |
| 51 | + |
| 52 | +### Form Schema Setup |
| 53 | +```typescript |
| 54 | +const formSchema = z.object({ |
| 55 | + fieldName: z.string().min(1, 'Field is required'), |
| 56 | +}); |
| 57 | +type FormData = z.infer<typeof formSchema>; |
| 58 | +``` |
| 59 | + |
| 60 | +### Component Wrapper Pattern |
| 61 | +```typescript |
| 62 | +const ControlledComponentExample = () => { |
| 63 | + const fetcher = useFetcher<{ message: string }>(); |
| 64 | + const methods = useRemixForm<FormData>({ |
| 65 | + resolver: zodResolver(formSchema), |
| 66 | + defaultValues: { /* defaults */ }, |
| 67 | + fetcher, |
| 68 | + submitConfig: { action: '/', method: 'post' }, |
| 69 | + }); |
| 70 | + |
| 71 | + return ( |
| 72 | + <RemixFormProvider {...methods}> |
| 73 | + <fetcher.Form onSubmit={methods.handleSubmit}> |
| 74 | + {/* Component and form elements */} |
| 75 | + </fetcher.Form> |
| 76 | + </RemixFormProvider> |
| 77 | + ); |
| 78 | +}; |
| 79 | +``` |
| 80 | + |
| 81 | +## Testing Patterns |
| 82 | + |
| 83 | +### User Interaction Best Practices |
| 84 | +```typescript |
| 85 | +// ✅ ALWAYS click before clearing inputs |
| 86 | +await userEvent.click(input); |
| 87 | +await userEvent.clear(input); |
| 88 | +await userEvent.type(input, 'new value'); |
| 89 | + |
| 90 | +// ✅ Use findBy* for async elements |
| 91 | +const message = await canvas.findByText('Success message'); |
| 92 | +expect(message).toBeInTheDocument(); |
| 93 | + |
| 94 | +// ✅ Use queryBy* to check non-existence |
| 95 | +expect(canvas.queryByText('Should not exist')).not.toBeInTheDocument(); |
| 96 | +``` |
| 97 | + |
| 98 | +### Three-Phase Test Structure |
| 99 | +```typescript |
| 100 | +export const Default: Story = { |
| 101 | + play: async (storyContext) => { |
| 102 | + // Phase 1: Test initial state |
| 103 | + testDefaultValues(storyContext); |
| 104 | + |
| 105 | + // Phase 2: Test validation/error states |
| 106 | + await testInvalidSubmission(storyContext); |
| 107 | + |
| 108 | + // Phase 3: Test success scenarios |
| 109 | + await testValidSubmission(storyContext); |
| 110 | + }, |
| 111 | + decorators: [withReactRouterStubDecorator({ /* config */ })], |
| 112 | +}; |
| 113 | +``` |
| 114 | + |
| 115 | +### React Router Stub Decorator |
| 116 | +```typescript |
| 117 | +withReactRouterStubDecorator({ |
| 118 | + routes: [{ |
| 119 | + path: '/', |
| 120 | + Component: ControlledComponentExample, |
| 121 | + action: async ({ request }) => { |
| 122 | + const { data, errors } = await getValidatedFormData<FormData>( |
| 123 | + request, |
| 124 | + zodResolver(formSchema) |
| 125 | + ); |
| 126 | + if (errors) return { errors }; |
| 127 | + return { message: 'Form submitted successfully' }; |
| 128 | + }, |
| 129 | + }], |
| 130 | +}) |
| 131 | +``` |
| 132 | + |
| 133 | +## Deprecated Patterns - DO NOT USE |
| 134 | + |
| 135 | +❌ **Never use getBy* for async elements** |
| 136 | +```typescript |
| 137 | +// BAD - will fail for async content |
| 138 | +const message = canvas.getByText('Success message'); |
| 139 | +``` |
| 140 | + |
| 141 | +❌ **Never clear inputs without clicking first** |
| 142 | +```typescript |
| 143 | +// BAD - unreliable |
| 144 | +await userEvent.clear(input); |
| 145 | +``` |
| 146 | + |
| 147 | +❌ **Never use regular forms instead of fetcher.Form** |
| 148 | +```typescript |
| 149 | +// BAD - won't work with React Router stub |
| 150 | +<form onSubmit={methods.handleSubmit}> |
| 151 | +``` |
| 152 | + |
| 153 | +❌ **Never test multiple unrelated scenarios in one story** |
| 154 | +```typescript |
| 155 | +// BAD - stories should be focused |
| 156 | +export const AllScenarios: Story = { /* testing everything */ }; |
| 157 | +``` |
| 158 | + |
| 159 | +## File Naming and Organization |
| 160 | +- Story files: `component-name.stories.tsx` in `apps/docs/src/remix-hook-form/` |
| 161 | +- Use kebab-case for file names |
| 162 | +- Group related test functions together |
| 163 | +- Export individual test functions for reusability |
| 164 | + |
| 165 | +## Testing Utilities and Helpers |
| 166 | + |
| 167 | +### Canvas Queries |
| 168 | +```typescript |
| 169 | +// Form elements |
| 170 | +const input = canvas.getByLabelText('Field Label'); |
| 171 | +const button = canvas.getByRole('button', { name: 'Submit' }); |
| 172 | +const select = canvas.getByRole('combobox'); |
| 173 | + |
| 174 | +// Async content |
| 175 | +const errorMessage = await canvas.findByText('Error message'); |
| 176 | +const successMessage = await canvas.findByText('Success'); |
| 177 | +``` |
| 178 | + |
| 179 | +### Common Test Patterns |
| 180 | +```typescript |
| 181 | +// Form validation testing |
| 182 | +const testInvalidSubmission = async ({ canvas }: StoryContext) => { |
| 183 | + const submitButton = canvas.getByRole('button', { name: 'Submit' }); |
| 184 | + await userEvent.click(submitButton); |
| 185 | + expect(await canvas.findByText('Field is required')).toBeInTheDocument(); |
| 186 | +}; |
| 187 | + |
| 188 | +// Conditional field testing |
| 189 | +const testConditionalFields = async ({ canvas }: StoryContext) => { |
| 190 | + const trigger = canvas.getByLabelText('Show advanced options'); |
| 191 | + expect(canvas.queryByLabelText('Advanced Field')).not.toBeInTheDocument(); |
| 192 | + await userEvent.click(trigger); |
| 193 | + expect(canvas.getByLabelText('Advanced Field')).toBeInTheDocument(); |
| 194 | +}; |
| 195 | +``` |
| 196 | + |
| 197 | +## Environment Setup Requirements |
| 198 | +- Node.js (version in .nvmrc) |
| 199 | +- Yarn 4.7.0 via corepack |
| 200 | +- Playwright browsers: `npx playwright install chromium` |
| 201 | + |
| 202 | +## Test Commands |
| 203 | +```bash |
| 204 | +# Development workflow |
| 205 | +cd apps/docs |
| 206 | +yarn dev # Start Storybook |
| 207 | +yarn test:local # Run tests against running Storybook |
| 208 | + |
| 209 | +# CI/Production |
| 210 | +yarn test # Build, serve, and test |
| 211 | +``` |
| 212 | + |
| 213 | +## Error Handling and Debugging |
| 214 | +- Use Storybook UI at http://localhost:6006 for visual debugging |
| 215 | +- Add console.logs for test execution flow debugging |
| 216 | +- Use browser dev tools during test execution |
| 217 | +- Check network tab for form submission verification |
| 218 | + |
| 219 | +## Verification Steps |
| 220 | +When creating or modifying Storybook tests, ensure: |
| 221 | + |
| 222 | +1. ✅ Story includes all three test phases (default, invalid, valid) |
| 223 | +2. ✅ Uses React Router stub decorator for form handling |
| 224 | +3. ✅ Follows click-before-clear pattern for inputs |
| 225 | +4. ✅ Uses findBy* for async assertions |
| 226 | +5. ✅ Tests both client-side and server-side validation |
| 227 | +6. ✅ Includes proper error handling and success scenarios |
| 228 | +7. ✅ Story serves as both documentation and test |
| 229 | +8. ✅ Component is properly isolated and focused |
| 230 | + |
| 231 | +## Common Pitfalls to Avoid |
| 232 | +- Port conflicts (6006 already in use) - kill existing processes |
| 233 | +- Missing Playwright system dependencies - run `npx playwright install-deps` |
| 234 | +- Test timeouts - add delays for complex async operations |
| 235 | +- Element not found errors - ensure proper async handling |
| 236 | +- Form submission issues - verify fetcher setup and decorator usage |
| 237 | + |
| 238 | +## Advanced Patterns |
| 239 | +- Create reusable test utilities in `apps/docs/src/lib/test-utils.ts` |
| 240 | +- Use story composition for different scenarios |
| 241 | +- Implement mock data factories for consistent test data |
| 242 | +- Group related stories with shared decorators |
| 243 | + |
| 244 | +Remember: Every story should test real user workflows and serve as living documentation. Focus on behavior, not implementation details. |
| 245 | + |
0 commit comments