Files
Classeo/frontend/e2e/image-rights.spec.ts
Mathias STRASSER dc2be898d5
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Provisionner automatiquement un nouvel établissement
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.
2026-04-16 09:27:25 +02:00

318 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);
// 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 — unique to this spec to avoid cross-spec collisions
const ADMIN_EMAIL = 'e2e-imgrights-admin@example.com';
const ADMIN_PASSWORD = 'ImgRightsAdmin123';
const STUDENT_EMAIL = 'e2e-imgrights-student@example.com';
const STUDENT_PASSWORD = 'ImgRightsStudent123';
let _studentUserId: string;
/**
* Extracts the User ID from the Symfony console table output.
*
* The create-test-user command outputs a table like:
* | Property | Value |
* | User ID | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
*/
function extractUserId(output: string): string {
const match = output.match(/User ID\s+([a-f0-9-]{36})/i);
if (!match) {
throw new Error(`Could not extract User ID from command output:\n${output}`);
}
return match[1];
}
test.describe('Image Rights Management', () => {
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const projectRoot = join(__dirname, '../..');
const composeFile = join(projectRoot, 'compose.yaml');
// Create admin user
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' }
);
// Create student user and capture userId
const studentOutput = execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT_EMAIL} --password=${STUDENT_PASSWORD} --role=ROLE_ELEVE 2>&1`,
{ encoding: 'utf-8' }
);
_studentUserId = extractUserId(studentOutput);
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
}
});
// 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 page.getByRole('button', { name: /se connecter/i }).click();
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
}
// Helper to login as student
async function loginAsStudent(page: import('@playwright/test').Page) {
await page.goto(`${ALPHA_URL}/login`);
await page.locator('#email').fill(STUDENT_EMAIL);
await page.locator('#password').fill(STUDENT_PASSWORD);
await page.getByRole('button', { name: /se connecter/i }).click();
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
}
/**
* Waits for the image rights page to finish loading.
*
* After hydration, the page either shows the student tables (sections with
* h2 headings) or the empty state. Waiting for one of these ensures the
* component is interactive and API data has been fetched.
*/
async function waitForPageLoaded(page: import('@playwright/test').Page) {
await expect(
page.getByRole('heading', { name: /droit à l'image/i })
).toBeVisible({ timeout: 15000 });
// Wait for either the stats bar (data loaded) or empty state (no students)
await expect(
page.locator('.stats-bar')
.or(page.locator('.empty-state'))
).toBeVisible({ timeout: 15000 });
}
// ============================================================================
// [P1] Page loads with correct structure
// ============================================================================
test('[P1] page loads with correct structure', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/image-rights`);
await waitForPageLoaded(page);
// Title
await expect(
page.getByRole('heading', { name: /droit à l'image/i })
).toBeVisible();
// Subtitle
await expect(
page.getByText(/consultez et gérez les autorisations/i)
).toBeVisible();
// Status filter dropdown
const filterSelect = page.locator('#filter-status');
await expect(filterSelect).toBeVisible();
// Verify filter options
const options = filterSelect.locator('option');
await expect(options.filter({ hasText: /tous les statuts/i })).toHaveCount(1);
await expect(options.filter({ hasText: /^Autorisé$/ })).toHaveCount(1);
await expect(options.filter({ hasText: /^Refusé$/ })).toHaveCount(1);
await expect(options.filter({ hasText: /^Non renseigné$/ })).toHaveCount(1);
// Search input
await expect(
page.locator('input[type="search"]')
).toBeVisible();
// Export CSV button
await expect(
page.getByRole('button', { name: /exporter csv/i })
).toBeVisible();
// Filter and reset buttons
await expect(
page.getByRole('button', { name: /^filtrer$/i })
).toBeVisible();
await expect(
page.getByRole('button', { name: /réinitialiser/i })
).toBeVisible();
// At least one student section should be visible (pagination may hide the other on page 1)
await expect(
page.getByRole('heading', { name: /élèves autorisés/i })
.or(page.getByRole('heading', { name: /élèves non autorisés/i }))
.first()
).toBeVisible();
// Stats bar
await expect(page.locator('.stats-bar')).toBeVisible();
});
// ============================================================================
// [P1] Filter by status works
// ============================================================================
test('[P1] filter by status works', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/image-rights`);
await waitForPageLoaded(page);
// Select "Autorisé" in the status filter
await page.locator('#filter-status').selectOption('authorized');
// Click "Filtrer" to apply
await page.getByRole('button', { name: /^filtrer$/i }).click();
// Wait for the page to reload with filtered data
await expect(page).toHaveURL(/status=authorized/);
await waitForPageLoaded(page);
// After filtering by "Autorisé", either:
// - The stats bar shows with 0 unauthorized, OR
// - The empty state shows (no authorized students found)
const statsBarVisible = await page.locator('.stats-bar').isVisible();
if (statsBarVisible) {
const dangerCount = await page.locator('.stat-count.stat-danger').count();
if (dangerCount > 0) {
const unauthorizedCount = await page.locator('.stat-count.stat-danger').textContent();
expect(parseInt(unauthorizedCount ?? '0', 10)).toBe(0);
}
// No stat-danger element means no unauthorized students — correct after filtering by "Autorisé"
} else {
await expect(page.locator('.empty-state')).toBeVisible();
}
// Reset filters to restore original state
await page.getByRole('button', { name: 'Réinitialiser', exact: true }).click();
await waitForPageLoaded(page);
// URL should no longer contain status filter
expect(page.url()).not.toContain('status=');
});
// ============================================================================
// [P1] Status update via dropdown
// ============================================================================
test('[P1] status update via dropdown', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/image-rights`);
await waitForPageLoaded(page);
// Find the first student's action select (in either authorized or unauthorized table)
const firstActionSelect = page.locator('td[data-label="Actions"] select').first();
await expect(firstActionSelect).toBeVisible({ timeout: 10000 });
// Get the current value
const currentValue = await firstActionSelect.inputValue();
// Pick a different status to switch to
const newValue = currentValue === 'authorized' ? 'refused' : 'authorized';
// Change the status
await firstActionSelect.selectOption(newValue);
// Success message should appear
await expect(
page.locator('.alert-success')
).toBeVisible({ timeout: 10000 });
await expect(
page.locator('.alert-success')
).toContainText(/statut mis à jour/i);
// Restore original status to leave test data clean
// Need to re-locate because the student may have moved to a different section
const restoredSelect = page.locator('td[data-label="Actions"] select').first();
await restoredSelect.selectOption(currentValue);
await expect(
page.locator('.alert-success')
).toBeVisible({ timeout: 10000 });
});
// ============================================================================
// [P1] Export CSV button triggers download
// ============================================================================
test('[P1] export CSV button triggers download', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/image-rights`);
await waitForPageLoaded(page);
// Listen for download event
const downloadPromise = page.waitForEvent('download', { timeout: 15000 });
// Click the export button
await page.getByRole('button', { name: /exporter csv/i }).click();
// Verify the download starts
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('droits-image.csv');
// Success message should appear
await expect(
page.locator('.alert-success')
).toBeVisible({ timeout: 10000 });
await expect(
page.locator('.alert-success')
).toContainText(/export csv/i);
});
// ============================================================================
// [P2] Text search filters students
// ============================================================================
test('[P2] text search filters students', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/image-rights`);
await waitForPageLoaded(page);
// Get the total student count before searching
const totalBefore = await page.locator('.stat-count.stat-total').textContent();
const totalBeforeNum = parseInt(totalBefore ?? '0', 10);
// Type a search term that matches the test student email prefix
const searchInput = page.locator('input[type="search"]');
await searchInput.fill('e2e-imgrights');
// Wait for the debounced search to apply (300ms debounce + some margin)
await page.waitForTimeout(500);
// The URL should be updated with the search parameter
await expect(page).toHaveURL(/search=e2e-imgrights/);
// The filtered count should be less than or equal to the total
// and specifically should find our test student
const totalAfter = await page.locator('.stat-count.stat-total').textContent();
const totalAfterNum = parseInt(totalAfter ?? '0', 10);
expect(totalAfterNum).toBeGreaterThanOrEqual(1);
expect(totalAfterNum).toBeLessThanOrEqual(totalBeforeNum);
// Clear the search
await searchInput.clear();
await page.waitForTimeout(500);
// Total should be restored
expect(page.url()).not.toContain('search=');
});
// ============================================================================
// [P2] Unauthorized role redirected
// ============================================================================
test('[P2] student role cannot access image rights page', async ({ page }) => {
await loginAsStudent(page);
await page.goto(`${ALPHA_URL}/admin/image-rights`);
// Admin guard in +layout.svelte redirects non-admin users to /dashboard
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
expect(page.url()).toContain('/dashboard');
});
});