veza/tests/e2e/20-network-errors.spec.ts
senke 6fad0ad68d fix: stabilize frontend — 98 TS errors to 0, align API endpoints, optimize bundle
- Fix 98 TypeScript errors across 37 files:
  - Service layer double-unwrapping (subscriptionService, distributionService, gearService)
  - Self-referencing variables in SearchPageResults
  - FeedView/ExploreView .posts→.items alignment
  - useQueueSync Zustand subscribe API
  - AdminAuditLogsView missing interface fields
  - Toast proxy type, interceptor type narrowing
  - 22 unused imports/variables removed
  - 5 storybook mock data fixes

- Align frontend API calls with backend endpoints:
  - Analytics: useAnalyticsView now calls /creator/analytics/dashboard (was /analytics)
  - Chat: chatService uses /conversations (was mock data), WS URL from backend token
  - Dashboard StatsSection: uses real /dashboard API data (was hardcoded zeros)
  - Settings: suppress 2FA toast error when endpoint unavailable

- Fix marketplace products: seed uses 'active' status (was 'published')
- Enrich seed: admin follows all creators (feed has content)

- Optimize bundle: vendor catch-all 793KB→318KB gzip (-60%)
  Split into vendor-charts, vendor-emoji, vendor-swagger, vendor-media, etc.

- Clean repo: remove ~100 orphaned screenshots, audit reports, logs from root

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:18:49 +01:00

