From 98cb60d4dd34f272cb0e07db10af6d5eadc97d4f Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 19:06:34 +0000 Subject: [PATCH 1/3] Add comprehensive Storybook Playwright testing guide for Codegen - Document complete setup process from environment to CI/CD - Explain story structure patterns and best practices - Include real examples from existing codebase - Cover troubleshooting and debugging techniques - Provide reusable patterns for form testing - Detail React Router stub decorator usage - Include advanced patterns and utilities --- codegen.md | 701 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 701 insertions(+) create mode 100644 codegen.md diff --git a/codegen.md b/codegen.md new file mode 100644 index 00000000..1d981ccc --- /dev/null +++ b/codegen.md @@ -0,0 +1,701 @@ +# Storybook Playwright Testing Guide for Codegen + +This guide explains how to write and run Storybook Playwright tests in the lambda-curry/forms repository. These tests combine Storybook's component isolation with Playwright's browser automation to create comprehensive, real-world testing scenarios. + +## Table of Contents + +1. [Overview](#overview) +2. [Environment Setup](#environment-setup) +3. [Project Structure](#project-structure) +4. [Writing Story Tests](#writing-story-tests) +5. [Best Practices](#best-practices) +6. [Running Tests](#running-tests) +7. [Common Patterns](#common-patterns) +8. [Troubleshooting](#troubleshooting) +9. [CI/CD Integration](#cicd-integration) + +## Overview + +The lambda-curry/forms repository uses a sophisticated testing setup that combines: + +- **Storybook 8.6.7** for component isolation and documentation +- **@storybook/test-runner** for Playwright-powered browser automation +- **@storybook/test** for testing utilities (userEvent, expect, canvas) +- **React Router stub decorator** for form handling and navigation +- **Remix Hook Form + Zod** for form validation testing + +### Key Benefits + +- **Dual-purpose stories**: Serve as both documentation and automated tests +- **Real browser testing**: Tests run in actual Chromium browser +- **Integration testing**: Test complete user workflows, not just isolated units +- **Visual regression**: Catch UI changes and interaction issues +- **Developer experience**: Write tests alongside component documentation + +## Environment Setup + +### Prerequisites + +- Node.js (version specified in `.nvmrc`) +- Yarn 4.7.0 (managed via corepack) +- Chromium browser (installed via Playwright) + +### Initial Setup + +1. **Enable Yarn Corepack**: + ```bash + corepack enable + ``` + +2. **Install Dependencies**: + ```bash + yarn install + ``` + +3. **Install Playwright Browsers**: + ```bash + cd apps/docs + npx playwright install chromium + npx playwright install-deps # Install system dependencies + ``` + +4. **Verify Setup**: + ```bash + # Build Storybook + yarn build-storybook + + # Run a quick test + yarn test:local + ``` + +### Development Dependencies + +The key testing dependencies in `apps/docs/package.json`: + +```json +{ + "devDependencies": { + "@storybook/test-runner": "^0.22.0", + "@storybook/testing-library": "^0.2.2", + "start-server-and-test": "^2.0.11", + "http-server": "^14.1.1" + }, + "dependencies": { + "@storybook/test": "^8.6.7" + } +} +``` + +## Project Structure + +``` +lambda-curry/forms/ +├── apps/ +│ └── docs/ # Storybook app +│ ├── .storybook/ # Storybook configuration +│ │ ├── main.ts # Main config +│ │ └── preview.ts # Preview config +│ ├── src/ +│ │ ├── remix-hook-form/ # Story files +│ │ │ ├── text-field.stories.tsx +│ │ │ ├── checkbox.stories.tsx +│ │ │ └── ... +│ │ └── lib/ +│ │ └── storybook/ +│ │ └── react-router-stub.tsx # Router decorator +│ ├── package.json # Test scripts and dependencies +│ └── storybook-static/ # Built Storybook (after build) +└── packages/ + └── components/ # Component library + └── src/ + ├── remix-hook-form/ # Form components + └── ui/ # UI components +``` + +## Writing Story Tests + +### Basic Story Structure + +Every story file follows this pattern: + +```typescript +import type { Meta, StoryContext, StoryObj } from '@storybook/react'; +import { expect, userEvent } from '@storybook/test'; +import { YourComponent } from '@lambdacurry/forms/path/to/component'; +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; + +// Component setup with form schema +const formSchema = z.object({ + fieldName: z.string().min(1, 'Field is required'), +}); + +type FormData = z.infer; + +// Component wrapper with form logic +const ControlledComponentExample = () => { + const fetcher = useFetcher<{ message: string }>(); + const methods = useRemixForm({ + resolver: zodResolver(formSchema), + defaultValues: { fieldName: '' }, + fetcher, + submitConfig: { action: '/', method: 'post' }, + }); + + return ( + + + + + {fetcher.data?.message && ( +

{fetcher.data.message}

