Un enseignant qui donne le même travail à plusieurs classes devait jusqu'ici recréer manuellement chaque devoir. La duplication permet de sélectionner les classes cibles, d'ajuster les dates d'échéance par classe, et de créer tous les devoirs en une seule opération atomique (transaction). La validation s'effectue par classe (affectation enseignant, date d'échéance) avec un rapport d'erreurs détaillé. L'infrastructure de warnings est prête pour les règles de timing de la Story 5.3. Le filtrage par classe dans la liste des devoirs passe côté serveur pour rester compatible avec la pagination.
647 lines
26 KiB
TypeScript
647 lines
26 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 TEACHER_EMAIL = 'e2e-homework-teacher@example.com';
|
|
const TEACHER_PASSWORD = 'HomeworkTest123';
|
|
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! };
|
|
}
|
|
|
|
/**
|
|
* Returns a future weekday date string (YYYY-MM-DD).
|
|
* Skips weekends to satisfy DueDateValidator.
|
|
*/
|
|
function getNextWeekday(daysFromNow: number): string {
|
|
const date = new Date();
|
|
date.setDate(date.getDate() + daysFromNow);
|
|
// Skip to Monday if weekend
|
|
const day = date.getDay();
|
|
if (day === 0) date.setDate(date.getDate() + 1); // Sunday → Monday
|
|
if (day === 6) date.setDate(date.getDate() + 2); // Saturday → Monday
|
|
// Use local date components to avoid UTC timezone shift from toISOString()
|
|
const y = date.getFullYear();
|
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
const d = String(date.getDate()).padStart(2, '0');
|
|
return `${y}-${m}-${d}`;
|
|
}
|
|
|
|
async function loginAsTeacher(page: import('@playwright/test').Page) {
|
|
await page.goto(`${ALPHA_URL}/login`);
|
|
await page.locator('#email').fill(TEACHER_EMAIL);
|
|
await page.locator('#password').fill(TEACHER_PASSWORD);
|
|
await Promise.all([
|
|
page.waitForURL(/\/dashboard/, { timeout: 30000 }),
|
|
page.getByRole('button', { name: /se connecter/i }).click()
|
|
]);
|
|
}
|
|
|
|
async function navigateToHomework(page: import('@playwright/test').Page) {
|
|
await page.goto(`${ALPHA_URL}/dashboard/teacher/homework`);
|
|
await expect(page.getByRole('heading', { name: /mes devoirs/i })).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
function seedTeacherAssignments() {
|
|
const { academicYearId } = resolveDeterministicIds();
|
|
try {
|
|
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
|
|
}
|
|
}
|
|
|
|
test.describe('Homework Management (Story 5.1)', () => {
|
|
test.beforeAll(async () => {
|
|
// 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' }
|
|
);
|
|
|
|
// Ensure class and subject exist
|
|
const { schoolId, academicYearId } = resolveDeterministicIds();
|
|
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-HW-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
runSql(
|
|
`INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-HW-Maths', 'E2EMAT', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
} catch {
|
|
// May already exist
|
|
}
|
|
|
|
seedTeacherAssignments();
|
|
clearCache();
|
|
});
|
|
|
|
test.beforeEach(async () => {
|
|
// Clean up homework data
|
|
try {
|
|
runSql(`DELETE FROM homework WHERE tenant_id = '${TENANT_ID}' AND teacher_id IN (SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}')`);
|
|
} catch {
|
|
// Table may not exist
|
|
}
|
|
|
|
// Re-ensure data exists
|
|
const { schoolId, academicYearId } = resolveDeterministicIds();
|
|
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-HW-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
runSql(
|
|
`INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-HW-Maths', 'E2EMAT', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
} catch {
|
|
// May already exist
|
|
}
|
|
|
|
seedTeacherAssignments();
|
|
clearCache();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Navigation
|
|
// ============================================================================
|
|
test.describe('Navigation', () => {
|
|
test('homework link appears in teacher navigation', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
const nav = page.locator('.desktop-nav');
|
|
await expect(nav.getByRole('link', { name: /devoirs/i })).toBeVisible({ timeout: 15000 });
|
|
});
|
|
|
|
test('can navigate to homework page', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await expect(page.getByRole('heading', { name: /mes devoirs/i })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Empty State
|
|
// ============================================================================
|
|
test.describe('Empty State', () => {
|
|
test('shows empty state when no homework exists', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await expect(page.getByText(/aucun devoir/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC1: Create Homework
|
|
// ============================================================================
|
|
test.describe('AC1: Create Homework', () => {
|
|
test('can create a new homework', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
// Open create modal
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Select class
|
|
const classSelect = page.locator('#hw-class');
|
|
await expect(classSelect).toBeVisible();
|
|
await classSelect.selectOption({ index: 1 });
|
|
|
|
// Select subject (AC2: filtered by class)
|
|
const subjectSelect = page.locator('#hw-subject');
|
|
await expect(subjectSelect).toBeEnabled();
|
|
await subjectSelect.selectOption({ index: 1 });
|
|
|
|
// Fill title
|
|
await page.locator('#hw-title').fill('Exercices chapitre 5');
|
|
|
|
// Fill description
|
|
await page.locator('#hw-description').fill('Pages 42-45, exercices 1 à 10');
|
|
|
|
// Set due date (next weekday, at least 2 days from now)
|
|
await page.locator('#hw-due-date').fill(getNextWeekday(3));
|
|
|
|
// Submit
|
|
await page.getByRole('button', { name: /créer le devoir/i }).click();
|
|
|
|
// Modal closes and homework appears in list
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Exercices chapitre 5')).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('cancel closes the modal without creating', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.getByRole('button', { name: /annuler/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC2: Subject Filtering
|
|
// ============================================================================
|
|
test.describe('AC2: Subject Filtering', () => {
|
|
test('subject dropdown is disabled until class is selected', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Subject should be disabled initially
|
|
await expect(page.locator('#hw-subject')).toBeDisabled();
|
|
|
|
// Select class
|
|
await page.locator('#hw-class').selectOption({ index: 1 });
|
|
|
|
// Subject should now be enabled
|
|
await expect(page.locator('#hw-subject')).toBeEnabled();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC5: Published Homework Appears in List
|
|
// ============================================================================
|
|
test.describe('AC5: Published Homework', () => {
|
|
test('created homework appears in list with correct info', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
// Create a homework
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.locator('#hw-class').selectOption({ index: 1 });
|
|
await page.locator('#hw-subject').selectOption({ index: 1 });
|
|
await page.locator('#hw-title').fill('Devoir de mathématiques');
|
|
await page.locator('#hw-due-date').fill(getNextWeekday(5));
|
|
await page.getByRole('button', { name: /créer le devoir/i }).click();
|
|
|
|
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify homework card shows
|
|
await expect(page.getByText('Devoir de mathématiques')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText(/publié/i)).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC6: Edit Homework
|
|
// ============================================================================
|
|
test.describe('AC6: Edit Homework', () => {
|
|
test('can modify an existing homework', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
// First create a homework
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
await page.locator('#hw-class').selectOption({ index: 1 });
|
|
await page.locator('#hw-subject').selectOption({ index: 1 });
|
|
await page.locator('#hw-title').fill('Devoir à modifier');
|
|
await page.locator('#hw-due-date').fill(getNextWeekday(5));
|
|
await page.getByRole('button', { name: /créer le devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Devoir à modifier')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Click edit
|
|
await page.getByRole('button', { name: /modifier/i }).first().click();
|
|
const editDialog = page.getByRole('dialog');
|
|
await expect(editDialog).toBeVisible({ timeout: 10000 });
|
|
|
|
// Change title
|
|
await page.locator('#edit-title').clear();
|
|
await page.locator('#edit-title').fill('Devoir modifié');
|
|
|
|
// Save
|
|
await page.getByRole('button', { name: /enregistrer/i }).click();
|
|
await expect(editDialog).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify updated title
|
|
await expect(page.getByText('Devoir modifié')).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('can delete an existing homework', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
// First create a homework
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
await page.locator('#hw-class').selectOption({ index: 1 });
|
|
await page.locator('#hw-subject').selectOption({ index: 1 });
|
|
await page.locator('#hw-title').fill('Devoir à supprimer');
|
|
await page.locator('#hw-due-date').fill(getNextWeekday(5));
|
|
await page.getByRole('button', { name: /créer le devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Devoir à supprimer')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Click delete
|
|
await page.getByRole('button', { name: /supprimer/i }).first().click();
|
|
|
|
// Confirm in alertdialog
|
|
const confirmDialog = page.getByRole('alertdialog');
|
|
await expect(confirmDialog).toBeVisible({ timeout: 5000 });
|
|
await expect(page.getByText(/êtes-vous sûr/i)).toBeVisible();
|
|
|
|
await confirmDialog.getByRole('button', { name: /supprimer/i }).click();
|
|
await expect(confirmDialog).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Homework should be gone from the list (or show as deleted)
|
|
await expect(page.getByText('Devoir à supprimer')).not.toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Date Validation
|
|
// ============================================================================
|
|
test.describe('Date Validation', () => {
|
|
test('due date input has min attribute set to tomorrow', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
const dueDateInput = page.locator('#hw-due-date');
|
|
const minValue = await dueDateInput.getAttribute('min');
|
|
const tomorrow = new Date();
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
const ey = tomorrow.getFullYear();
|
|
const em = String(tomorrow.getMonth() + 1).padStart(2, '0');
|
|
const ed = String(tomorrow.getDate()).padStart(2, '0');
|
|
expect(minValue).toBe(`${ey}-${em}-${ed}`);
|
|
});
|
|
|
|
test('backend rejects a past due date', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.locator('#hw-class').selectOption({ index: 1 });
|
|
await page.locator('#hw-subject').selectOption({ index: 1 });
|
|
await page.locator('#hw-title').fill('Devoir date passée');
|
|
await page.locator('#hw-description').fill('Test validation');
|
|
|
|
// Set a past date — fill() works with Svelte 5 bind:value
|
|
const yesterday = new Date();
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
const y = yesterday.getFullYear();
|
|
const m = String(yesterday.getMonth() + 1).padStart(2, '0');
|
|
const d = String(yesterday.getDate()).padStart(2, '0');
|
|
const pastDate = `${y}-${m}-${d}`;
|
|
await page.locator('#hw-due-date').fill(pastDate);
|
|
|
|
// Bypass HTML native validation (min attribute) to test backend validation
|
|
await page.locator('form.modal-body').evaluate((el) => el.setAttribute('novalidate', ''));
|
|
await page.getByRole('button', { name: /créer le devoir/i }).click();
|
|
|
|
// Backend should reject — an error alert appears
|
|
await expect(page.locator('.alert-error')).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC: Duplicate Homework (Story 5.2)
|
|
// ============================================================================
|
|
test.describe('Story 5.2: Duplicate Homework', () => {
|
|
test.beforeAll(async () => {
|
|
// Ensure a second class exists for duplication
|
|
const { schoolId, academicYearId } = resolveDeterministicIds();
|
|
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-HW-6B', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
} catch {
|
|
// May already exist
|
|
}
|
|
|
|
seedTeacherAssignments();
|
|
clearCache();
|
|
});
|
|
|
|
async function createHomework(page: import('@playwright/test').Page, title: string) {
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
await page.locator('#hw-class').selectOption({ index: 1 });
|
|
await page.locator('#hw-subject').selectOption({ index: 1 });
|
|
await page.locator('#hw-title').fill(title);
|
|
await page.locator('#hw-due-date').fill(getNextWeekday(5));
|
|
await page.getByRole('button', { name: /créer le devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText(title)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
test('duplicate button is visible on homework card', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await createHomework(page, 'Devoir dupliquer test');
|
|
|
|
await expect(page.getByRole('button', { name: /dupliquer/i })).toBeVisible();
|
|
});
|
|
|
|
test('opens duplicate modal with class selection', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await createHomework(page, 'Devoir à dupliquer');
|
|
|
|
await page.getByRole('button', { name: /dupliquer/i }).first().click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 10000 });
|
|
await expect(dialog.getByText(/dupliquer le devoir/i)).toBeVisible();
|
|
await expect(dialog.getByText('Devoir à dupliquer')).toBeVisible();
|
|
});
|
|
|
|
test('shows target classes checkboxes excluding source class', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await createHomework(page, 'Devoir classes cibles');
|
|
|
|
await page.getByRole('button', { name: /dupliquer/i }).first().click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 10000 });
|
|
|
|
// Should show checkboxes for target classes
|
|
const checkboxes = dialog.locator('input[type="checkbox"]');
|
|
await expect(checkboxes.first()).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('can duplicate homework to a target class', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await createHomework(page, 'Devoir duplication réussie');
|
|
|
|
await page.getByRole('button', { name: /dupliquer/i }).first().click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 10000 });
|
|
|
|
// Select first target class
|
|
const firstCheckbox = dialog.locator('input[type="checkbox"]').first();
|
|
await firstCheckbox.check();
|
|
|
|
// Click duplicate button
|
|
await page.getByRole('button', { name: /dupliquer \(1 classe\)/i }).click();
|
|
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Homework should now appear twice (original + duplicate)
|
|
await expect(page.getByText('Devoir duplication réussie')).toHaveCount(2, { timeout: 10000 });
|
|
});
|
|
|
|
test('duplicate button is disabled when no class selected', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await createHomework(page, 'Devoir sans sélection');
|
|
|
|
await page.getByRole('button', { name: /dupliquer/i }).first().click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 10000 });
|
|
|
|
// Duplicate button should be disabled
|
|
const duplicateBtn = dialog.getByRole('button', { name: /dupliquer/i }).last();
|
|
await expect(duplicateBtn).toBeDisabled();
|
|
});
|
|
|
|
test('cancel closes the duplicate modal', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await createHomework(page, 'Devoir annulation dupli');
|
|
|
|
await page.getByRole('button', { name: /dupliquer/i }).first().click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 10000 });
|
|
|
|
await dialog.getByRole('button', { name: /annuler/i }).click();
|
|
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('can duplicate homework to multiple classes', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await createHomework(page, 'Devoir multi-dupli');
|
|
|
|
await page.getByRole('button', { name: /dupliquer/i }).first().click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 10000 });
|
|
|
|
// Select ALL available checkboxes (at least 2 target classes)
|
|
const checkboxes = dialog.locator('input[type="checkbox"]');
|
|
const count = await checkboxes.count();
|
|
expect(count).toBeGreaterThanOrEqual(1);
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
await checkboxes.nth(i).check();
|
|
}
|
|
|
|
// Verify button text reflects the count
|
|
const duplicateButton = dialog.getByRole('button', {
|
|
name: new RegExp(`dupliquer \\(${count} classes?\\)`, 'i')
|
|
});
|
|
await expect(duplicateButton).toBeVisible();
|
|
await expect(duplicateButton).toBeEnabled();
|
|
|
|
// Click duplicate
|
|
await duplicateButton.click();
|
|
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify the homework title appears count+1 times (original + duplicates)
|
|
await expect(page.getByText('Devoir multi-dupli')).toHaveCount(count + 1, { timeout: 10000 });
|
|
});
|
|
|
|
test('can customize due date per target class', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await createHomework(page, 'Devoir date custom');
|
|
|
|
await page.getByRole('button', { name: /dupliquer/i }).first().click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 10000 });
|
|
|
|
// Select the first checkbox
|
|
const firstCheckbox = dialog.locator('input[type="checkbox"]').first();
|
|
await firstCheckbox.check();
|
|
|
|
// A date input should appear after checking the checkbox
|
|
const dueDateInput = dialog.locator('input[type="date"]').first();
|
|
await expect(dueDateInput).toBeVisible({ timeout: 5000 });
|
|
|
|
// Change the due date to a custom value
|
|
await dueDateInput.fill(getNextWeekday(10));
|
|
|
|
// Click duplicate
|
|
await page.getByRole('button', { name: /dupliquer \(1 classe\)/i }).click();
|
|
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify the homework appears twice (original + 1 duplicate)
|
|
await expect(page.getByText('Devoir date custom')).toHaveCount(2, { timeout: 10000 });
|
|
});
|
|
|
|
test('can filter homework list by class', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
await createHomework(page, 'Devoir filtre classe');
|
|
|
|
// Class filter should be visible
|
|
const classFilter = page.locator('select[aria-label="Filtrer par classe"]');
|
|
await expect(classFilter).toBeVisible();
|
|
|
|
// Select a class to filter
|
|
await classFilter.selectOption({ index: 1 });
|
|
|
|
// Should still show homework (it matches the filter)
|
|
await expect(page.getByText('Devoir filtre classe')).toBeVisible({ timeout: 5000 });
|
|
|
|
// Select "Toutes les classes" to reset
|
|
await classFilter.selectOption({ index: 0 });
|
|
await expect(page.getByText('Devoir filtre classe')).toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Partial Update
|
|
// ============================================================================
|
|
test.describe('Partial Update', () => {
|
|
test('can update only the title without changing other fields', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
const dueDate = getNextWeekday(5);
|
|
|
|
// Create a homework with description
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
await page.locator('#hw-class').selectOption({ index: 1 });
|
|
await page.locator('#hw-subject').selectOption({ index: 1 });
|
|
await page.locator('#hw-title').fill('Titre original');
|
|
await page.locator('#hw-description').fill('Description inchangée');
|
|
await page.locator('#hw-due-date').fill(dueDate);
|
|
await page.getByRole('button', { name: /créer le devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Titre original')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Open edit modal
|
|
await page.getByRole('button', { name: /modifier/i }).first().click();
|
|
const editDialog = page.getByRole('dialog');
|
|
await expect(editDialog).toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify pre-filled values
|
|
await expect(page.locator('#edit-description')).toHaveValue('Description inchangée');
|
|
await expect(page.locator('#edit-due-date')).toHaveValue(dueDate);
|
|
|
|
// Change only the title
|
|
await page.locator('#edit-title').clear();
|
|
await page.locator('#edit-title').fill('Titre mis à jour');
|
|
|
|
await page.getByRole('button', { name: /enregistrer/i }).click();
|
|
await expect(editDialog).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify title changed
|
|
await expect(page.getByText('Titre mis à jour')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Titre original')).not.toBeVisible();
|
|
|
|
// Verify description still visible
|
|
await expect(page.getByText('Description inchangée')).toBeVisible();
|
|
});
|
|
});
|
|
});
|