import { test, expect } from '@playwright/test'; /** * User Journey E2E Tests * * Simulates a complete user flow: register → login → browse → search → settings → logout. * All tests are serial because they share browser state (auth token, navigation). * * FORBIDDEN: Do NOT use page.waitForTimeout() — use waitForResponse() or waitForSelector() */ test.describe('User Journey E2E', () => { test.describe.configure({ mode: 'serial' }); const testData = { username: `e2e_user_${Date.now()}`, password: 'TestPass123!', }; // Register a new user account via the UI form test('should register a new user', async ({ page }) => { await page.goto('/login'); // Switch to the register tab await page.click('text=Inscription'); // Fill out the registration form await page.fill('#registerUsername', testData.username); await page.fill('#registerPassword', testData.password); await page.fill('#registerPasswordConfirm', testData.password); // Submit and wait for the API response const [response] = await Promise.all([ page.waitForResponse((resp) => resp.url().includes('/api/auth/register')), page.click('#registerSubmit'), ]); // Registration should succeed (201 or 200) expect(response.status()).toBeLessThan(400); // Verify the success message appears await page.locator('#authSuccess').waitFor({ state: 'visible', timeout: 10000 }); const successText = await page.locator('#authSuccess').textContent(); expect(successText).toMatch(/réussie|inscription/i); }); // Login with the credentials registered in the previous test test('should login with registered credentials', async ({ page }) => { await page.goto('/login'); await page.fill('#loginUsername', testData.username); await page.fill('#loginPassword', testData.password); // Submit and wait for the login API response const [response] = await Promise.all([ page.waitForResponse((resp) => resp.url().includes('/api/auth/login')), page.click('#loginSubmit'), ]); expect(response.status()).toBeLessThan(400); // Verify success message await page.locator('#authSuccess').waitFor({ state: 'visible', timeout: 10000 }); const successText = await page.locator('#authSuccess').textContent(); expect(successText).toMatch(/réussie/i); // Wait for redirect to /web await page.waitForURL('**/web**', { timeout: 10000 }); // Verify the auth token is stored in localStorage const token = await page.evaluate(() => localStorage.getItem('auth_token')); expect(token).toBeTruthy(); }); // Browse the homepage — verify layout loads without JS errors test('should browse homepage without errors', async ({ page }) => { // Collect JS page errors const errors: string[] = []; page.on('pageerror', (err) => errors.push(err.message)); // Ensure we are on /web (carried over from login) if (!page.url().includes('/web')) { await page.goto('/web'); } // Wait for main content area to be visible await page.locator('#main-content').waitFor({ state: 'visible', timeout: 10000 }); // Verify the header heading await expect(page.locator('header h1')).toContainText('Ohm Stream'); // Verify at least one navigation tab is visible await expect(page.locator('.tab').first()).toBeVisible(); // Verify the user info panel (logged-in state indicator) await expect(page.locator('#userInfo')).toBeVisible(); // No JavaScript errors should have been thrown expect(errors).toHaveLength(0); }); // Search for an anime via the Anime tab — results may be empty but the UI must respond test('should search for anime', async ({ page }) => { // Navigate to the Anime tab await page.click('.tab:has-text("Anime")'); await page.locator('#tab-anime').waitFor({ state: 'visible', timeout: 5000 }); // Fill the search input — HTMX debounce triggers the request automatically await page.fill('#animeSearchInput', 'Naruto'); // Wait for either results, an empty-state message, or the loading spinner to disappear await Promise.race([ page.locator('#animeSearchResults .sr-card').first().waitFor({ timeout: 15000 }), page.locator('#animeSearchResults .sr-empty').first().waitFor({ timeout: 15000 }), page.locator('#search-loading').waitFor({ state: 'detached', timeout: 15000 }), ]); // The search results container must be present regardless of result count await expect(page.locator('#animeSearchResults')).toBeVisible(); }); // Change a setting (language) and verify the PATCH response and toast notification test('should update settings', async ({ page }) => { // Open the settings tab await page.click('.tab:has-text("Paramètres")'); // Settings panel is loaded dynamically via HTMX — wait for the form await page.locator('#default_lang').waitFor({ state: 'visible', timeout: 15000 }); // Change the default language await page.selectOption('#default_lang', 'vf'); // Submit the settings form and capture the PATCH response const [response] = await Promise.all([ page.waitForResponse( (resp) => resp.url().includes('/api/settings') && resp.request().method() === 'PATCH' ), page.locator('form[hx-patch="/api/settings"] button[type="submit"]').click(), ]); expect(response.status()).toBe(200); // Verify a toast notification appears confirming the save await page.locator('.toast').first().waitFor({ state: 'visible', timeout: 5000 }); }); // Logout — verify the API call succeeds, redirect happens, and token is cleared test('should logout successfully', async ({ page }) => { // Click the logout button and wait for the API response const [response] = await Promise.all([ page.waitForResponse((resp) => resp.url().includes('/api/auth/logout')), page.locator('#userInfo button:has-text("Déconnexion")').click(), ]); expect(response.status()).toBeLessThan(400); // Should be redirected back to the login page await page.waitForURL('**/login**', { timeout: 10000 }); // The auth token must be cleared from localStorage const token = await page.evaluate(() => localStorage.getItem('auth_token')); expect(token).toBeNull(); }); });