L'élève n'avait aucun moyen de voir les devoirs assignés à sa classe. Cette fonctionnalité ajoute la consultation complète : liste triée par échéance, détail avec pièces jointes, filtrage par matière, et marquage personnel « fait » en localStorage. Le dashboard élève affiche désormais les devoirs à venir avec ouverture du détail en modale, et un lien vers la page complète. L'accès API est sécurisé par vérification de la classe de l'élève (pas d'IDOR) et validation du chemin des pièces jointes (pas de path traversal).
571 lines
22 KiB
TypeScript
571 lines
22 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 STUDENT_EMAIL = 'e2e-student-homework@example.com';
|
|
const STUDENT_PASSWORD = 'StudentHomework123';
|
|
const TEACHER_EMAIL = 'e2e-student-hw-teacher@example.com';
|
|
const TEACHER_PASSWORD = 'TeacherHomework123';
|
|
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 getNextWeekday(daysFromNow: number): string {
|
|
const date = new Date();
|
|
date.setDate(date.getDate() + daysFromNow);
|
|
const day = date.getDay();
|
|
if (day === 0) date.setDate(date.getDate() + 1);
|
|
if (day === 6) date.setDate(date.getDate() + 2);
|
|
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 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 Promise.all([
|
|
page.waitForURL(/\/dashboard/, { timeout: 30000 }),
|
|
page.getByRole('button', { name: /se connecter/i }).click()
|
|
]);
|
|
}
|
|
|
|
test.describe('Student Homework Consultation (Story 5.7)', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
const dueDate1 = getNextWeekday(5);
|
|
const dueDate2 = getNextWeekday(10);
|
|
|
|
test.beforeAll(async () => {
|
|
try {
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear cache.rate_limiter users.cache --env=dev 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
} catch {
|
|
// Cache pools may not exist
|
|
}
|
|
|
|
// Create student user
|
|
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' }
|
|
);
|
|
|
|
// 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 class exists
|
|
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-StudentHW-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
} catch {
|
|
// May already exist
|
|
}
|
|
|
|
// Ensure 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-StudentHW-Maths', 'E2ESHWMAT', '#3b82f6', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
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-StudentHW-Français', 'E2ESHWFRA', '#ef4444', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
} catch {
|
|
// May already exist
|
|
}
|
|
|
|
// Assign student to class
|
|
runSql(
|
|
`INSERT INTO class_assignments (id, tenant_id, user_id, school_class_id, academic_year_id, assigned_at, created_at, updated_at) ` +
|
|
`SELECT gen_random_uuid(), '${TENANT_ID}', u.id, c.id, '${academicYearId}', NOW(), NOW(), NOW() ` +
|
|
`FROM users u, school_classes c ` +
|
|
`WHERE u.email = '${STUDENT_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
|
|
`AND c.name = 'E2E-StudentHW-6A' AND c.tenant_id = '${TENANT_ID}' ` +
|
|
`ON CONFLICT DO NOTHING`
|
|
);
|
|
|
|
// Clean up homework data
|
|
try {
|
|
runSql(
|
|
`DELETE FROM homework WHERE tenant_id = '${TENANT_ID}' AND class_id IN ` +
|
|
`(SELECT id FROM school_classes WHERE name = 'E2E-StudentHW-6A' AND tenant_id = '${TENANT_ID}')`
|
|
);
|
|
} catch {
|
|
// Table may not exist
|
|
}
|
|
|
|
// Seed homework for the student's class
|
|
runSql(
|
|
`INSERT INTO homework (id, tenant_id, class_id, subject_id, teacher_id, title, description, due_date, status, created_at, updated_at) ` +
|
|
`SELECT gen_random_uuid(), '${TENANT_ID}', c.id, s.id, t.id, 'Exercices chapitre 3', 'Faire les exercices 1 à 10 page 42', '${dueDate1}', 'published', NOW(), NOW() ` +
|
|
`FROM school_classes c, ` +
|
|
`(SELECT id FROM subjects WHERE code = 'E2ESHWMAT' AND tenant_id = '${TENANT_ID}' LIMIT 1) s, ` +
|
|
`(SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') t ` +
|
|
`WHERE c.name = 'E2E-StudentHW-6A' AND c.tenant_id = '${TENANT_ID}'`
|
|
);
|
|
|
|
runSql(
|
|
`INSERT INTO homework (id, tenant_id, class_id, subject_id, teacher_id, title, description, due_date, status, created_at, updated_at) ` +
|
|
`SELECT gen_random_uuid(), '${TENANT_ID}', c.id, s.id, t.id, 'Rédaction sur les vacances', 'Écrire une rédaction de 200 mots', '${dueDate2}', 'published', NOW(), NOW() ` +
|
|
`FROM school_classes c, ` +
|
|
`(SELECT id FROM subjects WHERE code = 'E2ESHWFRA' AND tenant_id = '${TENANT_ID}' LIMIT 1) s, ` +
|
|
`(SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') t ` +
|
|
`WHERE c.name = 'E2E-StudentHW-6A' AND c.tenant_id = '${TENANT_ID}'`
|
|
);
|
|
|
|
// Create a dummy attachment file in the container
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php sh -c "mkdir -p /app/var/uploads && echo 'Test PDF content for E2E' > /app/var/uploads/e2e-exercice.pdf"`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
|
|
// Seed attachment for "Exercices chapitre 3" homework
|
|
runSql(
|
|
`INSERT INTO homework_attachments (id, homework_id, filename, file_path, file_size, mime_type, uploaded_at) ` +
|
|
`SELECT gen_random_uuid(), h.id, 'exercice.pdf', '/app/var/uploads/e2e-exercice.pdf', 1024, 'application/pdf', NOW() ` +
|
|
`FROM homework h ` +
|
|
`WHERE h.title = 'Exercices chapitre 3' AND h.tenant_id = '${TENANT_ID}' ` +
|
|
`ON CONFLICT DO NOTHING`
|
|
);
|
|
|
|
clearCache();
|
|
});
|
|
|
|
// ======================================================================
|
|
// AC1: Liste devoirs
|
|
// ======================================================================
|
|
test.describe('AC1: Homework List', () => {
|
|
test('student can navigate to homework page', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
await expect(
|
|
page.getByRole('heading', { name: /mes devoirs/i })
|
|
).toBeVisible({ timeout: 15000 });
|
|
});
|
|
|
|
test('homework list shows pending items sorted by due date', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
await expect(
|
|
page.getByRole('heading', { name: /mes devoirs/i })
|
|
).toBeVisible({ timeout: 15000 });
|
|
|
|
// Wait for homework cards to appear
|
|
const cards = page.locator('.homework-card');
|
|
await expect(cards.first()).toBeVisible({ timeout: 10000 });
|
|
await expect(cards).toHaveCount(2);
|
|
|
|
// Verify sorted by due date (closest first)
|
|
const firstTitle = await cards.nth(0).locator('.card-title').textContent();
|
|
const secondTitle = await cards.nth(1).locator('.card-title').textContent();
|
|
expect(firstTitle).toBe('Exercices chapitre 3');
|
|
expect(secondTitle).toBe('Rédaction sur les vacances');
|
|
});
|
|
});
|
|
|
|
// ======================================================================
|
|
// AC2: Affichage devoir
|
|
// ======================================================================
|
|
test.describe('AC2: Homework Display', () => {
|
|
test('each homework card shows subject, title and due date', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
const card = page.locator('.homework-card').first();
|
|
await expect(card).toBeVisible({ timeout: 10000 });
|
|
|
|
// Subject name visible
|
|
await expect(card.locator('.subject-name')).toContainText(/maths/i);
|
|
// Title visible
|
|
await expect(card.locator('.card-title')).toContainText('Exercices chapitre 3');
|
|
// Due date visible
|
|
await expect(card.locator('.due-date')).toBeVisible();
|
|
// Status visible
|
|
await expect(card.locator('.status-badge')).toContainText(/à faire/i);
|
|
});
|
|
});
|
|
|
|
// ======================================================================
|
|
// AC3: Détail devoir
|
|
// ======================================================================
|
|
test.describe('AC3: Homework Detail', () => {
|
|
test('clicking a card shows the detail view', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
const card = page.locator('.homework-card').first();
|
|
await expect(card).toBeVisible({ timeout: 10000 });
|
|
await card.click();
|
|
|
|
// Detail view should appear
|
|
await expect(page.locator('.homework-detail')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('.detail-title')).toContainText('Exercices chapitre 3');
|
|
await expect(page.locator('.detail-description')).toContainText('Faire les exercices 1 à 10 page 42');
|
|
|
|
// Teacher name visible
|
|
await expect(page.locator('.teacher-name')).toBeVisible();
|
|
});
|
|
|
|
test('back button returns to list', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
const card = page.locator('.homework-card').first();
|
|
await expect(card).toBeVisible({ timeout: 10000 });
|
|
await card.click();
|
|
await expect(page.locator('.homework-detail')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Click back
|
|
await page.locator('.back-button').click();
|
|
await expect(page.locator('.homework-card').first()).toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
// ======================================================================
|
|
// AC3 (extended): Fichiers joints dans le détail
|
|
// ======================================================================
|
|
test.describe('AC3: Homework Detail - Attachments', () => {
|
|
test('detail view shows attachment list when homework has attachments', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
// Click on "Exercices chapitre 3" which has an attachment
|
|
const card = page.locator('.homework-card').first();
|
|
await expect(card).toBeVisible({ timeout: 10000 });
|
|
await card.click();
|
|
|
|
await expect(page.locator('.homework-detail')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Attachments section should be visible
|
|
await expect(page.locator('.detail-attachments')).toBeVisible();
|
|
await expect(page.locator('.attachment-item')).toBeVisible();
|
|
await expect(page.locator('.attachment-name')).toContainText('exercice.pdf');
|
|
});
|
|
});
|
|
|
|
// ======================================================================
|
|
// AC4: Téléchargement fichiers joints
|
|
// ======================================================================
|
|
test.describe('AC4: Attachment Download', () => {
|
|
test('clicking an attachment triggers file download', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
const card = page.locator('.homework-card').first();
|
|
await expect(card).toBeVisible({ timeout: 10000 });
|
|
await card.click();
|
|
|
|
await expect(page.locator('.homework-detail')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('.attachment-item')).toBeVisible();
|
|
|
|
// Intercept the attachment download request
|
|
const responsePromise = page.waitForResponse(
|
|
(resp) => resp.url().includes('/attachments/') && resp.status() === 200
|
|
);
|
|
|
|
await page.locator('.attachment-item').first().click();
|
|
|
|
const response = await responsePromise;
|
|
expect(response.status()).toBe(200);
|
|
});
|
|
});
|
|
|
|
// ======================================================================
|
|
// AC5: Filtrage par matière
|
|
// ======================================================================
|
|
test.describe('AC5: Subject Filter', () => {
|
|
test('filter buttons appear when multiple subjects exist', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
await expect(page.locator('.homework-card').first()).toBeVisible({ timeout: 10000 });
|
|
|
|
// Filter bar should be visible
|
|
const filterBar = page.locator('.filter-bar');
|
|
await expect(filterBar).toBeVisible();
|
|
|
|
// "Toutes" chip + 2 subject chips
|
|
const chips = filterBar.locator('.filter-chip');
|
|
await expect(chips).toHaveCount(3);
|
|
});
|
|
|
|
test('clicking a subject filter shows only that subject homework', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
await expect(page.locator('.homework-card').first()).toBeVisible({ timeout: 10000 });
|
|
|
|
// Click on Maths filter
|
|
await page.locator('.filter-chip', { hasText: /maths/i }).click();
|
|
|
|
const cards = page.locator('.homework-card');
|
|
await expect(cards).toHaveCount(1, { timeout: 5000 });
|
|
await expect(cards.first().locator('.card-title')).toContainText('Exercices chapitre 3');
|
|
});
|
|
|
|
test('clicking "Toutes" shows all homework again', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
await expect(page.locator('.homework-card').first()).toBeVisible({ timeout: 10000 });
|
|
|
|
// Filter then unfilter
|
|
await page.locator('.filter-chip', { hasText: /maths/i }).click();
|
|
await expect(page.locator('.homework-card')).toHaveCount(1, { timeout: 5000 });
|
|
|
|
await page.locator('.filter-chip', { hasText: /tous/i }).click();
|
|
await expect(page.locator('.homework-card')).toHaveCount(2, { timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
// ======================================================================
|
|
// AC6: Marquage "Fait"
|
|
// ======================================================================
|
|
test.describe('AC6: Toggle Done', () => {
|
|
test('toggling done moves homework to "Terminés" section', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
const firstCard = page.locator('.homework-card').first();
|
|
await expect(firstCard).toBeVisible({ timeout: 10000 });
|
|
|
|
// Click toggle button on first card
|
|
const toggleBtn = firstCard.locator('.toggle-done');
|
|
await toggleBtn.click();
|
|
|
|
// A "Terminés" section should appear
|
|
await expect(page.getByText(/terminés/i)).toBeVisible({ timeout: 5000 });
|
|
|
|
// The card should now be in done state
|
|
const doneCard = page.locator('.homework-card.done');
|
|
await expect(doneCard).toBeVisible();
|
|
await expect(doneCard.locator('.status-badge')).toContainText(/fait/i);
|
|
});
|
|
|
|
test('done state persists after page reload', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
const firstCard = page.locator('.homework-card').first();
|
|
await expect(firstCard).toBeVisible({ timeout: 10000 });
|
|
|
|
// Mark as done
|
|
await firstCard.locator('.toggle-done').click();
|
|
await expect(page.locator('.homework-card.done')).toBeVisible({ timeout: 5000 });
|
|
|
|
// Reload the page
|
|
await page.reload();
|
|
await expect(page.locator('.homework-card').first()).toBeVisible({ timeout: 10000 });
|
|
|
|
// Done state should persist (localStorage)
|
|
await expect(page.locator('.homework-card.done')).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('toggling done again restores homework to pending section', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
const firstCard = page.locator('.homework-card').first();
|
|
await expect(firstCard).toBeVisible({ timeout: 10000 });
|
|
|
|
// Mark as done
|
|
await firstCard.locator('.toggle-done').click();
|
|
await expect(page.locator('.homework-card.done')).toBeVisible({ timeout: 5000 });
|
|
|
|
// Toggle back to undone
|
|
const doneCard = page.locator('.homework-card.done');
|
|
await doneCard.locator('.toggle-done').click();
|
|
|
|
// Card should no longer have done class
|
|
await expect(page.locator('.homework-card.done')).toHaveCount(0, { timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
// ======================================================================
|
|
// AC7: Mode offline
|
|
// Skipped: Service Worker cannot cache cross-origin API requests in E2E
|
|
// (API runs on a different port). Works in production (same origin).
|
|
// ======================================================================
|
|
test.describe('AC7: Offline Mode', () => {
|
|
test.skip('cached homework is displayed when offline', async ({ page, context }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
// Wait for homework to load (populates Service Worker cache)
|
|
await expect(page.locator('.homework-card').first()).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('.homework-card')).toHaveCount(2);
|
|
|
|
// Go offline
|
|
await context.setOffline(true);
|
|
|
|
// Reload the page — Service Worker should serve cached data
|
|
await page.reload();
|
|
|
|
// Offline banner should appear
|
|
await expect(page.locator('.offline-banner')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Cached homework should still be visible
|
|
await expect(page.locator('.homework-card').first()).toBeVisible({ timeout: 10000 });
|
|
|
|
// Restore connectivity
|
|
await context.setOffline(false);
|
|
});
|
|
|
|
test.skip('marking homework as done works offline and persists', async ({ page, context }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/homework`);
|
|
|
|
// Wait for homework to load
|
|
await expect(page.locator('.homework-card').first()).toBeVisible({ timeout: 10000 });
|
|
|
|
// Go offline
|
|
await context.setOffline(true);
|
|
await page.reload();
|
|
await expect(page.locator('.offline-banner')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Mark homework as done while offline (should work via localStorage)
|
|
const firstCard = page.locator('.homework-card').first();
|
|
await expect(firstCard).toBeVisible({ timeout: 10000 });
|
|
await firstCard.locator('.toggle-done').click();
|
|
|
|
// Done state should be applied
|
|
await expect(page.locator('.homework-card.done')).toBeVisible({ timeout: 5000 });
|
|
|
|
// Come back online and reload
|
|
await context.setOffline(false);
|
|
await page.reload();
|
|
await expect(page.locator('.homework-card').first()).toBeVisible({ timeout: 10000 });
|
|
|
|
// Done state should persist (synced from localStorage)
|
|
await expect(page.locator('.homework-card.done')).toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
// ======================================================================
|
|
// Dashboard integration
|
|
// ======================================================================
|
|
test.describe('Dashboard Widget', () => {
|
|
test('dashboard shows homework widget with real data', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
// Dashboard should load
|
|
await expect(
|
|
page.getByRole('heading', { name: /mon espace/i })
|
|
).toBeVisible({ timeout: 15000 });
|
|
|
|
// Homework section should show homework items
|
|
const homeworkSection = page.locator('.dashboard-grid').locator('section', { hasText: /mes devoirs/i });
|
|
await expect(homeworkSection).toBeVisible({ timeout: 10000 });
|
|
|
|
// Should have a "Voir tous les devoirs" link
|
|
await expect(page.getByText(/voir tous les devoirs/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('dashboard "Voir tous les devoirs" link navigates to homework page', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: /mon espace/i })
|
|
).toBeVisible({ timeout: 15000 });
|
|
|
|
await page.getByText(/voir tous les devoirs/i).click();
|
|
await expect(
|
|
page.getByRole('heading', { name: /mes devoirs/i })
|
|
).toBeVisible({ timeout: 15000 });
|
|
});
|
|
|
|
test('clicking a homework item opens detail modal on dashboard', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: /mon espace/i })
|
|
).toBeVisible({ timeout: 15000 });
|
|
|
|
// Click on a homework item in the widget
|
|
const homeworkBtn = page.locator('button.homework-item').first();
|
|
await expect(homeworkBtn).toBeVisible({ timeout: 10000 });
|
|
await homeworkBtn.click();
|
|
|
|
// Modal with detail should appear
|
|
const modal = page.locator('[role="dialog"]');
|
|
await expect(modal).toBeVisible({ timeout: 10000 });
|
|
await expect(modal.locator('.detail-title')).toBeVisible();
|
|
await expect(modal.locator('.teacher-name')).toBeVisible();
|
|
});
|
|
|
|
test('homework detail modal closes with X button', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: /mon espace/i })
|
|
).toBeVisible({ timeout: 15000 });
|
|
|
|
const homeworkBtn = page.locator('button.homework-item').first();
|
|
await expect(homeworkBtn).toBeVisible({ timeout: 10000 });
|
|
await homeworkBtn.click();
|
|
|
|
const modal = page.locator('[role="dialog"]');
|
|
await expect(modal).toBeVisible({ timeout: 10000 });
|
|
|
|
// Close modal
|
|
await page.locator('.homework-modal-close').click();
|
|
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|
|
});
|