diff --git a/frontend/docs/MSW_AUTO_GENERATION.md b/frontend/docs/MSW_AUTO_GENERATION.md index 6accc7f..677fa6a 100644 --- a/frontend/docs/MSW_AUTO_GENERATION.md +++ b/frontend/docs/MSW_AUTO_GENERATION.md @@ -28,11 +28,13 @@ src/mocks/handlers/ ### 1. Automatic Generation When you run: + ```bash npm run generate:api ``` The system: + 1. Fetches `/api/v1/openapi.json` from backend 2. Generates TypeScript API client (`src/lib/api/generated/`) 3. **NEW:** Generates MSW handlers (`src/mocks/handlers/generated.ts`) @@ -42,12 +44,14 @@ The system: The generator (`scripts/generate-msw-handlers.ts`) creates handlers with: **Smart Response Logic:** + - **Auth endpoints** → Use `validateCredentials()` and `setCurrentUser()` - **User endpoints** → Use `currentUser` and mock data - **Admin endpoints** → Check `is_superuser` + return paginated data - **Generic endpoints** → Return success response **Example Generated Handler:** + ```typescript /** * Login @@ -91,10 +95,7 @@ export const overrideHandlers = [ http.post(`${API_BASE_URL}/api/v1/auth/login`, async ({ request }) => { // 10% chance of rate limit if (Math.random() < 0.1) { - return HttpResponse.json( - { detail: 'Too many login attempts' }, - { status: 429 } - ); + return HttpResponse.json({ detail: 'Too many login attempts' }, { status: 429 }); } // Fall through to generated handler }), @@ -105,10 +106,7 @@ export const overrideHandlers = [ // Custom validation logic if (body.email.endsWith('@blocked.com')) { - return HttpResponse.json( - { detail: 'Email domain not allowed' }, - { status: 400 } - ); + return HttpResponse.json({ detail: 'Email domain not allowed' }, { status: 400 }); } // Fall through to generated handler @@ -124,6 +122,7 @@ Overrides are applied FIRST, so they take precedence over generated handlers. ### ✅ Zero Manual Work **Before:** + ```bash # Backend adds new endpoint # 1. Run npm run generate:api @@ -134,6 +133,7 @@ Overrides are applied FIRST, so they take precedence over generated handlers. ``` **After:** + ```bash # Backend adds new endpoint npm run generate:api # Done! MSW auto-synced @@ -160,6 +160,7 @@ import { adminStats } from '../data/stats'; ### ✅ Batteries Included Generated handlers include: + - ✅ Network delays (300ms - realistic UX) - ✅ Auth checks (401/403 responses) - ✅ Pagination support @@ -218,6 +219,7 @@ If generated handler doesn't fit your needs: 3. **Override takes precedence** automatically Example: + ```typescript // overrides.ts export const overrideHandlers = [ @@ -227,10 +229,7 @@ export const overrideHandlers = [ // Simulate 2FA requirement for admin users if (body.email.includes('admin') && !body.two_factor_code) { - return HttpResponse.json( - { detail: 'Two-factor authentication required' }, - { status: 403 } - ); + return HttpResponse.json({ detail: 'Two-factor authentication required' }, { status: 403 }); } // Fall through to generated handler @@ -254,6 +253,7 @@ export const demoUser: UserResponse = { ``` **To update:** + 1. Edit `data/*.ts` files 2. Handlers automatically use updated data 3. No regeneration needed! @@ -263,6 +263,7 @@ export const demoUser: UserResponse = { The generator (`scripts/generate-msw-handlers.ts`) does: 1. **Parse OpenAPI spec** + ```typescript const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8')); ``` @@ -284,12 +285,14 @@ The generator (`scripts/generate-msw-handlers.ts`) does: ### Generated handler doesn't work **Check:** + 1. Is backend running? (`npm run generate:api` requires backend) 2. Check console for `[MSW]` warnings 3. Verify `generated.ts` exists and has your endpoint 4. Check path parameters match exactly **Debug:** + ```bash # See what endpoints were generated cat src/mocks/handlers/generated.ts | grep "http\." diff --git a/frontend/scripts/sync-msw-with-openapi.md b/frontend/scripts/sync-msw-with-openapi.md index 1cd2b5c..c160990 100644 --- a/frontend/scripts/sync-msw-with-openapi.md +++ b/frontend/scripts/sync-msw-with-openapi.md @@ -1,6 +1,7 @@ # Keeping MSW Handlers Synced with OpenAPI Spec ## Problem + MSW handlers can drift out of sync with the backend API as it evolves. ## Solution Options @@ -60,6 +61,7 @@ Add to `package.json`: Our MSW handlers currently cover: **Auth Endpoints:** + - POST `/api/v1/auth/register` - POST `/api/v1/auth/login` - POST `/api/v1/auth/refresh` @@ -70,6 +72,7 @@ Our MSW handlers currently cover: - POST `/api/v1/auth/change-password` **User Endpoints:** + - GET `/api/v1/users/me` - PATCH `/api/v1/users/me` - DELETE `/api/v1/users/me` @@ -80,6 +83,7 @@ Our MSW handlers currently cover: - DELETE `/api/v1/sessions/:id` **Admin Endpoints:** + - GET `/api/v1/admin/stats` - GET `/api/v1/admin/users` - GET `/api/v1/admin/users/:id` diff --git a/frontend/src/components/demo/DemoModeBanner.tsx b/frontend/src/components/demo/DemoModeBanner.tsx index bc43b6c..62b112b 100644 --- a/frontend/src/components/demo/DemoModeBanner.tsx +++ b/frontend/src/components/demo/DemoModeBanner.tsx @@ -11,11 +11,7 @@ import { useState } from 'react'; import config from '@/config/app.config'; import { Sparkles } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; export function DemoModeBanner() { // Only show in demo mode diff --git a/frontend/src/components/docs/CodeBlock.tsx b/frontend/src/components/docs/CodeBlock.tsx index 92d3658..eb1ffc7 100644 --- a/frontend/src/components/docs/CodeBlock.tsx +++ b/frontend/src/components/docs/CodeBlock.tsx @@ -55,11 +55,7 @@ export function CodeBlock({ children, className, title }: CodeBlockProps) { onClick={handleCopy} aria-label="Copy code" > - {copied ? ( - - ) : ( - - )} + {copied ? : } diff --git a/frontend/src/components/docs/MarkdownContent.tsx b/frontend/src/components/docs/MarkdownContent.tsx index 8b3db7b..d1561a4 100644 --- a/frontend/src/components/docs/MarkdownContent.tsx +++ b/frontend/src/components/docs/MarkdownContent.tsx @@ -136,7 +136,10 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) { return ( {children} @@ -147,7 +150,10 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) { return ( {children} diff --git a/frontend/src/mocks/browser.ts b/frontend/src/mocks/browser.ts index 6a9af04..d277662 100644 --- a/frontend/src/mocks/browser.ts +++ b/frontend/src/mocks/browser.ts @@ -68,7 +68,12 @@ export async function startMockServiceWorker() { } // Ignore locale routes (Next.js i18n) - if (url.pathname === '/en' || url.pathname === '/it' || url.pathname.startsWith('/en/') || url.pathname.startsWith('/it/')) { + if ( + url.pathname === '/en' || + url.pathname === '/it' || + url.pathname.startsWith('/en/') || + url.pathname.startsWith('/it/') + ) { return; } diff --git a/frontend/tests/components/charts/RegistrationActivityChart.test.tsx b/frontend/tests/components/charts/RegistrationActivityChart.test.tsx index 12a6c34..37b70b8 100644 --- a/frontend/tests/components/charts/RegistrationActivityChart.test.tsx +++ b/frontend/tests/components/charts/RegistrationActivityChart.test.tsx @@ -8,80 +8,80 @@ import type { RegistrationActivityData } from '@/components/charts/RegistrationA // Mock recharts to avoid rendering issues in tests jest.mock('recharts', () => { - const OriginalModule = jest.requireActual('recharts'); - return { - ...OriginalModule, - ResponsiveContainer: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - }; + const OriginalModule = jest.requireActual('recharts'); + return { + ...OriginalModule, + ResponsiveContainer: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + }; }); describe('RegistrationActivityChart', () => { - const mockData: RegistrationActivityData[] = [ - { date: 'Jan 1', registrations: 5 }, - { date: 'Jan 2', registrations: 8 }, - { date: 'Jan 3', registrations: 3 }, + const mockData: RegistrationActivityData[] = [ + { date: 'Jan 1', registrations: 5 }, + { date: 'Jan 2', registrations: 8 }, + { date: 'Jan 3', registrations: 3 }, + ]; + + it('renders chart card with title and description', () => { + render(); + + expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); + expect(screen.getByText('New user registrations over the last 14 days')).toBeInTheDocument(); + }); + + it('renders chart with provided data', () => { + render(); + + expect(screen.getByTestId('responsive-container')).toBeInTheDocument(); + }); + + it('shows empty state when no data is provided', () => { + render(); + + expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); + expect(screen.getByText('No registration data available')).toBeInTheDocument(); + expect(screen.queryByTestId('responsive-container')).not.toBeInTheDocument(); + }); + + it('shows empty state when data array is empty', () => { + render(); + + expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); + expect(screen.getByText('No registration data available')).toBeInTheDocument(); + expect(screen.queryByTestId('responsive-container')).not.toBeInTheDocument(); + }); + + it('shows empty state when data has no registrations', () => { + const emptyData = [ + { date: 'Jan 1', registrations: 0 }, + { date: 'Jan 2', registrations: 0 }, ]; + render(); - it('renders chart card with title and description', () => { - render(); + expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); + expect(screen.getByText('No registration data available')).toBeInTheDocument(); + expect(screen.queryByTestId('responsive-container')).not.toBeInTheDocument(); + }); - expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); - expect(screen.getByText('New user registrations over the last 14 days')).toBeInTheDocument(); - }); + it('shows loading state', () => { + render(); - it('renders chart with provided data', () => { - render(); + expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); - expect(screen.getByTestId('responsive-container')).toBeInTheDocument(); - }); + // Chart should not be visible when loading + expect(screen.queryByTestId('responsive-container')).not.toBeInTheDocument(); + expect(screen.queryByText('No registration data available')).not.toBeInTheDocument(); + }); - it('shows empty state when no data is provided', () => { - render(); + it('shows error state', () => { + render(); - expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); - expect(screen.getByText('No registration data available')).toBeInTheDocument(); - expect(screen.queryByTestId('responsive-container')).not.toBeInTheDocument(); - }); + expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); + expect(screen.getByText('Failed to load chart data')).toBeInTheDocument(); - it('shows empty state when data array is empty', () => { - render(); - - expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); - expect(screen.getByText('No registration data available')).toBeInTheDocument(); - expect(screen.queryByTestId('responsive-container')).not.toBeInTheDocument(); - }); - - it('shows empty state when data has no registrations', () => { - const emptyData = [ - { date: 'Jan 1', registrations: 0 }, - { date: 'Jan 2', registrations: 0 }, - ]; - render(); - - expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); - expect(screen.getByText('No registration data available')).toBeInTheDocument(); - expect(screen.queryByTestId('responsive-container')).not.toBeInTheDocument(); - }); - - it('shows loading state', () => { - render(); - - expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); - - // Chart should not be visible when loading - expect(screen.queryByTestId('responsive-container')).not.toBeInTheDocument(); - expect(screen.queryByText('No registration data available')).not.toBeInTheDocument(); - }); - - it('shows error state', () => { - render(); - - expect(screen.getByText('User Registration Activity')).toBeInTheDocument(); - expect(screen.getByText('Failed to load chart data')).toBeInTheDocument(); - - // Chart should not be visible when error - expect(screen.queryByTestId('responsive-container')).not.toBeInTheDocument(); - }); + // Chart should not be visible when error + expect(screen.queryByTestId('responsive-container')).not.toBeInTheDocument(); + }); }); diff --git a/frontend/tests/components/home/DemoCredentialsModal.test.tsx b/frontend/tests/components/home/DemoCredentialsModal.test.tsx index 5810ea9..d999897 100644 --- a/frontend/tests/components/home/DemoCredentialsModal.test.tsx +++ b/frontend/tests/components/home/DemoCredentialsModal.test.tsx @@ -86,7 +86,9 @@ describe('DemoCredentialsModal', () => { fireEvent.click(adminCopyButton!); await waitFor(() => { - expect(navigator.clipboard.writeText).toHaveBeenCalledWith('admin@example.com\nAdminPass1234!'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'admin@example.com\nAdminPass1234!' + ); const copiedButtons = screen.getAllByRole('button'); const copiedButton = copiedButtons.find((btn) => btn.textContent?.includes('Copied!')); expect(copiedButton).toBeInTheDocument();