373 lines
13 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
import { loginViaAPI,
CONFIG,
navigateTo,
assertNotBroken,
SELECTORS,
} from './helpers';
// =============================================================================
// Helper: collect page errors during an action
// =============================================================================
function collectPageErrors(page: import('@playwright/test').Page): string[] {
const errors: string[] = [];
page.on('pageerror', (err) => {
errors.push(err.message);
});
return errors;
}
/**
* Assert the page did not crash: body has meaningful content,
* no unhandled JS errors leaked into the visible text.
*/
async function assertNoCrash(page: import('@playwright/test').Page): Promise<void> {
const body = await page.textContent('body') || '';
expect(body.length).toBeGreaterThan(100);
expect(body).not.toMatch(/TypeError|Cannot read|undefined is not|Unhandled/i);
}
// =============================================================================
// NETWORK ERRORS — Gestion des erreurs reseau
// =============================================================================
test.describe('NETWORK ERRORS — Gestion des erreurs reseau @feature-errors', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
test('Dashboard — API down → message d\'erreur user-friendly @critical', async ({ page }) => {
const pageErrors = collectPageErrors(page);
// Navigate first to establish session
await navigateTo(page, '/dashboard');
// Block API calls
await page.route('**/api/v1/dashboard**', (route) => route.abort('connectionrefused'));
await page.route('**/api/v1/tracks**', (route) => route.abort('connectionrefused'));
await page.route('**/api/v1/stats**', (route) => route.abort('connectionrefused'));
// Reload to trigger API calls with blocked routes
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
await page.waitForTimeout(2000);
// Should show error message, NOT a blank page or unhandled error
const body = await page.textContent('body') || '';
expect(body.length).toBeGreaterThan(100); // Not blank
expect(body).not.toMatch(/TypeError|Cannot read|undefined is not/i);
// Page errors should not include unhandled promise rejections crashing the app
const criticalErrors = pageErrors.filter(
(e) => e.includes('TypeError') || e.includes('Cannot read'),
);
console.log(` Dashboard API down: ${criticalErrors.length} critical JS errors, body length: ${body.length}`);
});
test('Discover — API timeout → loading puis erreur', async ({ page }) => {
const pageErrors = collectPageErrors(page);
// Simulate extremely slow API (will effectively timeout)
await page.route('**/api/v1/tracks**', async (route) => {
// Hold the request — it will be aborted when the page navigates away or test ends
await new Promise((resolve) => setTimeout(resolve, 30000));
route.abort();
});
await page.route('**/api/v1/genres**', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 30000));
route.abort();
});
await navigateTo(page, '/discover');
await page.waitForTimeout(3000);
// Should show loading state or graceful timeout — not a crash
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/TypeError|unhandled|Cannot read/i);
expect(body.length).toBeGreaterThan(50);
console.log(' Discover API timeout: no crash');
});
test('Search — API 500 → message d\'erreur', async ({ page }) => {
const pageErrors = collectPageErrors(page);
await navigateTo(page, '/search');
// Intercept search API with 500
await page.route('**/api/v1/search**', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },
}),
}),
);
await page.route('**/api/v1/tracks**', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },
}),
}),
);
// Perform a search to trigger the API call
const searchInput = page.locator(SELECTORS.searchInput)
.or(page.getByPlaceholder(/search|rechercher/i))
.or(page.locator('input[type="search"]'));
const inputVisible = await searchInput.first().isVisible().catch(() => false);
if (inputVisible) {
await searchInput.first().fill('test query');
await page.waitForTimeout(2000);
} else {
// Reload to trigger any initial API calls
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
}
await assertNoCrash(page);
console.log(' Search API 500: no crash');
});
test('Playlists — API 500 → message d\'erreur pas de crash', async ({ page }) => {
const pageErrors = collectPageErrors(page);
await page.route('**/api/v1/playlists**', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },
}),
}),
);
await navigateTo(page, '/playlists');
await page.waitForTimeout(2000);
await assertNoCrash(page);
console.log(' Playlists API 500: no crash');
});
test('Library — API 500 → message d\'erreur pas de crash', async ({ page }) => {
const pageErrors = collectPageErrors(page);
await page.route('**/api/v1/library**', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },
}),
}),
);
await page.route('**/api/v1/tracks**', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },
}),
}),
);
await navigateTo(page, '/library');
await page.waitForTimeout(2000);
await assertNoCrash(page);
console.log(' Library API 500: no crash');
});
test('Marketplace — API 500 → message d\'erreur', async ({ page }) => {
const pageErrors = collectPageErrors(page);
await page.route('**/api/v1/marketplace**', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },
}),
}),
);
await page.route('**/api/v1/products**', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },
}),
}),
);
await navigateTo(page, '/marketplace');
await page.waitForTimeout(2000);
await assertNoCrash(page);
console.log(' Marketplace API 500: no crash');
});
test('Profile — API 404 → page d\'erreur ou message', async ({ page }) => {
const pageErrors = collectPageErrors(page);
await page.route('**/api/v1/users/**', (route) =>
route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({
error: { code: 'USER_NOT_FOUND', message: 'User not found' },
}),
}),
);
await page.route('**/api/v1/profile**', (route) =>
route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({
error: { code: 'USER_NOT_FOUND', message: 'User not found' },
}),
}),
);
await navigateTo(page, '/profile/nonexistent-user-12345');
await page.waitForTimeout(2000);
const body = await page.textContent('body') || '';
expect(body.length).toBeGreaterThan(50);
expect(body).not.toMatch(/TypeError|Cannot read|undefined is not/i);
console.log(' Profile 404: no crash');
});
test('Login — API down → message d\'erreur clair', async ({ page }) => {
test.setTimeout(60_000);
// This test does NOT need prior login — clear any auth state from beforeEach
await page.evaluate(() => localStorage.removeItem('auth-storage'));
await page.context().clearCookies();
// Go to login page fresh
await page.goto('/login', { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
// Fill and submit login form
const emailInput = page.getByLabel(/^email$/i).or(page.locator('input[type="email"]'));
await emailInput.first().waitFor({ state: 'visible', timeout: CONFIG.timeouts.navigation });
await emailInput.first().fill('test@test.com');
const passwordInput = page.getByLabel(/^password$|^mot de passe$/i).or(page.locator('input[type="password"]'));
await passwordInput.first().fill('password123');
// Block auth API AFTER the page has loaded but BEFORE submitting
await page.route('**/api/v1/auth/login', (route) => route.abort('connectionrefused'));
await page.route('**/api/v1/auth/**', (route) => {
if (route.request().url().includes('/login')) {
return route.abort('connectionrefused');
}
return route.continue();
});
const submitBtn = page.getByRole('button', { name: /sign in|se connecter|log in|login/i });
await submitBtn.click();
// Wait for error to appear — give the app time to handle the network failure
await page.waitForTimeout(5000);
// Check for error in multiple places: toast, inline error, role="alert", or body text
const errorLocator = page.locator('[role="alert"]')
.or(page.locator('.text-destructive'))
.or(page.locator('[data-testid="toast-alert"]'))
.or(page.locator('.toast'))
.or(page.locator('[class*="error"]'))
.or(page.locator('[class*="Error"]'));
const hasVisibleError = await errorLocator.first().isVisible({ timeout: 10_000 }).catch(() => false);
// Page should not have unhandled JS errors visible
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/TypeError|Cannot read|undefined is not/i);
// Either we see an error message, or the page at least didn't crash (body has content)
// The login page should still be visible with the form
expect(body.trim().length).toBeGreaterThan(10);
// Also check body text for error patterns
const hasBodyError = /error|erreur|connexion|network|réseau|failed|échec|fetch/i.test(body);
// The test passes if any error indicator is shown OR if the page simply didn't crash
// (some apps silently handle network errors without visible messages)
console.log(` Login API down: ${hasVisibleError ? 'error element shown' : hasBodyError ? 'error text in body' : 'no visible error but page did not crash'}`);
});
test('API retourne du JSON malformé → pas de crash', async ({ page }) => {
const pageErrors = collectPageErrors(page);
await page.route('**/api/v1/tracks**', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: '{"data": [INVALID JSON HERE',
}),
);
await page.route('**/api/v1/dashboard**', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: '{not valid json at all!!!',
}),
);
await navigateTo(page, '/dashboard');
await page.waitForTimeout(2000);
// The page should handle malformed JSON gracefully
const body = await page.textContent('body') || '';
expect(body.length).toBeGreaterThan(50);
// Allow SyntaxError in console, but it should not appear in the visible page
expect(body).not.toMatch(/SyntaxError|Unexpected token/i);
console.log(` Malformed JSON: ${pageErrors.length} JS errors caught, no visible crash`);
});
test('API retourne 429 (rate limited) → message approprié', async ({ page }) => {
const pageErrors = collectPageErrors(page);
await page.route('**/api/v1/tracks**', (route) =>
route.fulfill({
status: 429,
contentType: 'application/json',
headers: { 'Retry-After': '60' },
body: JSON.stringify({
error: {
code: 'RATE_LIMITED',
message: 'Too many requests. Please try again later.',
},
}),
}),
);
await page.route('**/api/v1/dashboard**', (route) =>
route.fulfill({
status: 429,
contentType: 'application/json',
headers: { 'Retry-After': '60' },
body: JSON.stringify({
error: {
code: 'RATE_LIMITED',
message: 'Too many requests. Please try again later.',
},
}),
}),
);
await navigateTo(page, '/dashboard');
await page.waitForTimeout(2000);
// Page should not crash on 429
const body = await page.textContent('body') || '';
expect(body.length).toBeGreaterThan(50);
expect(body).not.toMatch(/TypeError|Cannot read|undefined is not/i);
console.log(' Rate limit 429: no crash');
});
});