veza/tests/e2e/14-edge-cases.spec.ts
senke 3640aec716 test(e2e): convert all remaining 298 console.log to real expect()
Convert 20 files from fake assertions (console.log with ✓/✗) to real
expect() assertions. This completes the conversion started in the
previous session — zero console.log calls remain in the E2E suite.

Files converted (by batch):
Batch 1: 16-forms-validation (38→0), 13-workflows (18→0), 14-edge-cases (8→0)
Batch 2: 15-routes-coverage (8→0), 20-network-errors (5→0), 04-tracks (4→0),
         32-deep-pages (4→0), 19-responsive (3→0), 11-accessibility-ethics (3→0)
Batch 3: 25-profile (2→0), 12-api (2→0), 29-chat-functional (2→0),
         30-marketplace-checkout (1→0), 22-performance (1→0),
         31-auth-sessions (1→0), 26-smoke (1→0), 02-navigation (1→0)
Batch 4: 24-cross-browser (0 fakes, 12 info→0), 34-workflows-empty (0→0),
         33-visual-bugs (0→0)

Total: 139 fake assertions → real expect(), 159 informational logs removed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:50:17 +02:00

553 lines
22 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
import { loginViaAPI,
CONFIG,
navigateTo,
assertNotBroken,
assertNoDebugText,
collectNetworkErrors,
playFirstTrack,
SELECTORS,
} from './helpers';
// =============================================================================
// EDGE CASES — Formulaires vides
// =============================================================================
test.describe('EDGE CASES — Formulaires vides', () => {
test('01. Submit empty login form shows validation errors', async ({ page }) => {
await navigateTo(page, '/login');
// Click submit without filling anything
const submitBtn = page.getByRole('button', { name: /sign in|se connecter/i });
await submitBtn.click();
// Should stay on login page
await expect(page).toHaveURL(/login/);
// Should show validation error(s) or HTML5 validation prevents submission
const body = await page.textContent('body') || '';
const hasValidation = /required|obligatoire|email|invalid|invalide/i.test(body);
const emailInput = page.locator('input[type="email"]').first();
const validationMessage = await emailInput.evaluate(
(el: HTMLInputElement) => el.validationMessage,
).catch(() => '');
expect(hasValidation || validationMessage.length > 0).toBeTruthy();
});
test('02. Submit empty register form shows validation errors', async ({ page }) => {
await navigateTo(page, '/register');
// Click submit without filling anything
const submitBtn = page.getByRole('button', { name: /s'inscrire|create account/i });
await submitBtn.click();
// Should stay on register page
await expect(page).toHaveURL(/register/);
// Check for validation errors
const body = await page.textContent('body') || '';
const hasValidation = /required|obligatoire|invalid|invalide|trop court|too short/i.test(body);
const usernameInput = page.locator('#register-username');
const validationMessage = await usernameInput.evaluate(
(el: HTMLInputElement) => el.validationMessage,
).catch(() => '');
expect(hasValidation || validationMessage.length > 0).toBeTruthy();
});
test('03. Submit empty search does not crash', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await navigateTo(page, '/search');
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
.or(page.getByPlaceholder(/search for tracks/i));
const searchVisible = await searchInput.first().isVisible().catch(() => false);
test.skip(!searchVisible, 'Search input not visible on this page');
// Clear the input and press Enter
await searchInput.first().fill('');
await searchInput.first().press('Enter');
await page.waitForTimeout(1_000);
await assertNotBroken(page);
});
test('04. Login with only email filled shows password error', async ({ page }) => {
await navigateTo(page, '/login');
await page.getByLabel(/^email$/i).or(page.locator('input[type="email"]')).first().fill('test@example.com');
await page.getByRole('button', { name: /sign in|se connecter/i }).click();
// Should stay on login
await expect(page).toHaveURL(/login/);
// Password field should show validation
const passwordInput = page.locator('input[type="password"]').first();
const validationMessage = await passwordInput.evaluate(
(el: HTMLInputElement) => el.validationMessage,
).catch(() => '');
const body = await page.textContent('body') || '';
const hasError = validationMessage.length > 0 || /required|password|mot de passe/i.test(body);
expect(hasError).toBeTruthy();
});
});
// =============================================================================
// EDGE CASES — Caracteres speciaux et injection
// =============================================================================
test.describe('EDGE CASES — Caracteres speciaux', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
test('05. XSS attempt in search does not execute @critical', async ({ page }) => {
await navigateTo(page, '/search');
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
.or(page.getByPlaceholder(/search for tracks/i));
const searchVisible = await searchInput.first().isVisible().catch(() => false);
test.skip(!searchVisible, 'Search input not visible on this page');
const xssPayload = '<script>alert("xss")</script>';
await searchInput.first().fill(xssPayload);
await page.waitForTimeout(1_500);
// Verify no alert dialog appeared
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError/i);
// The script tag should be sanitized — not rendered as HTML
const scriptElements = await page.locator('script:has-text("xss")').count();
expect(scriptElements).toBe(0);
});
test('06. SQL injection attempt in search does not crash @critical', async ({ page }) => {
await navigateTo(page, '/search');
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
.or(page.getByPlaceholder(/search for tracks/i));
const searchVisible = await searchInput.first().isVisible().catch(() => false);
test.skip(!searchVisible, 'Search input not visible on this page');
const sqlPayload = "'; DROP TABLE users; --";
await searchInput.first().fill(sqlPayload);
await page.waitForTimeout(1_500);
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError|syntax error|SQL/i);
});
test('07. Very long string in search does not crash', async ({ page }) => {
await navigateTo(page, '/search');
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
.or(page.getByPlaceholder(/search for tracks/i));
const searchVisible = await searchInput.first().isVisible().catch(() => false);
test.skip(!searchVisible, 'Search input not visible on this page');
const longString = 'a'.repeat(600);
await searchInput.first().fill(longString);
await page.waitForTimeout(1_500);
await assertNotBroken(page);
});
test('08. Emoji search works without crash', async ({ page }) => {
await navigateTo(page, '/search');
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
.or(page.getByPlaceholder(/search for tracks/i));
const searchVisible = await searchInput.first().isVisible().catch(() => false);
test.skip(!searchVisible, 'Search input not visible on this page');
await searchInput.first().fill('music vibes');
await page.waitForTimeout(1_500);
await assertNotBroken(page);
});
test('09. Unicode and special characters in search', async ({ page }) => {
await navigateTo(page, '/search');
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
.or(page.getByPlaceholder(/search for tracks/i));
const searchVisible = await searchInput.first().isVisible().catch(() => false);
test.skip(!searchVisible, 'Search input not visible on this page');
const specialChars = 'cafe\u0301 mu\u0308sik \u00e9l\u00e8ve \u00f1';
await searchInput.first().fill(specialChars);
await page.waitForTimeout(1_500);
await assertNotBroken(page);
});
test('10. HTML entities in login email field', async ({ page }) => {
// This test needs to be on the login page, so clear the authenticated state
// (beforeEach logged in via API — we need to undo that for this test)
await page.evaluate(() => localStorage.removeItem('auth-storage'));
await page.context().clearCookies();
await navigateTo(page, '/login');
// Wait for the login form to be fully visible before interacting
await page.getByTestId('login-form').waitFor({ state: 'visible', timeout: 15_000 });
const emailInput = page.locator('input[type="email"]');
await emailInput.first().waitFor({ state: 'visible', timeout: 10_000 });
await emailInput.first().fill('test&amp;<b>bold</b>@example.com');
await page.getByLabel(/^password$|^mot de passe$/i).or(page.locator('input[type="password"]')).first().fill('Password123!');
await page.getByRole('button', { name: /sign in|se connecter/i }).click();
// Should show error (invalid email format), not crash
await page.waitForTimeout(1_500);
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError/i);
});
});
// =============================================================================
// EDGE CASES — Erreurs reseau
// =============================================================================
test.describe('EDGE CASES — Erreurs reseau', () => {
test('11. Simulated 500 error on API shows error message, no crash @critical', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
// Intercept a common API route and return 500
await page.route('**/api/v1/tracks**', (route) => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: { code: 'INTERNAL_ERROR', message: 'Simulated server error' } }),
});
});
await navigateTo(page, '/library');
await page.waitForTimeout(2_000);
// Page should not crash — should show an error state or empty state
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
expect(body.length).toBeGreaterThan(50);
});
test('12. Simulated network timeout shows loading or error state', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
// Intercept API and simulate a timeout (abort after delay)
await page.route('**/api/v1/search**', async (route) => {
// Delay then abort to simulate timeout
await new Promise((resolve) => setTimeout(resolve, 5_000));
route.abort('timedout');
});
await navigateTo(page, '/search');
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
.or(page.getByPlaceholder(/search for tracks/i));
const searchVisible = await searchInput.first().isVisible().catch(() => false);
test.skip(!searchVisible, 'Search input not visible on this page');
await searchInput.first().fill('timeout test');
// Wait a moment - should show loading indicator or remain stable
await page.waitForTimeout(2_000);
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
});
test('13. API returning malformed JSON does not crash page', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await page.route('**/api/v1/tracks**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: '{ invalid json !!!',
});
});
await navigateTo(page, '/library');
await page.waitForTimeout(2_000);
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError|Unexpected token/i);
expect(body.length).toBeGreaterThan(50);
});
});
// =============================================================================
// EDGE CASES — Ressources inexistantes
// =============================================================================
test.describe('EDGE CASES — Ressources inexistantes', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
test('14. /tracks/nonexistent-id shows 404 or error page @critical', async ({ page }) => {
await navigateTo(page, '/tracks/nonexistent-id-99999');
const body = await page.textContent('body') || '';
// Should show a 404 page, error message, or redirect — not crash
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
const handled = /not found|introuvable|404|error|does not exist|n'existe pas/i.test(body) ||
page.url().includes('/404') ||
page.url().includes('/dashboard');
expect(handled).toBeTruthy();
});
test('15. /playlists/nonexistent-id shows 404 or error page', async ({ page }) => {
await navigateTo(page, '/playlists/nonexistent-id-99999');
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
const handled = /not found|introuvable|404|error/i.test(body) ||
page.url().includes('/404');
expect(handled).toBeTruthy();
});
test('16. /u/nonexistent-user shows 404 or error page', async ({ page }) => {
await navigateTo(page, '/u/this-user-does-not-exist-at-all');
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
const handled = /not found|introuvable|404|error|n'existe pas/i.test(body) ||
page.url().includes('/404');
expect(handled).toBeTruthy();
});
test('17. Completely unknown route shows 404 page', async ({ page }) => {
await navigateTo(page, '/this-route-definitely-does-not-exist');
// Wait a bit for redirects to settle
await page.waitForTimeout(2_000);
const body = await page.textContent('body') || '';
// Should show 404 page or redirect, not blank or crash
expect(body).not.toMatch(/crash|TypeError/i);
// Body should have some content (at least a heading or navigation)
expect(body.trim().length).toBeGreaterThan(10);
const is404 = /404|not found|introuvable|page not found/i.test(body) ||
page.url().includes('/404');
expect(is404).toBeTruthy();
});
test('18. /marketplace/products/nonexistent-id handles gracefully', async ({ page }) => {
await navigateTo(page, '/marketplace/products/nonexistent-product-id');
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
});
});
// =============================================================================
// EDGE CASES — Double actions
// =============================================================================
test.describe('EDGE CASES — Double actions', () => {
test('19. Double-click on login submit does not cause duplicate requests @critical', async ({ page }) => {
await navigateTo(page, '/login');
await page.getByLabel(/^email$/i).or(page.locator('input[type="email"]')).first().fill(CONFIG.users.listener.email);
await page.getByLabel(/^password$|^mot de passe$/i).or(page.locator('input[type="password"]')).first().fill(CONFIG.users.listener.password);
// Track API calls
const loginRequests: string[] = [];
page.on('request', (req) => {
if (req.url().includes('/auth/login') && req.method() === 'POST') {
loginRequests.push(req.url());
}
});
const submitBtn = page.getByRole('button', { name: /sign in|se connecter/i });
// Double-click rapidly
await submitBtn.dblclick();
// Wait for response
await page.waitForTimeout(3_000);
// Should have sent at most 2 requests (double-click), ideally 1 if debounced
expect(loginRequests.length).toBeLessThanOrEqual(2);
// The page should not crash regardless
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError/i);
});
test('20. Rapid page navigation does not crash the app', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
// Click through pages rapidly without waiting
const pages = ['/dashboard', '/library', '/discover', '/search', '/playlists', '/profile', '/settings'];
for (const route of pages) {
page.goto(route, { waitUntil: 'commit' }).catch(() => {});
// Minimal delay to trigger navigation
await page.waitForTimeout(200);
}
// Wait for final page to settle
await page.waitForLoadState('domcontentloaded').catch(() => {});
await page.waitForTimeout(3_000);
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
// During rapid navigation, body may be minimal — just ensure no crash
expect(body.trim().length).toBeGreaterThan(10);
});
test('21. Double-click on like button toggles correctly', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await navigateTo(page, '/discover');
// Find a like button
const likeBtn = page.getByRole('button', { name: /ajouter aux favoris|add to favorites/i }).first();
const likeBtnVisible = await likeBtn.isVisible().catch(() => false);
test.skip(!likeBtnVisible, 'No like button visible on discover page');
// Double-click to toggle like twice
await likeBtn.dblclick();
await page.waitForTimeout(1_000);
// Should not crash — state may or may not have changed
await assertNotBroken(page);
});
});
// =============================================================================
// EDGE CASES — Etat du navigateur
// =============================================================================
test.describe('EDGE CASES — Etat du navigateur', () => {
test('22. Clearing localStorage forces re-login', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await page.waitForTimeout(2_000);
// Clear auth storage
await page.evaluate(() => {
localStorage.removeItem('auth-storage');
localStorage.clear();
});
// Navigate to a protected page
await navigateTo(page, '/dashboard');
await page.waitForTimeout(2_000);
// Should redirect to login or show unauthenticated state
const url = page.url();
const isLoggedOut = url.includes('/login') || url.includes('/register');
expect(isLoggedOut).toBeTruthy();
});
test('23. Accessing app with expired/invalid token shows login', async ({ page }) => {
// Set an invalid auth state
await page.goto('/login', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => {
localStorage.setItem('auth-storage', JSON.stringify({
state: { isAuthenticated: true, isLoading: false, error: null },
version: 1,
}));
});
// Try to access protected page with fake auth
await navigateTo(page, '/dashboard');
await page.waitForTimeout(3_000);
// The API should reject the invalid session and redirect to login
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError/i);
const url = page.url();
const handledInvalidToken = url.includes('/login') || url.includes('/register') ||
/unauthorized|session|expired|sign in/i.test(body);
expect(handledInvalidToken).toBeTruthy();
});
test('24. Page loads correctly with JavaScript-disabled cookies notice', async ({ page }) => {
// Verify the page loads and doesn't depend on cookies being pre-set
await page.context().clearCookies();
await navigateTo(page, '/login');
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/crash|TypeError/i);
expect(body.length).toBeGreaterThan(50);
});
});
// =============================================================================
// EDGE CASES — Concurrent interactions
// =============================================================================
test.describe('EDGE CASES — Interactions concurrentes', () => {
test('25. Multiple search queries in quick succession', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await navigateTo(page, '/search');
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
.or(page.getByPlaceholder(/search for tracks/i));
const searchVisible = await searchInput.first().isVisible().catch(() => false);
test.skip(!searchVisible, 'Search input not visible on this page');
// Type multiple queries rapidly to test debounce handling
const queries = ['rock', 'jazz', 'electronic', 'hip hop', 'classical'];
for (const query of queries) {
await searchInput.first().fill(query);
await page.waitForTimeout(100); // Very short delay between queries
}
// Wait for the final debounced search to resolve
await page.waitForTimeout(2_000);
await assertNotBroken(page);
});
test('26. Opening search while player is active does not break either', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
// Start playing a track
await navigateTo(page, '/discover');
// Play first track — hover on card then click play button
const trackCard = page.locator('[role="article"]').first();
if (await trackCard.isVisible().catch(() => false)) {
await trackCard.hover();
await page.waitForTimeout(300);
}
const playBtn = page.getByRole('button', { name: /^lire |^play /i }).first()
.or(page.locator('[aria-label*="Lire"]').first())
.or(page.locator('[aria-label*="Play"]').first());
if (await playBtn.isVisible().catch(() => false)) {
await playBtn.click();
await page.waitForTimeout(CONFIG.timeouts.animation);
}
// Navigate to search while track might be playing
await navigateTo(page, '/search');
await assertNotBroken(page);
// Search should work
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
.or(page.getByPlaceholder(/search for tracks/i));
const searchVisible = await searchInput.first().isVisible().catch(() => false);
if (searchVisible) {
await searchInput.first().fill('test');
await page.waitForTimeout(1_500);
await assertNotBroken(page);
}
});
});