feat: Permettre la création et modification de l'emploi du temps des classes
L'administration a besoin de construire et maintenir les emplois du temps hebdomadaires pour chaque classe, en s'assurant que les enseignants ne sont pas en conflit (même créneau, classes différentes) et que les affectations enseignant-matière-classe sont respectées. Cette implémentation couvre le CRUD complet des créneaux (ScheduleSlot), la détection de conflits (classe, enseignant, salle) avec possibilité de forcer, la validation des affectations côté serveur (AC2), l'intégration calendrier pour les jours bloqués, une vue mobile-first avec onglets jour par jour, et le drag-and-drop pour réorganiser les créneaux sur desktop.
This commit is contained in:
524
frontend/e2e/schedule-advanced.spec.ts
Normal file
524
frontend/e2e/schedule-advanced.spec.ts
Normal file
@@ -0,0 +1,524 @@
|
||||
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-schedule-admin@example.com';
|
||||
const ADMIN_PASSWORD = 'ScheduleTest123';
|
||||
const TEACHER_EMAIL = 'e2e-schedule-teacher@example.com';
|
||||
const TEACHER_PASSWORD = 'ScheduleTeacher123';
|
||||
const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
||||
|
||||
const projectRoot = join(__dirname, '../..');
|
||||
const composeFile = join(projectRoot, 'compose.yaml');
|
||||
|
||||
function runSql(sql: string) {
|
||||
execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "${sql}" 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDeterministicIds(): { schoolId: string; academicYearId: string } {
|
||||
const output = execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php -r '` +
|
||||
`require "/app/vendor/autoload.php"; ` +
|
||||
`$t="${TENANT_ID}"; $ns="6ba7b814-9dad-11d1-80b4-00c04fd430c8"; ` +
|
||||
`echo Ramsey\\Uuid\\Uuid::uuid5($ns,"school-$t")->toString()."\\n"; ` +
|
||||
`$m=(int)date("n"); $s=$m>=9?(int)date("Y"):(int)date("Y")-1; $e=$s+1; ` +
|
||||
`echo Ramsey\\Uuid\\Uuid::uuid5($ns,"$t:$s-$e")->toString();` +
|
||||
`' 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
).trim();
|
||||
const [schoolId, academicYearId] = output.split('\n');
|
||||
return { schoolId: schoolId!, academicYearId: academicYearId! };
|
||||
}
|
||||
|
||||
function cleanupScheduleData() {
|
||||
try {
|
||||
runSql(`DELETE FROM schedule_slots WHERE tenant_id = '${TENANT_ID}'`);
|
||||
} catch {
|
||||
// Table may not exist yet
|
||||
}
|
||||
}
|
||||
|
||||
function seedTeacherAssignments() {
|
||||
const { academicYearId } = resolveDeterministicIds();
|
||||
try {
|
||||
// Assign test teacher to ALL classes × ALL subjects so any dropdown combo is valid
|
||||
runSql(
|
||||
`INSERT INTO teacher_assignments (id, tenant_id, teacher_id, school_class_id, subject_id, academic_year_id, status, start_date, created_at, updated_at) ` +
|
||||
`SELECT gen_random_uuid(), '${TENANT_ID}', u.id, c.id, s.id, '${academicYearId}', 'active', NOW(), NOW(), NOW() ` +
|
||||
`FROM users u CROSS JOIN school_classes c CROSS JOIN subjects s ` +
|
||||
`WHERE u.email = '${TEACHER_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
|
||||
`AND c.tenant_id = '${TENANT_ID}' ` +
|
||||
`AND s.tenant_id = '${TENANT_ID}' ` +
|
||||
`ON CONFLICT DO NOTHING`
|
||||
);
|
||||
} catch {
|
||||
// Table may not exist
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupCalendarEntries() {
|
||||
try {
|
||||
runSql(`DELETE FROM school_calendar_entries WHERE tenant_id = '${TENANT_ID}'`);
|
||||
} catch {
|
||||
// Table may not exist
|
||||
}
|
||||
}
|
||||
|
||||
function seedBlockedDate(date: string, label: string, type: string) {
|
||||
const { academicYearId } = resolveDeterministicIds();
|
||||
runSql(
|
||||
`INSERT INTO school_calendar_entries (id, tenant_id, academic_year_id, entry_type, start_date, end_date, label, created_at) ` +
|
||||
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${academicYearId}', '${type}', '${date}', '${date}', '${label}', NOW()) ` +
|
||||
`ON CONFLICT DO NOTHING`
|
||||
);
|
||||
}
|
||||
|
||||
function getWeekdayInCurrentWeek(isoDay: number): string {
|
||||
const now = new Date();
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() - ((now.getDay() + 6) % 7));
|
||||
const target = new Date(monday);
|
||||
target.setDate(monday.getDate() + (isoDay - 1));
|
||||
return target.toISOString().split('T')[0]!;
|
||||
}
|
||||
|
||||
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 waitForScheduleReady(page: import('@playwright/test').Page) {
|
||||
await expect(page.getByRole('heading', { name: /emploi du temps/i })).toBeVisible({
|
||||
timeout: 15000
|
||||
});
|
||||
// Wait for either the grid or the empty state to appear
|
||||
await expect(page.locator('.schedule-grid, .empty-state, .alert-error')).toBeVisible({
|
||||
timeout: 15000
|
||||
});
|
||||
}
|
||||
|
||||
async function fillSlotForm(
|
||||
dialog: import('@playwright/test').Locator,
|
||||
options: {
|
||||
className?: string;
|
||||
dayValue?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
room?: string;
|
||||
} = {}
|
||||
) {
|
||||
const { className, dayValue = '1', startTime = '09:00', endTime = '10:00', room } = options;
|
||||
|
||||
if (className) {
|
||||
await dialog.locator('#slot-class').selectOption({ label: className });
|
||||
}
|
||||
// Wait for assignments to load — only the test teacher is assigned,
|
||||
// so the teacher dropdown filters down to 1 option
|
||||
const teacherOptions = dialog.locator('#slot-teacher option:not([value=""])');
|
||||
await expect(teacherOptions).toHaveCount(1, { timeout: 10000 });
|
||||
await dialog.locator('#slot-subject').selectOption({ index: 1 });
|
||||
await dialog.locator('#slot-teacher').selectOption({ index: 1 });
|
||||
await dialog.locator('#slot-day').selectOption(dayValue);
|
||||
await dialog.locator('#slot-start').fill(startTime);
|
||||
await dialog.locator('#slot-end').fill(endTime);
|
||||
if (room) {
|
||||
await dialog.locator('#slot-room').fill(room);
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Schedule Management - Modification & Conflicts & Calendar (Story 4.1)', () => {
|
||||
// Tests share database state (same tenant, users, slots) so they must run sequentially
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
// 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 teacher user
|
||||
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' }
|
||||
);
|
||||
|
||||
const { schoolId, academicYearId } = resolveDeterministicIds();
|
||||
|
||||
// Ensure test classes exist
|
||||
try {
|
||||
runSql(
|
||||
`INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
||||
);
|
||||
} catch {
|
||||
// May already exist
|
||||
}
|
||||
|
||||
try {
|
||||
runSql(
|
||||
`INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-5A', '5ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
||||
);
|
||||
} catch {
|
||||
// May already exist
|
||||
}
|
||||
|
||||
// Ensure test subjects exist
|
||||
try {
|
||||
runSql(
|
||||
`INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Maths', 'E2ESCHEDMATH', '#3b82f6', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
||||
);
|
||||
} catch {
|
||||
// May already exist
|
||||
}
|
||||
|
||||
try {
|
||||
runSql(
|
||||
`INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Français', 'E2ESCHEDFRA', '#ef4444', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
||||
);
|
||||
} catch {
|
||||
// May already exist
|
||||
}
|
||||
|
||||
cleanupScheduleData();
|
||||
cleanupCalendarEntries();
|
||||
clearCache();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
cleanupScheduleData();
|
||||
cleanupCalendarEntries();
|
||||
try {
|
||||
runSql(`DELETE FROM teacher_assignments WHERE tenant_id = '${TENANT_ID}'`);
|
||||
} catch {
|
||||
// Table may not exist
|
||||
}
|
||||
seedTeacherAssignments();
|
||||
clearCache();
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// AC3: Slot Modification & Deletion
|
||||
// ==========================================================================
|
||||
test.describe('AC3: Slot Modification & Deletion', () => {
|
||||
test('clicking a slot opens edit modal', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/schedule`);
|
||||
|
||||
await waitForScheduleReady(page);
|
||||
|
||||
// First create a slot
|
||||
const timeCell = page.locator('.time-cell').first();
|
||||
await timeCell.click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await fillSlotForm(dialog);
|
||||
|
||||
await dialog.getByRole('button', { name: /créer/i }).click();
|
||||
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click on the created slot
|
||||
const slotCard = page.locator('.slot-card').first();
|
||||
await expect(slotCard).toBeVisible({ timeout: 10000 });
|
||||
await slotCard.click();
|
||||
|
||||
// Edit modal should appear
|
||||
const editDialog = page.getByRole('dialog');
|
||||
await expect(editDialog).toBeVisible({ timeout: 10000 });
|
||||
await expect(
|
||||
editDialog.getByRole('heading', { name: /modifier le créneau/i })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('can delete a slot via edit modal', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/schedule`);
|
||||
|
||||
await waitForScheduleReady(page);
|
||||
|
||||
// Create a slot
|
||||
const timeCell = page.locator('.time-cell').first();
|
||||
await timeCell.click();
|
||||
|
||||
let dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await fillSlotForm(dialog, { dayValue: '2', startTime: '14:00', endTime: '15:00' });
|
||||
|
||||
await dialog.getByRole('button', { name: /créer/i }).click();
|
||||
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click on the slot to edit
|
||||
const slotCard = page.locator('.slot-card').first();
|
||||
await expect(slotCard).toBeVisible({ timeout: 10000 });
|
||||
await slotCard.click();
|
||||
|
||||
dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click delete button
|
||||
await dialog.getByRole('button', { name: /supprimer/i }).click();
|
||||
|
||||
// Confirmation modal should appear
|
||||
const deleteModal = page.getByRole('alertdialog');
|
||||
await expect(deleteModal).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Confirm deletion
|
||||
await deleteModal.getByRole('button', { name: /supprimer/i }).click();
|
||||
|
||||
// Modal should close and slot should disappear
|
||||
await expect(deleteModal).not.toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.slot-card')).not.toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('can modify a slot and see updated data in grid', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/schedule`);
|
||||
|
||||
await waitForScheduleReady(page);
|
||||
|
||||
// Create initial slot with room
|
||||
const timeCell = page.locator('.time-cell').first();
|
||||
await timeCell.click();
|
||||
|
||||
let dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await fillSlotForm(dialog, { room: 'A101' });
|
||||
|
||||
await dialog.getByRole('button', { name: /créer/i }).click();
|
||||
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify initial slot with room A101
|
||||
const slotCard = page.locator('.slot-card').first();
|
||||
await expect(slotCard).toBeVisible({ timeout: 10000 });
|
||||
await expect(slotCard.getByText('A101')).toBeVisible();
|
||||
|
||||
// Click to open edit modal
|
||||
await slotCard.click();
|
||||
|
||||
dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
await expect(
|
||||
dialog.getByRole('heading', { name: /modifier le créneau/i })
|
||||
).toBeVisible();
|
||||
|
||||
// Change room to B202
|
||||
await dialog.locator('#slot-room').clear();
|
||||
await dialog.locator('#slot-room').fill('B202');
|
||||
|
||||
// Submit modification
|
||||
await dialog.getByRole('button', { name: /modifier/i }).click();
|
||||
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify success and updated data
|
||||
await expect(page.getByText('Créneau modifié.')).toBeVisible({ timeout: 5000 });
|
||||
const updatedSlot = page.locator('.slot-card').first();
|
||||
await expect(updatedSlot.getByText('B202')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// AC4: Conflict Detection
|
||||
// ==========================================================================
|
||||
test.describe('AC4: Conflict Detection', () => {
|
||||
test('displays conflict warning when creating slot with same teacher at overlapping time', async ({
|
||||
page
|
||||
}) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/schedule`);
|
||||
|
||||
await waitForScheduleReady(page);
|
||||
|
||||
// Step 1: Create first slot (class 6A, Wednesday 10:00-11:00)
|
||||
const timeCell = page.locator('.time-cell').first();
|
||||
await timeCell.click();
|
||||
|
||||
let dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await fillSlotForm(dialog, { dayValue: '3', startTime: '10:00', endTime: '11:00' });
|
||||
|
||||
await dialog.getByRole('button', { name: /créer/i }).click();
|
||||
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.slot-card')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Step 2: Create conflicting slot with DIFFERENT class but SAME teacher at same time
|
||||
const timeCell2 = page.locator('.time-cell').first();
|
||||
await timeCell2.click();
|
||||
|
||||
dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await fillSlotForm(dialog, {
|
||||
className: 'E2E-Schedule-5A',
|
||||
dayValue: '3',
|
||||
startTime: '10:00',
|
||||
endTime: '11:00'
|
||||
});
|
||||
|
||||
// Submit - should trigger conflict detection
|
||||
await dialog.getByRole('button', { name: /créer/i }).click();
|
||||
|
||||
// Conflict warning should appear inside the dialog
|
||||
await expect(dialog.locator('.alert-warning')).toBeVisible({ timeout: 10000 });
|
||||
await expect(dialog.getByText(/conflits détectés/i)).toBeVisible();
|
||||
|
||||
// Force checkbox should be available
|
||||
await expect(dialog.getByText(/forcer la création/i)).toBeVisible();
|
||||
|
||||
// Dialog should still be open (not closed)
|
||||
await expect(dialog).toBeVisible();
|
||||
});
|
||||
|
||||
test('can force creation despite detected conflict', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/schedule`);
|
||||
|
||||
await waitForScheduleReady(page);
|
||||
|
||||
// Step 1: Create first slot (class 6A, Thursday 14:00-15:00)
|
||||
const timeCell = page.locator('.time-cell').first();
|
||||
await timeCell.click();
|
||||
|
||||
let dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await fillSlotForm(dialog, { dayValue: '4', startTime: '14:00', endTime: '15:00' });
|
||||
|
||||
await dialog.getByRole('button', { name: /créer/i }).click();
|
||||
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.slot-card')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Step 2: Create conflicting slot with different class, same teacher, same time
|
||||
const timeCell2 = page.locator('.time-cell').first();
|
||||
await timeCell2.click();
|
||||
|
||||
dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await fillSlotForm(dialog, {
|
||||
className: 'E2E-Schedule-5A',
|
||||
dayValue: '4',
|
||||
startTime: '14:00',
|
||||
endTime: '15:00'
|
||||
});
|
||||
|
||||
// First submit - triggers conflict warning
|
||||
await dialog.getByRole('button', { name: /créer/i }).click();
|
||||
await expect(dialog.locator('.alert-warning')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Check force checkbox
|
||||
await dialog.locator('.force-checkbox input[type="checkbox"]').check();
|
||||
|
||||
// Submit again with force enabled
|
||||
await dialog.getByRole('button', { name: /créer/i }).click();
|
||||
|
||||
// Modal should close - slot created despite conflict
|
||||
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Success message should appear
|
||||
await expect(page.getByText('Créneau créé.')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// AC5: Calendar Respect (Blocked Days)
|
||||
// ==========================================================================
|
||||
test.describe('AC5: Calendar Respect', () => {
|
||||
test('time validation prevents end before start', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/schedule`);
|
||||
|
||||
await waitForScheduleReady(page);
|
||||
|
||||
// Open creation modal
|
||||
const timeCell = page.locator('.time-cell').first();
|
||||
await timeCell.click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Set end time before start time
|
||||
await dialog.locator('#slot-start').fill('10:00');
|
||||
await dialog.locator('#slot-end').fill('09:00');
|
||||
|
||||
// Error message should appear
|
||||
await expect(
|
||||
dialog.getByText(/l'heure de fin doit être après/i)
|
||||
).toBeVisible();
|
||||
|
||||
// Submit should be disabled
|
||||
await expect(dialog.getByRole('button', { name: /créer/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('blocked day is visually marked in the grid', async ({ page }) => {
|
||||
// Seed a holiday on Wednesday of current week
|
||||
const wednesdayDate = getWeekdayInCurrentWeek(3);
|
||||
seedBlockedDate(wednesdayDate, 'Jour férié test', 'holiday');
|
||||
clearCache();
|
||||
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/schedule`);
|
||||
await waitForScheduleReady(page);
|
||||
|
||||
// The third day-column (Wednesday) should have the blocked class
|
||||
const dayColumns = page.locator('.day-column');
|
||||
await expect(dayColumns.nth(2)).toHaveClass(/day-blocked/, { timeout: 10000 });
|
||||
|
||||
// Should display the reason badge in the header
|
||||
await expect(page.getByText('Jour férié test')).toBeVisible();
|
||||
});
|
||||
|
||||
test('cannot create a slot on a blocked day', async ({ page }) => {
|
||||
// Seed a vacation on Tuesday of current week
|
||||
const tuesdayDate = getWeekdayInCurrentWeek(2);
|
||||
seedBlockedDate(tuesdayDate, 'Vacances test', 'vacation');
|
||||
clearCache();
|
||||
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`${ALPHA_URL}/admin/schedule`);
|
||||
await waitForScheduleReady(page);
|
||||
|
||||
// Tuesday column should be blocked
|
||||
const dayColumns = page.locator('.day-column');
|
||||
await expect(dayColumns.nth(1)).toHaveClass(/day-blocked/, { timeout: 10000 });
|
||||
|
||||
// Attempt to click a time cell in the blocked day — dialog should NOT open
|
||||
// Use dispatchEvent to bypass pointer-events: none
|
||||
await dayColumns.nth(1).locator('.time-cell').first().dispatchEvent('click');
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).not.toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user