diff --git a/apps/web/package.json b/apps/web/package.json index 1935f686b..e269a1dc4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -59,7 +59,7 @@ "test:storybook:playwright": "echo 'Storybook tests moved to repo root: npm run e2e -- --grep storybook'", "validate:storybook": "node scripts/validate-storybook.cjs", "chromatic": "chromatic --project-token=${CHROMATIC_PROJECT_TOKEN}", - "test:storybook:vitest": "vitest --project storybook" + "test:storybook:vitest": "vitest --config vitest.storybook.config.ts" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx index e37d90852..804873285 100644 --- a/apps/web/src/app/App.tsx +++ b/apps/web/src/app/App.tsx @@ -57,32 +57,39 @@ export function App() { return cleanup; }, [queryClient]); - // Initialiser l'application + // P1.2: Initialize auth state before rendering app + // With httpOnly cookies we cannot read tokens in JS; always call refreshUser() + // so getMe() is used to verify auth (cookies sent automatically). + // CSRF token is fetched AFTER auth succeeds (no timing hack needed). useEffect(() => { - // CRITIQUE FIX #18: refreshUser est maintenant appelé par useStateHydration - // Ne pas appeler refreshUser ici pour éviter les appels multiples - // useStateHydration gère déjà l'hydratation de l'état d'authentification - // Ce useEffect ne fait plus qu'initialiser les autres aspects de l'app - - // Récupérer le token CSRF si l'utilisateur est déjà authentifié - // (refreshUser() est asynchrone, donc on vérifie après un court délai) - const checkAndFetchCSRF = async () => { - // Attendre un peu pour que refreshUser() se termine - await new Promise((resolve) => setTimeout(resolve, 100)); - const { isAuthenticated } = useAuthStore.getState(); - if (isAuthenticated) { - csrfService.refreshToken().catch((error) => { - const msg = error instanceof Error ? error.message : String(error); - if (!msg.includes('HTML page instead of JSON')) { - logger.warn('Failed to fetch CSRF token on app init', { message: msg }); - } + const initAuth = async () => { + try { + await refreshUser(); + // Fetch CSRF only after auth is confirmed + const { isAuthenticated } = useAuthStore.getState(); + if (isAuthenticated) { + csrfService.refreshToken().catch((error) => { + const msg = error instanceof Error ? error.message : String(error); + if (!msg.includes('HTML page instead of JSON')) { + logger.warn('Failed to fetch CSRF token on app init', { message: msg }); + } + }); + } + } catch (error) { + logger.error('[App] Auth initialization failed', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, }); + } finally { + setIsAuthReady(true); } }; - checkAndFetchCSRF(); - // Appliquer le thème au chargement (le store persist le fait déjà, mais on s'assure qu'il est appliqué) - // Forcer dark mode par défaut si pas encore défini + initAuth(); + }, [refreshUser]); + + // Apply theme on load + useEffect(() => { if (!theme || theme === 'system') { const root = document.documentElement; if ( @@ -97,7 +104,7 @@ export function App() { setTheme(theme); } - // Synchroniser la langue avec i18n au chargement + // Sync language with i18n on load if (typeof window !== 'undefined' && window.i18n) { const currentLang = window.i18n.language || language; if (currentLang !== language) { @@ -108,26 +115,6 @@ export function App() { } }, [setTheme, theme, language, setLanguage]); - // P1.2: Initialize auth state before rendering app - // With httpOnly cookies we cannot read tokens in JS; always call refreshUser() - // so getMe() is used to verify auth (cookies sent automatically). - useEffect(() => { - const initAuth = async () => { - try { - await refreshUser(); - } catch (error) { - logger.error('[App] Auth initialization failed', { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - }); - } finally { - setIsAuthReady(true); - } - }; - - initAuth(); - }, [refreshUser]); - // Écouter les changements de préférence système pour le mode 'system' useEffect(() => { if (theme !== 'system') return; diff --git a/apps/web/src/mocks/handlers.ts b/apps/web/src/mocks/handlers.ts index b9dad3859..dffdfb884 100644 --- a/apps/web/src/mocks/handlers.ts +++ b/apps/web/src/mocks/handlers.ts @@ -15,7 +15,6 @@ * - handlers-colistening: co-listening sessions (v0.10.7 F481) */ -import { http, HttpResponse } from 'msw'; import { handlersCommon } from './handlers-common'; import { handlersAuth } from './handlers-auth'; import { handlersAdmin } from './handlers-admin'; @@ -44,10 +43,4 @@ export const handlers = [ ...handlersStreaming, ...handlersLive, ...handlersColistening, - - // Catch-all for API to prevent network leaks (Phase 1: Stabilization) - http.all('*/api/v1/*', ({ request }) => { - console.warn('[MSW] Intercepted unhandled API request:', request.method, request.url); - return HttpResponse.json({ success: true, message: 'Mocked fallback response from Storybook' }); - }), ]; diff --git a/apps/web/src/test/setup.ts b/apps/web/src/test/setup.ts index 583a26ca2..0d8118387 100644 --- a/apps/web/src/test/setup.ts +++ b/apps/web/src/test/setup.ts @@ -14,15 +14,15 @@ beforeAll(() => { }); // Mock BroadcastChannel to avoid serialization issues in tests -if (typeof global.BroadcastChannel === 'undefined') { - (global as any).BroadcastChannel = class MockBroadcastChannel { +if (typeof globalThis.BroadcastChannel === 'undefined') { + globalThis.BroadcastChannel = class MockBroadcastChannel { postMessage = vi.fn(); close = vi.fn(); addEventListener = vi.fn(); removeEventListener = vi.fn(); onmessage = null; constructor(public name: string) {} - }; + } as unknown as typeof BroadcastChannel; } // Cleanup après chaque test diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts index 96b401f8d..8e21e8995 100644 --- a/apps/web/vitest.config.ts +++ b/apps/web/vitest.config.ts @@ -18,6 +18,8 @@ export default defineConfig({ exclude: [ ...configDefaults.exclude, '**/e2e/**', // Playwright E2E tests (run via playwright test) + '**/*.stories.tsx', // Storybook stories run via `vitest --project storybook` + '**/*.stories.ts', ], pool: 'threads', poolOptions: { @@ -50,26 +52,6 @@ export default defineConfig({ }, }, }, - projects: [ - { - extends: true, - plugins: [ - storybookTest({ - configDir: path.join(dirname, '.storybook'), - }), - ], - test: { - name: 'storybook', - browser: { - enabled: true, - headless: true, - provider: 'playwright', - instances: [{ browser: 'chromium' }], - }, - setupFiles: ['.storybook/vitest.setup.ts'], - }, - }, - ], }, resolve: { alias: {