Files
Classeo/frontend/e2e/sessions.spec.ts
Mathias STRASSER b45ef735db feat: Dashboard placeholder avec preview Score Sérénité
Permet aux parents de visualiser une démo du Score Sérénité dès leur
première connexion, avant même que les données réelles soient disponibles.
Les autres rôles (enseignant, élève, admin) ont également leur dashboard
adapté avec des sections placeholder.

La landing page redirige automatiquement vers /dashboard si l'utilisateur
est déjà authentifié, offrant un accès direct au tableau de bord.
2026-02-04 18:34:08 +01:00

337 lines
11 KiB
TypeScript

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('/dashboard'), { 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
const logoutButton = page.getByRole('button', { name: /d[eé]connexion/i });
await expect(logoutButton).toBeVisible();
await logoutButton.click();
// Wait for redirect to login
await expect(page).toHaveURL(/login/, { timeout: 10000 });
});
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
const logoutButton = page.getByRole('button', { name: /d[eé]connexion/i });
await expect(logoutButton).toBeVisible();
await logoutButton.click();
// Wait for redirect to login
await expect(page).toHaveURL(/login/, { timeout: 10000 });
// 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();
});
});
});