Le menu d'administration contenait 13 liens à plat dans le header, ce qui débordait sur desktop et rendait le drawer mobile trop long à scanner. Les liens sont maintenant regroupés en 4 catégories (Personnes, Organisation, Année scolaire, Paramètres) avec des dropdowns au survol sur desktop et des accordéons repliables dans le drawer mobile. Le nombre d'éléments visibles passe de 13 à 5 (1 lien direct + 4 catégories), la catégorie active s'auto-déplie dans le menu mobile.
309 lines
12 KiB
TypeScript
309 lines
12 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-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`);
|
|
|
|
// 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: /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();
|
|
});
|
|
});
|
|
});
|