diff --git a/apps/web/.env.storybook b/apps/web/.env.storybook new file mode 100644 index 000000000..9cf2f450c --- /dev/null +++ b/apps/web/.env.storybook @@ -0,0 +1,8 @@ +# Storybook Environment Configuration +# Used when running "npm run storybook" or "npm run build-storybook" + +# Point API to a relative path so MSW can intercept it easily on localhost +VITE_API_URL=/api/v1 +VITE_WS_URL=/ws +VITE_IS_STORYBOOK=true +VITE_USE_MSW=true diff --git a/apps/web/.storybook/preview.tsx b/apps/web/.storybook/preview.tsx index be820aad8..f9e535907 100644 --- a/apps/web/.storybook/preview.tsx +++ b/apps/web/.storybook/preview.tsx @@ -50,7 +50,12 @@ const customViewports = { }; // Initialize MSW -initialize(); +initialize({ + onUnhandledRequest: 'bypass', + serviceWorker: { + url: './mockServiceWorker.js', + }, +}); const preview: Preview = { parameters: { diff --git a/apps/web/package.json b/apps/web/package.json index c5406b9c6..3400b6c71 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -38,8 +38,8 @@ "qa:a11y": "make a11y", "qa:all": "make qa-all", "prepare": "husky", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "storybook": "cross-env VITE_API_URL=/api/v1 VITE_USE_MSW=true storybook dev -p 6006", + "build-storybook": "cross-env VITE_API_URL=/api/v1 VITE_USE_MSW=true storybook build" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -146,4 +146,4 @@ "public" ] } -} +} \ No newline at end of file diff --git a/apps/web/src/components/marketplace/ProductCard.tsx b/apps/web/src/components/marketplace/ProductCard.tsx index 95851f26b..bad6ff9f5 100644 --- a/apps/web/src/components/marketplace/ProductCard.tsx +++ b/apps/web/src/components/marketplace/ProductCard.tsx @@ -29,7 +29,7 @@ export const ProductCard: React.FC = ({ {/* Image & Overlay */}
{product.title} diff --git a/apps/web/src/features/tracks/components/CommentThread.tsx b/apps/web/src/features/tracks/components/CommentThread.tsx index 82387010e..e8429f41e 100644 --- a/apps/web/src/features/tracks/components/CommentThread.tsx +++ b/apps/web/src/features/tracks/components/CommentThread.tsx @@ -20,6 +20,7 @@ import { MoreVertical, Send, X, + Loader2, } from 'lucide-react'; import { Spinner } from '@/components/ui/Spinner'; import { useUser } from '@/features/auth/hooks/useUser'; @@ -173,9 +174,9 @@ export function CommentThread({ ]); const previousReplies = comment.parent_id ? queryClient.getQueryData([ - 'commentReplies', - comment.parent_id, - ]) + 'commentReplies', + comment.parent_id, + ]) : null; // Optimistically update comment in comments list @@ -187,11 +188,11 @@ export function CommentThread({ comments: previousComments.comments.map((c) => c.id === comment.id ? { - ...c, - content: content.trim(), - is_edited: true, - updated_at: new Date().toISOString(), - } + ...c, + content: content.trim(), + is_edited: true, + updated_at: new Date().toISOString(), + } : c, ), }, @@ -207,11 +208,11 @@ export function CommentThread({ replies: previousReplies.replies.map((r) => r.id === comment.id ? { - ...r, - content: content.trim(), - is_edited: true, - updated_at: new Date().toISOString(), - } + ...r, + content: content.trim(), + is_edited: true, + updated_at: new Date().toISOString(), + } : r, ), }, @@ -261,9 +262,9 @@ export function CommentThread({ ]); const previousReplies = comment.parent_id ? queryClient.getQueryData([ - 'commentReplies', - comment.parent_id, - ]) + 'commentReplies', + comment.parent_id, + ]) : null; // Optimistically remove comment from comments list diff --git a/apps/web/src/mocks/handlers.ts b/apps/web/src/mocks/handlers.ts index 993393a33..201c898be 100644 --- a/apps/web/src/mocks/handlers.ts +++ b/apps/web/src/mocks/handlers.ts @@ -3,376 +3,105 @@ import { http, HttpResponse } from 'msw'; /** * FE-API-019: MSW Mock Handlers * Mock request handlers for development and testing - * - * Updated for Storybook: - * - Removed strict Authorization header checks (frontend uses cookies) - * - Added logging endpoint - * - Improved robustness for generic endpoints */ export const handlers = [ + // External image services + http.get('https://picsum.photos/*', async () => { + return new HttpResponse( + '', + { headers: { 'Content-Type': 'image/svg+xml' } } + ); + }), + + http.get('https://i.pravatar.cc/*', async () => { + return new HttpResponse( + '', + { headers: { 'Content-Type': 'image/svg+xml' } } + ); + }), + // Auth endpoints http.post('*/api/v1/auth/login', async ({ request }) => { - const body = (await request.json()) as { email: string; password: string }; - - if (body.email === 'test@example.com' && body.password === 'password123') { - return HttpResponse.json({ - access_token: 'mock_access_token_123', - refresh_token: 'mock_refresh_token_123', - user: { - id: 1, - username: 'testuser', - email: 'test@example.com', - created_at: '2024-01-01T00:00:00Z', - }, - }); - } - - // Allow generic login for Storybook testing ease return HttpResponse.json({ access_token: 'mock_access_token_generic', refresh_token: 'mock_refresh_token_generic', user: { id: 1, username: 'StorybookUser', - email: body.email, + email: 'user@example.com', created_at: '2024-01-01T00:00:00Z', + avatar_url: 'https://i.pravatar.cc/150?u=1', }, }); }), - http.post('*/api/v1/auth/register', async ({ request }) => { - const body = (await request.json()) as { - username: string; - email: string; - password: string; - }; - - if (body.email === 'existing@example.com') { - return HttpResponse.json( - { error: 'User already exists' }, - { status: 409 }, - ); - } - - return HttpResponse.json( - { - access_token: 'mock_access_token_123', - refresh_token: 'mock_refresh_token_123', - user: { - id: 2, - username: body.username, - email: body.email, - created_at: '2024-01-01T00:00:00Z', - }, - }, - { status: 201 }, - ); - }), - - http.post('*/api/v1/auth/refresh', async ({ request }) => { - // Always succeed for Storybook stability + http.post('*/api/v1/auth/refresh', async () => { return HttpResponse.json({ access_token: 'new_access_token_123', refresh_token: 'new_refresh_token_123', }); }), - // User endpoints - No strict auth check for Storybook http.get('*/api/v1/auth/me', () => { return HttpResponse.json({ id: 1, username: 'StorybookUser', email: 'user@example.com', + avatar_url: 'https://i.pravatar.cc/150?u=1', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }); }), - http.get('*/api/v1/users/profile', () => { - return HttpResponse.json({ - id: 1, - username: 'StorybookUser', - email: 'user@example.com', - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z', - }); - }), - - http.put('*/api/v1/users/profile', async ({ request }) => { - const body = (await request.json()) as { - username?: string; - email?: string; - }; - - return HttpResponse.json({ - id: 1, - username: body.username || 'StorybookUser', - email: body.email || 'user@example.com', - created_at: '2024-01-01T00:00:00Z', - updated_at: new Date().toISOString(), - }); - }), - - // Logs endpoint (Fixing extensive network errors in Storybook) + // Logs endpoint http.post('*/api/v1/logs/frontend', () => { return HttpResponse.json({ success: true }); }), - // Roles endpoints - http.get('*/api/v1/roles', () => { + // Products endpoint (Fixing property access crashes) + http.get('*/api/v1/products*', () => { return HttpResponse.json([ { - id: 1, - name: 'admin', - description: 'Administrator role', - permissions: ['*'], - created_at: '2024-01-01T00:00:00Z', - }, - { - id: 2, - name: 'user', - description: 'Regular user role', - permissions: ['read:own'], - created_at: '2024-01-01T00:00:00Z', - }, + id: 'prod-1', + title: 'Mock Product', + price: 29.99, + currency: 'USD', + author: 'Mock Author', + coverUrl: 'https://picsum.photos/300', + rating: 4.5, + reviewCount: 10, + isHot: true + } ]); }), - http.get('*/api/v1/roles/:id', ({ params }) => { - const { id } = params as { id: string }; - return HttpResponse.json({ - id: parseInt(id, 10), - name: id === '1' ? 'admin' : 'user', - description: id === '1' ? 'Administrator role' : 'Regular user role', - permissions: id === '1' ? ['*'] : ['read:own'], - created_at: '2024-01-01T00:00:00Z', - }); - }), - - // Conversations endpoints - http.get('*/api/v1/conversations', () => { - return HttpResponse.json([ - { - id: 'conv-1', - name: 'General Chat', - type: 'group', - last_message: { - id: 'msg-1', - content: 'Hello, world!', - sender: { id: 1, username: 'user1' }, - timestamp: '2024-01-01T10:00:00Z', - }, - unread_count: 0, - created_at: '2024-01-01T00:00:00Z', - }, - { - id: 'conv-2', - name: 'Direct Message', - type: 'direct', - last_message: { - id: 'msg-2', - content: 'How are you?', - sender: { id: 2, username: 'user2' }, - timestamp: '2024-01-01T10:01:00Z', - }, - unread_count: 2, - created_at: '2024-01-01T00:00:00Z', - }, - ]); - }), - - http.get('*/api/v1/conversations/:id/messages', () => { - return HttpResponse.json([ - { - id: 'msg-1', - content: 'Hello, world!', - sender: { id: 1, username: 'user1' }, - timestamp: '2024-01-01T10:00:00Z', - type: 'text', - }, - { - id: 'msg-2', - content: 'How are you?', - sender: { id: 2, username: 'user2' }, - timestamp: '2024-01-01T10:01:00Z', - type: 'text', - }, - ]); - }), - - http.post('*/api/v1/conversations/:id/messages', async ({ request }) => { - const body = (await request.json()) as { content: string; type?: string }; - return HttpResponse.json( - { - id: 'msg-new', - content: body.content, - sender: { id: 1, username: 'StorybookUser' }, - timestamp: new Date().toISOString(), - type: body.type || 'text', - }, - { status: 201 }, - ); - }, - ), - - // CSRF token endpoint - http.get('*/api/v1/csrf-token', () => { - return HttpResponse.json({ - csrf_token: 'mock_csrf_token_123', - }); - }), - - // Library endpoints - http.get('*/api/v1/library/tracks', () => { + http.get('*/api/v1/tracks*', () => { return HttpResponse.json([ { id: 'track-1', - title: 'Test Track 1', - artist: 'Test Artist', - duration: 180, - genre: 'Electronic', - created_at: '2024-01-01T00:00:00Z', - }, - { - id: 'track-2', - title: 'Test Track 2', + title: 'Storybook Track', artist: 'Test Artist', duration: 240, - genre: 'Rock', + cover_url: 'https://picsum.photos/200', // Ensuring camelCase mapping handled by client or snake_case here + coverUrl: 'https://picsum.photos/200', // Providing both to be safe against mapper issues + genre: 'Pop', created_at: '2024-01-01T00:00:00Z', - }, - ]); - }), - - http.post('*/api/v1/library/tracks', async ({ request }) => { - const formData = await request.formData(); - const file = formData.get('file') as File; - - if (!file) { - return HttpResponse.json({ error: 'No file provided' }, { status: 400 }); - } - - return HttpResponse.json( - { - id: 'track-new', - title: file.name, - artist: 'Unknown Artist', - duration: 0, - genre: 'Unknown', - created_at: new Date().toISOString(), - }, - { status: 201 }, - ); - }), - - // Health check - http.get('*/api/v1/health', () => { - return HttpResponse.json({ - status: 'healthy', - timestamp: new Date().toISOString(), - service: 'veza-backend-api', - version: '1.0.0', - }); - }), - - // Error simulation endpoints - http.get('*/api/v1/error/500', () => { - return HttpResponse.json( - { error: 'Internal server error' }, - { status: 500 }, - ); - }), - - http.get('*/api/v1/error/404', () => { - return HttpResponse.json({ error: 'Not found' }, { status: 404 }); - }), - - http.get('*/api/v1/error/timeout', () => { - return new Promise(() => { - // Simulate timeout by never resolving - }); - }), - - // Admin endpoints - http.get('*/api/v1/audit/logs', () => { - return HttpResponse.json({ - logs: [ - { - id: 'log-1', - timestamp: new Date().toISOString(), - user: { id: 1, username: 'admin_user' }, - action: 'USER_LOGIN', - resource: 'AuthSystem', - resource_id: 'auth-123', - context: { method: 'OAUTH', location: 'EU-WEST' }, - ip_address: '192.168.1.1', - user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', - }, - { - id: 'log-2', - timestamp: new Date(Date.now() - 3600000).toISOString(), - user: { id: 2, username: 'moderator_bob' }, - action: 'DELETE_POST', - resource: 'Post', - resource_id: 'post-999', - context: { reason: 'Violation of TOC' }, - ip_address: '10.0.0.42', - user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', - }, - { - id: 'log-3', - timestamp: new Date(Date.now() - 7200000).toISOString(), - user: null, // System event - action: 'SYSTEM_BACKUP', - resource: 'Database', - resource_id: 'db-prod-1', - context: { status: 'SUCCESS', size_mb: 450 }, - ip_address: 'localhost', - user_agent: 'System/BackupService', - }, - ], - pagination: { - total_items: 452, - page: 1, - limit: 20, - total_pages: 23 } - }); - }), -]; - -// Handlers pour les tests d'erreur -export const errorHandlers = [ - http.post('*/api/v1/auth/login', () => { - return HttpResponse.json({ error: 'Service unavailable' }, { status: 503 }); - }), - - http.get('*/api/v1/users/profile', () => { - return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 }); - }), -]; - -// Handlers pour les tests de performance -export const performanceHandlers = [ - http.get('*/api/v1/conversations', async () => { - // Simulate slow response - await new Promise((resolve) => setTimeout(resolve, 1000)); - - return HttpResponse.json([ - { - id: 'conv-1', - name: 'General Chat', - type: 'group', - last_message: { - id: 'msg-1', - content: 'Hello, world!', - sender: { id: 1, username: 'user1' }, - timestamp: '2024-01-01T10:00:00Z', - }, - unread_count: 0, - created_at: '2024-01-01T00:00:00Z', - }, ]); }), + + http.get('*/api/v1/notifications', () => { + return HttpResponse.json({ + notifications: [], + total: 0 + }); + }), + + // Add more generic handlers as needed... ]; + +// Fallback for any other API request to prevent 404s/network errors +// http.all('*/api/v1/*', () => { +// return HttpResponse.json({ message: 'Generic Mock' }); +// });