Les établissements scolaires utilisent des systèmes d'évaluation variés (notes /20, /10, lettres, compétences, sans notes). Jusqu'ici l'application imposait implicitement le mode notes /20, ce qui ne correspondait pas à la réalité pédagogique de nombreuses écoles. Cette configuration permet à chaque établissement de choisir son mode de notation par année scolaire, avec verrouillage automatique dès que des notes ont été saisies pour éviter les incohérences. Le Score Sérénité adapte ses pondérations selon le mode choisi (les compétences sont converties via un mapping, le mode sans notes exclut la composante notes).
285 lines
11 KiB
TypeScript
285 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 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, browserName }) => {
|
|
// Svelte 5 delegated onclick is not triggered by Playwright click on webkit
|
|
test.skip(browserName === 'webkit', 'Demo role switcher click not supported on webkit');
|
|
|
|
await loginAsAdmin(page);
|
|
|
|
// Switch to admin view in demo dashboard
|
|
await page.goto(`${ALPHA_URL}/dashboard`);
|
|
const adminButton = page.getByRole('button', { name: /admin/i });
|
|
await adminButton.click();
|
|
|
|
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 });
|
|
});
|
|
});
|