+ )} +
+
+ ); +}; + +// Form submission handler +const handleFormSubmission = async (request: Request) => { + const { data, errors } = await getValidatedFormData( + request, + zodResolver(formSchema) + ); + + if (errors) return { errors }; + return { message: 'Form submitted successfully' }; +}; + +// Story metadata +const meta: Meta = { + title: 'Category/ComponentName', + component: YourComponent, + parameters: { layout: 'centered' }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +// Test functions +const testDefaultValues = ({ canvas }: StoryContext) => { + const input = canvas.getByLabelText('Field Label'); + expect(input).toHaveValue(''); +}; + +const testInvalidSubmission = async ({ canvas }: StoryContext) => { + const submitButton = canvas.getByRole('button', { name: 'Submit' }); + await userEvent.click(submitButton); + + const errorMessage = await canvas.findByText('Field is required'); + expect(errorMessage).toBeInTheDocument(); +}; + +const testValidSubmission = async ({ canvas }: StoryContext) => { + const input = canvas.getByLabelText('Field Label'); + const submitButton = canvas.getByRole('button', { name: 'Submit' }); + + await userEvent.type(input, 'Valid input'); + await userEvent.click(submitButton); + + const successMessage = await canvas.findByText('Form submitted successfully'); + expect(successMessage).toBeInTheDocument(); +}; + +// Story with tests +export const Default: Story = { + play: async (storyContext) => { + testDefaultValues(storyContext); + await testInvalidSubmission(storyContext); + await testValidSubmission(storyContext); + }, + decorators: [ + withReactRouterStubDecorator({ + routes: [{ + path: '/', + Component: ControlledComponentExample, + action: async ({ request }) => handleFormSubmission(request), + }], + }), + ], +}; +``` + +### Key Components Explained + +#### 1. Story Context and Canvas + +```typescript +const testFunction = ({ canvas }: StoryContext) => { + // canvas provides access to testing-library queries + const element = canvas.getByLabelText('Label Text'); + const button = canvas.getByRole('button', { name: 'Submit' }); +}; +``` + +#### 2. User Interactions + +```typescript +// Always click before clearing inputs +await userEvent.click(input); +await userEvent.clear(input); +await userEvent.type(input, 'New value'); + +// Click buttons +await userEvent.click(submitButton); + +// Select options +await userEvent.selectOptions(select, 'option-value'); +``` + +#### 3. Async Assertions + +```typescript +// Use findBy* for elements that appear after async operations +const errorMessage = await canvas.findByText('Error message'); +expect(errorMessage).toBeInTheDocument(); + +// Add delays for complex async operations +await new Promise(resolve => setTimeout(resolve, 1000)); +``` + +#### 4. React Router Stub Decorator + +```typescript +withReactRouterStubDecorator({ + routes: [{ + path: '/', + Component: YourComponentWrapper, + action: async ({ request }) => { + // Handle form submission + const formData = await request.formData(); + // Process and return response + return { message: 'Success' }; + }, + }], + initialPath: '/', // Optional: default path +}) +``` + +## Best Practices + +### 1. Test Structure Pattern + +Follow the three-phase testing pattern: + +```typescript +export const ComponentStory: Story = { + play: async (storyContext) => { + // Phase 1: Test initial state + testDefaultValues(storyContext); + + // Phase 2: Test validation/error states + await testInvalidSubmission(storyContext); + + // Phase 3: Test success scenarios + await testValidSubmission(storyContext); + }, +}; +``` + +### 2. User Interaction Best Practices + +```typescript +// ✅ Good: Click before clearing +await userEvent.click(input); +await userEvent.clear(input); +await userEvent.type(input, 'new value'); + +// ❌ Bad: Clear without clicking first +await userEvent.clear(input); // May not work reliably +``` + +### 3. Async Testing + +```typescript +// ✅ Good: Use findBy* for async elements +const message = await canvas.findByText('Success message'); +expect(message).toBeInTheDocument(); + +// ❌ Bad: Use getBy* for async elements +const message = canvas.getByText('Success message'); // May fail +``` + +### 4. Error Handling + +```typescript +// Test both client-side and server-side validation +const testValidation = async ({ canvas }: StoryContext) => { + // Test client-side validation + await userEvent.click(submitButton); + expect(await canvas.findByText('Required field')).toBeInTheDocument(); + + // Test server-side validation + await userEvent.type(input, 'invalid-value'); + await userEvent.click(submitButton); + expect(await canvas.findByText('Server error')).toBeInTheDocument(); +}; +``` + +### 5. Component Isolation + +```typescript +// ✅ Good: Each story tests one component scenario +export const DefaultState: Story = { /* ... */ }; +export const ErrorState: Story = { /* ... */ }; +export const LoadingState: Story = { /* ... */ }; + +// ❌ Bad: One story testing multiple unrelated scenarios +export const AllScenarios: Story = { /* ... */ }; +``` + +## Running Tests + +### Local Development + +```bash +# Navigate to docs app +cd apps/docs + +# Run tests with server startup (recommended) +yarn test + +# Run tests against already running Storybook +yarn test:local + +# Build and serve Storybook manually +yarn build-storybook +yarn serve # Serves on http://localhost:6006 +``` + +### Test Scripts Explained + +From `apps/docs/package.json`: + +```json +{ + "scripts": { + "dev": "storybook dev -p 6006", + "build-storybook": "storybook build", + "serve": "http-server ./storybook-static -p 6006 -s", + "test": "start-server-and-test serve http://127.0.0.1:6006 'test-storybook --url http://127.0.0.1:6006'", + "test:local": "test-storybook" + } +} +``` + +- `test`: Builds Storybook, starts server, runs tests, then stops server +- `test:local`: Runs tests against already running Storybook (faster for development) + +### Development Workflow + +1. **Start Storybook in development mode**: + ```bash + yarn dev + ``` + +2. **Write/modify stories** in `src/remix-hook-form/` + +3. **Run tests in another terminal**: + ```bash + yarn test:local + ``` + +4. **Debug failing tests** by viewing Storybook UI at http://localhost:6006 + +## Common Patterns + +### 1. Form Component Testing + +```typescript +// Standard form testing pattern +const testFormComponent = async ({ canvas }: StoryContext) => { + const input = canvas.getByLabelText('Field Label'); + const submitButton = canvas.getByRole('button', { name: 'Submit' }); + + // Test empty submission + await userEvent.click(submitButton); + expect(await canvas.findByText('Field is required')).toBeInTheDocument(); + + // Test valid submission + await userEvent.click(input); + await userEvent.type(input, 'valid input'); + await userEvent.click(submitButton); + expect(await canvas.findByText('Success')).toBeInTheDocument(); +}; +``` + +### 2. Multi-Step Form Testing + +```typescript +const testMultiStepForm = async ({ canvas }: StoryContext) => { + // Step 1 + await userEvent.type(canvas.getByLabelText('First Name'), 'John'); + await userEvent.click(canvas.getByRole('button', { name: 'Next' })); + + // Step 2 + await userEvent.type(canvas.getByLabelText('Email'), 'john@example.com'); + await userEvent.click(canvas.getByRole('button', { name: 'Submit' })); + + // Verify completion + expect(await canvas.findByText('Form completed')).toBeInTheDocument(); +}; +``` + +### 3. Conditional Field Testing + +```typescript +const testConditionalFields = async ({ canvas }: StoryContext) => { + const trigger = canvas.getByLabelText('Show advanced options'); + + // Initially hidden + expect(canvas.queryByLabelText('Advanced Field')).not.toBeInTheDocument(); + + // Show conditional field + await userEvent.click(trigger); + expect(canvas.getByLabelText('Advanced Field')).toBeInTheDocument(); +}; +``` + +### 4. Data Table Testing + +```typescript +const testDataTable = async ({ canvas }: StoryContext) => { + // Test sorting + const nameHeader = canvas.getByRole('button', { name: 'Name' }); + await userEvent.click(nameHeader); + + // Test filtering + const filterInput = canvas.getByPlaceholderText('Filter names...'); + await userEvent.type(filterInput, 'John'); + + // Test pagination + const nextButton = canvas.getByRole('button', { name: 'Next page' }); + await userEvent.click(nextButton); +}; +``` + +## Troubleshooting + +### Common Issues and Solutions + +#### 1. Port Already in Use + +```bash +# Error: EADDRINUSE: address already in use 0.0.0.0:6006 +# Solution: Kill existing processes +pkill -f "6006" +# Or use a different port +yarn serve --port 6007 +``` + +#### 2. Playwright Browser Issues + +```bash +# Error: Host system is missing dependencies +# Solution: Install system dependencies +npx playwright install-deps + +# Error: Browser not found +# Solution: Reinstall browsers +npx playwright install chromium +``` + +#### 3. Test Timeouts + +```typescript +// Increase timeout for slow operations +await new Promise(resolve => setTimeout(resolve, 2000)); + +// Use waitFor for complex conditions +await waitFor(async () => { + expect(canvas.getByText('Expected text')).toBeInTheDocument(); +}, { timeout: 10000 }); +``` + +#### 4. Element Not Found + +```typescript +// ✅ Use findBy* for async elements +const element = await canvas.findByText('Async text'); + +// ✅ Use queryBy* to check non-existence +expect(canvas.queryByText('Should not exist')).not.toBeInTheDocument(); + +// ✅ Wait for element to be ready +await userEvent.click(input); +await userEvent.clear(input); +await userEvent.type(input, 'text'); +``` + +#### 5. Form Submission Issues + +```typescript +// Ensure form is properly set up with fetcher +const fetcher = useFetcher(); +const methods = useRemixForm({ + // ... config + fetcher, // Important: pass fetcher + submitConfig: { + action: '/', + method: 'post', + }, +}); + +// Use fetcher.Form, not regular form + + {/* form content */} + +``` + +### Debugging Tips + +1. **Use Storybook UI**: View stories at http://localhost:6006 to see visual state +2. **Add console.logs**: Debug test execution flow +3. **Use browser dev tools**: Inspect elements during test execution +4. **Check network tab**: Verify form submissions and responses +5. **Use Playwright debug mode**: Add `--debug` flag to test command + +## CI/CD Integration + +### GitHub Actions Example + +```yaml +name: Storybook Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: yarn install --immutable + + - name: Install Playwright + run: | + cd apps/docs + npx playwright install chromium + npx playwright install-deps + + - name: Build Storybook + run: yarn build-storybook + + - name: Run Storybook tests + run: | + cd apps/docs + yarn test +``` + +### Test Configuration + +The test runner uses default configuration but can be customized with a `.test-runner.js` file: + +```javascript +// apps/docs/.test-runner.js (optional) +module.exports = { + // Customize test runner behavior + browsers: ['chromium'], + testTimeout: 30000, + // Add custom test configuration +}; +``` + +## Advanced Patterns + +### 1. Custom Test Utilities + +Create reusable test utilities: + +```typescript +// apps/docs/src/lib/test-utils.ts +export const fillForm = async (canvas: any, fields: Record) => { + for (const [label, value] of Object.entries(fields)) { + const input = canvas.getByLabelText(label); + await userEvent.click(input); + await userEvent.clear(input); + await userEvent.type(input, value); + } +}; + +export const submitForm = async (canvas: any) => { + const submitButton = canvas.getByRole('button', { name: /submit/i }); + await userEvent.click(submitButton); +}; +``` + +### 2. Story Composition + +Reuse story components across different scenarios: + +```typescript +// Base story setup +const BaseStory = { + decorators: [withReactRouterStubDecorator(/* config */)], +}; + +// Extend for different scenarios +export const DefaultState: Story = { + ...BaseStory, + play: testDefaultBehavior, +}; + +export const ErrorState: Story = { + ...BaseStory, + play: testErrorHandling, +}; +``` + +### 3. Mock Data Management + +```typescript +// Create mock data factories +const createMockUser = (overrides = {}) => ({ + id: 'user-1', + name: 'John Doe', + email: 'john@example.com', + ...overrides, +}); + +// Use in stories +const users = Array.from({ length: 10 }, (_, i) => + createMockUser({ id: `user-${i}`, name: `User ${i}` }) +); +``` + +## Conclusion + +This testing setup provides a powerful foundation for ensuring component quality and preventing regressions. The combination of Storybook's component isolation with Playwright's browser automation creates comprehensive tests that closely mirror real user interactions. + +### Key Takeaways + +1. **Write tests as you write stories** - they serve dual purposes +2. **Follow the established patterns** - consistency makes maintenance easier +3. **Test user workflows, not implementation details** - focus on behavior +4. **Use async assertions** - forms and interactions are often asynchronous +5. **Keep tests focused** - one story should test one scenario well + +### Next Steps + +1. Start with simple form components using the patterns shown +2. Gradually add more complex scenarios (multi-step forms, data tables) +3. Create reusable test utilities for common patterns +4. Set up CI/CD integration for automated testing +5. Expand testing to cover edge cases and accessibility + +For questions or issues, refer to the troubleshooting section or check the existing story files for examples. + From 2accc809355151c0927b3b6ed11eb666f3db0fb5 Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 19:41:38 +0000 Subject: [PATCH 2/3] Add Cursor rules for Storybook Playwright testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert comprehensive testing guide into .cursorrules format - Include essential code patterns and best practices - Define three-phase testing structure (default → invalid → valid) - Specify React Router stub decorator usage - Document user interaction patterns and async testing - Include verification steps and common pitfalls - Provide reusable code snippets and examples - Follow Cursor rules best practices with clear structure --- .cursorrules | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 .cursorrules diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 00000000..5d90fe4e --- /dev/null +++ b/.cursorrules @@ -0,0 +1,240 @@ +You are an expert in Storybook, Playwright testing, React, TypeScript, Remix Hook Form, Zod validation, and the lambda-curry/forms monorepo architecture. + +# Project Context +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. + +## Key Technologies +- Storybook 8.6.7 with React and Vite +- @storybook/test-runner for Playwright automation +- @storybook/test for testing utilities (userEvent, expect, canvas) +- React Router stub decorator for form handling +- Remix Hook Form + Zod for validation testing +- Yarn 4.7.0 with corepack +- TypeScript throughout + +## Project Structure +``` +lambda-curry/forms/ +├── apps/docs/ # Storybook app +│ ├── .storybook/ # Storybook configuration +│ ├── src/remix-hook-form/ # Story files with tests +│ └── package.json # Test scripts +├── packages/components/ # Component library +│ └── src/ +│ ├── remix-hook-form/ # Form components +│ └── ui/ # UI components +└── codegen.md # Testing guide +``` + +# Core Principles for Storybook Testing + +## Story Structure Pattern +- Follow the three-phase testing pattern: Default state → Invalid submission → Valid submission +- Each story serves dual purposes: documentation AND automated tests +- Use play functions for comprehensive interaction testing +- Test complete user workflows, not isolated units + +## Essential Code Elements +Always include these in Storybook test stories: + +### Required Imports +```typescript +import type { Meta, StoryContext, StoryObj } from '@storybook/react'; +import { expect, userEvent } from '@storybook/test'; +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; +``` + +### Form Schema Setup +```typescript +const formSchema = z.object({ + fieldName: z.string().min(1, 'Field is required'), +}); +type FormData = z.infer; +``` + +### Component Wrapper Pattern +```typescript +const ControlledComponentExample = () => { + const fetcher = useFetcher<{ message: string }>(); + const methods = useRemixForm({ + resolver: zodResolver(formSchema), + defaultValues: { /* defaults */ }, + fetcher, + submitConfig: { action: '/', method: 'post' }, + }); + + return ( + + + {/* Component and form elements */} + + + ); +}; +``` + +## Testing Patterns + +### User Interaction Best Practices +```typescript +// ✅ ALWAYS click before clearing inputs +await userEvent.click(input); +await userEvent.clear(input); +await userEvent.type(input, 'new value'); + +// ✅ Use findBy* for async elements +const message = await canvas.findByText('Success message'); +expect(message).toBeInTheDocument(); + +// ✅ Use queryBy* to check non-existence +expect(canvas.queryByText('Should not exist')).not.toBeInTheDocument(); +``` + +### Three-Phase Test Structure +```typescript +export const Default: Story = { + play: async (storyContext) => { + // Phase 1: Test initial state + testDefaultValues(storyContext); + + // Phase 2: Test validation/error states + await testInvalidSubmission(storyContext); + + // Phase 3: Test success scenarios + await testValidSubmission(storyContext); + }, + decorators: [withReactRouterStubDecorator({ /* config */ })], +}; +``` + +### React Router Stub Decorator +```typescript +withReactRouterStubDecorator({ + routes: [{ + path: '/', + Component: ControlledComponentExample, + action: async ({ request }) => { + const { data, errors } = await getValidatedFormData( + request, + zodResolver(formSchema) + ); + if (errors) return { errors }; + return { message: 'Form submitted successfully' }; + }, + }], +}) +``` + +## Deprecated Patterns - DO NOT USE + +❌ **Never use getBy* for async elements** +```typescript +// BAD - will fail for async content +const message = canvas.getByText('Success message'); +``` + +❌ **Never clear inputs without clicking first** +```typescript +// BAD - unreliable +await userEvent.clear(input); +``` + +❌ **Never use regular forms instead of fetcher.Form** +```typescript +// BAD - won't work with React Router stub +
+``` + +❌ **Never test multiple unrelated scenarios in one story** +```typescript +// BAD - stories should be focused +export const AllScenarios: Story = { /* testing everything */ }; +``` + +## File Naming and Organization +- Story files: `component-name.stories.tsx` in `apps/docs/src/remix-hook-form/` +- Use kebab-case for file names +- Group related test functions together +- Export individual test functions for reusability + +## Testing Utilities and Helpers + +### Canvas Queries +```typescript +// Form elements +const input = canvas.getByLabelText('Field Label'); +const button = canvas.getByRole('button', { name: 'Submit' }); +const select = canvas.getByRole('combobox'); + +// Async content +const errorMessage = await canvas.findByText('Error message'); +const successMessage = await canvas.findByText('Success'); +``` + +### Common Test Patterns +```typescript +// Form validation testing +const testInvalidSubmission = async ({ canvas }: StoryContext) => { + const submitButton = canvas.getByRole('button', { name: 'Submit' }); + await userEvent.click(submitButton); + expect(await canvas.findByText('Field is required')).toBeInTheDocument(); +}; + +// Conditional field testing +const testConditionalFields = async ({ canvas }: StoryContext) => { + const trigger = canvas.getByLabelText('Show advanced options'); + expect(canvas.queryByLabelText('Advanced Field')).not.toBeInTheDocument(); + await userEvent.click(trigger); + expect(canvas.getByLabelText('Advanced Field')).toBeInTheDocument(); +}; +``` + +## Environment Setup Requirements +- Node.js (version in .nvmrc) +- Yarn 4.7.0 via corepack +- Playwright browsers: `npx playwright install chromium` + +## Test Commands +```bash +# Development workflow +cd apps/docs +yarn dev # Start Storybook +yarn test:local # Run tests against running Storybook + +# CI/Production +yarn test # Build, serve, and test +``` + +## Error Handling and Debugging +- Use Storybook UI at http://localhost:6006 for visual debugging +- Add console.logs for test execution flow debugging +- Use browser dev tools during test execution +- Check network tab for form submission verification + +## Verification Steps +When creating or modifying Storybook tests, ensure: + +1. ✅ Story includes all three test phases (default, invalid, valid) +2. ✅ Uses React Router stub decorator for form handling +3. ✅ Follows click-before-clear pattern for inputs +4. ✅ Uses findBy* for async assertions +5. ✅ Tests both client-side and server-side validation +6. ✅ Includes proper error handling and success scenarios +7. ✅ Story serves as both documentation and test +8. ✅ Component is properly isolated and focused + +## Common Pitfalls to Avoid +- Port conflicts (6006 already in use) - kill existing processes +- Missing Playwright system dependencies - run `npx playwright install-deps` +- Test timeouts - add delays for complex async operations +- Element not found errors - ensure proper async handling +- Form submission issues - verify fetcher setup and decorator usage + +## Advanced Patterns +- Create reusable test utilities in `apps/docs/src/lib/test-utils.ts` +- Use story composition for different scenarios +- Implement mock data factories for consistent test data +- Group related stories with shared decorators + +Remember: Every story should test real user workflows and serve as living documentation. Focus on behavior, not implementation details. + From 58a76a8c2ce6545d1dfb9930cf9e40b7b79b6a2e Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 19:45:26 +0000 Subject: [PATCH 3/3] Migrate to new Cursor rules directory pattern - Remove legacy .cursorrules file and codegen.md - Create .cursor/rules/storybook-testing.mdc following new best practices - Use MDC format with proper metadata (type: Always) - Organize rules in proper directory structure for better maintainability - Follow Cursor's recommended approach for project-specific rules --- .../rules/storybook-testing.mdc | 7 +- codegen.md | 701 ------------------ 2 files changed, 6 insertions(+), 702 deletions(-) rename .cursorrules => .cursor/rules/storybook-testing.mdc (97%) delete mode 100644 codegen.md diff --git a/.cursorrules b/.cursor/rules/storybook-testing.mdc similarity index 97% rename from .cursorrules rename to .cursor/rules/storybook-testing.mdc index 5d90fe4e..a238996d 100644 --- a/.cursorrules +++ b/.cursor/rules/storybook-testing.mdc @@ -1,3 +1,8 @@ +--- +type: Always +description: Rules for writing Storybook Playwright tests in the lambda-curry/forms repository +--- + You are an expert in Storybook, Playwright testing, React, TypeScript, Remix Hook Form, Zod validation, and the lambda-curry/forms monorepo architecture. # Project Context @@ -23,7 +28,7 @@ lambda-curry/forms/ │ └── src/ │ ├── remix-hook-form/ # Form components │ └── ui/ # UI components -└── codegen.md # Testing guide +└── .cursor/rules/ # Cursor rules directory ``` # Core Principles for Storybook Testing diff --git a/codegen.md b/codegen.md deleted file mode 100644 index 1d981ccc..00000000 --- a/codegen.md +++ /dev/null @@ -1,701 +0,0 @@ -# Storybook Playwright Testing Guide for Codegen - -This guide explains how to write and run Storybook Playwright tests in the lambda-curry/forms repository. These tests combine Storybook's component isolation with Playwright's browser automation to create comprehensive, real-world testing scenarios. - -## Table of Contents - -1. [Overview](#overview) -2. [Environment Setup](#environment-setup) -3. [Project Structure](#project-structure) -4. [Writing Story Tests](#writing-story-tests) -5. [Best Practices](#best-practices) -6. [Running Tests](#running-tests) -7. [Common Patterns](#common-patterns) -8. [Troubleshooting](#troubleshooting) -9. [CI/CD Integration](#cicd-integration) - -## Overview - -The lambda-curry/forms repository uses a sophisticated testing setup that combines: - -- **Storybook 8.6.7** for component isolation and documentation -- **@storybook/test-runner** for Playwright-powered browser automation -- **@storybook/test** for testing utilities (userEvent, expect, canvas) -- **React Router stub decorator** for form handling and navigation -- **Remix Hook Form + Zod** for form validation testing - -### Key Benefits - -- **Dual-purpose stories**: Serve as both documentation and automated tests -- **Real browser testing**: Tests run in actual Chromium browser -- **Integration testing**: Test complete user workflows, not just isolated units -- **Visual regression**: Catch UI changes and interaction issues -- **Developer experience**: Write tests alongside component documentation - -## Environment Setup - -### Prerequisites - -- Node.js (version specified in `.nvmrc`) -- Yarn 4.7.0 (managed via corepack) -- Chromium browser (installed via Playwright) - -### Initial Setup - -1. **Enable Yarn Corepack**: - ```bash - corepack enable - ``` - -2. **Install Dependencies**: - ```bash - yarn install - ``` - -3. **Install Playwright Browsers**: - ```bash - cd apps/docs - npx playwright install chromium - npx playwright install-deps # Install system dependencies - ``` - -4. **Verify Setup**: - ```bash - # Build Storybook - yarn build-storybook - - # Run a quick test - yarn test:local - ``` - -### Development Dependencies - -The key testing dependencies in `apps/docs/package.json`: - -```json -{ - "devDependencies": { - "@storybook/test-runner": "^0.22.0", - "@storybook/testing-library": "^0.2.2", - "start-server-and-test": "^2.0.11", - "http-server": "^14.1.1" - }, - "dependencies": { - "@storybook/test": "^8.6.7" - } -} -``` - -## Project Structure - -``` -lambda-curry/forms/ -├── apps/ -│ └── docs/ # Storybook app -│ ├── .storybook/ # Storybook configuration -│ │ ├── main.ts # Main config -│ │ └── preview.ts # Preview config -│ ├── src/ -│ │ ├── remix-hook-form/ # Story files -│ │ │ ├── text-field.stories.tsx -│ │ │ ├── checkbox.stories.tsx -│ │ │ └── ... -│ │ └── lib/ -│ │ └── storybook/ -│ │ └── react-router-stub.tsx # Router decorator -│ ├── package.json # Test scripts and dependencies -│ └── storybook-static/ # Built Storybook (after build) -└── packages/ - └── components/ # Component library - └── src/ - ├── remix-hook-form/ # Form components - └── ui/ # UI components -``` - -## Writing Story Tests - -### Basic Story Structure - -Every story file follows this pattern: - -```typescript -import type { Meta, StoryContext, StoryObj } from '@storybook/react'; -import { expect, userEvent } from '@storybook/test'; -import { YourComponent } from '@lambdacurry/forms/path/to/component'; -import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; - -// Component setup with form schema -const formSchema = z.object({ - fieldName: z.string().min(1, 'Field is required'), -}); - -type FormData = z.infer; - -// Component wrapper with form logic -const ControlledComponentExample = () => { - const fetcher = useFetcher<{ message: string }>(); - const methods = useRemixForm({ - resolver: zodResolver(formSchema), - defaultValues: { fieldName: '' }, - fetcher, - submitConfig: { action: '/', method: 'post' }, - }); - - return ( - - - - - {fetcher.data?.message && ( -

