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-periods-admin@example.com'; const ADMIN_PASSWORD = 'PeriodsTest123'; const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; // Force serial execution — empty state must run first test.describe.configure({ mode: 'serial' }); test.describe('Periods Management (Story 2.3)', () => { 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' } ); execSync( `docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "DELETE FROM academic_periods WHERE tenant_id = '${TENANT_ID}'" 2>&1`, { encoding: 'utf-8' } ); }); 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() ]); } // ============================================================================ // Empty State // ============================================================================ test.describe('Empty State', () => { test('shows empty state when no periods 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 academic_periods WHERE tenant_id = '${TENANT_ID}'" 2>&1`, { encoding: 'utf-8' } ); } catch { // Ignore cleanup errors } await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); await expect(page.getByRole('heading', { name: /périodes scolaires/i })).toBeVisible(); await expect(page.getByText(/aucune période configurée/i)).toBeVisible(); await expect( page.getByRole('button', { name: /configurer les périodes/i }) ).toBeVisible(); }); }); // ============================================================================ // Year Selector Tabs // ============================================================================ test.describe('Year Selector', () => { test('displays three year tabs', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); const tabs = page.getByRole('tab'); await expect(tabs).toHaveCount(3); }); test('current year tab is active by default', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); const tabs = page.getByRole('tab'); // Middle tab (current) should be active — wait for Svelte hydration await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'true', { timeout: 10000 }); }); test('can switch between year tabs', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); const tabs = page.getByRole('tab'); // Wait for Svelte hydration and initial load to complete 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 }); // Wait for load triggered by tab switch await page.waitForLoadState('networkidle'); // Click previous year tab await tabs.nth(0).click(); await expect(tabs.nth(0)).toHaveAttribute('aria-selected', 'true', { timeout: 10000 }); }); }); // ============================================================================ // Period Configuration // ============================================================================ test.describe('Period Configuration', () => { test('can configure trimesters', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); // Click "Configurer les périodes" button await page.getByRole('button', { name: /configurer les périodes/i }).click(); // Modal should open const dialog = page.getByRole('dialog'); await expect(dialog).toBeVisible({ timeout: 5000 }); // Select trimester (should be default) await expect(dialog.locator('#period-type')).toHaveValue('trimester'); // Verify preview shows 3 trimesters await expect(dialog.getByText(/T1/)).toBeVisible(); await expect(dialog.getByText(/T2/)).toBeVisible(); await expect(dialog.getByText(/T3/)).toBeVisible(); // Submit await dialog.getByRole('button', { name: /configurer$/i }).click(); // Modal should close await expect(dialog).not.toBeVisible({ timeout: 10000 }); // Period cards should appear await expect(page.getByRole('heading', { name: 'T1' })).toBeVisible({ timeout: 10000 }); await expect(page.getByRole('heading', { name: 'T2' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'T3' })).toBeVisible(); }); test('shows trimester badge after configuration', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); await expect(page.getByText('Trimestres', { exact: true })).toBeVisible({ timeout: 10000 }); }); test('shows dates on each period card', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); // Wait for periods to load await expect(page.getByRole('heading', { name: 'T1' })).toBeVisible({ timeout: 10000 }); // Each period card should have start and end dates const periodCards = page.locator('.period-card'); const count = await periodCards.count(); expect(count).toBe(3); // Verify date labels exist await expect(page.getByText(/début/i).first()).toBeVisible(); await expect(page.getByText(/fin/i).first()).toBeVisible(); }); test('configure button no longer visible when periods exist', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); // Wait for periods to load await expect(page.getByRole('heading', { name: 'T1' })).toBeVisible({ timeout: 10000 }); // Configure button should not be visible await expect( page.getByRole('button', { name: /configurer les périodes/i }) ).not.toBeVisible(); }); test('can configure semesters on next year', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); // Wait for initial load to complete before switching tab const tabs = page.getByRole('tab'); await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'true', { timeout: 10000 }); await page.waitForLoadState('networkidle'); // Switch to next year tab await tabs.nth(2).click(); // Should show empty state for next year await expect(page.getByText(/aucune période configurée/i)).toBeVisible({ timeout: 10000 }); // Configure semesters for next year await page.getByRole('button', { name: /configurer les périodes/i }).click(); const dialog = page.getByRole('dialog'); await expect(dialog).toBeVisible({ timeout: 5000 }); // Select semester await dialog.locator('#period-type').selectOption('semester'); // Verify preview shows 2 semesters await expect(dialog.getByText(/S1/)).toBeVisible(); await expect(dialog.getByText(/S2/)).toBeVisible(); // Submit await dialog.getByRole('button', { name: /configurer$/i }).click(); // Modal should close and period cards appear await expect(dialog).not.toBeVisible({ timeout: 10000 }); await expect(page.getByRole('heading', { name: 'S1' })).toBeVisible({ timeout: 10000 }); await expect(page.getByRole('heading', { name: 'S2' })).toBeVisible(); }); }); // ============================================================================ // Period Date Modification // ============================================================================ test.describe('Period Date Modification', () => { test('each period card has a modify button', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); await expect(page.getByRole('heading', { name: 'T1' })).toBeVisible({ timeout: 10000 }); const modifyButtons = page.getByRole('button', { name: /modifier les dates/i }); await expect(modifyButtons).toHaveCount(3); }); test('opens edit modal when clicking modify', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); await expect(page.getByRole('heading', { name: 'T1' })).toBeVisible({ timeout: 10000 }); // Click modify on first period const modifyButtons = page.getByRole('button', { name: /modifier les dates/i }); await modifyButtons.first().click(); // Edit modal should open const dialog = page.getByRole('dialog'); await expect(dialog).toBeVisible({ timeout: 5000 }); await expect(dialog.getByText(/modifier T1/i)).toBeVisible(); // Date fields should be present await expect(dialog.locator('#edit-start-date')).toBeVisible(); await expect(dialog.locator('#edit-end-date')).toBeVisible(); }); test('can cancel date modification', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); await expect(page.getByRole('heading', { name: 'T1' })).toBeVisible({ timeout: 10000 }); const modifyButtons = page.getByRole('button', { name: /modifier les dates/i }); await modifyButtons.first().click(); const dialog = page.getByRole('dialog'); await expect(dialog).toBeVisible({ timeout: 5000 }); // Cancel await dialog.getByRole('button', { name: /annuler/i }).click(); await expect(dialog).not.toBeVisible({ timeout: 5000 }); }); }); // ============================================================================ // Navigation // ============================================================================ test.describe('Navigation', () => { test('can access periods page from admin navigation', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin`); // Click on periods link in the admin navigation await page.getByRole('link', { name: /périodes/i }).click(); await expect(page).toHaveURL(/\/admin\/academic-year\/periods/); await expect(page.getByRole('heading', { name: /périodes scolaires/i })).toBeVisible(); }); test('can access periods page directly', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/academic-year/periods`); await expect(page).toHaveURL(/\/admin\/academic-year\/periods/); await expect(page.getByRole('heading', { name: /périodes scolaires/i })).toBeVisible(); }); }); });