Les utilisateurs Classeo étaient limités à un seul rôle, alors que dans la réalité scolaire un directeur peut aussi être enseignant, ou un parent peut avoir un rôle vie scolaire. Cette limitation obligeait à créer des comptes distincts par fonction. Le modèle User supporte désormais plusieurs rôles simultanés avec basculement via le header. L'admin peut attribuer/retirer des rôles depuis l'interface de gestion, avec des garde-fous : pas d'auto- destitution, pas d'escalade de privilèges (seul SUPER_ADMIN peut attribuer SUPER_ADMIN), vérification du statut actif pour le switch de rôle, et TTL explicite sur le cache de rôle actif.
280 lines
10 KiB
TypeScript
280 lines
10 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 baseUrl = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:4173';
|
|
const urlMatch = baseUrl.match(/:(\d+)$/);
|
|
const PORT = urlMatch ? urlMatch[1] : '4173';
|
|
const ALPHA_URL = `http://ecole-alpha.classeo.local:${PORT}`;
|
|
|
|
const ADMIN_EMAIL = 'e2e-pedagogy-admin@example.com';
|
|
const ADMIN_PASSWORD = 'PedagogyTest123';
|
|
|
|
test.describe('Pedagogy - Grading Mode Configuration (Story 2.4)', () => {
|
|
test.beforeAll(async () => {
|
|
const projectRoot = join(__dirname, '../..');
|
|
const composeFile = join(projectRoot, 'compose.yaml');
|
|
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${ADMIN_EMAIL} --password=${ADMIN_PASSWORD} --role=ROLE_ADMIN 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
|
|
// Reset grading mode to default (numeric_20) to ensure clean state
|
|
try {
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php -r "
|
|
require 'vendor/autoload.php';
|
|
\\$kernel = new App\\Kernel('dev', true);
|
|
\\$kernel->boot();
|
|
\\$conn = \\$kernel->getContainer()->get('doctrine')->getConnection();
|
|
\\$conn->executeStatement('DELETE FROM school_grading_configurations');
|
|
" 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
} catch {
|
|
// Table might not exist yet, that's OK — default mode applies
|
|
}
|
|
});
|
|
|
|
async function loginAsAdmin(page: import('@playwright/test').Page) {
|
|
await page.goto(`${ALPHA_URL}/login`);
|
|
await page.locator('#email').fill(ADMIN_EMAIL);
|
|
await page.locator('#password').fill(ADMIN_PASSWORD);
|
|
await page.getByRole('button', { name: /se connecter/i }).click();
|
|
await expect(page).toHaveURL(/\/dashboard/, { timeout: 10000 });
|
|
}
|
|
|
|
// =========================================================================
|
|
// Navigation
|
|
// =========================================================================
|
|
|
|
test('pedagogy link is visible in admin navigation', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/classes`);
|
|
|
|
const pedagogyLink = page.getByRole('link', { name: /pédagogie/i });
|
|
await expect(pedagogyLink).toBeVisible();
|
|
});
|
|
|
|
test('pedagogy link navigates to pedagogy page', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/classes`);
|
|
|
|
await page.getByRole('link', { name: /pédagogie/i }).click();
|
|
await expect(page).toHaveURL(/\/admin\/pedagogy/, { timeout: 10000 });
|
|
});
|
|
|
|
test('pedagogy card is visible on admin dashboard', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
|
|
// Authenticated admin sees admin dashboard directly via role context
|
|
await page.goto(`${ALPHA_URL}/dashboard`);
|
|
|
|
await expect(page.getByRole('heading', { name: /administration/i })).toBeVisible({
|
|
timeout: 10000
|
|
});
|
|
|
|
const pedagogyCard = page.getByRole('link', { name: /pédagogie/i });
|
|
await expect(pedagogyCard).toBeVisible();
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC1: Display grading mode options
|
|
// =========================================================================
|
|
|
|
test('shows page title and subtitle', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
await expect(page.getByRole('heading', { name: /mode de notation/i })).toBeVisible();
|
|
await expect(page.getByText(/système d'évaluation/i)).toBeVisible();
|
|
});
|
|
|
|
test('shows current mode banner', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
// Wait for loading to finish
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Should show "Mode actuel" banner
|
|
await expect(page.getByText(/mode actuel/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('displays all five grading modes', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// All 5 mode cards should be visible (scope to .mode-card to avoid ambiguous matches)
|
|
const modeCards = page.locator('.mode-card');
|
|
await expect(modeCards).toHaveCount(5, { timeout: 10000 });
|
|
await expect(modeCards.filter({ hasText: /notes \/20/i })).toBeVisible();
|
|
await expect(modeCards.filter({ hasText: /notes \/10/i })).toBeVisible();
|
|
await expect(modeCards.filter({ hasText: /lettres/i })).toBeVisible();
|
|
await expect(modeCards.filter({ hasText: /compétences/i })).toBeVisible();
|
|
await expect(modeCards.filter({ hasText: /sans notes/i })).toBeVisible();
|
|
});
|
|
|
|
test('default mode is Notes sur 20', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// The "Notes /20" mode card should be selected (has mode-selected class)
|
|
const selectedCard = page.locator('.mode-card.mode-selected');
|
|
await expect(selectedCard).toBeVisible({ timeout: 10000 });
|
|
await expect(selectedCard).toContainText(/notes \/20/i);
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC1: Year selector
|
|
// =========================================================================
|
|
|
|
test('can switch between year tabs', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
const tabs = page.getByRole('tab');
|
|
|
|
// Wait for page to load
|
|
await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'true', { timeout: 10000 });
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Click next year tab
|
|
await tabs.nth(2).click();
|
|
await expect(tabs.nth(2)).toHaveAttribute('aria-selected', 'true', { timeout: 10000 });
|
|
|
|
// Click previous year tab
|
|
await tabs.nth(0).click();
|
|
await expect(tabs.nth(0)).toHaveAttribute('aria-selected', 'true', { timeout: 10000 });
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC2-4: Mode selection and preview
|
|
// =========================================================================
|
|
|
|
test('selecting a different mode shows save button', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Save button should not be visible initially
|
|
await expect(page.getByRole('button', { name: /enregistrer/i })).not.toBeVisible();
|
|
|
|
// Click a different mode
|
|
const competenciesCard = page.locator('.mode-card').filter({ hasText: /compétences/i });
|
|
await competenciesCard.click();
|
|
|
|
// Save and cancel buttons should appear
|
|
await expect(page.getByRole('button', { name: /enregistrer/i })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /annuler/i })).toBeVisible();
|
|
});
|
|
|
|
test('cancel button reverts mode selection', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Select a different mode
|
|
const lettersCard = page.locator('.mode-card').filter({ hasText: /lettres/i });
|
|
await lettersCard.click();
|
|
|
|
// Click cancel
|
|
await page.getByRole('button', { name: /annuler/i }).click();
|
|
|
|
// Save button should disappear
|
|
await expect(page.getByRole('button', { name: /enregistrer/i })).not.toBeVisible();
|
|
});
|
|
|
|
test('selecting competencies mode shows competency-specific description', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
await expect(page.getByText(/mode actuel/i)).toBeVisible({ timeout: 10000 });
|
|
|
|
const competenciesCard = page.locator('.mode-card').filter({ hasText: /compétences/i });
|
|
await competenciesCard.click();
|
|
|
|
// Check description inside the card (scoped to avoid matching bulletin impact text)
|
|
await expect(competenciesCard.locator('.mode-description')).toContainText(
|
|
/acquis.*en cours.*non acquis/i
|
|
);
|
|
});
|
|
|
|
test('selecting sans notes mode shows no-grades description', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
await expect(page.getByText(/mode actuel/i)).toBeVisible({ timeout: 10000 });
|
|
|
|
const noGradesCard = page.locator('.mode-card').filter({ hasText: /sans notes/i });
|
|
await noGradesCard.click();
|
|
|
|
// Check description inside the card (scoped to avoid matching bulletin impact text)
|
|
await expect(noGradesCard.locator('.mode-description')).toContainText(
|
|
/appréciations textuelles/i
|
|
);
|
|
});
|
|
|
|
test('shows bulletin impact preview when mode is selected', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Impact section should be visible
|
|
await expect(page.getByText(/impact sur les bulletins/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC2: Can change to a different mode
|
|
// =========================================================================
|
|
|
|
test('can save a new grading mode', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
await expect(page.getByText(/mode actuel/i)).toBeVisible({ timeout: 10000 });
|
|
|
|
// Select "Notes sur 10"
|
|
const numeric10Card = page.locator('.mode-card').filter({ hasText: /notes \/10/i });
|
|
await numeric10Card.click();
|
|
|
|
// Save
|
|
await page.getByRole('button', { name: /enregistrer/i }).click();
|
|
|
|
// Success message should appear
|
|
await expect(page.getByText(/mis à jour avec succès/i)).toBeVisible({ timeout: 10000 });
|
|
|
|
// The selected mode should now be "Notes /10"
|
|
const selectedCard = page.locator('.mode-card.mode-selected');
|
|
await expect(selectedCard).toContainText(/notes \/10/i);
|
|
|
|
// Reload the page to verify server-side persistence
|
|
await page.reload();
|
|
await page.waitForLoadState('networkidle');
|
|
await expect(page.locator('.mode-card.mode-selected')).toContainText(/notes \/10/i, {
|
|
timeout: 10000
|
|
});
|
|
|
|
// Restore default mode for other tests
|
|
const numeric20Card = page.locator('.mode-card').filter({ hasText: /notes \/20/i });
|
|
await numeric20Card.click();
|
|
await page.getByRole('button', { name: /enregistrer/i }).click();
|
|
await expect(page.getByText(/mis à jour avec succès/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|