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).
342 lines
12 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|