Files
Classeo/frontend/e2e/homework-rules.spec.ts
Mathias STRASSER dc2be898d5
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: Provisionner automatiquement un nouvel établissement
Lorsqu'un super-admin crée un établissement via l'interface, le système
doit automatiquement créer la base tenant, exécuter les migrations,
créer le premier utilisateur admin et envoyer l'invitation — le tout
de manière asynchrone pour ne pas bloquer la réponse HTTP.

Ce mécanisme rend chaque établissement opérationnel dès sa création
sans intervention manuelle sur l'infrastructure.
2026-04-16 09:27:25 +02:00

212 lines
7.4 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-homework-rules-admin@example.com';
const ADMIN_PASSWORD = 'HomeworkRulesAdmin123';
const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
test.describe('Homework Rules Configuration', () => {
// Serial: les tests d'historique dépendent des entrées créées par les tests précédents
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const projectRoot = join(__dirname, '../..');
const composeFile = join(projectRoot, 'compose.yaml');
// Clear rate limiter to prevent login throttling across serial tests
try {
execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear cache.rate_limiter --env=dev 2>&1`,
{ encoding: 'utf-8' }
);
} catch {
// Cache pool may not exist
}
// Create admin user
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' }
);
// Clean up homework rules from previous test runs
execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "DELETE FROM homework_rules_history WHERE tenant_id = '${TENANT_ID}'" 2>&1`,
{ encoding: 'utf-8' }
);
execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "DELETE FROM homework_rules WHERE tenant_id = '${TENANT_ID}'" 2>&1`,
{ encoding: 'utf-8' }
);
try {
execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear paginated_queries.cache 2>&1`,
{ encoding: 'utf-8' }
);
} catch {
// Cache pool may not exist
}
});
test.beforeEach(async () => {
const projectRoot = join(__dirname, '../..');
const composeFile = join(projectRoot, 'compose.yaml');
try {
execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear cache.rate_limiter --env=dev 2>&1`,
{ encoding: 'utf-8' }
);
} catch {
// Cache pool may not exist
}
});
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 page.waitForURL(/\/dashboard/, { timeout: 60000 });
}
async function waitForPageLoaded(page: import('@playwright/test').Page) {
await expect(
page.getByRole('heading', { name: /règles de devoirs/i })
).toBeVisible({ timeout: 15000 });
await expect(
page.locator('.card').first()
).toBeVisible({ timeout: 15000 });
}
// AC1: Page configuration pédagogique → section "Règles de devoirs" accessible
test('page affiche la configuration des règles de devoirs', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/homework-rules`);
await waitForPageLoaded(page);
await expect(
page.getByRole('heading', { name: /règles de devoirs/i })
).toBeVisible();
// Mode selector visible
await expect(page.getByRole('radio', { name: /avertissement/i })).toBeVisible();
await expect(page.getByRole('radio', { name: /blocage/i })).toBeVisible();
await expect(page.getByRole('radio', { name: /désactivé/i })).toBeVisible();
});
// AC5: Mode par défaut = "Soft"
test('mode par défaut est Avertissement (soft)', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/homework-rules`);
await waitForPageLoaded(page);
const softRadio = page.getByRole('radio', { name: /avertissement/i });
await expect(softRadio).toHaveAttribute('aria-checked', 'true');
});
// AC2: Règle définissable : délai minimum
test('configurer la règle de délai minimum', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/homework-rules`);
await waitForPageLoaded(page);
// Enable minimum delay rule
const delayCheckbox = page.getByRole('checkbox', { name: /délai minimum/i });
await delayCheckbox.check();
// Set 3 days
const daysInput = page.locator('input[type="number"]');
await daysInput.fill('3');
// Save
await page.getByRole('button', { name: /enregistrer/i }).click();
// Success message
await expect(page.getByText(/mises à jour avec succès/i)).toBeVisible({ timeout: 10000 });
});
// AC2: Règle définissable : jours concernés (pas de devoir pour lundi)
test('configurer la règle pas de devoir pour lundi', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/homework-rules`);
await waitForPageLoaded(page);
// Enable no-monday rule
const mondayCheckbox = page.getByRole('checkbox', { name: /pas de devoir pour lundi/i });
await mondayCheckbox.check();
// Save
await page.getByRole('button', { name: /enregistrer/i }).click();
await expect(page.getByText(/mises à jour avec succès/i)).toBeVisible({ timeout: 10000 });
});
// AC5: Choix mode Hard (blocage)
test('changer le mode en Blocage', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/homework-rules`);
await waitForPageLoaded(page);
// Select hard mode
await page.getByRole('radio', { name: /blocage/i }).click();
// Save
await page.getByRole('button', { name: /enregistrer/i }).click();
await expect(page.getByText(/mises à jour avec succès/i)).toBeVisible({ timeout: 10000 });
});
// AC7: Règles désactivables complètement
test('désactiver les règles complètement', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/homework-rules`);
await waitForPageLoaded(page);
// Select disabled mode
await page.getByRole('radio', { name: /désactivé/i }).click();
// Rules section should disappear
await expect(page.getByText(/les enseignants peuvent créer des devoirs librement/i)).toBeVisible();
// Save
await page.getByRole('button', { name: /enregistrer/i }).click();
await expect(page.getByText(/mises à jour avec succès/i)).toBeVisible({ timeout: 10000 });
});
// AC6: Historique des changements conservé
test('voir l\'historique des changements avec contenu', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/homework-rules`);
await waitForPageLoaded(page);
// Click history button
await page.getByRole('button', { name: /voir l'historique/i }).click();
// Modal should appear
const dialog = page.getByRole('dialog', { name: /historique des changements/i });
await expect(dialog).toBeVisible({ timeout: 10000 });
// Verify history is not empty (previous tests created entries)
await expect(dialog.getByText(/aucun changement/i)).not.toBeVisible();
// Verify at least one history entry with mode and date
const firstEntry = dialog.locator('.history-entry').first();
await expect(firstEntry).toBeVisible();
await expect(firstEntry.locator('.history-mode')).toBeVisible();
await expect(firstEntry.locator('.history-date')).toBeVisible();
});
});