geocrop-platform./apps/nextgen/client/e2e/portals/admin.spec.ts

172 lines
6.1 KiB
TypeScript

/**
* Admin portal smoke + happy-path coverage.
*
* Covers `school_admin` (operational admin — the role with the largest
* surface). The `systems_admin` block is included but skipped when the
* demo seed doesn't include that user — `sysadmin@school.com` is
* promised by Login.tsx's demoLogin() map but is not in
* server/src/database/seeds/demo.js today. Fix the seed to un-skip.
*
* The smoke group walks every route the role can reach (per the App.tsx
* route map). The happy-path group exercises one real interaction per
* major module so a feature that renders but is dead on arrival still
* shows up red.
*/
import { test, expect } from '@playwright/test';
import { loginAs, isRoleAvailable } from '../helpers/auth';
import { ROUTES_PER_ROLE, ROLE_LANDING } from '../helpers/nav';
import {
captureConsoleErrors,
expectNoConsoleErrors,
expectPageMounted,
} from '../helpers/assertions';
const BASE = 'http://localhost:3000';
test.describe('Admin portal — school_admin smoke @smoke', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, 'school_admin');
});
for (const route of ROUTES_PER_ROLE.school_admin) {
test(`mounts ${route}`, async ({ page }) => {
const cap = captureConsoleErrors(page);
try {
await page.goto(`${BASE}${route}`);
await expectPageMounted(page, route, [route.split('/').pop() || route, 'Africa Alert']);
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
}
});
test.describe('Admin portal — systems_admin smoke @smoke', () => {
test.beforeAll(async () => {
const ok = await isRoleAvailable('systems_admin');
test.skip(!ok, 'systems_admin demo account not in seed see server/src/database/seeds/demo.js');
});
test.beforeEach(async ({ page }) => {
await loginAs(page, 'systems_admin');
});
for (const route of ROUTES_PER_ROLE.systems_admin) {
test(`mounts ${route}`, async ({ page }) => {
const cap = captureConsoleErrors(page);
try {
await page.goto(`${BASE}${route}`);
await expectPageMounted(page, route, [route.split('/').pop() || route, 'Africa Alert']);
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
}
});
test.describe('Admin portal happy path @flow', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, 'school_admin');
});
test('lands on the school admin dashboard after login', async ({ page }) => {
await page.goto(`${BASE}/`);
await expect(page).toHaveURL(new RegExp(`${ROLE_LANDING.school_admin.replace(/\//g, '\\/')}$`));
});
test('front-office: visitor log page is interactive', async ({ page }) => {
const cap = captureConsoleErrors(page);
try {
await page.goto(`${BASE}/front-office/visitors`);
await expect(page.locator('h1, h2').first()).toBeVisible();
// The add button or the empty state should be reachable. Use a
// generous locator — the label can be "Add", "New", or "Log visitor".
const addOrEmpty = page
.getByRole('button', { name: /add|new|log visitor/i })
.or(page.getByText(/no visitors|empty/i));
await expect(addOrEmpty.first()).toBeVisible({ timeout: 5_000 });
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
test('finance: expenses list page renders', async ({ page }) => {
const cap = captureConsoleErrors(page);
try {
await page.goto(`${BASE}/finance/expenses`);
await expect(page.locator('h1, h2').first()).toBeVisible();
// The page either has a table of expenses or an empty state — both prove
// the route mounted and the API call returned.
const tableOrEmpty = page
.locator('table')
.or(page.getByText(/no expenses|no records|empty/i));
await expect(tableOrEmpty.first()).toBeVisible({ timeout: 5_000 });
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
test('marks: bulk-marks page renders the upload surface', async ({ page }) => {
const cap = captureConsoleErrors(page);
try {
await page.goto(`${BASE}/marks/bulk`);
await expect(page.locator('h1, h2').first()).toBeVisible();
// Should have either a file input or a CSV/paste input.
const fileInput = page.locator('input[type="file"]');
await expect(fileInput.first()).toBeAttached({ timeout: 5_000 });
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
test('notice board: admin can see the post surface', async ({ page }) => {
const cap = captureConsoleErrors(page);
try {
await page.goto(`${BASE}/notice-board`);
// Admin can post — look for a button, textarea, or a known label.
const postSurface = page
.getByRole('button', { name: /post|create|new|add/i })
.or(page.locator('textarea'))
.or(page.getByText(/notice|announcement/i));
await expect(postSurface.first()).toBeVisible({ timeout: 5_000 });
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
test('library: catalog page is reachable', async ({ page }) => {
const cap = captureConsoleErrors(page);
try {
await page.goto(`${BASE}/library`);
await expect(page.locator('h1, h2').first()).toBeVisible();
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
// Exam-review workflow (added 2026-07-21). The page either shows
// the queue table or the "queue clear" empty state — both prove the
// /api/exams/groups/pending endpoint works and the page mounted.
test('exams: review queue page renders', async ({ page }) => {
const cap = captureConsoleErrors(page);
try {
await page.goto(`${BASE}/exams/review`);
await expectPageMounted(page, '/exams/review', ['Exam Review', 'Queue']);
const queueOrEmpty = page
.locator('table')
.or(page.getByText(/queue clear|no.*awaiting/i));
await expect(queueOrEmpty.first()).toBeVisible({ timeout: 5_000 });
} finally {
cap.dispose();
expectNoConsoleErrors(cap.errors);
}
});
});