14 — QA Checklist (Academics Module)
- 1. Functional — CRUD & uniqueness
- 2. Functional — set-current & status
- 3. Functional — hierarchy & ordering
- 4. Functional — delete & cascade behaviour
- 5. Functional — referential & tenant isolation
- 6. Visual / layout
- 7. Accessibility (00-shared/09)
- 8. Performance
- 9. Offline & network (00-shared/10 §2)
- 10. Tablet / desktop
- 11. Localization
- 12. Dark mode / animations / security
- 13. Server-backed edge mirrors
- QA scripts
Module QA specific to framework + shared 00-shared/10. Every check derives from a specific backend behavior so failure analysis is direct.
1. Functional — CRUD & uniqueness
-
Create academic year returns 200 with
status:"upcoming"default (academic-year.service.ts:30-33). -
Duplicate year name → 409
DUPLICATE_RESOURCEmessage verbatim ("Academic year "X" already exists.",academic-year.service.ts:27-28). -
Duplicate grade name → 409 (
grade.service.ts:23). -
Duplicate subject
code→ 409 (subject.service.ts:23-24); race path: two parallel POSTs → one succeeds, second 409 via unique index (subject.schema.ts:38) — client must surface as duplicate, not 5xx. -
Duplicate class
(academicYearId, gradeId, sectionId, name)→ server accepts (OQ-4) — client shows "Duplicate class?" warn before submit; verify no false block when the exact combo differs by one field. -
Duplicate section
(gradeId, name)→ server accepts (OQ-4) — client warning only. -
Duplicate assignment
(classId, subjectId)→ server accepts (OQ-4) — client picker disables already-assigned pairs. -
Update endpoints accept partial bodies only (all-optional DTOs) — empty
PATCH {}returns 200 unchanged doc, no error. -
PATCH /academic-years/:idwith duplicatename→ no 409 (update path has no name check, OQ-2) — client pre-checks from cache. -
PATCH /subjects/:idcannot changetheoryMarks/practicalMarks— fields not in DTO (update-subject.dto.ts:4-42); unknown fields → 400VALIDATION_ERROR(whitelist validation).
2. Functional — set-current & status
-
set-currentun-flags ALL other years, flags target, setsstatus:"active"(academic-year.service.ts:69-83) — verify with 3 years in DB. -
set-currenton anarchivedyear → becomesactive(server allows; confirm copy reflects). -
Exactly one
isCurrent:trueafter any sequence of set-current calls (singleton rule,COLLECTIONS.md:1023). -
Year
statusaccepts onlyupcoming|active|archivedat the client (enumacademic-year.schema.ts:7-11); server accepts any string (IsString) — client must never send unknown values. -
Grade/section/class
statusfree string: UI treats only'active'as active; arbitrary server value renders as archived-style badge without crashing (OQ-2).
3. Functional — hierarchy & ordering
-
Grades list ordered by
displayOrderasc, default 0 first (grade.service.ts:37-39); two grades with same order → stable (insertion) order, no client re-sort. -
Years list default
createdAt:-1(newest first) (academic-year.service.ts:45-47). -
Classes/sections/subjects keep insertion order (
class.service.ts:41-53,section.service.ts:41-48,subject.service.ts:35-47). -
GET /classes/by-year/:idreturns only that year's classes;GET /sections/by-grade/:idonly that grade's sections (server-scoped). -
sort=-createdAtquery param maps to desc (prefix-handling,academic-year.service.ts:45-47);sort=displayOrderon grades works. -
Pagination:
limitclamped 1–100 (400 outside),page≥ 1; metatotalPages/hasNext/hasPreviouscorrect at boundaries (page 1 of 1, last page, empty list →totalPages:1viaMath.max(1,…),pagination-query.dto.ts:46). -
qparam sent → server ignores it; list returns unfiltered (OQ-7) — client search is local; verify no error thrown.
4. Functional — delete & cascade behaviour
-
Delete (any entity) = soft delete: row disappears from lists; reappears if
isDeletedfilter changes (platform admin view,base.repository.ts:20-30) — client never relies on permanence. - Delete grade with live sections/classes → no server error, no cascade (OQ-3); orphan sections/classes still appear under other views; client orphan banners fire.
- Delete class → timetable/attendance rows referencing it still exist server-side (OQ-3); client re-resolves and shows "class no longer exists".
-
Delete assignment → 404 on second delete ("Assignment not found.",
subject-assignment.service.ts:34) → UI treats as removed. -
versionincrements on every update (base.repository.ts:63) — verify via doc read (no client optimistic concurrency).
5. Functional — referential & tenant isolation
-
Class create with non-existent
academicYearId/gradeId/sectionId→ server accepts (no ref checks,class.service.ts:16-18) — client pickers prevent, but a pasted bad ObjectId is still renderable as "Unknown". -
tenantIdnever sent in body/query; cross-tenant ID returns 404 (scoped filter,base.repository.ts:38-40) — verify with two tenants. -
Platform admin sees all tenants' data only via admin bypass (
base.repository.ts:21-23) — client never renders this path. - All 6 controllers require valid JWT (401 without); no RBAC — any authed role can create/update/delete (OQ-1) — QA must confirm with a student token: writes currently succeed (document as known gap).
-
class.campusIdaccepts any ObjectId (ref typoClass,class.schema.ts:12-13, OQ-6) — client hides field;roomIdfree string round-trips.
6. Visual / layout
- Dark-light parity across all lists, explorer, matrix sheet (no hardcoded colors).
- Current-year row distinct (primaryContainer) in both themes; status badges legible.
-
Chips wrap at 200% text scale;
SectionChips"+N more" overflow works. - Long subject names + codes truncate with ellipsis, never overflow the roster table.
- Empty states per entity with correct primary CTA.
7. Accessibility (00-shared/09)
-
TalkBack: explorer tree fully operable (expand/collapse announced,
Semantics(expanded:)); cascading pickers announce enabled/disabled state. - VoiceOver separately.
-
Keyboard-only desktop: Tab order Year→Grade→Section→Class→Subject;
N,Ctrl+Enter,Escwork; focus ring visible. - Conflict banners in live region; snackbars announced.
-
Contrast: body 4.5:1; conflict
tertiaryContainer3:1 for borders/dots. - Reduced motion: no slide/scale; tree animates as fades only.
8. Performance
- 12 grades × 4 sections × 40 classes: list first frame ≤ 200 ms skeleton → content ≤ 2 s (00-shared/10 §1).
- Explorer with 200 nodes: expand/collapse no jank (≤ 50 open nodes policy, 07 §1); children lazy-loaded.
- Reference cache load-once: navigating between 6 list screens triggers ≤ 5 collection fetches total (then cache hits).
-
Infinite scroll no duplicate requests;
hasNext:falsestops loader. - Memory stable over 20-min session with roster cache LRU 20 (13 §1).
9. Offline & network (00-shared/10 §2)
- Offline: all writes blocked + banner; reads render from 24 h cache (with stale badge "Offline — data from HH:mm").
- 24 h TTL expiry → revalidate on next foreground; failure → keep stale + banner.
- Slow network × 429 countdown correct; no auto-retry on 429.
-
5xx on create → generic +
requestId, form preserved, retry safe (idempotency not guaranteed — 409 tells the truth on retry).
10. Tablet / desktop
- Master-detail at ≥ 840 dp: list left, detail right; back button pops branch not shell.
-
RosterTablesticky header, column sorting is disabled (server order preserved). - Right-click = context menu; hover tooltips on conflict icons (500 ms).
- Scrollbar always visible.
11. Localization
- All strings i18n; server 409 messages shown as-is (not translated) for debug accuracy — policy decision: translate or quote server copy verbatim (recommend verbatim + prefix icon).
-
Dates locale-formatted (
dd MMM yyyy) but submitted as ISOYYYY-MM-DD(IsDateString,create-academic-year.dto.ts:9-15). - RTL: chips, tree indents, roster columns mirror.
12. Dark mode / animations / security
- Full visual pass; conflict/error surfaces distinct in dark (11 §6).
- Motion reduced-motion pass; no flash on set-current badge swap.
-
No IDs/tenant data in analytics events; screenshots blur class/student names
(proposed). -
Error screens never leak stack traces; only
error.code+requestIdshown.
13. Server-backed edge mirrors
- 409 paths: year name, grade name, subject code (create) — exact server copy asserted in widget tests.
-
404 paths: every
GET/:id,DELETE/:id,set-currentwith stale id → treated as removed/refreshed, never a stuck screen. -
Year with
endDate < startDate→ server accepts (no check, OQ-5) — client blocks before submit. - Subject marks cross-field violations → server accepts (OQ-5) — client blocks before submit; verify server accepts direct API call (documented gap).
QA scripts
- Golden per component (
HierarchyTree,SectionChips,SubjectAssignmentRow,YearStatusBadge,CascadingEntityPicker) light/dark × phone/tablet/desktop. - E2E (device-cloud P0): fresh tenant → create year → set current → create grades 1–12 → sections A–D → classes → create subjects → assign 3 teachers → verify timetable/attendance screens reflect the new class.
- E2E conflict drill: create duplicate class + duplicate assignment via raw API, verify client banners + no crash.
- E2E multi-device: set-current from device B while device A is on years list → A corrects badge on focus.
- Security smoke: student-token write attempt (documents OQ-1 gap); cross-tenant ID read → 404.