diff --git a/tests/e2e/user_journey.spec.ts b/tests/e2e/user_journey.spec.ts index e61ca22..804d0c7 100644 --- a/tests/e2e/user_journey.spec.ts +++ b/tests/e2e/user_journey.spec.ts @@ -12,56 +12,153 @@ import { test, expect } from '@playwright/test'; test.describe('User Journey E2E', () => { test.describe.configure({ mode: 'serial' }); - test.fixme('should register a new user', async ({ page }) => { - // TODO: Navigate to /web or /login - // Switch to register tab (text=Inscription) - // Fill #registerUsername with unique username - // Fill #registerPassword and #registerPasswordConfirm - // Click #registerSubmit - // Wait for API response via waitForResponse(r => r.url().includes('/api/auth/register')) - // Verify #authSuccess becomes visible or contains success message + 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); }); - test.fixme('should login with registered credentials', async ({ page }) => { - // TODO: Navigate to /login - // Fill #loginUsername and #loginPassword - // Click #loginSubmit - // Wait for response via waitForResponse(r => r.url().includes('/api/auth/login')) - // Verify redirect to /web or home page - // Verify auth token is stored (check localStorage or cookie) + // 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(); }); - test.fixme('should browse homepage without errors', async ({ page }) => { - // TODO: Navigate to /web - // Wait for page to load via waitForSelector for main content area - // Verify no console errors - // Verify page title or main heading is visible - // Note: content may be empty in test env — just verify no crash + // 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); }); - test.fixme('should search for anime', async ({ page }) => { - // TODO: Click on anime search tab (if tabs exist) - // Fill #animeSearchInput with "Naruto" - // Submit the search form (trigger HTMX request) - // Wait for response via waitForResponse(r => r.url().includes('/api/anime/search')) - // Verify search results appear or "no results" message shown - // Verify results container has expected selectors + // 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(); }); - test.fixme('should update settings', async ({ page }) => { - // TODO: Click on settings tab or navigate to settings section - // Wait for settings panel to load via waitForResponse(r => r.url().includes('/api/settings')) - // Verify #default_lang dropdown exists - // Change language setting (select different option) - // Submit/save settings form - // Wait for response via waitForResponse(r => r.url().includes('/api/settings') && r.request().method() === 'PATCH') - // Verify success toast notification appears + // 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 }); }); - test.fixme('should logout successfully', async ({ page }) => { - // TODO: Click logout button - // Wait for response via waitForResponse(r => r.url().includes('/api/auth/logout')) - // Verify redirect to login page or auth state is cleared - // Verify protected content is no longer accessible + // 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(); }); });