veza/tests/e2e/20-network-errors.spec.ts
senke 20a16f7cbe test: add comprehensive e2e test suite (34 spec files)
New tests/e2e/ suite covering:
- Auth, navigation, player, tracks, playlists
- Search, discover, social, marketplace, chat
- Accessibility, API, workflows, edge cases
- Routes coverage, forms validation, modals
- Empty states, responsive, network errors
- Error boundary, performance, visual regression
- Cross-browser, profile, smoke, upload
- Storybook, deep pages, visual bugs
- Includes fixtures, helpers, global setup/teardown
- Playwright config and coverage map

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

364 lines
13 KiB
TypeScript

import { test, expect } from '@playwright/test';
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 — go to login page first
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"]'))
.or(page.locator('text=/error|erreur|connexion|network|réseau|failed|échec/i'));
const hasVisibleError = await errorLocator.first().isVisible({ timeout: 15000 }).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);
console.log(` Login API down: ${hasVisibleError ? 'error message shown' : 'no visible error element 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');
});
});