14 — QA Checklist (Search Module)
- 1. Query contract
- 2. Minimum characters
- 3. Special characters
- 4. Tenant isolation
- 5. Result limits & pagination
- 6. Stale data
- 7. Fallback parity
- 8. Scoring
- 9. Security & auth
- 10. UX & a11y
- 11. Performance
Module-specific quality gates on top of 00-shared/10_QA_Baseline.md (performance budgets, offline matrix, security checklist, device matrix). Each item is traceable to source.
1. Query contract
-
qis trimmed; whitespace-onlyqis never sent (server returnsdata: []—search.service.ts:28-30). -
page ≥ 1,limit ∈ 1..50(defaults 1/20 —search-query.dto.ts:16-29); values outside → server400 VALIDATION_ERROR. - Debounce fires only after 300 ms of keystroke silence; submit skips it (13 §3).
- Stale responses dropped (guard token) — rapid typing never renders out-of-order results.
2. Minimum characters
-
Server accepts 1-char queries (no
@Minonq—search-query.dto.ts:6-9); verify 1-char results render and the fallback path does not degrade response time (regex full-scan —search-index.repository.ts:36-50). - No query → landing screen, zero network calls.
3. Special characters
-
( ) [ ] { } * + ? ^ $ | . \insideqmust never 500. Risk: the fallback builds{ $regex: q, $options: 'i' }from raw input (search-index.repository.ts:40) — invalid regex throws. Gap: mitigation(proposed)is client-side escaping (08 §1.4). -
Punctuation in
$textqueries (ADM-20,Mr. John) — verify expected match behavior vs fallback (tokenization differs — 09 §1). -
%,&,+,=, UTF-8 (Iñigo, Școala) round-trip through URL encoding and match by substring/word. - 100+ char queries: server unbounded (08 §1.2) — confirm p95 latency still within budget (00-shared/10 §1).
4. Tenant isolation
-
Cross-tenant rows never leak: every query uses
scopedFilter(search-index.repository.ts:24-26) overBaseSchema.tenantId(base.schema.ts:9-11); upserts key on(tenantId, entityType, entityId)(search-index.repository.ts:84-89). - Two tenants searching the same string see disjoint results.
-
entityIdfrom tenant A is not resolvable in tenant B (result rows are pointers into owner modules — 12 §8).
5. Result limits & pagination
-
limitrespected (max 50) — server slices via.skip().limit()(search-index.repository.ts:32-33). -
Pagination walk:
meta.hasNexttrue → page+1 appends; final page stops (pagination-query.dto.ts:46-54). -
Known bug-behavior (flagged):
entityTypefiltering happens on the fetched page only (search.service.ts:44-49); withlimit=20and 30 Students behind 50 mixed rows, page 1 shows 20 Students but "See all" page 2 may show none/partial — verify drill-down UX copy ("N in this view", 07 §3) and consider a(planned)server-side type filter.
6. Stale data
-
Rename student/teacher/staff/parent/book/user → re-search within
seconds shows new title (event → upsert —
search-indexer.service.ts:48-96). -
Soft-delete an entity → row disappears from search (
removeByEntitysetsisDeleted—search-index.repository.ts:92-98; scoped filter excludes —base.schema.ts:20-21). -
Known gap:
LeadUpdated,LeadDeleted,AnnouncementUpdated,AnnouncementDeletedare unmapped (search-indexer.service.ts:29-31) → edited/deleted leads & announcements persist stale rows. Verify the(planned)reindex job resolves; until then document behavior. -
Indexer idempotency: duplicate events upsert, never duplicate rows
(
search-index.repository.ts:85-89).
7. Fallback parity
-
Force
$textfailure (test hook) → regex fallback returns results,totalItemsconsistent per path (search.service.ts:35-41). -
Fallback order differs (no textScore sort —
search-index.repository.ts:49): acceptable, verify no UI regression.
8. Scoring
-
Exact-title match ranks above substring (
$texttextScore sort —search-index.repository.ts:28-33); score never serialized (search.service.ts:51-57).
9. Security & auth
-
401 unauthenticated →
UNAUTHENTICATED; 403 withoutsearchperm (permissions.constants.ts:93) →PERMISSION_DENIED(12 §5). - No query text in crash logs or analytics payloads beyond the analytics contract (00-shared/10 §3).
- Rate limits respected; 429 handled with guidance (00-shared/07 §4).
10. UX & a11y
- 1-char queries, 300 ms debounce → no input lag, no flicker (prior results dim, 06 §2.2).
- Zero-results copy quotes raw query; clear CTA returns to landing.
-
Live-region announces result count; group headers are headings;
tiles labeled
"{title}, {type}, {description}"(07 §2). - Offline → banner + cached last results + retry on restore (00-shared/10 §2).
-
Keyboard:
/focuses,Escclears, enter submits (desktop — 06 §6).
11. Performance
-
Search p95 < 1 s (target) — 2 queries per search (search + count,
search.service.ts:36-37). -
No duplicate parallel requests for the same
(q, type, page)key (00-shared/10 §1). - Long lists: lazy tile rendering, no rebuild storm on pagination append.