import { test, expect } from '@playwright/test'; import { execSync } from 'child_process'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const TEST_PASSWORD = 'SessionTest123'; // Extract port from PLAYWRIGHT_BASE_URL or use default (same pattern as login.spec.ts) const baseUrl = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:4173'; const urlMatch = baseUrl.match(/:(\d+)$/); const FRONTEND_PORT = urlMatch ? urlMatch[1] : '4173'; function getTestEmail(browserName: string): string { return `e2e-sessions-${browserName}@example.com`; } function getTenantUrl(path: string): string { return `http://ecole-alpha.classeo.local:${FRONTEND_PORT}${path}`; } // eslint-disable-next-line no-empty-pattern test.beforeAll(async ({}, testInfo) => { const browserName = testInfo.project.name; try { const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); const email = getTestEmail(browserName); const result = execSync( `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --email=${email} --password=${TEST_PASSWORD} 2>&1`, { encoding: 'utf-8' } ); console.warn( `[${browserName}] Sessions test user:`, result.includes('already exists') ? 'exists' : 'created' ); } catch (error) { console.error(`[${browserName}] Failed to create test user:`, error); } }); async function login(page: import('@playwright/test').Page, email: string) { await page.goto(getTenantUrl('/login')); await page.locator('#email').fill(email); await page.locator('#password').fill(TEST_PASSWORD); await page.getByRole('button', { name: /se connecter/i }).click(); await page.waitForURL(getTenantUrl('/'), { timeout: 10000 }); } test.describe('Sessions Management', () => { test.describe('Sessions List (AC1)', () => { test('displays current session with badge', async ({ page }, testInfo) => { const email = getTestEmail(testInfo.project.name); await login(page, email); await page.goto(getTenantUrl('/settings/sessions')); // Page should load await expect(page.getByRole('heading', { name: /mes sessions/i })).toBeVisible(); // Should show at least one session await expect(page.getByText(/session.* active/i)).toBeVisible(); // Current session should have the badge await expect(page.getByText(/session actuelle/i)).toBeVisible(); }); test('displays session metadata', async ({ page }, testInfo) => { const email = getTestEmail(testInfo.project.name); await login(page, email); await page.goto(getTenantUrl('/settings/sessions')); // Wait for sessions to load await expect(page.getByText(/sessions? actives?/i)).toBeVisible({ timeout: 10000 }); // Should display browser info (at least one of these should be visible) const browserInfo = page.locator('.session-device'); await expect(browserInfo.first()).toBeVisible(); // Should display activity time (À l'instant or Il y a) await expect(page.getByText(/l'instant|il y a/i).first()).toBeVisible(); }); }); test.describe('Revoke Single Session (AC2)', () => { test('can revoke another session', async ({ browser }, testInfo) => { const email = getTestEmail(testInfo.project.name); // Create two sessions using two browser contexts const context1 = await browser.newContext(); const context2 = await browser.newContext(); const page1 = await context1.newPage(); const page2 = await context2.newPage(); try { // Login from both contexts await login(page1, email); await login(page2, email); // Go to sessions page on first context await page1.goto(getTenantUrl('/settings/sessions')); // Wait for sessions to load await expect(page1.getByText(/sessions? actives?/i)).toBeVisible({ timeout: 10000 }); // Should see 2 sessions (or more if there were previous sessions) // Filter out the "revoke all" button if present, get single session revoke button // Look for session cards that don't have the "Session actuelle" badge const otherSessionCard = page1 .locator('.session-card') .filter({ hasNot: page1.getByText(/session actuelle/i) }) .first(); if ((await otherSessionCard.count()) > 0) { // Click revoke on a non-current session await otherSessionCard.getByRole('button', { name: /déconnecter/i }).click(); // Confirm revocation await otherSessionCard.getByRole('button', { name: /confirmer/i }).click(); // Wait for the session to be removed from the list await page1.waitForTimeout(1500); // Verify the second browser context is logged out await page2.reload(); // Should be redirected to login or show unauthenticated state await expect(page2).toHaveURL(/login/, { timeout: 10000 }); } } finally { await context1.close(); await context2.close(); } }); test('cannot revoke current session via button', async ({ page }, testInfo) => { const email = getTestEmail(testInfo.project.name); await login(page, email); await page.goto(getTenantUrl('/settings/sessions')); // Wait for sessions to load await expect(page.getByText(/sessions? actives?/i)).toBeVisible({ timeout: 10000 }); // The current session should have the "Session actuelle" badge const currentBadge = page.getByText(/session actuelle/i); await expect(currentBadge).toBeVisible(); // The session card with the badge should not have a disconnect button const currentSession = page.locator('.session-card').filter({ has: page.getByText(/session actuelle/i) }); await expect(currentSession).toBeVisible(); // Should not have a disconnect button inside this card const revokeButton = currentSession.getByRole('button', { name: /déconnecter/i }); await expect(revokeButton).toHaveCount(0); }); }); test.describe('Revoke All Sessions (AC3)', () => { test('can revoke all other sessions', async ({ browser }, testInfo) => { const email = getTestEmail(testInfo.project.name); // Create multiple sessions const context1 = await browser.newContext(); const context2 = await browser.newContext(); const page1 = await context1.newPage(); const page2 = await context2.newPage(); try { await login(page1, email); await login(page2, email); await page1.goto(getTenantUrl('/settings/sessions')); // Wait for sessions to load await expect(page1.getByText(/sessions? actives?/i)).toBeVisible({ timeout: 10000 }); // Look for "revoke all" button const revokeAllButton = page1.getByRole('button', { name: /déconnecter toutes les autres/i }); if ((await revokeAllButton.count()) > 0) { await revokeAllButton.click(); // Confirm await page1.getByRole('button', { name: /confirmer/i }).click(); // Wait for operation to complete await page1.waitForTimeout(1500); // Current session should still work await page1.reload(); await expect(page1).toHaveURL(/settings\/sessions/); // Other session should be logged out await page2.reload(); await expect(page2).toHaveURL(/login/, { timeout: 10000 }); } } finally { await context1.close(); await context2.close(); } }); test('shows confirmation before revoking all', async ({ page }, testInfo) => { const email = getTestEmail(testInfo.project.name); await login(page, email); // Create a second session to enable "revoke all" button const context2 = await page.context().browser()!.newContext(); const page2 = await context2.newPage(); await login(page2, email); await context2.close(); await page.goto(getTenantUrl('/settings/sessions')); // Wait for sessions to load await expect(page.getByText(/sessions? actives?/i)).toBeVisible({ timeout: 10000 }); const revokeAllButton = page.getByRole('button', { name: /déconnecter toutes les autres/i }); if ((await revokeAllButton.count()) > 0) { await revokeAllButton.click(); // Should show confirmation dialog/section await expect(page.getByText(/déconnecter.*session/i)).toBeVisible(); await expect(page.getByRole('button', { name: /confirmer/i })).toBeVisible(); await expect(page.getByRole('button', { name: /annuler/i })).toBeVisible(); // Cancel should dismiss the confirmation await page.getByRole('button', { name: /annuler/i }).click(); // Confirmation should be hidden await expect(page.getByRole('button', { name: /confirmer/i })).not.toBeVisible(); } }); }); test.describe('Logout (AC4)', () => { test('logout button redirects to login', async ({ page, browserName }, testInfo) => { // Skip on webkit due to navigation timing issues with SvelteKit test.skip(browserName === 'webkit', 'Webkit has navigation timing issues with SvelteKit'); const email = getTestEmail(testInfo.project.name); await login(page, email); await page.goto(getTenantUrl('/settings')); // Click logout button and wait for navigation const logoutButton = page.getByRole('button', { name: /déconnexion/i }); await expect(logoutButton).toBeVisible(); await Promise.all([ page.waitForURL(/login/, { timeout: 10000 }), logoutButton.click() ]); }); test('logout clears authentication', async ({ page, browserName }, testInfo) => { // Skip on webkit due to navigation timing issues with SvelteKit test.skip(browserName === 'webkit', 'Webkit has navigation timing issues with SvelteKit'); const email = getTestEmail(testInfo.project.name); await login(page, email); await page.goto(getTenantUrl('/settings')); // Logout - wait for navigation to complete const logoutButton = page.getByRole('button', { name: /déconnexion/i }); await expect(logoutButton).toBeVisible(); await Promise.all([ page.waitForURL(/login/, { timeout: 10000 }), logoutButton.click() ]); // Try to access protected page await page.goto(getTenantUrl('/settings/sessions')); // Should redirect to login await expect(page).toHaveURL(/login/, { timeout: 5000 }); }); }); test.describe('Navigation', () => { test('can navigate from settings to sessions', async ({ page, browserName }, testInfo) => { // Skip on webkit due to navigation timing issues with SvelteKit test.skip(browserName === 'webkit', 'Webkit has navigation timing issues with SvelteKit'); const email = getTestEmail(testInfo.project.name); await login(page, email); await page.goto(getTenantUrl('/settings')); // Click on sessions link/card await page.getByText(/mes sessions/i).click(); await expect(page).toHaveURL(/settings\/sessions/); await expect(page.getByRole('heading', { name: /mes sessions/i })).toBeVisible(); }); test('back button returns to settings', async ({ page, browserName }, testInfo) => { // Skip on webkit due to navigation timing issues with SvelteKit test.skip(browserName === 'webkit', 'Webkit has navigation timing issues with SvelteKit'); const email = getTestEmail(testInfo.project.name); await login(page, email); await page.goto(getTenantUrl('/settings/sessions')); // Wait for page to load await expect(page.getByRole('heading', { name: /mes sessions/i })).toBeVisible(); // Click back button (contains "Retour" text) await page.locator('.back-button').click(); // Wait for navigation - URL should no longer contain /sessions await expect(page).not.toHaveURL(/\/sessions/); // Verify we're on the main settings page await expect(page.getByText(/paramètres|mes sessions/i).first()).toBeVisible(); }); }); });