Les enseignants ont besoin de moyennes à jour immédiatement après la publication ou modification des notes, sans attendre un batch nocturne. Le système recalcule via Domain Events synchrones : statistiques d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées (normalisation /20), et moyenne générale par élève. Les résultats sont stockés dans des tables dénormalisées avec cache Redis (TTL 5 min). Trois endpoints API exposent les données avec contrôle d'accès par rôle. Une commande console permet le backfill des données historiques au déploiement.
685 lines
26 KiB
TypeScript
685 lines
26 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);
|
|
|
|
// Extract port from PLAYWRIGHT_BASE_URL or use default (4173 matches playwright.config.ts)
|
|
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}`;
|
|
|
|
// Test credentials
|
|
const ADMIN_EMAIL = 'e2e-subjects-admin@example.com';
|
|
const ADMIN_PASSWORD = 'SubjectsTest123';
|
|
const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
|
|
|
const projectRoot = join(__dirname, '../..');
|
|
const composeFile = join(projectRoot, 'compose.yaml');
|
|
|
|
function runSql(sql: string) {
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "${sql}" 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
}
|
|
|
|
function clearCache() {
|
|
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 in all environments
|
|
}
|
|
}
|
|
|
|
function cleanupSubjects() {
|
|
const sqls = [
|
|
// Delete homework-related data (subjects FK prevents deletion)
|
|
`DELETE FROM submission_attachments WHERE submission_id IN (SELECT hs.id FROM homework_submissions hs JOIN homework h ON hs.homework_id = h.id WHERE h.tenant_id = '${TENANT_ID}')`,
|
|
`DELETE FROM homework_submissions WHERE homework_id IN (SELECT id FROM homework WHERE tenant_id = '${TENANT_ID}')`,
|
|
`DELETE FROM homework_rule_exceptions WHERE homework_id IN (SELECT id FROM homework WHERE tenant_id = '${TENANT_ID}')`,
|
|
`DELETE FROM homework_attachments WHERE homework_id IN (SELECT id FROM homework WHERE tenant_id = '${TENANT_ID}')`,
|
|
`DELETE FROM homework WHERE tenant_id = '${TENANT_ID}'`,
|
|
// Delete grades and evaluations (grades FK → evaluations, evaluations FK → subjects)
|
|
`DELETE FROM grade_events WHERE grade_id IN (SELECT id FROM grades WHERE tenant_id = '${TENANT_ID}')`,
|
|
`DELETE FROM grades WHERE tenant_id = '${TENANT_ID}'`,
|
|
`DELETE FROM evaluations WHERE tenant_id = '${TENANT_ID}'`,
|
|
// Delete schedule slots (subjects FK with CASCADE)
|
|
`DELETE FROM schedule_slots WHERE tenant_id = '${TENANT_ID}'`,
|
|
// Delete assignments
|
|
`DELETE FROM replacement_classes WHERE replacement_id IN (SELECT id FROM teacher_replacements WHERE tenant_id = '${TENANT_ID}')`,
|
|
`DELETE FROM teacher_assignments WHERE tenant_id = '${TENANT_ID}'`,
|
|
`DELETE FROM subjects WHERE tenant_id = '${TENANT_ID}'`,
|
|
];
|
|
for (const sql of sqls) {
|
|
try {
|
|
runSql(sql);
|
|
} catch {
|
|
// Table may not exist yet
|
|
}
|
|
}
|
|
}
|
|
|
|
// Force serial execution to ensure Empty State runs first
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.describe('Subjects Management (Story 2.2)', () => {
|
|
// Create admin user and clean up subjects before running tests
|
|
test.beforeAll(async () => {
|
|
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' }
|
|
);
|
|
|
|
cleanupSubjects();
|
|
clearCache();
|
|
});
|
|
|
|
// Helper to login as admin
|
|
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 Promise.all([
|
|
page.waitForURL(/\/dashboard/, { timeout: 60000 }),
|
|
page.getByRole('button', { name: /se connecter/i }).click()
|
|
]);
|
|
}
|
|
|
|
// Helper to open "Nouvelle matière" dialog with proper wait
|
|
async function openNewSubjectDialog(page: import('@playwright/test').Page) {
|
|
const button = page.getByRole('button', { name: /nouvelle matière/i });
|
|
await button.waitFor({ state: 'visible' });
|
|
|
|
// Wait for any pending network requests to finish before clicking
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Click the button
|
|
await button.click();
|
|
|
|
// Wait for dialog to appear - retry click if needed (webkit timing issue)
|
|
const dialog = page.getByRole('dialog');
|
|
try {
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
} catch {
|
|
// Retry once - webkit sometimes needs a second click
|
|
await button.click();
|
|
await expect(dialog).toBeVisible({ timeout: 10000 });
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// EMPTY STATE - Must run FIRST before any subject is created
|
|
// ============================================================================
|
|
test.describe('Empty State', () => {
|
|
test('shows empty state message when no subjects exist', async ({ page }) => {
|
|
// Clean up subjects and all dependent tables right before this test
|
|
cleanupSubjects();
|
|
clearCache();
|
|
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// Wait for page to load
|
|
await expect(page.getByRole('heading', { name: /gestion des matières/i })).toBeVisible();
|
|
|
|
// Should show empty state
|
|
await expect(page.locator('.empty-state')).toBeVisible();
|
|
await expect(page.getByText(/aucune matière/i)).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /créer une matière/i })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// List Display
|
|
// ============================================================================
|
|
test.describe('List Display', () => {
|
|
test('displays all created subjects in the list', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// Create multiple subjects
|
|
const subjects = [
|
|
{ name: `Math-${Date.now()}`, code: 'MATH1' },
|
|
{ name: `Français-${Date.now()}`, code: 'FR1' },
|
|
{ name: `Histoire-${Date.now()}`, code: 'HIST1' }
|
|
];
|
|
|
|
for (const subject of subjects) {
|
|
await openNewSubjectDialog(page);
|
|
await page.locator('#subject-name').fill(subject.name);
|
|
await page.locator('#subject-code').fill(subject.code);
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
// Verify ALL subjects appear in the list
|
|
for (const subject of subjects) {
|
|
await expect(page.getByText(subject.name)).toBeVisible();
|
|
}
|
|
|
|
// Verify the number of subject cards matches (at least the ones we created)
|
|
const subjectCards = page.locator('.subject-card');
|
|
const count = await subjectCards.count();
|
|
expect(count).toBeGreaterThanOrEqual(subjects.length);
|
|
});
|
|
|
|
test('displays subject details correctly (code, color)', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// Create a subject with all details
|
|
const subjectName = `Details-${Date.now()}`;
|
|
const subjectCode = `DET${Date.now() % 10000}`;
|
|
await openNewSubjectDialog(page);
|
|
await page.locator('#subject-name').fill(subjectName);
|
|
await page.locator('#subject-code').fill(subjectCode);
|
|
|
|
// Select blue color
|
|
await page.locator('.color-swatch').first().click();
|
|
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Find the subject card
|
|
const subjectCard = page.locator('.subject-card', { hasText: subjectName });
|
|
await expect(subjectCard).toBeVisible();
|
|
|
|
// Verify code is displayed (uppercase)
|
|
await expect(subjectCard.getByText(subjectCode.toUpperCase())).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC1: Subject Creation
|
|
// ============================================================================
|
|
test.describe('AC1: Subject Creation', () => {
|
|
test('can create a new subject with all fields', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
|
|
// Navigate to subjects page
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
await expect(page.getByRole('heading', { name: /gestion des matières/i })).toBeVisible();
|
|
|
|
// Click "Nouvelle matière" button
|
|
await openNewSubjectDialog(page);
|
|
await expect(page.getByRole('heading', { name: /nouvelle matière/i })).toBeVisible();
|
|
|
|
// Fill form
|
|
const uniqueName = `Test-E2E-${Date.now()}`;
|
|
const uniqueCode = `TE${Date.now() % 100000}`;
|
|
await page.locator('#subject-name').fill(uniqueName);
|
|
await page.locator('#subject-code').fill(uniqueCode);
|
|
|
|
// Select a color (blue - Mathématiques)
|
|
await page.locator('.color-swatch').first().click();
|
|
|
|
// Note: Description is only available on the edit page, not in the create modal
|
|
|
|
// Submit
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
|
|
// Modal should close and subject should appear in list
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText(uniqueName)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('can create a subject with only required fields (name, code)', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
await openNewSubjectDialog(page);
|
|
|
|
// Fill only the required fields
|
|
const uniqueName = `Minimal-${Date.now()}`;
|
|
const uniqueCode = `MIN${Date.now() % 10000}`;
|
|
await page.locator('#subject-name').fill(uniqueName);
|
|
await page.locator('#subject-code').fill(uniqueCode);
|
|
|
|
// Submit button should be enabled
|
|
const submitButton = page.getByRole('button', { name: /créer la matière/i });
|
|
await expect(submitButton).toBeEnabled();
|
|
|
|
await submitButton.click();
|
|
|
|
// Subject should be created
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText(uniqueName)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('submit button is disabled when required fields are empty', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
await openNewSubjectDialog(page);
|
|
|
|
// Don't fill anything
|
|
const submitButton = page.getByRole('button', { name: /créer la matière/i });
|
|
await expect(submitButton).toBeDisabled();
|
|
|
|
// Fill name but not code
|
|
await page.locator('#subject-name').fill('Test');
|
|
await expect(submitButton).toBeDisabled();
|
|
|
|
// Fill code - button should enable
|
|
await page.locator('#subject-code').fill('TST');
|
|
await expect(submitButton).toBeEnabled();
|
|
});
|
|
|
|
test('can cancel subject creation', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
await openNewSubjectDialog(page);
|
|
|
|
// Fill form
|
|
await page.locator('#subject-name').fill('Should-Not-Be-Created');
|
|
await page.locator('#subject-code').fill('NOPE');
|
|
|
|
// Click cancel
|
|
await page.getByRole('button', { name: /annuler/i }).click();
|
|
|
|
// Modal should close
|
|
await expect(page.getByRole('dialog')).not.toBeVisible();
|
|
|
|
// Subject should not appear in list
|
|
await expect(page.getByText('Should-Not-Be-Created')).not.toBeVisible();
|
|
});
|
|
|
|
test('code is automatically converted to uppercase', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
await openNewSubjectDialog(page);
|
|
|
|
const uniqueName = `Upper-${Date.now()}`;
|
|
await page.locator('#subject-name').fill(uniqueName);
|
|
await page.locator('#subject-code').fill('lowercase');
|
|
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify code is displayed in uppercase
|
|
const subjectCard = page.locator('.subject-card', { hasText: uniqueName });
|
|
await expect(subjectCard.getByText('LOWERCASE')).toBeVisible();
|
|
});
|
|
|
|
test('shows error for duplicate code', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// Create first subject
|
|
const uniqueCode = `DUP${Date.now() % 10000}`;
|
|
await openNewSubjectDialog(page);
|
|
await page.locator('#subject-name').fill(`First-${Date.now()}`);
|
|
await page.locator('#subject-code').fill(uniqueCode);
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Try to create second subject with same code
|
|
await openNewSubjectDialog(page);
|
|
await page.locator('#subject-name').fill(`Second-${Date.now()}`);
|
|
await page.locator('#subject-code').fill(uniqueCode);
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
|
|
// Should show error
|
|
await expect(page.getByText(/existe déjà|already exists/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC2: Subject Modification
|
|
// ============================================================================
|
|
test.describe('AC2: Subject Modification', () => {
|
|
test('can modify an existing subject', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// First create a subject to modify
|
|
await openNewSubjectDialog(page);
|
|
const originalName = `ToModify-${Date.now()}`;
|
|
const originalCode = `MOD${Date.now() % 10000}`;
|
|
await page.locator('#subject-name').fill(originalName);
|
|
await page.locator('#subject-code').fill(originalCode);
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Find the subject card and click modify
|
|
const subjectCard = page.locator('.subject-card', { hasText: originalName });
|
|
await subjectCard.getByRole('button', { name: /modifier/i }).click();
|
|
|
|
// Should navigate to edit page
|
|
await expect(page).toHaveURL(/\/admin\/subjects\/[\w-]+/);
|
|
|
|
// Modify the name
|
|
const newName = `Modified-${Date.now()}`;
|
|
await page.locator('#subject-name').fill(newName);
|
|
|
|
// Save
|
|
await page.getByRole('button', { name: /enregistrer/i }).click();
|
|
|
|
// Should show success message
|
|
await expect(page.getByText(/mise à jour avec succès/i)).toBeVisible({ timeout: 10000 });
|
|
|
|
// Go back to list and verify
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
await expect(page.getByText(newName)).toBeVisible();
|
|
});
|
|
|
|
test('can cancel modification', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// Create a subject
|
|
await openNewSubjectDialog(page);
|
|
const originalName = `NoChange-${Date.now()}`;
|
|
const originalCode = `NCH${Date.now() % 10000}`;
|
|
await page.locator('#subject-name').fill(originalName);
|
|
await page.locator('#subject-code').fill(originalCode);
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Click modify
|
|
const subjectCard = page.locator('.subject-card', { hasText: originalName });
|
|
await subjectCard.getByRole('button', { name: /modifier/i }).click();
|
|
|
|
// Modify but cancel
|
|
await page.locator('#subject-name').fill('Should-Not-Change');
|
|
await page.getByRole('button', { name: /annuler/i }).click();
|
|
|
|
// Should go back to list
|
|
await expect(page).toHaveURL(/\/admin\/subjects$/);
|
|
|
|
// Original name should still be there
|
|
await expect(page.getByText(originalName)).toBeVisible();
|
|
await expect(page.getByText('Should-Not-Change')).not.toBeVisible();
|
|
});
|
|
|
|
test('can change subject color', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// Create a subject with blue color
|
|
await openNewSubjectDialog(page);
|
|
const subjectName = `ColorChange-${Date.now()}`;
|
|
const subjectCode = `CLR${Date.now() % 10000}`;
|
|
await page.locator('#subject-name').fill(subjectName);
|
|
await page.locator('#subject-code').fill(subjectCode);
|
|
await page.locator('.color-swatch').first().click(); // Blue
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Click modify
|
|
const subjectCard = page.locator('.subject-card', { hasText: subjectName });
|
|
await subjectCard.getByRole('button', { name: /modifier/i }).click();
|
|
|
|
// Change to red color (second swatch)
|
|
await page.locator('.color-swatch').nth(1).click();
|
|
|
|
// Save
|
|
await page.getByRole('button', { name: /enregistrer/i }).click();
|
|
await expect(page.getByText(/mise à jour avec succès/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('can remove subject color', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// Create a subject with a color
|
|
await openNewSubjectDialog(page);
|
|
const subjectName = `RemoveColor-${Date.now()}`;
|
|
const subjectCode = `RMC${Date.now() % 10000}`;
|
|
await page.locator('#subject-name').fill(subjectName);
|
|
await page.locator('#subject-code').fill(subjectCode);
|
|
await page.locator('.color-swatch').first().click();
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Click modify
|
|
const subjectCard = page.locator('.subject-card', { hasText: subjectName });
|
|
await subjectCard.getByRole('button', { name: /modifier/i }).click();
|
|
|
|
// Click "no color" button
|
|
await page.locator('.color-none').click();
|
|
|
|
// Save
|
|
await page.getByRole('button', { name: /enregistrer/i }).click();
|
|
await expect(page.getByText(/mise à jour avec succès/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC3: Deletion with warning for subjects with grades
|
|
// ============================================================================
|
|
test.describe('AC3: Deletion with warning for grades', () => {
|
|
// SKIP REASON: The Grades module is not yet implemented.
|
|
// HasGradesForSubjectHandler currently returns false (stub), so all subjects
|
|
// appear without grades and can be deleted without warning. This test will
|
|
// be enabled once the Grades module allows recording grades for subjects.
|
|
//
|
|
// When enabled, this test should:
|
|
// 1. Create a subject
|
|
// 2. Add at least one grade to it
|
|
// 3. Attempt to delete the subject
|
|
// 4. Verify the warning message about grades
|
|
// 5. Require explicit confirmation
|
|
test.skip('shows warning when trying to delete subject with grades', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
// Implementation pending Grades module
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC4: Subject deletion (soft delete)
|
|
// ============================================================================
|
|
test.describe('AC4: Subject deletion (soft delete)', () => {
|
|
test('can delete a subject without grades', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// Create a subject to delete
|
|
await openNewSubjectDialog(page);
|
|
const subjectName = `ToDelete-${Date.now()}`;
|
|
const subjectCode = `DEL${Date.now() % 10000}`;
|
|
await page.locator('#subject-name').fill(subjectName);
|
|
await page.locator('#subject-code').fill(subjectCode);
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText(subjectName)).toBeVisible();
|
|
|
|
// Find and click delete button
|
|
const subjectCard = page.locator('.subject-card', { hasText: subjectName });
|
|
await subjectCard.getByRole('button', { name: /supprimer/i }).click();
|
|
|
|
// Confirmation modal should appear
|
|
const deleteModal = page.getByRole('alertdialog');
|
|
await expect(deleteModal).toBeVisible({ timeout: 10000 });
|
|
await expect(deleteModal.getByText(subjectName)).toBeVisible();
|
|
|
|
// Confirm deletion
|
|
await deleteModal.getByRole('button', { name: /supprimer/i }).click();
|
|
|
|
// Modal should close and subject should no longer appear in list
|
|
await expect(deleteModal).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText(subjectName)).not.toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('can cancel deletion', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// Create a subject
|
|
await openNewSubjectDialog(page);
|
|
const subjectName = `NoDelete-${Date.now()}`;
|
|
const subjectCode = `NDL${Date.now() % 10000}`;
|
|
await page.locator('#subject-name').fill(subjectName);
|
|
await page.locator('#subject-code').fill(subjectCode);
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Find and click delete
|
|
const subjectCard = page.locator('.subject-card', { hasText: subjectName });
|
|
await subjectCard.getByRole('button', { name: /supprimer/i }).click();
|
|
|
|
// Confirmation modal should appear
|
|
const deleteModal = page.getByRole('alertdialog');
|
|
await expect(deleteModal).toBeVisible({ timeout: 10000 });
|
|
|
|
// Cancel deletion
|
|
await deleteModal.getByRole('button', { name: /annuler/i }).click();
|
|
|
|
// Modal should close and subject should still be there
|
|
await expect(deleteModal).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText(subjectName)).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Navigation
|
|
// ============================================================================
|
|
test.describe('Navigation', () => {
|
|
test('can access subjects page directly', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
|
|
// Navigate directly to subjects page
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
await expect(page).toHaveURL(/\/admin\/subjects/);
|
|
await expect(page.getByRole('heading', { name: /gestion des matières/i })).toBeVisible();
|
|
});
|
|
|
|
test('back button navigation works on edit page', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
// Create a subject
|
|
await openNewSubjectDialog(page);
|
|
const subjectName = `BackNav-${Date.now()}`;
|
|
const subjectCode = `BCK${Date.now() % 10000}`;
|
|
await page.locator('#subject-name').fill(subjectName);
|
|
await page.locator('#subject-code').fill(subjectCode);
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Go to edit page
|
|
const subjectCard = page.locator('.subject-card', { hasText: subjectName });
|
|
await subjectCard.getByRole('button', { name: /modifier/i }).click();
|
|
|
|
// Click back button to go back
|
|
await page.getByRole('button', { name: /retour aux matières/i }).click();
|
|
|
|
await expect(page).toHaveURL(/\/admin\/subjects$/);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Validation
|
|
// ============================================================================
|
|
test.describe('Validation', () => {
|
|
test('shows validation for subject name length', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
await openNewSubjectDialog(page);
|
|
|
|
// Try a name that's too short (1 char)
|
|
await page.locator('#subject-name').fill('A');
|
|
await page.locator('#subject-code').fill('TEST');
|
|
|
|
// The HTML5 minlength validation should prevent submission
|
|
const nameInput = page.locator('#subject-name');
|
|
const isInvalid = await nameInput.evaluate((el: HTMLInputElement) => !el.validity.valid);
|
|
expect(isInvalid).toBe(true);
|
|
});
|
|
|
|
test('shows validation for subject code format', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
await openNewSubjectDialog(page);
|
|
|
|
// Try a code that's too short (1 char)
|
|
await page.locator('#subject-name').fill('Test Subject');
|
|
await page.locator('#subject-code').fill('A');
|
|
|
|
// The HTML5 minlength validation should prevent submission
|
|
const codeInput = page.locator('#subject-code');
|
|
const isInvalid = await codeInput.evaluate((el: HTMLInputElement) => !el.validity.valid);
|
|
expect(isInvalid).toBe(true);
|
|
});
|
|
|
|
test('code rejects special characters (backend validation)', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
await openNewSubjectDialog(page);
|
|
|
|
// Try a code with special characters
|
|
await page.locator('#subject-name').fill('Test Subject');
|
|
await page.locator('#subject-code').fill('TEST-123');
|
|
|
|
// Submit - backend should reject it
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
|
|
// Should show error from backend
|
|
await expect(page.getByText(/alphanumériques|alphanumeric/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Color Picker
|
|
// ============================================================================
|
|
test.describe('Color Picker', () => {
|
|
test('can select predefined colors', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
await openNewSubjectDialog(page);
|
|
|
|
// Verify all color swatches are visible
|
|
const colorSwatches = page.locator('.color-swatch:not(.color-none)');
|
|
const count = await colorSwatches.count();
|
|
expect(count).toBeGreaterThanOrEqual(7); // At least 7 predefined colors
|
|
|
|
// Click each color and verify it gets selected
|
|
for (let i = 0; i < Math.min(count, 3); i++) {
|
|
await colorSwatches.nth(i).click();
|
|
await expect(colorSwatches.nth(i)).toHaveClass(/selected/);
|
|
}
|
|
});
|
|
|
|
test('can use custom color picker', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/subjects`);
|
|
|
|
await openNewSubjectDialog(page);
|
|
|
|
// The color input should be visible
|
|
const colorInput = page.locator('input[type="color"]');
|
|
await expect(colorInput).toBeVisible();
|
|
|
|
// Set a custom color
|
|
await colorInput.fill('#FF5733');
|
|
|
|
// Fill required fields and submit
|
|
const uniqueName = `CustomColor-${Date.now()}`;
|
|
const uniqueCode = `CC${Date.now() % 10000}`;
|
|
await page.locator('#subject-name').fill(uniqueName);
|
|
await page.locator('#subject-code').fill(uniqueCode);
|
|
|
|
await page.getByRole('button', { name: /créer la matière/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Subject should be created
|
|
await expect(page.getByText(uniqueName)).toBeVisible();
|
|
});
|
|
});
|
|
});
|