/** * Teacher portal smoke + happy-path coverage. * * Routes verified against client/src/App.tsx (teacher role block). */ import { test, expect } from '@playwright/test'; import { loginAs } from '../helpers/auth'; import { ROUTES_PER_ROLE, ROLE_LANDING } from '../helpers/nav'; import { captureConsoleErrors, expectNoConsoleErrors, expectPageMounted, } from '../helpers/assertions'; const BASE = 'http://localhost:3000'; const TEACHER_CHROME = ['Africa Alert', 'Teacher', 'Dashboard']; test.describe('Teacher portal — smoke @smoke', () => { test.beforeEach(async ({ page }) => { await loginAs(page, 'teacher'); }); for (const route of ROUTES_PER_ROLE.teacher) { test(`mounts ${route}`, async ({ page }) => { const cap = captureConsoleErrors(page); try { await page.goto(`${BASE}${route}`); await expectPageMounted(page, route, [route.split('/').pop() || route, ...TEACHER_CHROME]); } finally { cap.dispose(); expectNoConsoleErrors(cap.errors); } }); } }); test.describe('Teacher portal — happy path @flow', () => { test.beforeEach(async ({ page }) => { await loginAs(page, 'teacher'); }); test('lands on the teacher dashboard after login', async ({ page }) => { await page.goto(`${BASE}/`); await expect(page).toHaveURL(new RegExp(`${ROLE_LANDING.teacher.replace(/\//g, '\\/')}$`)); }); test('my-courses: course list page renders', async ({ page }) => { await page.goto(`${BASE}/my-courses`); await expect(page.locator('h1, h2').first()).toBeVisible(); // The course list either has a list of course cards or an empty state. const list = page .locator('article, .course, [data-testid="course"]') .or(page.getByText(/no courses|no classes|empty/i)); await expect(list.first()).toBeVisible({ timeout: 5_000 }); }); test('attendance/take: page renders an attendance surface', async ({ page }) => { await page.goto(`${BASE}/attendance/take`); await expect(page.locator('h1, h2').first()).toBeVisible(); // Should have a class picker, a date picker, or a roster. const attendanceSurface = page .locator('select, input[type="date"]') .or(page.getByText(/attendance|present|absent/i)); await expect(attendanceSurface.first()).toBeVisible({ timeout: 5_000 }); }); test('assignments: list page is reachable', async ({ page }) => { await page.goto(`${BASE}/assignments`); await expect(page.locator('h1, h2').first()).toBeVisible(); }); test('students: roster page renders', async ({ page }) => { await page.goto(`${BASE}/students`); await expect(page.locator('h1, h2').first()).toBeVisible(); }); test('class-room: course scheme page renders', async ({ page }) => { await page.goto(`${BASE}/class-room`); await expect(page.locator('h1, h2').first()).toBeVisible(); }); // Exam-review workflow (added 2026-07-21). The exam registry at // /exams/manage should mount for teachers and show either the // table (with or without a Submit button on draft/rejected rows) // or an empty state. test('exams: registry page renders the exam table or empty state', async ({ page }) => { await page.goto(`${BASE}/exams/manage`); await expect(page.locator('h1, h2').first()).toBeVisible(); const tableOrEmpty = page .locator('table') .or(page.getByText(/no.*exam.*module|empty|no active/i)); await expect(tableOrEmpty.first()).toBeVisible({ timeout: 5_000 }); }); });