{fetcher.data.message}

- )} -
-
- ); -}; - -// Form submission handler -const handleFormSubmission = async (request: Request) => { - const { data, errors } = await getValidatedFormData( - request, - zodResolver(formSchema) - ); - - if (errors) return { errors }; - return { message: 'Form submitted successfully' }; -}; - -// Story metadata -const meta: Meta = { - title: 'Category/ComponentName', - component: YourComponent, - parameters: { layout: 'centered' }, - tags: ['autodocs'], -}; - -export default meta; -type Story = StoryObj; - -// Test functions -const testDefaultValues = ({ canvas }: StoryContext) => { - const input = canvas.getByLabelText('Field Label'); - expect(input).toHaveValue(''); -}; - -const testInvalidSubmission = async ({ canvas }: StoryContext) => { - const submitButton = canvas.getByRole('button', { name: 'Submit' }); - await userEvent.click(submitButton); - - const errorMessage = await canvas.findByText('Field is required'); - expect(errorMessage).toBeInTheDocument(); -}; - -const testValidSubmission = async ({ canvas }: StoryContext) => { - const input = canvas.getByLabelText('Field Label'); - const submitButton = canvas.getByRole('button', { name: 'Submit' }); - - await userEvent.type(input, 'Valid input'); - await userEvent.click(submitButton); - - const successMessage = await canvas.findByText('Form submitted successfully'); - expect(successMessage).toBeInTheDocument(); -}; - -// Story with tests -export const Default: Story = { - play: async (storyContext) => { - testDefaultValues(storyContext); - await testInvalidSubmission(storyContext); - await testValidSubmission(storyContext); - }, - decorators: [ - withReactRouterStubDecorator({ - routes: [{ - path: '/', - Component: ControlledComponentExample, - action: async ({ request }) => handleFormSubmission(request), - }], - }), - ], -}; -``` - -### Key Components Explained - -#### 1. Story Context and Canvas - -```typescript -const testFunction = ({ canvas }: StoryContext) => { - // canvas provides access to testing-library queries - const element = canvas.getByLabelText('Label Text'); - const button = canvas.getByRole('button', { name: 'Submit' }); -}; -``` - -#### 2. User Interactions - -```typescript -// Always click before clearing inputs -await userEvent.click(input); -await userEvent.clear(input); -await userEvent.type(input, 'New value'); - -// Click buttons -await userEvent.click(submitButton); - -// Select options -await userEvent.selectOptions(select, 'option-value'); -``` - -#### 3. Async Assertions - -```typescript -// Use findBy* for elements that appear after async operations -const errorMessage = await canvas.findByText('Error message'); -expect(errorMessage).toBeInTheDocument(); - -// Add delays for complex async operations -await new Promise(resolve => setTimeout(resolve, 1000)); -``` - -#### 4. React Router Stub Decorator - -```typescript -withReactRouterStubDecorator({ - routes: [{ - path: '/', - Component: YourComponentWrapper, - action: async ({ request }) => { - // Handle form submission - const formData = await request.formData(); - // Process and return response - return { message: 'Success' }; - }, - }], - initialPath: '/', // Optional: default path -}) -``` - -## Best Practices - -### 1. Test Structure Pattern - -Follow the three-phase testing pattern: - -```typescript -export const ComponentStory: Story = { - play: async (storyContext) => { - // Phase 1: Test initial state - testDefaultValues(storyContext); - - // Phase 2: Test validation/error states - await testInvalidSubmission(storyContext); - - // Phase 3: Test success scenarios - await testValidSubmission(storyContext); - }, -}; -``` - -### 2. User Interaction Best Practices - -```typescript -// ✅ Good: Click before clearing -await userEvent.click(input); -await userEvent.clear(input); -await userEvent.type(input, 'new value'); - -// ❌ Bad: Clear without clicking first -await userEvent.clear(input); // May not work reliably -``` - -### 3. Async Testing - -```typescript -// ✅ Good: Use findBy* for async elements -const message = await canvas.findByText('Success message'); -expect(message).toBeInTheDocument(); - -// ❌ Bad: Use getBy* for async elements -const message = canvas.getByText('Success message'); // May fail -``` - -### 4. Error Handling - -```typescript -// Test both client-side and server-side validation -const testValidation = async ({ canvas }: StoryContext) => { - // Test client-side validation - await userEvent.click(submitButton); - expect(await canvas.findByText('Required field')).toBeInTheDocument(); - - // Test server-side validation - await userEvent.type(input, 'invalid-value'); - await userEvent.click(submitButton); - expect(await canvas.findByText('Server error')).toBeInTheDocument(); -}; -``` - -### 5. Component Isolation - -```typescript -// ✅ Good: Each story tests one component scenario -export const DefaultState: Story = { /* ... */ }; -export const ErrorState: Story = { /* ... */ }; -export const LoadingState: Story = { /* ... */ }; - -// ❌ Bad: One story testing multiple unrelated scenarios -export const AllScenarios: Story = { /* ... */ }; -``` - -## Running Tests - -### Local Development - -```bash -# Navigate to docs app -cd apps/docs - -# Run tests with server startup (recommended) -yarn test - -# Run tests against already running Storybook -yarn test:local - -# Build and serve Storybook manually -yarn build-storybook -yarn serve # Serves on http://localhost:6006 -``` - -### Test Scripts Explained - -From `apps/docs/package.json`: - -```json -{ - "scripts": { - "dev": "storybook dev -p 6006", - "build-storybook": "storybook build", - "serve": "http-server ./storybook-static -p 6006 -s", - "test": "start-server-and-test serve http://127.0.0.1:6006 'test-storybook --url http://127.0.0.1:6006'", - "test:local": "test-storybook" - } -} -``` - -- `test`: Builds Storybook, starts server, runs tests, then stops server -- `test:local`: Runs tests against already running Storybook (faster for development) - -### Development Workflow - -1. **Start Storybook in development mode**: - ```bash - yarn dev - ``` - -2. **Write/modify stories** in `src/remix-hook-form/` - -3. **Run tests in another terminal**: - ```bash - yarn test:local - ``` - -4. **Debug failing tests** by viewing Storybook UI at http://localhost:6006 - -## Common Patterns - -### 1. Form Component Testing - -```typescript -// Standard form testing pattern -const testFormComponent = async ({ canvas }: StoryContext) => { - const input = canvas.getByLabelText('Field Label'); - const submitButton = canvas.getByRole('button', { name: 'Submit' }); - - // Test empty submission - await userEvent.click(submitButton); - expect(await canvas.findByText('Field is required')).toBeInTheDocument(); - - // Test valid submission - await userEvent.click(input); - await userEvent.type(input, 'valid input'); - await userEvent.click(submitButton); - expect(await canvas.findByText('Success')).toBeInTheDocument(); -}; -``` - -### 2. Multi-Step Form Testing - -```typescript -const testMultiStepForm = async ({ canvas }: StoryContext) => { - // Step 1 - await userEvent.type(canvas.getByLabelText('First Name'), 'John'); - await userEvent.click(canvas.getByRole('button', { name: 'Next' })); - - // Step 2 - await userEvent.type(canvas.getByLabelText('Email'), 'john@example.com'); - await userEvent.click(canvas.getByRole('button', { name: 'Submit' })); - - // Verify completion - expect(await canvas.findByText('Form completed')).toBeInTheDocument(); -}; -``` - -### 3. Conditional Field Testing - -```typescript -const testConditionalFields = async ({ canvas }: StoryContext) => { - const trigger = canvas.getByLabelText('Show advanced options'); - - // Initially hidden - expect(canvas.queryByLabelText('Advanced Field')).not.toBeInTheDocument(); - - // Show conditional field - await userEvent.click(trigger); - expect(canvas.getByLabelText('Advanced Field')).toBeInTheDocument(); -}; -``` - -### 4. Data Table Testing - -```typescript -const testDataTable = async ({ canvas }: StoryContext) => { - // Test sorting - const nameHeader = canvas.getByRole('button', { name: 'Name' }); - await userEvent.click(nameHeader); - - // Test filtering - const filterInput = canvas.getByPlaceholderText('Filter names...'); - await userEvent.type(filterInput, 'John'); - - // Test pagination - const nextButton = canvas.getByRole('button', { name: 'Next page' }); - await userEvent.click(nextButton); -}; -``` - -## Troubleshooting - -### Common Issues and Solutions - -#### 1. Port Already in Use - -```bash -# Error: EADDRINUSE: address already in use 0.0.0.0:6006 -# Solution: Kill existing processes -pkill -f "6006" -# Or use a different port -yarn serve --port 6007 -``` - -#### 2. Playwright Browser Issues - -```bash -# Error: Host system is missing dependencies -# Solution: Install system dependencies -npx playwright install-deps - -# Error: Browser not found -# Solution: Reinstall browsers -npx playwright install chromium -``` - -#### 3. Test Timeouts - -```typescript -// Increase timeout for slow operations -await new Promise(resolve => setTimeout(resolve, 2000)); - -// Use waitFor for complex conditions -await waitFor(async () => { - expect(canvas.getByText('Expected text')).toBeInTheDocument(); -}, { timeout: 10000 }); -``` - -#### 4. Element Not Found - -```typescript -// ✅ Use findBy* for async elements -const element = await canvas.findByText('Async text'); - -// ✅ Use queryBy* to check non-existence -expect(canvas.queryByText('Should not exist')).not.toBeInTheDocument(); - -// ✅ Wait for element to be ready -await userEvent.click(input); -await userEvent.clear(input); -await userEvent.type(input, 'text'); -``` - -#### 5. Form Submission Issues - -```typescript -// Ensure form is properly set up with fetcher -const fetcher = useFetcher(); -const methods = useRemixForm({ - // ... config - fetcher, // Important: pass fetcher - submitConfig: { - action: '/', - method: 'post', - }, -}); - -// Use fetcher.Form, not regular form - - {/* form content */} - -``` - -### Debugging Tips - -1. **Use Storybook UI**: View stories at http://localhost:6006 to see visual state -2. **Add console.logs**: Debug test execution flow -3. **Use browser dev tools**: Inspect elements during test execution -4. **Check network tab**: Verify form submissions and responses -5. **Use Playwright debug mode**: Add `--debug` flag to test command - -## CI/CD Integration - -### GitHub Actions Example - -```yaml -name: Storybook Tests - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - - name: Enable Corepack - run: corepack enable - - - name: Install dependencies - run: yarn install --immutable - - - name: Install Playwright - run: | - cd apps/docs - npx playwright install chromium - npx playwright install-deps - - - name: Build Storybook - run: yarn build-storybook - - - name: Run Storybook tests - run: | - cd apps/docs - yarn test -``` - -### Test Configuration - -The test runner uses default configuration but can be customized with a `.test-runner.js` file: - -```javascript -// apps/docs/.test-runner.js (optional) -module.exports = { - // Customize test runner behavior - browsers: ['chromium'], - testTimeout: 30000, - // Add custom test configuration -}; -``` - -## Advanced Patterns - -### 1. Custom Test Utilities - -Create reusable test utilities: - -```typescript -// apps/docs/src/lib/test-utils.ts -export const fillForm = async (canvas: any, fields: Record) => { - for (const [label, value] of Object.entries(fields)) { - const input = canvas.getByLabelText(label); - await userEvent.click(input); - await userEvent.clear(input); - await userEvent.type(input, value); - } -}; - -export const submitForm = async (canvas: any) => { - const submitButton = canvas.getByRole('button', { name: /submit/i }); - await userEvent.click(submitButton); -}; -``` - -### 2. Story Composition - -Reuse story components across different scenarios: - -```typescript -// Base story setup -const BaseStory = { - decorators: [withReactRouterStubDecorator(/* config */)], -}; - -// Extend for different scenarios -export const DefaultState: Story = { - ...BaseStory, - play: testDefaultBehavior, -}; - -export const ErrorState: Story = { - ...BaseStory, - play: testErrorHandling, -}; -``` - -### 3. Mock Data Management - -```typescript -// Create mock data factories -const createMockUser = (overrides = {}) => ({ - id: 'user-1', - name: 'John Doe', - email: 'john@example.com', - ...overrides, -}); - -// Use in stories -const users = Array.from({ length: 10 }, (_, i) => - createMockUser({ id: `user-${i}`, name: `User ${i}` }) -); -``` - -## Conclusion - -This testing setup provides a powerful foundation for ensuring component quality and preventing regressions. The combination of Storybook's component isolation with Playwright's browser automation creates comprehensive tests that closely mirror real user interactions. - -### Key Takeaways - -1. **Write tests as you write stories** - they serve dual purposes -2. **Follow the established patterns** - consistency makes maintenance easier -3. **Test user workflows, not implementation details** - focus on behavior -4. **Use async assertions** - forms and interactions are often asynchronous -5. **Keep tests focused** - one story should test one scenario well - -### Next Steps - -1. Start with simple form components using the patterns shown -2. Gradually add more complex scenarios (multi-step forms, data tables) -3. Create reusable test utilities for common patterns -4. Set up CI/CD integration for automated testing -5. Expand testing to cover edge cases and accessibility - -For questions or issues, refer to the troubleshooting section or check the existing story files for examples. -