|
1 | 1 | --- |
2 | | -type: Always |
3 | | -description: Rules for writing Storybook Playwright tests in the lambda-curry/forms repository |
| 2 | +description: |
| 3 | +globs: **/*.stories.tsx,apps/docs/**/*.mdx |
| 4 | +alwaysApply: false |
4 | 5 | --- |
5 | 6 |
|
6 | | -You are an expert in Storybook, Playwright testing, React, TypeScript, Remix Hook Form, react-hook-form, @medusajs/ui, Zod validation, and the lambda-curry/forms monorepo architecture. |
| 7 | +You are an expert in Storybook, Playwright testing, React, TypeScript, react-hook-form, @medusajs/ui, Zod validation, and the lambda-curry/medusa-forms monorepo architecture. |
7 | 8 |
|
8 | 9 | # 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 | +This is a monorepo containing form components with comprehensive Storybook interaction testing. The testing setup combines Storybook's component isolation with modern interaction testing patterns using play functions and the @storybook/test utilities. |
10 | 11 |
|
11 | 12 | ## Key Technologies |
12 | | -- Storybook 8.6.7 with React and Vite |
| 13 | +- Storybook 9.0.6 with React and Vite |
| 14 | +- @storybook/test for interaction testing utilities (userEvent, expect, canvas) |
13 | 15 | - @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 (main components) |
17 | 16 | - react-hook-form + @medusajs/ui for Medusa Forms components |
| 17 | +- Zod validation for form validation testing |
18 | 18 | - Yarn 4.7.0 with corepack |
19 | 19 | - TypeScript throughout |
20 | 20 |
|
21 | | -## Project Structure |
22 | | -``` |
23 | | -lambda-curry/forms/ |
24 | | -├── apps/docs/ # Storybook app |
25 | | -│ ├── .storybook/ # Storybook configuration |
26 | | -│ ├── src/remix-hook-form/ # Remix Hook Form story files with tests |
27 | | -│ ├── src/medusa-forms/ # Medusa Forms story files with tests |
28 | | -│ ├── simple-server.js # Custom static server for testing |
29 | | -│ └── package.json # Test scripts |
30 | | -├── packages/components/ # Main component library (Remix Hook Form) |
31 | | -│ └── src/ |
32 | | -│ ├── remix-hook-form/ # Form components |
33 | | -│ └── ui/ # UI components |
34 | | -├── packages/medusa-forms/ # Medusa Forms component library |
35 | | -│ └── src/ |
36 | | -│ ├── controlled/ # Controlled components using react-hook-form |
37 | | -│ └── ui/ # UI components using @medusajs/ui |
38 | | -└── .cursor/rules/ # Cursor rules directory |
39 | | -``` |
40 | | - |
41 | | -# Environment Setup and Testing Infrastructure |
42 | | - |
43 | | -## Prerequisites |
44 | | -Before running Playwright tests locally, ensure the following setup is complete: |
45 | | - |
46 | | -### 1. System Dependencies |
47 | | -```bash |
48 | | -# Install Node.js dependencies |
49 | | -cd apps/docs |
50 | | -yarn install |
51 | | - |
52 | | -# Install Playwright browsers |
53 | | -npx playwright install |
54 | | - |
55 | | -# Install system dependencies for Playwright |
56 | | -npx playwright install-deps |
57 | | -``` |
58 | | - |
59 | | -### 2. Build Storybook Static Files |
60 | | -```bash |
61 | | -cd apps/docs |
62 | | -yarn build # Creates storybook-static directory |
63 | | -``` |
64 | | - |
65 | | -### 3. Server Setup for Local Testing |
66 | | -Due to common port conflicts in development environments, use the custom static server for local testing: |
67 | | - |
| 21 | +### Local Development Workflow |
68 | 22 | ```bash |
69 | | -# Start the custom static server (handles port conflicts) |
| 23 | +# Local development commands |
70 | 24 | cd apps/docs |
71 | | -node simple-server.js & # Runs on port 45678 |
72 | | -``` |
73 | | - |
74 | | -The `simple-server.js` file provides: |
75 | | -- Static file serving with proper MIME types |
76 | | -- CORS headers for cross-origin requests |
77 | | -- SPA routing fallback to index.html |
78 | | -- Conflict-free port allocation (45678) |
| 25 | +yarn dev # Start Storybook for development |
| 26 | +yarn test:local # Run tests against running Storybook (if available) |
79 | 27 |
|
80 | | -### 4. Run Tests Locally |
81 | | -```bash |
82 | | -# Run tests against the static server |
83 | | -cd apps/docs |
84 | | -npx test-storybook --url http://127.0.0.1:45678 |
| 28 | +# Local testing of built Storybook |
| 29 | +yarn build # Build static Storybook |
| 30 | +node simple-server.js & # Start custom server |
| 31 | +npx test-storybook --url http://127.0.0.1:45678 # Test built version |
85 | 32 | ``` |
86 | 33 |
|
87 | | -## Complete Local Testing Workflow |
| 34 | +### Codegen Testing Workflow |
| 35 | +This setup is optimized for Codegen agents and local development testing: |
88 | 36 | ```bash |
89 | | -# Full local testing workflow from scratch |
| 37 | +# Codegen workflow for testing built Storybook |
90 | 38 | cd apps/docs |
91 | | - |
92 | | -# 1. Install dependencies (if needed) |
93 | 39 | yarn install |
94 | 40 | npx playwright install |
95 | 41 | npx playwright install-deps |
96 | | - |
97 | | -# 2. Build Storybook |
98 | 42 | yarn build |
99 | | - |
100 | | -# 3. Start static server |
101 | 43 | node simple-server.js & |
102 | | - |
103 | | -# 4. Run tests |
104 | 44 | npx test-storybook --url http://127.0.0.1:45678 |
105 | | - |
106 | | -# 5. Stop server when done |
107 | | -pkill -f "simple-server.js" |
108 | 45 | ``` |
109 | 46 |
|
110 | | -## Troubleshooting Common Issues |
111 | | - |
112 | | -### Port Conflicts |
113 | | -If you encounter "EADDRINUSE" errors: |
114 | | -- **Problem**: Default ports (6006, 6007, 8080, etc.) are occupied |
115 | | -- **Solution**: Use the custom static server on port 45678 |
116 | | -- **Alternative**: Find available ports with `netstat -tulpn | grep :PORT` |
117 | | - |
118 | | -### Browser Installation Issues |
119 | | -If Playwright can't find browsers: |
120 | | -```bash |
121 | | -# Reinstall browsers |
122 | | -npx playwright install chromium |
123 | | - |
124 | | -# Install system dependencies |
125 | | -npx playwright install-deps |
| 47 | +## Project Structure |
126 | 48 | ``` |
127 | | - |
128 | | -### Build Issues |
129 | | -If Storybook build fails: |
130 | | -```bash |
131 | | -# Clean and rebuild |
132 | | -rm -rf storybook-static |
133 | | -yarn build |
| 49 | +lambda-curry/medusa-forms/ |
| 50 | +├── apps/docs/ # Storybook app |
| 51 | +│ ├── .storybook/ # Storybook configuration |
| 52 | +│ ├── src/medusa-forms/ # Medusa Forms story files with tests |
| 53 | +│ ├── simple-server.js # Custom static server for testing |
| 54 | +│ └── package.json # Test scripts |
| 55 | +├── packages/medusa-forms/ # Medusa Forms component library |
| 56 | +│ └── src/ |
| 57 | +│ ├── controlled/ # Controlled components using react-hook-form |
| 58 | +│ └── ui/ # UI components using @medusajs/ui |
| 59 | +└── .cursor/rules/ # Cursor rules directory |
134 | 60 | ``` |
135 | 61 |
|
136 | | -### Test Execution Issues |
137 | | -- **Timeout errors**: Increase timeout in test configuration |
138 | | -- **Element not found**: Ensure proper async handling with `findBy*` |
139 | | -- **Server not responding**: Verify static server is running on correct port |
140 | | - |
141 | | -# Core Principles for Storybook Testing |
| 62 | +# Modern Storybook Interaction Testing |
142 | 63 |
|
143 | | -## Story Structure Pattern |
144 | | -- Follow the three-phase testing pattern: Default state → Invalid submission → Valid submission |
145 | | -- Each story serves dual purposes: documentation AND automated tests |
146 | | -- Use play functions for comprehensive interaction testing |
147 | | -- Test complete user workflows, not isolated units |
| 64 | +## Core Principles |
| 65 | +- **Stories as Tests**: Every story can be a render test; complex stories include interaction tests |
| 66 | +- **Play Functions**: Use play functions to simulate user behavior and assert on results |
| 67 | +- **Canvas Queries**: Use Testing Library queries through the canvas parameter |
| 68 | +- **User Events**: Simulate real user interactions with userEvent |
| 69 | +- **Step Grouping**: Organize complex interactions with the step function |
| 70 | +- **Visual Debugging**: Debug tests visually in the Storybook UI |
148 | 71 |
|
149 | | -## Essential Code Elements |
150 | | -Always include these in Storybook test stories: |
151 | | - |
152 | | -### Required Imports |
| 72 | +## Essential Imports for Interaction Testing |
153 | 73 | ```typescript |
154 | | -import type { Meta, StoryContext, StoryObj } from '@storybook/react'; |
155 | | -import { expect, userEvent } from '@storybook/test'; |
156 | | -import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; |
| 74 | +import type { Meta, StoryObj } from '@storybook/react'; |
| 75 | +import { expect, userEvent, within } from '@storybook/test'; |
| 76 | +import { FormProvider, useForm } from 'react-hook-form'; |
157 | 77 | ``` |
158 | 78 |
|
159 | | -### Form Schema Setup |
| 79 | +## Story Structure with Play Functions |
| 80 | + |
| 81 | +### Basic Interaction Test Pattern |
160 | 82 | ```typescript |
161 | | -const formSchema = z.object({ |
162 | | - fieldName: z.string().min(1, 'Field is required'), |
163 | | -}); |
164 | | -type FormData = z.infer<typeof formSchema>; |
| 83 | +export const FilledForm: Story = { |
| 84 | + play: async ({ canvas, userEvent }) => { |
| 85 | + // 👇 Simulate interactions with the component |
| 86 | + await userEvent.type(canvas.getByTestId('email'), 'email@provider.com'); |
| 87 | + await userEvent.type(canvas.getByTestId('password'), 'a-random-password'); |
| 88 | + |
| 89 | + // 👇 Trigger form submission |
| 90 | + await userEvent.click(canvas.getByRole('button', { name: 'Submit' })); |
| 91 | + |
| 92 | + // 👇 Assert DOM structure |
| 93 | + await expect( |
| 94 | + canvas.getByText('Form submitted successfully!') |
| 95 | + ).toBeInTheDocument(); |
| 96 | + }, |
| 97 | +}; |
165 | 98 | ``` |
166 | 99 |
|
167 | | -### Component Wrapper Pattern |
| 100 | +### Advanced Pattern with Step Grouping |
168 | 101 | ```typescript |
169 | | -const ControlledComponentExample = () => { |
170 | | - const fetcher = useFetcher<{ message: string }>(); |
171 | | - const methods = useRemixForm<FormData>({ |
172 | | - resolver: zodResolver(formSchema), |
173 | | - defaultValues: { /* defaults */ }, |
174 | | - fetcher, |
175 | | - submitConfig: { action: '/', method: 'post' }, |
176 | | - }); |
177 | | - |
178 | | - return ( |
179 | | - <RemixFormProvider {...methods}> |
180 | | - <fetcher.Form onSubmit={methods.handleSubmit}> |
181 | | - {/* Component and form elements */} |
182 | | - </fetcher.Form> |
183 | | - </RemixFormProvider> |
184 | | - ); |
| 102 | +export const CompleteWorkflow: Story = { |
| 103 | + play: async ({ canvas, userEvent, step }) => { |
| 104 | + await step('Fill out form fields', async () => { |
| 105 | + await userEvent.type(canvas.getByLabelText('Email'), 'user@example.com'); |
| 106 | + await userEvent.type(canvas.getByLabelText('Password'), 'securepassword'); |
| 107 | + }); |
| 108 | + |
| 109 | + await step('Submit form', async () => { |
| 110 | + await userEvent.click(canvas.getByRole('button', { name: 'Submit' })); |
| 111 | + }); |
| 112 | + |
| 113 | + await step('Verify success state', async () => { |
| 114 | + await expect( |
| 115 | + canvas.getByText('Welcome! Your account is ready.') |
| 116 | + ).toBeInTheDocument(); |
| 117 | + }); |
| 118 | + }, |
185 | 119 | }; |
186 | 120 | ``` |
187 | 121 |
|
@@ -413,3 +347,109 @@ When creating or modifying Storybook tests, ensure: |
413 | 347 | - Fast feedback loop optimized for developer productivity |
414 | 348 |
|
415 | 349 | Remember: Every story should test real user workflows and serve as living documentation. Focus on behavior, not implementation details. The testing infrastructure should be reliable, fast, and easy to maintain for local development and Codegen workflows. |
| 350 | + |
| 351 | +## Canvas Queries - Testing Library Integration |
| 352 | + |
| 353 | +### Query Types and When to Use Them |
| 354 | +| Query Type | 0 Matches | 1 Match | >1 Matches | Awaited | Use Case | |
| 355 | +|------------|-----------|---------|------------|---------|----------| |
| 356 | +| `getBy*` | Throw error | Return element | Throw error | No | Elements that should exist | |
| 357 | +| `queryBy*` | Return null | Return element | Throw error | No | Elements that may not exist | |
| 358 | +| `findBy*` | Throw error | Return element | Throw error | Yes | Async elements | |
| 359 | +| `getAllBy*` | Throw error | Return array | Return array | No | Multiple elements | |
| 360 | +| `queryAllBy*` | Return [] | Return array | Return array | No | Multiple elements (optional) | |
| 361 | +| `findAllBy*` | Throw error | Return array | Return array | Yes | Multiple async elements | |
| 362 | + |
| 363 | +### Query Priority (Recommended Order) |
| 364 | +1. **ByRole** - Find elements by accessible role (most user-like) |
| 365 | +2. **ByLabelText** - Find form elements by associated label |
| 366 | +3. **ByPlaceholderText** - Find inputs by placeholder |
| 367 | +4. **ByText** - Find elements by text content |
| 368 | +5. **ByDisplayValue** - Find inputs by current value |
| 369 | +6. **ByAltText** - Find images by alt text |
| 370 | +7. **ByTitle** - Find elements by title attribute |
| 371 | +8. **ByTestId** - Find by data-testid (last resort) |
| 372 | + |
| 373 | +### Common Query Examples |
| 374 | +```typescript |
| 375 | +// Semantic queries (preferred) |
| 376 | +const submitButton = canvas.getByRole('button', { name: 'Submit' }); |
| 377 | +const emailInput = canvas.getByLabelText('Email Address'); |
| 378 | +const dropdown = canvas.getByRole('combobox', { name: 'Country' }); |
| 379 | + |
| 380 | +// Async queries for dynamic content |
| 381 | +const successMessage = await canvas.findByText('Form submitted successfully'); |
| 382 | +const errorList = await canvas.findAllByRole('alert'); |
| 383 | + |
| 384 | +// Conditional queries |
| 385 | +const optionalField = canvas.queryByLabelText('Optional Field'); |
| 386 | +expect(optionalField).not.toBeInTheDocument(); |
| 387 | +``` |
| 388 | + |
| 389 | +## UserEvent Interactions |
| 390 | + |
| 391 | +### Common UserEvent Methods |
| 392 | +```typescript |
| 393 | +// Clicking elements |
| 394 | +await userEvent.click(element); |
| 395 | +await userEvent.dblClick(element); |
| 396 | + |
| 397 | +// Typing and input |
| 398 | +await userEvent.type(input, 'text to type'); |
| 399 | +await userEvent.clear(input); |
| 400 | +await userEvent.paste(input, 'pasted text'); |
| 401 | + |
| 402 | +// Keyboard interactions |
| 403 | +await userEvent.keyboard('{Enter}'); |
| 404 | +await userEvent.tab(); |
| 405 | + |
| 406 | +// Selection |
| 407 | +await userEvent.selectOptions(select, 'option-value'); |
| 408 | +await userEvent.deselectOptions(select, 'option-value'); |
| 409 | + |
| 410 | +// File uploads |
| 411 | +await userEvent.upload(fileInput, file); |
| 412 | + |
| 413 | +// Hover interactions |
| 414 | +await userEvent.hover(element); |
| 415 | +await userEvent.unhover(element); |
| 416 | +``` |
| 417 | + |
| 418 | +### Form Interaction Best Practices |
| 419 | +```typescript |
| 420 | +// ✅ ALWAYS click before clearing inputs (for focus) |
| 421 | +await userEvent.click(input); |
| 422 | +await userEvent.clear(input); |
| 423 | +await userEvent.type(input, 'new value'); |
| 424 | + |
| 425 | +// ✅ Use proper selection for dropdowns |
| 426 | +await userEvent.click(canvas.getByRole('combobox')); |
| 427 | +await userEvent.click(canvas.getByRole('option', { name: 'Option Text' })); |
| 428 | + |
| 429 | +// ✅ Handle file uploads properly |
| 430 | +const file = new File(['content'], 'test.txt', { type: 'text/plain' }); |
| 431 | +await userEvent.upload(canvas.getByLabelText('Upload File'), file); |
| 432 | +``` |
| 433 | + |
| 434 | +## Component Wrapper Pattern for Medusa Forms |
| 435 | + |
| 436 | +### Controlled Component Setup |
| 437 | +```typescript |
| 438 | +const ControlledComponentExample = () => { |
| 439 | + const form = useForm<FormData>({ |
| 440 | + resolver: zodResolver(formSchema), |
| 441 | + defaultValues: { /* defaults */ }, |
| 442 | + }); |
| 443 | + |
| 444 | + return ( |
| 445 | + <FormProvider {...form}> |
| 446 | + <form onSubmit={form.handleSubmit((data) => console.log(data))}> |
| 447 | + {/* Medusa Forms components */} |
| 448 | + <ControlledInput name="email" label="Email" /> |
| 449 | + <ControlledSelect name="country" label="Country" options={countryOptions} /> |
| 450 | + <Button type="submit">Submit</Button> |
| 451 | + </form> |
| 452 | + </FormProvider> |
| 453 | + ); |
| 454 | +}; |
| 455 | +``` |
0 commit comments