geocrop-platform./apps/nextgen/client/e2e/cohorts.spec.ts

118 lines
4.3 KiB
TypeScript

/**
* Cohorts (Phase 1 PR 2) — smoke + happy-path coverage.
*
* Exercises the /admin/cohorts surface end-to-end:
* 1. List page mounts (admin)
* 2. Create new cohort via /admin/cohorts/new
* 3. Detail page shows tabs + counts
* 4. Add a student (modal pick) and verify the count increments
* 5. Link a class (modal pick) and verify it appears
* 6. Unlink class (admin)
* 7. Soft-delete the cohort via the list
* 8. Read-only access: principal sees the list but no New / Add buttons
* 9. RBAC: teacher is denied 403 on the list API
*
* Skips cleanly if the demo seed lacks the principal user (the seed is
* still being filled in). The smoke tests run against a fresh
* `npm run db:init` so a clean state is assumed (no leftover cohorts).
*/
import { test, expect, request as pwRequest } from '@playwright/test';
import { loginAs, isRoleAvailable, apiLogin, DEMO_ACCOUNTS } from './helpers/auth';
import { captureConsoleErrors, expectNoConsoleErrors, expectPageMounted } from './helpers/assertions';
const BASE = 'http://localhost:3000';
const API = 'http://localhost:3001';
test.describe('Cohorts — admin happy path @flow', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, 'school_admin');
});
test('list page mounts and renders header', async ({ page }) => {
const cap = captureConsoleErrors(page);
try {
await page.goto(`${BASE}/admin/cohorts`);
await expectPageMounted(page, '/admin/cohorts', ['Student Cohorts']);
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
test('create new cohort end-to-end via the form', async ({ page }) => {
const cap = captureConsoleErrors(page);
try {
const cohortName = `E2E Cohort ${Date.now()}`;
await page.goto(`${BASE}/admin/cohorts/new`);
// Fill the form. The label is "Name"; the programme select is
// the first <select> on the form. Use accessible selectors.
await page.getByLabel(/^name/i).first().fill(cohortName);
// Programme select — pick IGCSE
await page.locator('select').first().selectOption('igcse');
// Level — datalist input
await page.getByLabel(/^level/i).first().fill('Year 1');
// Academic year
await page.getByLabel(/academic year/i).first().fill('2026');
await page.getByRole('button', { name: /create cohort|save cohort/i }).click();
// On success we navigate back to /admin/cohorts and the new card
// should be visible. The list shows newest cohorts first.
await expect(page).toHaveURL(/\/admin\/cohorts$/, { timeout: 10_000 });
await expect(page.getByText(cohortName)).toBeVisible({ timeout: 10_000 });
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
});
test.describe('Cohorts — RBAC @rbac', () => {
test('teacher gets 403 on the cohort list API', async ({ page }) => {
await loginAs(page, 'teacher');
const { token } = await apiLogin('teacher');
const ctx = await pwRequest.newContext({
baseURL: API,
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
});
try {
const res = await ctx.get('/api/cohorts');
expect(res.status()).toBe(403);
} finally {
await ctx.dispose();
}
});
test('principal sees the list (read-only) and no New Cohort button', async ({ page }) => {
const ok = await isRoleAvailable('principal');
test.skip(!ok, 'principal demo account not in seed');
await loginAs(page, 'principal');
const cap = captureConsoleErrors(page);
try {
await page.goto(`${BASE}/admin/cohorts`);
await expectPageMounted(page, '/admin/cohorts', ['Student Cohorts']);
// Principal can read but not manage — no New Cohort button.
const newButton = page.getByRole('button', { name: /new cohort/i });
await expect(newButton).toHaveCount(0);
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
test('student gets 403 on the cohort list API', async ({ page }) => {
const { token } = await apiLogin('student');
const ctx = await pwRequest.newContext({
baseURL: API,
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
});
try {
const res = await ctx.get('/api/cohorts');
expect(res.status()).toBe(403);
} finally {
await ctx.dispose();
}
});
});