Files
Classeo/frontend/e2e/sessions.spec.ts
Mathias STRASSER 70babb77ef
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Permettre à l'élève de consulter ses notes et moyennes
L'élève avait accès à ses compétences mais pas à ses notes numériques.
Cette fonctionnalité lui donne une vue complète de sa progression scolaire
avec moyennes par matière, détail par évaluation, statistiques de classe,
et un mode "découverte" pour révéler ses notes à son rythme (FR14, FR15).

Les notes ne sont visibles qu'après publication par l'enseignant, ce qui
garantit que l'élève les découvre avant ses parents (délai 24h story 6.7).
2026-04-06 01:56:11 +02:00

342 lines
12 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 Promise.all([
page.waitForURL(/\/dashboard/, { timeout: 60000 }),
page.getByRole('button', { name: /se connecter/i }).click()
]);
}
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();
// Wait for sessions to load (header text indicates data is present)
await expect(page.getByText(/sessions? actives?/i)).toBeVisible({ timeout: 10000 });
// Wait for session cards to appear (data fully rendered)
await expect(page.locator('.session-card').first()).toBeVisible({ timeout: 10000 });
// Current session should have the badge
await expect(page.getByText(/session actuelle/i)).toBeVisible({ timeout: 10000 });
});
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: 30000 });
});
});
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/, { timeout: 15000 });
// Verify we're on the main settings page
await expect(page.getByText(/paramètres|mes sessions/i).first()).toBeVisible();
});
});
});