Le commit 23dd717 a introduit un cache Redis (paginated_queries.cache)
pour les requêtes paginées. Les tests E2E qui modifient les données via
SQL direct (beforeAll, cleanup) contournent la couche applicative et ne
déclenchent pas l'invalidation du cache, provoquant des données obsolètes.
De plus, plusieurs problèmes d'isolation entre tests ont été découverts :
- Les tests classes.spec.ts supprimaient les données d'autres specs via
DELETE FROM school_classes sans nettoyer les FK dépendantes
- Les tests user-blocking utilisaient des emails partagés entre les
projets Playwright (chromium/firefox/webkit) exécutés en parallèle,
causant des race conditions sur l'état du compte utilisateur
- Le handler NotifyTeachersPedagogicalDayHandler s'exécutait de manière
synchrone, bloquant la réponse HTTP pendant l'envoi des emails
- La sélection d'un enseignant remplaçant effaçait l'autre dropdown car
{#if} supprimait l'option sélectionnée du DOM
Corrections appliquées :
- Ajout de cache:pool:clear après chaque modification SQL directe
- Nettoyage des FK dépendantes avant les DELETE (classes, subjects)
- Emails uniques par projet navigateur pour éviter les race conditions
- Routage de JourneePedagogiqueAjoutee vers le transport async
- Remplacement de {#if} par disabled sur les selects de remplacement
- Recherche par nom sur la page classes pour gérer la pagination
- Patterns toPass() pour la fiabilité Firefox sur les color pickers
562 lines
20 KiB
TypeScript
562 lines
20 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-calendar-admin@example.com';
|
|
const ADMIN_PASSWORD = 'CalendarTest123';
|
|
const TEACHER_EMAIL = 'e2e-calendar-teacher@example.com';
|
|
const TEACHER_PASSWORD = 'CalendarTeacher123';
|
|
const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
|
|
|
// Dynamic future weekday for pedagogical day (avoids stale hardcoded dates, French holidays, and summer vacation)
|
|
const PED_DAY_DATE = (() => {
|
|
const d = new Date();
|
|
d.setDate(d.getDate() + 30); // ~1 month ahead, stays within school time (avoids summer vacation)
|
|
while (d.getDay() === 0 || d.getDay() === 6) d.setDate(d.getDate() + 1);
|
|
// Skip known French fixed holidays (MM-DD)
|
|
const holidays = ['01-01', '05-01', '05-08', '07-14', '08-15', '11-01', '11-11', '12-25'];
|
|
let mmdd = `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
if (holidays.includes(mmdd)) d.setDate(d.getDate() + 1);
|
|
while (d.getDay() === 0 || d.getDay() === 6) d.setDate(d.getDate() + 1);
|
|
// Double-check after weekend skip
|
|
mmdd = `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
if (holidays.includes(mmdd)) d.setDate(d.getDate() + 1);
|
|
while (d.getDay() === 0 || d.getDay() === 6) d.setDate(d.getDate() + 1);
|
|
return d.toISOString().split('T')[0];
|
|
})();
|
|
const PED_DAY_LABEL = 'Formation enseignants';
|
|
|
|
// Serial: empty state must be verified before import, display after import
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.describe('Calendar Management (Story 2.11)', () => {
|
|
test.beforeAll(async () => {
|
|
const projectRoot = join(__dirname, '../..');
|
|
const composeFile = join(projectRoot, 'compose.yaml');
|
|
|
|
// Create admin and teacher test users
|
|
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' }
|
|
);
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${TEACHER_EMAIL} --password=${TEACHER_PASSWORD} --role=ROLE_PROF 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
|
|
// Clean calendar entries to ensure empty state
|
|
try {
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "DELETE FROM school_calendar_entries WHERE tenant_id = '${TENANT_ID}'" 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
} catch {
|
|
// Table might not have data yet
|
|
}
|
|
|
|
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
|
|
}
|
|
});
|
|
|
|
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: 30000 }),
|
|
page.getByRole('button', { name: /se connecter/i }).click()
|
|
]);
|
|
}
|
|
|
|
async function loginAsTeacher(page: import('@playwright/test').Page) {
|
|
await page.goto(`${ALPHA_URL}/login`);
|
|
await page.locator('#email').fill(TEACHER_EMAIL);
|
|
await page.locator('#password').fill(TEACHER_PASSWORD);
|
|
await Promise.all([
|
|
page.waitForURL(/\/dashboard/, { timeout: 30000 }),
|
|
page.getByRole('button', { name: /se connecter/i }).click()
|
|
]);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Navigation (AC1)
|
|
// ============================================================================
|
|
test.describe('Navigation', () => {
|
|
test('[P1] can access calendar page from admin navigation', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/classes`);
|
|
|
|
// Hover "Année scolaire" category to reveal dropdown
|
|
const nav = page.locator('.desktop-nav');
|
|
await nav.getByRole('button', { name: /année scolaire/i }).hover();
|
|
await nav.getByRole('menuitem', { name: /calendrier/i }).click();
|
|
|
|
await expect(page).toHaveURL(/\/admin\/calendar/);
|
|
await expect(
|
|
page.getByRole('heading', { name: /calendrier scolaire/i })
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('[P1] can access calendar page directly', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await expect(page).toHaveURL(/\/admin\/calendar/);
|
|
await expect(
|
|
page.getByRole('heading', { name: /calendrier scolaire/i })
|
|
).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Authorization (AC1)
|
|
// ============================================================================
|
|
test.describe('Authorization', () => {
|
|
test('[P0] teacher can access admin layout but calendar returns error', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
// Teacher can access admin layout (ROLE_PROF in ADMIN_ROLES for image-rights)
|
|
// but calendar page may show access denied from backend
|
|
await expect(page).toHaveURL(/\/admin\/calendar/, { timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Empty State (AC1)
|
|
// ============================================================================
|
|
test.describe('Empty State', () => {
|
|
test('[P1] shows empty state when no calendar configured', async ({ page }) => {
|
|
// Clean up right before test to avoid race conditions
|
|
const projectRoot = join(__dirname, '../..');
|
|
const composeFile = join(projectRoot, 'compose.yaml');
|
|
try {
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "DELETE FROM school_calendar_entries WHERE tenant_id = '${TENANT_ID}'" 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
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
|
|
}
|
|
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: /calendrier scolaire/i })
|
|
).toBeVisible();
|
|
await expect(page.getByText(/aucun calendrier configuré/i)).toBeVisible({
|
|
timeout: 10000
|
|
});
|
|
await expect(
|
|
page.getByRole('button', { name: /importer le calendrier officiel/i })
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('[P1] displays three year selector tabs', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
const tabs = page.locator('.year-tab');
|
|
await expect(tabs).toHaveCount(3);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Import Official Calendar (AC2)
|
|
// ============================================================================
|
|
test.describe('Import Official Calendar', () => {
|
|
test('[P1] import button opens modal with zone selector', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await page.getByRole('button', { name: /importer calendrier officiel/i }).first().click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
await expect(dialog.getByText(/importer le calendrier officiel/i)).toBeVisible();
|
|
});
|
|
|
|
test('[P2] import modal shows zones A, B, C', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await page.getByRole('button', { name: /importer calendrier officiel/i }).first().click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
await expect(dialog.getByText('Zone A')).toBeVisible();
|
|
await expect(dialog.getByText('Zone B')).toBeVisible();
|
|
await expect(dialog.getByText('Zone C')).toBeVisible();
|
|
});
|
|
|
|
test('[P2] can cancel import modal', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await page.getByRole('button', { name: /importer calendrier officiel/i }).first().click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
await dialog.getByRole('button', { name: /annuler/i }).click();
|
|
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('[P2] modal closes on Escape key', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await page.getByRole('button', { name: /importer calendrier officiel/i }).first().click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
// Press Escape to dismiss the modal
|
|
await page.keyboard.press('Escape');
|
|
|
|
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('[P0] shows error when import API fails', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
|
|
// Intercept import POST to simulate server error
|
|
await page.route('**/calendar/import-official', (route) =>
|
|
route.fulfill({
|
|
status: 500,
|
|
contentType: 'application/ld+json',
|
|
body: JSON.stringify({ 'hydra:description': 'Erreur serveur interne' })
|
|
})
|
|
);
|
|
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await page.getByRole('button', { name: /importer calendrier officiel/i }).first().click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
// Submit import (intercepted → 500)
|
|
await dialog.getByRole('button', { name: /^importer$/i }).click();
|
|
|
|
// Close modal to reveal error message on the page
|
|
await dialog.getByRole('button', { name: /annuler/i }).click();
|
|
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
|
|
|
// Error alert should be visible
|
|
await expect(page.locator('.alert-error')).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('[P0] importing zone A populates calendar with entries', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
// Wait for empty state to be displayed
|
|
await expect(page.getByText(/aucun calendrier configuré/i)).toBeVisible({
|
|
timeout: 10000
|
|
});
|
|
|
|
// Open import modal from empty state CTA
|
|
await page.getByRole('button', { name: /importer le calendrier officiel/i }).click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
// Zone A is pre-selected, click import
|
|
await dialog.getByRole('button', { name: /^importer$/i }).click();
|
|
|
|
// Modal should close
|
|
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Success message
|
|
await expect(page.getByText(/calendrier officiel importé/i)).toBeVisible({
|
|
timeout: 10000
|
|
});
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Calendar Display after Import (AC1, AC3, AC4)
|
|
// ============================================================================
|
|
test.describe('Calendar Display', () => {
|
|
test('[P1] shows zone badge after import', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await expect(page.locator('.zone-badge')).toContainText('Zone A', { timeout: 10000 });
|
|
});
|
|
|
|
test('[P0] shows holidays section with actual entries', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
// Verify the section heading exists with a count > 0
|
|
await expect(
|
|
page.getByRole('heading', { name: /jours fériés/i })
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify specific imported holiday entries are displayed
|
|
await expect(page.getByText('Toussaint', { exact: true })).toBeVisible();
|
|
await expect(page.getByText('Noël', { exact: true })).toBeVisible();
|
|
|
|
// Verify entry cards exist (not just the heading)
|
|
const holidaySection = page.locator('.entry-section').filter({
|
|
has: page.getByRole('heading', { name: /jours fériés/i })
|
|
});
|
|
await expect(holidaySection.locator('.entry-card').first()).toBeVisible();
|
|
});
|
|
|
|
test('[P1] shows vacations section with specific entries', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: /vacances scolaires/i })
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify specific vacation entry names from Zone A official data
|
|
await expect(page.getByText('Hiver')).toBeVisible();
|
|
await expect(page.getByText('Printemps')).toBeVisible();
|
|
|
|
// Verify entry cards exist within the vacation section
|
|
const vacationSection = page.locator('.entry-section').filter({
|
|
has: page.getByRole('heading', { name: /vacances scolaires/i })
|
|
});
|
|
await expect(vacationSection.locator('.entry-card').first()).toBeVisible();
|
|
const cardCount = await vacationSection.locator('.entry-card').count();
|
|
expect(cardCount).toBeGreaterThanOrEqual(4);
|
|
});
|
|
|
|
test('[P2] shows legend with color indicators', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
// Wait for calendar to load
|
|
await expect(
|
|
page.getByRole('heading', { name: /jours fériés/i })
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
await expect(
|
|
page.locator('.legend-item').filter({ hasText: /jours fériés/i })
|
|
).toBeVisible();
|
|
await expect(
|
|
page.locator('.legend-item').filter({ hasText: /vacances/i })
|
|
).toBeVisible();
|
|
await expect(
|
|
page.locator('.legend-item').filter({ hasText: /journées pédagogiques/i })
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('[P2] can switch between year tabs', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
// Wait for calendar to load
|
|
await expect(
|
|
page.getByRole('heading', { name: /jours fériés/i })
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
const tabs = page.locator('.year-tab');
|
|
|
|
// Middle tab (current year) should be active by default
|
|
await expect(tabs.nth(1)).toHaveClass(/year-tab-active/);
|
|
|
|
// Click next year tab
|
|
await tabs.nth(2).click();
|
|
await expect(tabs.nth(2)).toHaveClass(/year-tab-active/, { timeout: 5000 });
|
|
|
|
// Click previous year tab
|
|
await tabs.nth(0).click();
|
|
await expect(tabs.nth(0)).toHaveClass(/year-tab-active/, { timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Pedagogical Day (AC5)
|
|
// ============================================================================
|
|
test.describe('Pedagogical Day', () => {
|
|
// Clean up any existing ped day with the same date before the serial ped day tests
|
|
test.beforeAll(async () => {
|
|
const projectRoot = join(__dirname, '../..');
|
|
const composeFile = join(projectRoot, 'compose.yaml');
|
|
try {
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "DELETE FROM school_calendar_entries WHERE tenant_id = '${TENANT_ID}' AND type = 'pedagogical_day' AND start_date = '${PED_DAY_DATE}'" 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
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
|
|
}
|
|
});
|
|
|
|
test('[P1] add pedagogical day button is visible', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await expect(
|
|
page.getByRole('button', { name: /ajouter journée pédagogique/i })
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('[P1] pedagogical day modal opens with form fields', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await page.getByRole('button', { name: /ajouter journée pédagogique/i }).click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
// Form fields
|
|
await expect(dialog.locator('#ped-date')).toBeVisible();
|
|
await expect(dialog.locator('#ped-label')).toBeVisible();
|
|
await expect(dialog.locator('#ped-description')).toBeVisible();
|
|
});
|
|
|
|
test('[P2] can cancel pedagogical day modal', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await page.getByRole('button', { name: /ajouter journée pédagogique/i }).click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
await dialog.getByRole('button', { name: /annuler/i }).click();
|
|
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('[P1] submit button disabled when fields empty', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
await page.getByRole('button', { name: /ajouter journée pédagogique/i }).click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
const submitButton = dialog.getByRole('button', { name: /^ajouter$/i });
|
|
|
|
// Both fields empty → button disabled
|
|
await expect(submitButton).toBeDisabled();
|
|
|
|
// Fill only date → still disabled (label missing)
|
|
await dialog.locator('#ped-date').fill(PED_DAY_DATE);
|
|
await expect(submitButton).toBeDisabled();
|
|
|
|
// Clear date, fill only label → still disabled (date missing)
|
|
await dialog.locator('#ped-date').fill('');
|
|
await dialog.locator('#ped-label').fill(PED_DAY_LABEL);
|
|
await expect(submitButton).toBeDisabled();
|
|
|
|
// Fill both date and label → button enabled
|
|
await dialog.locator('#ped-date').fill(PED_DAY_DATE);
|
|
await expect(submitButton).toBeEnabled();
|
|
});
|
|
|
|
test('[P0] can add a pedagogical day successfully', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
// Wait for calendar to load
|
|
await expect(
|
|
page.getByRole('heading', { name: /jours fériés/i })
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
// Open modal
|
|
await page.getByRole('button', { name: /ajouter journée pédagogique/i }).click();
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
// Fill form with dynamic future date
|
|
await dialog.locator('#ped-date').fill(PED_DAY_DATE);
|
|
await dialog.locator('#ped-label').fill(PED_DAY_LABEL);
|
|
await dialog.locator('#ped-description').fill('Journée de formation continue');
|
|
|
|
// Submit and wait for API response
|
|
const responsePromise = page.waitForResponse(
|
|
(resp) => resp.url().includes('/calendar/pedagogical-day') && resp.request().method() === 'POST'
|
|
);
|
|
await dialog.getByRole('button', { name: /^ajouter$/i }).click();
|
|
const response = await responsePromise;
|
|
expect(response.status()).toBeLessThan(400);
|
|
|
|
// Modal should close
|
|
await expect(dialog).not.toBeVisible({ timeout: 15000 });
|
|
|
|
// Success message
|
|
await expect(
|
|
page.getByText(/journée pédagogique ajoutée/i)
|
|
).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('[P1] added pedagogical day appears in list', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
// Wait for calendar to load
|
|
await expect(
|
|
page.getByRole('heading', { name: /jours fériés/i })
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
// Pedagogical day section should exist with the added day
|
|
await expect(
|
|
page.getByRole('heading', { name: /journées pédagogiques/i })
|
|
).toBeVisible();
|
|
await expect(page.getByText(PED_DAY_LABEL)).toBeVisible();
|
|
});
|
|
|
|
test('[P2] pedagogical day shows distinct amber styling', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await page.goto(`${ALPHA_URL}/admin/calendar`);
|
|
|
|
// Wait for calendar to load
|
|
await expect(
|
|
page.getByRole('heading', { name: /journées pédagogiques/i })
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify the pedagogical day section has an amber indicator dot
|
|
const pedSection = page.locator('.entry-section').filter({
|
|
has: page.getByRole('heading', { name: /journées pédagogiques/i })
|
|
});
|
|
const sectionDot = pedSection.locator('.section-dot');
|
|
await expect(sectionDot).toBeVisible();
|
|
await expect(sectionDot).toHaveCSS('background-color', 'rgb(245, 158, 11)');
|
|
});
|
|
});
|
|
});
|