Refactor tests, documentation, and component code for consistent formatting and improved readability
- Reformatted test files (`RegistrationActivityChart.test.tsx`, `DemoCredentialsModal.test.tsx`) for indentation consistency. - Reduced inline style verbosity across components and docs (`DemoModeBanner`, `CodeBlock`, `MarkdownContent`). - Enhanced Markdown documentation (`sync-msw-with-openapi.md`, `MSW_AUTO_GENERATION.md`) with spacing updates for improved clarity. - Updated MSW configuration to simplify locale route handling in `browser.ts`.
This commit is contained in:
@@ -28,11 +28,13 @@ src/mocks/handlers/
|
|||||||
### 1. Automatic Generation
|
### 1. Automatic Generation
|
||||||
|
|
||||||
When you run:
|
When you run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run generate:api
|
npm run generate:api
|
||||||
```
|
```
|
||||||
|
|
||||||
The system:
|
The system:
|
||||||
|
|
||||||
1. Fetches `/api/v1/openapi.json` from backend
|
1. Fetches `/api/v1/openapi.json` from backend
|
||||||
2. Generates TypeScript API client (`src/lib/api/generated/`)
|
2. Generates TypeScript API client (`src/lib/api/generated/`)
|
||||||
3. **NEW:** Generates MSW handlers (`src/mocks/handlers/generated.ts`)
|
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:
|
The generator (`scripts/generate-msw-handlers.ts`) creates handlers with:
|
||||||
|
|
||||||
**Smart Response Logic:**
|
**Smart Response Logic:**
|
||||||
|
|
||||||
- **Auth endpoints** → Use `validateCredentials()` and `setCurrentUser()`
|
- **Auth endpoints** → Use `validateCredentials()` and `setCurrentUser()`
|
||||||
- **User endpoints** → Use `currentUser` and mock data
|
- **User endpoints** → Use `currentUser` and mock data
|
||||||
- **Admin endpoints** → Check `is_superuser` + return paginated data
|
- **Admin endpoints** → Check `is_superuser` + return paginated data
|
||||||
- **Generic endpoints** → Return success response
|
- **Generic endpoints** → Return success response
|
||||||
|
|
||||||
**Example Generated Handler:**
|
**Example Generated Handler:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
/**
|
/**
|
||||||
* Login
|
* Login
|
||||||
@@ -91,10 +95,7 @@ export const overrideHandlers = [
|
|||||||
http.post(`${API_BASE_URL}/api/v1/auth/login`, async ({ request }) => {
|
http.post(`${API_BASE_URL}/api/v1/auth/login`, async ({ request }) => {
|
||||||
// 10% chance of rate limit
|
// 10% chance of rate limit
|
||||||
if (Math.random() < 0.1) {
|
if (Math.random() < 0.1) {
|
||||||
return HttpResponse.json(
|
return HttpResponse.json({ detail: 'Too many login attempts' }, { status: 429 });
|
||||||
{ detail: 'Too many login attempts' },
|
|
||||||
{ status: 429 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Fall through to generated handler
|
// Fall through to generated handler
|
||||||
}),
|
}),
|
||||||
@@ -105,10 +106,7 @@ export const overrideHandlers = [
|
|||||||
|
|
||||||
// Custom validation logic
|
// Custom validation logic
|
||||||
if (body.email.endsWith('@blocked.com')) {
|
if (body.email.endsWith('@blocked.com')) {
|
||||||
return HttpResponse.json(
|
return HttpResponse.json({ detail: 'Email domain not allowed' }, { status: 400 });
|
||||||
{ detail: 'Email domain not allowed' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall through to generated handler
|
// Fall through to generated handler
|
||||||
@@ -124,6 +122,7 @@ Overrides are applied FIRST, so they take precedence over generated handlers.
|
|||||||
### ✅ Zero Manual Work
|
### ✅ Zero Manual Work
|
||||||
|
|
||||||
**Before:**
|
**Before:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Backend adds new endpoint
|
# Backend adds new endpoint
|
||||||
# 1. Run npm run generate:api
|
# 1. Run npm run generate:api
|
||||||
@@ -134,6 +133,7 @@ Overrides are applied FIRST, so they take precedence over generated handlers.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**After:**
|
**After:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Backend adds new endpoint
|
# Backend adds new endpoint
|
||||||
npm run generate:api # Done! MSW auto-synced
|
npm run generate:api # Done! MSW auto-synced
|
||||||
@@ -160,6 +160,7 @@ import { adminStats } from '../data/stats';
|
|||||||
### ✅ Batteries Included
|
### ✅ Batteries Included
|
||||||
|
|
||||||
Generated handlers include:
|
Generated handlers include:
|
||||||
|
|
||||||
- ✅ Network delays (300ms - realistic UX)
|
- ✅ Network delays (300ms - realistic UX)
|
||||||
- ✅ Auth checks (401/403 responses)
|
- ✅ Auth checks (401/403 responses)
|
||||||
- ✅ Pagination support
|
- ✅ Pagination support
|
||||||
@@ -218,6 +219,7 @@ If generated handler doesn't fit your needs:
|
|||||||
3. **Override takes precedence** automatically
|
3. **Override takes precedence** automatically
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// overrides.ts
|
// overrides.ts
|
||||||
export const overrideHandlers = [
|
export const overrideHandlers = [
|
||||||
@@ -227,10 +229,7 @@ export const overrideHandlers = [
|
|||||||
|
|
||||||
// Simulate 2FA requirement for admin users
|
// Simulate 2FA requirement for admin users
|
||||||
if (body.email.includes('admin') && !body.two_factor_code) {
|
if (body.email.includes('admin') && !body.two_factor_code) {
|
||||||
return HttpResponse.json(
|
return HttpResponse.json({ detail: 'Two-factor authentication required' }, { status: 403 });
|
||||||
{ detail: 'Two-factor authentication required' },
|
|
||||||
{ status: 403 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall through to generated handler
|
// Fall through to generated handler
|
||||||
@@ -254,6 +253,7 @@ export const demoUser: UserResponse = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**To update:**
|
**To update:**
|
||||||
|
|
||||||
1. Edit `data/*.ts` files
|
1. Edit `data/*.ts` files
|
||||||
2. Handlers automatically use updated data
|
2. Handlers automatically use updated data
|
||||||
3. No regeneration needed!
|
3. No regeneration needed!
|
||||||
@@ -263,6 +263,7 @@ export const demoUser: UserResponse = {
|
|||||||
The generator (`scripts/generate-msw-handlers.ts`) does:
|
The generator (`scripts/generate-msw-handlers.ts`) does:
|
||||||
|
|
||||||
1. **Parse OpenAPI spec**
|
1. **Parse OpenAPI spec**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8'));
|
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
|
### Generated handler doesn't work
|
||||||
|
|
||||||
**Check:**
|
**Check:**
|
||||||
|
|
||||||
1. Is backend running? (`npm run generate:api` requires backend)
|
1. Is backend running? (`npm run generate:api` requires backend)
|
||||||
2. Check console for `[MSW]` warnings
|
2. Check console for `[MSW]` warnings
|
||||||
3. Verify `generated.ts` exists and has your endpoint
|
3. Verify `generated.ts` exists and has your endpoint
|
||||||
4. Check path parameters match exactly
|
4. Check path parameters match exactly
|
||||||
|
|
||||||
**Debug:**
|
**Debug:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# See what endpoints were generated
|
# See what endpoints were generated
|
||||||
cat src/mocks/handlers/generated.ts | grep "http\."
|
cat src/mocks/handlers/generated.ts | grep "http\."
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Keeping MSW Handlers Synced with OpenAPI Spec
|
# Keeping MSW Handlers Synced with OpenAPI Spec
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
MSW handlers can drift out of sync with the backend API as it evolves.
|
MSW handlers can drift out of sync with the backend API as it evolves.
|
||||||
|
|
||||||
## Solution Options
|
## Solution Options
|
||||||
@@ -60,6 +61,7 @@ Add to `package.json`:
|
|||||||
Our MSW handlers currently cover:
|
Our MSW handlers currently cover:
|
||||||
|
|
||||||
**Auth Endpoints:**
|
**Auth Endpoints:**
|
||||||
|
|
||||||
- POST `/api/v1/auth/register`
|
- POST `/api/v1/auth/register`
|
||||||
- POST `/api/v1/auth/login`
|
- POST `/api/v1/auth/login`
|
||||||
- POST `/api/v1/auth/refresh`
|
- POST `/api/v1/auth/refresh`
|
||||||
@@ -70,6 +72,7 @@ Our MSW handlers currently cover:
|
|||||||
- POST `/api/v1/auth/change-password`
|
- POST `/api/v1/auth/change-password`
|
||||||
|
|
||||||
**User Endpoints:**
|
**User Endpoints:**
|
||||||
|
|
||||||
- GET `/api/v1/users/me`
|
- GET `/api/v1/users/me`
|
||||||
- PATCH `/api/v1/users/me`
|
- PATCH `/api/v1/users/me`
|
||||||
- DELETE `/api/v1/users/me`
|
- DELETE `/api/v1/users/me`
|
||||||
@@ -80,6 +83,7 @@ Our MSW handlers currently cover:
|
|||||||
- DELETE `/api/v1/sessions/:id`
|
- DELETE `/api/v1/sessions/:id`
|
||||||
|
|
||||||
**Admin Endpoints:**
|
**Admin Endpoints:**
|
||||||
|
|
||||||
- GET `/api/v1/admin/stats`
|
- GET `/api/v1/admin/stats`
|
||||||
- GET `/api/v1/admin/users`
|
- GET `/api/v1/admin/users`
|
||||||
- GET `/api/v1/admin/users/:id`
|
- GET `/api/v1/admin/users/:id`
|
||||||
|
|||||||
@@ -11,11 +11,7 @@ import { useState } from 'react';
|
|||||||
import config from '@/config/app.config';
|
import config from '@/config/app.config';
|
||||||
import { Sparkles } from 'lucide-react';
|
import { Sparkles } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from '@/components/ui/popover';
|
|
||||||
|
|
||||||
export function DemoModeBanner() {
|
export function DemoModeBanner() {
|
||||||
// Only show in demo mode
|
// Only show in demo mode
|
||||||
|
|||||||
@@ -55,11 +55,7 @@ export function CodeBlock({ children, className, title }: CodeBlockProps) {
|
|||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
aria-label="Copy code"
|
aria-label="Copy code"
|
||||||
>
|
>
|
||||||
{copied ? (
|
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -136,7 +136,10 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
className={cn("opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-primary ml-2 no-underline", className)}
|
className={cn(
|
||||||
|
'opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-primary ml-2 no-underline',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -147,7 +150,10 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
className={cn("font-medium text-primary underline decoration-primary/30 underline-offset-4 hover:decoration-primary/60 hover:text-primary/90 transition-all", className)}
|
className={cn(
|
||||||
|
'font-medium text-primary underline decoration-primary/30 underline-offset-4 hover:decoration-primary/60 hover:text-primary/90 transition-all',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -68,7 +68,12 @@ export async function startMockServiceWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ignore locale routes (Next.js i18n)
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,9 @@ describe('DemoCredentialsModal', () => {
|
|||||||
fireEvent.click(adminCopyButton!);
|
fireEvent.click(adminCopyButton!);
|
||||||
|
|
||||||
await waitFor(() => {
|
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 copiedButtons = screen.getAllByRole('button');
|
||||||
const copiedButton = copiedButtons.find((btn) => btn.textContent?.includes('Copied!'));
|
const copiedButton = copiedButtons.find((btn) => btn.textContent?.includes('Copied!'));
|
||||||
expect(copiedButton).toBeInTheDocument();
|
expect(copiedButton).toBeInTheDocument();
|
||||||
|
|||||||
Reference in New Issue
Block a user