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.
307 lines
12 KiB
TypeScript
307 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 page.getByRole('button', { name: /se connecter/i }).click();
|
|
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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, { timeout: 10000 });
|
|
});
|
|
|
|
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: 20000 });
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|