Roadmap v17.1: Comprehensive Test Coverage
Target Date: 2026-01-17
Status: ✅ COMPLETED (2026-01-19)
Priority: P0 (Critical)
Owner: jer
Goal
Achieve 75%+ unit/integration test coverage across the codebase, focusing on security-critical paths (Auth, RLS), core search logic, and offline synchronization mechanisms. Establish a robust CI pipeline with coverage gating.
Current Coverage: 64% statements Target Coverage: 75%+ statements
Executive Summary
This release significantly increases test coverage from 45% to 75%+ by systematically addressing untested critical paths. The focus is on high-impact areas: search engine core, AI system, offline infrastructure, and critical UI components.
Testing Philosophy
Quality over quantity: A well-designed test that catches real bugs is worth more than 10 tests checking trivial behaviors.
Guiding Principles:
- Test behavior, not implementation - Tests shouldn't break when refactoring
- Prioritize critical paths - Search, offline sync, authorization
- Security tests first - Authorization, input validation (aligns with v17.0)
- Integration over unit where possible - User journeys catch more bugs
- No flaky tests - If a test fails intermittently, fix or delete it
Strategy (Priority Order)
- Phase 0: Security-critical tests (authorization, input validation) - from v17.0
- Phase 1: Critical library modules (search, AI, offline)
- Phase 2: Major UI components
- Phase 3: API routes and integration tests
- Phase 4: Edge cases and error scenarios
Phase 0: Security-Critical Tests (from v17.0)
Goal: Ensure authorization and security features have comprehensive tests before other test coverage work.
[!NOTE] These tests are defined in v17.0 but executed as part of v17.1 testing effort.
0.1 RLS Policy Tests
New file: tests/api/rls-policies.test.ts
describe("RLS Policies", () => {
describe("services table", () => {
it("allows public read of verified services (L1+)")
it("blocks read of unverified services (L0)")
it("blocks read of soft-deleted services")
it("allows org member to insert for own org")
it("blocks insert for other org")
it("allows editor/admin to update own org services")
it("blocks viewer from updating")
it("blocks update of other org services")
it("allows admin/owner to delete own org services")
it("blocks editor from deleting")
})
describe("organization_members table", () => {
it("allows users to see own memberships")
it("blocks viewing other users memberships")
it("allows org admins to manage members")
})
})
Estimated test count: 15-20 tests Priority: P0 - Must pass before production
0.2 Authorization Utility Tests
New file: tests/lib/auth/authorization.test.ts
describe("assertServiceOwnership", () => {
it("passes for service owner")
it("throws AuthorizationError for non-owner")
it("throws for deleted service")
})
describe("assertOrganizationMembership", () => {
it("passes for any org member")
it("passes for required role")
it("throws for insufficient role")
it("throws for non-member")
})
describe("getEffectivePermissions", () => {
it("returns correct permissions for owner")
it("returns correct permissions for viewer")
})
Estimated test count: 10-12 tests
Phase 0.5: Next.js 15 App Router Testing Patterns (CRITICAL)
Goal: Establish standardized mocking patterns for Next.js 15's async server component APIs to prevent test failures.
[!IMPORTANT] Lesson from v17.0: All API route tests failed initially because
next/headersand@supabase/ssrweren't mocked. This phase establishes the standard patterns to prevent similar issues.
0.5.1 Standard Mock Setup
New file: tests/setup/next-mocks.ts
import { vi } from "vitest"
/**
* Standard Next.js 15 mocking setup for API route tests.
* Import this at the top of EVERY API route test file.
*/
// Mock next/headers (required for all route handlers using cookies/headers)
vi.mock("next/headers", () => ({
cookies: vi.fn().mockReturnValue({
getAll: vi.fn().mockReturnValue([]),
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
}),
headers: vi.fn().mockReturnValue(new Map()),
}))
// Mock @supabase/ssr (required for all auth-protected routes)
vi.mock("@supabase/ssr", () => ({
createServerClient: vi.fn().mockReturnValue({
auth: {
getUser: vi.fn().mockResolvedValue({
data: { user: { id: "test-user-id", email: "test@example.com" } },
error: null,
}),
},
from: vi.fn().mockReturnValue({
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi.fn().mockResolvedValue({ data: null, error: null }),
}),
}),
}))
Estimated setup time: 2 hours
0.5.2 API Route Testing Template
All API route tests MUST follow this pattern:
/** @vitest-environment node */
import { describe, it, expect, vi, beforeEach } from "vitest"
import { NextRequest } from "next/server"
// CRITICAL: Import mocks BEFORE route handler
vi.mock("next/headers", () => ({
cookies: vi.fn().mockReturnValue({
getAll: vi.fn().mockReturnValue([]),
}),
}))
vi.mock("@supabase/ssr", () => ({
createServerClient: vi.fn().mockReturnValue({
auth: {
getUser: vi.fn().mockResolvedValue({
data: { user: { id: "test-user-id" } },
error: null,
}),
},
}),
}))
// Now import the route handler
import { GET } from "@/app/api/v1/services/route"
describe("GET /api/v1/services", () => {
it("returns 200 with services", async () => {
// CRITICAL: Set Content-Type header
const request = new NextRequest("http://localhost:3000/api/v1/services", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
const response = await GET(request)
expect(response.status).toBe(200)
})
})
Key Requirements:
- ✅ Use
/** @vitest-environment node */comment - ✅ Mock
next/headersbefore importing route - ✅ Mock
@supabase/ssrfor auth routes - ✅ Set
Content-Type: application/jsonheader for POST/PUT/PATCH requests - ✅ Use
NextRequestconstructor for request objects
0.5.3 Content-Type Validation Pattern
All mutation endpoints (POST/PUT/PATCH) now enforce Content-Type: application/json (from v17.0).
Required pattern for POST/PUT/PATCH tests:
const request = new NextRequest("http://localhost:3000/api/v1/services", {
method: "POST",
headers: {
"Content-Type": "application/json", // REQUIRED or you get 415
},
body: JSON.stringify({ name: "Test Service" }),
})
Common error if missing:
0.5.4 Authorization Mock Patterns
For routes using assertServiceOwnership:
vi.mock("@/lib/auth/authorization", () => ({
assertServiceOwnership: vi.fn().mockResolvedValue(true),
assertAdminRole: vi.fn().mockResolvedValue(true),
}))
For testing authorization failures:
import { AuthorizationError } from "@/lib/api-utils"
vi.mock("@/lib/auth/authorization", () => ({
assertServiceOwnership: vi.fn().mockRejectedValue(new AuthorizationError("Access denied")),
}))
Estimated test count: 5-8 mock pattern tests Priority: P0 - Must be established before any test writing
0.5.5 Verify Mock Setup
New file: tests/setup/verify-mocks.test.ts
import { describe, it, expect } from "vitest"
describe("Mock Setup Verification", () => {
it("next/headers is mocked", () => {
const { cookies } = require("next/headers")
expect(cookies).toBeDefined()
expect(typeof cookies).toBe("function")
})
it("@supabase/ssr is mocked", () => {
const { createServerClient } = require("@supabase/ssr")
expect(createServerClient).toBeDefined()
})
it("authorization utilities are mockable", () => {
const { assertServiceOwnership } = require("@/lib/auth/authorization")
expect(assertServiceOwnership).toBeDefined()
})
})
Success Criteria:
- [ ] All mock verification tests pass
- [ ] Template pattern documented
- [ ] At least 2 API route tests refactored to use pattern successfully
Phase 1: Library Core Coverage (1.5 weeks)
1.1 Search Engine Core (lib/search/)
Current Status: 72.61% (variable)
lib/search/data.ts: 0% → 80%lib/search/index.ts: 34% → 65%lib/search/vector.ts: 7% → 50%lib/search/lifecycle.ts: 0% → 70%lib/search/search-mode.ts: 0% → 70%
1.1.1 Data Loading (lib/search/data.ts)
New file: tests/lib/search/data.test.ts
Test coverage for data loading orchestration:
describe("loadServices", () => {
describe("with Supabase available", () => {
it("loads services from Supabase")
it("overlays AI metadata from JSON")
it("caches result in memory")
it("returns correct Service[] type")
})
describe("with Supabase unavailable", () => {
it("falls back to local JSON")
it("loads from IndexedDB on client")
it("logs fallback event")
})
describe("data transformation", () => {
it("converts VerificationLevel to multiplier")
it("merges synthetic_queries from JSON")
it("handles missing optional fields")
})
describe("error handling", () => {
it("throws on malformed JSON")
it("retries Supabase connection once")
it("surfaces error with helpful message")
})
})
describe("getServiceById", () => {
it("returns service from cache")
it("falls back to Supabase query")
it("returns null for missing service")
})
Estimated test count: 12-15 tests
1.1.2 Search Orchestrator (lib/search/index.ts)
Modify: tests/lib/search/index.test.ts
Expand from 34% to 65% coverage:
describe("searchServices", () => {
describe("happy path", () => {
it("returns results for valid query")
it("applies category filters")
it("filters by openNow status")
it("applies verification multipliers")
it("sorts by score descending")
})
describe("keyword matching", () => {
it("matches service name")
it("matches description")
it("matches category")
it("handles synonym expansion")
it("matches partial words")
})
describe("crisis detection", () => {
it("detects suicide keywords")
it("detects abuse keywords")
it("boosts crisis services to top")
it("returns crisis services regardless of score")
})
describe("scoring", () => {
it("applies L3 multiplier (1.5x)")
it("applies L2 multiplier (1.2x)")
it("applies L1 multiplier (1.0x)")
it("boosts recently verified services")
})
describe("vector search", () => {
it("uses embeddings if provided")
it("weights vector score (30%)")
it("weights keyword score (70%)")
})
describe("geo-distance", () => {
it("calculates distance correctly")
it("applies proximity decay multiplier")
it("handles missing coordinates gracefully")
})
describe("edge cases", () => {
it("handles empty query string")
it("handles very long query")
it("returns empty array for no matches")
it("handles null/undefined inputs")
})
})
Estimated test count: 25-30 tests Files to modify:
tests/lib/search/index.test.ts- Add missing test casestests/lib/search/scoring.test.ts- Expand edge case coverage
1.1.3 Vector Similarity (lib/search/vector.ts)
New file: tests/lib/search/vector.test.ts
Expand from 7% to 50% coverage:
describe("cosineSimilarity", () => {
it("returns 1.0 for identical vectors")
it("returns 0.0 for orthogonal vectors")
it("returns negative values for opposite vectors")
it("handles zero vectors")
it("normalizes high-dimensional vectors")
})
describe("searchByVector", () => {
it("ranks services by similarity")
it("filters by similarity threshold")
it("handles empty embedding database")
it("handles query with no embedding")
})
describe("vectorToQueryString", () => {
it("converts vector to SQL format")
it("escapes special characters")
})
describe("edge cases", () => {
it("handles NaN values in vectors")
it("handles Infinity values")
it("handles very small floating point errors")
it("performs well with 1000+ services")
})
Estimated test count: 12-15 tests
1.1.4 Vector Store Lifecycle (lib/search/lifecycle.ts)
New file: tests/lib/search/lifecycle.test.ts
describe("initializeVectorStore", () => {
it("loads embeddings from file")
it("creates in-memory index")
it("returns vector count")
it("handles missing embeddings file")
})
describe("vector persistence", () => {
it("saves vectors to IndexedDB")
it("loads vectors from IndexedDB cache")
it("validates vector integrity")
it("updates on new embeddings")
})
describe("error recovery", () => {
it("recovers from corrupted IndexedDB")
it("falls back to file-based embeddings")
it("logs initialization errors")
})
Estimated test count: 10 tests
1.1.5 Search Mode Detection (lib/search/search-mode.ts)
New file: tests/lib/search/search-mode.test.ts
describe("getSearchMode", () => {
it('returns "local" when NEXT_PUBLIC_SEARCH_MODE=local')
it('returns "server" when NEXT_PUBLIC_SEARCH_MODE=server')
it('defaults to "local"')
})
describe("searchServices", () => {
describe("local mode", () => {
it("uses client-side search")
it("does not send query to server")
})
describe("server mode", () => {
it("sends POST to /api/v1/search/services")
it("passes query and filters")
it("returns server results")
})
describe("error handling", () => {
it("falls back to local on server error")
it("logs server failures")
})
})
Estimated test count: 8 tests
1.2 AI System (lib/ai/)
Current Status: 66.91%
lib/ai/engine.ts: 57% → 85%lib/ai/webllm.worker.ts: 0% → 50%
1.2.1 AI Engine (lib/ai/engine.ts)
Modify: tests/lib/ai/engine.test.ts
Expand from 57% to 85% coverage:
describe("refineSearchQuery", () => {
describe("input processing", () => {
it("expands query with synonyms")
it("detects implicit filters")
it("extracts location context")
it("identifies service type")
})
describe("JSON output", () => {
it("returns valid JSON structure")
it("includes expanded_query field")
it("includes implicit_filters field")
it("includes confidence scores")
})
describe("crisis handling", () => {
it("bypasses AI for suicide keywords")
it("returns crisis flag immediately")
})
describe("error handling", () => {
it("handles WebGPU unavailable")
it("handles model load failure")
it("returns original query on error")
})
})
describe("chat context management", () => {
it("maintains conversation history")
it("enforces max history length")
it("includes relevant service data")
it("truncates long messages")
})
describe("model lifecycle", () => {
it("loads model on first use")
it("caches model in memory")
it("unloads after idle timeout")
it("reloads after unload")
})
describe("streaming", () => {
it("streams response tokens")
it("yields partial results")
it("completes stream normally")
it("handles stream interruption")
})
describe("inference", () => {
it("generates chat responses")
it("respects system prompt")
it("includes service context")
it("stays on-topic for social services")
})
Estimated test count: 20-25 tests
1.2.2 WebLLM Engine Logic (lib/ai/webllm-engine.ts)
[!IMPORTANT] Architectural Decision: Vitest cannot reliably test Web Workers. Instead, extract testable logic from the worker into a separate module and test that module directly. E2E test the worker integration separately.
Step 1: Extract Worker Logic (Refactor)
Create new file: lib/ai/webllm-engine.ts
/**
* Extracted WebLLM logic for unit testing.
* This logic will be called by the worker in lib/ai/webllm.worker.ts
*/
export class WebLLMEngine {
private model: any = null
private isInitialized = false
async loadModel(modelId: string) {
// Extract current worker logic here
// Return status
}
async runInference(prompt: string, options: any) {
if (!this.isInitialized) {
throw new Error("Model not loaded")
}
// Extract inference logic
}
unload() {
this.model = null
this.isInitialized = false
}
get ready() {
return this.isInitialized
}
}
Step 2: Refactor Worker to Use Engine
Modify: lib/ai/webllm.worker.ts
import { WebLLMEngine } from "./webllm-engine"
const engine = new WebLLMEngine()
self.onmessage = async (e) => {
if (e.data.type === "load") {
try {
await engine.loadModel(e.data.modelId)
self.postMessage({ type: "ready" })
} catch (error) {
self.postMessage({ type: "error", error })
}
}
if (e.data.type === "infer") {
try {
const result = await engine.runInference(e.data.prompt, e.data.options)
self.postMessage({ type: "result", data: result })
} catch (error) {
self.postMessage({ type: "error", error })
}
}
}
Step 3: Test the Extracted Engine
New file: tests/lib/ai/webllm-engine.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest"
import { WebLLMEngine } from "@/lib/ai/webllm-engine"
describe("WebLLMEngine", () => {
let engine: WebLLMEngine
beforeEach(() => {
engine = new WebLLMEngine()
})
describe("loadModel", () => {
it("initializes engine", async () => {
await engine.loadModel("test-model-id")
expect(engine.ready).toBe(true)
})
it("throws on invalid modelId", async () => {
await expect(engine.loadModel("")).rejects.toThrow()
})
})
describe("runInference", () => {
it("runs inference when ready", async () => {
await engine.loadModel("test-model")
const result = await engine.runInference("test prompt", {})
expect(result).toBeDefined()
})
it("throws if model not loaded", async () => {
await expect(engine.runInference("test", {})).rejects.toThrow("Model not loaded")
})
})
describe("unload", () => {
it("resets engine state", async () => {
await engine.loadModel("test-model")
engine.unload()
expect(engine.ready).toBe(false)
})
})
})
Step 4: E2E Test Worker Integration (Playwright)
Worker integration should be tested via E2E:
// tests/e2e/ai-worker.spec.ts (Playwright)
test("AI worker loads and responds", async ({ page }) => {
await page.goto("/chat")
await page.click('[data-testid="enable-ai"]')
await page.waitForSelector('[data-testid="ai-ready"]')
// Verify worker loaded successfully
})
Estimated test count: 10-12 tests (engine logic only) Note: Worker integration verified via E2E, not unit tests
1.3 Offline Infrastructure (lib/offline/)
Current Status: 53.71% (CRITICAL GAPS)
1.3.1 Offline Feedback Sync (lib/offline/feedback.ts)
Current: 2.17% (CRITICAL - only 1 line tested)
New file: tests/lib/offline/feedback.test.ts
describe("queueFeedback", () => {
it("stores feedback in pending queue")
it("assigns pending UUID")
it("sets created timestamp")
it("marks as unsync on offline")
})
describe("syncFeedback", () => {
describe("with network available", () => {
it("sends all pending feedback")
it("removes feedback on success")
it("updates sync status")
it("returns successful count")
})
describe("with network unavailable", () => {
it("returns queued without sending")
it("keeps feedback in queue")
})
})
describe("retry logic", () => {
it("retries failed feedback (max 5 times)")
it("exponential backoff (1s, 2s, 4s, 8s, 16s)")
it("clears feedback after max retries")
it("logs retry attempts")
})
describe("cleanup", () => {
it("deletes sent feedback from IndexedDB")
it("handles deletion errors gracefully")
it("maintains data integrity")
})
describe("edge cases", () => {
it("handles network interruption mid-sync")
it("recovers from corrupted queue data")
it("handles IndexedDB quota exceeded")
it("deduplicates identical feedback")
})
describe("offline state transitions", () => {
it("queues feedback when going offline")
it("syncs when coming back online")
it("handles rapid online/offline cycles")
})
Estimated test count: 25-30 tests
1.3.2 Offline Cache (lib/offline/cache.ts)
Modify: tests/lib/offline/cache.test.ts
Expand from 56% to 80% coverage:
describe("cacheServices", () => {
it("stores services in IndexedDB")
it("updates existing services")
it("prunes old entries")
it("handles quota exceeded")
})
describe("cache invalidation", () => {
it("invalidates by timestamp")
it("invalidates by service ID")
it("invalidates all on refresh")
it("logs invalidation events")
})
describe("time-based expiration", () => {
it("marks stale after 24 hours")
it("removes expired entries")
it("respects custom TTL")
})
describe("error scenarios", () => {
it("handles IndexedDB errors")
it("falls back to memory if DB fails")
it("recovers from corrupted cache")
})
Estimated test count: 15-18 tests
1.3.3 Offline Database (lib/offline/db.ts)
Modify: tests/lib/offline/db.test.ts
Expand from 56% to 80% coverage:
describe("database operations", () => {
describe("CRUD", () => {
it("creates service record")
it("reads service by ID")
it("updates service")
it("deletes service")
})
describe("bulk operations", () => {
it("loads all services")
it("bulk inserts (196 services)")
it("bulk updates")
it("clears all records")
})
})
describe("schema", () => {
it("creates stores on first init")
it("validates schema version")
it("handles schema migration")
it("indexes by ID")
})
describe("performance", () => {
it("handles 1000+ records efficiently")
it("indexes optimize queries")
it("cursor operations work")
})
describe("error handling", () => {
it("handles IndexedDB not available")
it("recovers from quota exceeded")
it("handles corrupted records")
it("logs database errors")
})
Estimated test count: 18-20 tests
1.4 Analytics (lib/analytics/)
Current Status: 26.31%
1.4.1 Search Analytics (lib/analytics/search-analytics.ts)
New file: tests/lib/analytics/search-analytics.test.ts
describe("recordSearchEvent", () => {
describe("privacy", () => {
it("does NOT log query text")
it("does NOT log user IP")
it("logs only result count")
it("logs search timestamp")
})
describe("result bucketing", () => {
it('buckets 0 results as "0"')
it('buckets 1-5 as "1-5"')
it('buckets 5+ as "5+"')
})
describe("submission", () => {
it("submits to Supabase analytics table")
it("retries on network error")
it("handles Supabase unavailable")
})
describe("error scenarios", () => {
it("continues if analytics fails")
it("does not block search")
it("logs errors")
})
})
describe("compliance", () => {
it("complies with PIPEDA (no PII)")
it("complies with GDPR (no tracking)")
})
Estimated test count: 12-15 tests
Phase 2: Component Testing (1 week)
2.1 Critical Components
2.1.1 ChatAssistant (components/ai/ChatAssistant.tsx)
New file: tests/components/ai/ChatAssistant.test.tsx
449 lines, currently 0% coverage.
describe("ChatAssistant", () => {
describe("rendering", () => {
it("displays chat interface")
it("shows message history")
it("shows input field")
it("shows send button")
})
describe("user interactions", () => {
it("sends message on button click")
it("sends message on Enter key")
it("clears input after send")
it("disables input while loading")
})
describe("AI responses", () => {
it("receives and displays AI response")
it("streams response token-by-token")
it("shows loading indicator while streaming")
})
describe("conversation context", () => {
it("maintains message history")
it("includes recent messages in context")
it("truncates old messages")
})
describe("error handling", () => {
it("shows error on AI failure")
it("allows retry after error")
it("continues chat after error")
})
describe("accessibility", () => {
it("has proper ARIA labels")
it("is keyboard navigable")
it("announces new messages")
})
})
Estimated test count: 18-22 tests
2.1.2 SearchBar (components/home/SearchBar.tsx)
New file: tests/components/home/SearchBar.test.tsx
99 lines, currently 0% coverage.
describe("SearchBar", () => {
describe("rendering", () => {
it("displays search input")
it("displays search button")
it("displays voice input button")
})
describe("search submission", () => {
it("submits search on button click")
it("submits search on Enter key")
it("calls onSearch callback")
it("passes query value")
})
describe("voice input", () => {
it("starts recording on voice button click")
it("stops recording on second click")
it("shows recording indicator")
it("transcribes audio")
})
describe("input handling", () => {
it("updates value on input change")
it("trims whitespace")
it("handles special characters")
})
describe("saved searches", () => {
it("shows save search button")
it("saves search on click")
it("shows saved searches dropdown")
})
describe("accessibility", () => {
it("has proper labels")
it("is keyboard navigable")
it("works with screen readers")
})
})
Estimated test count: 15-18 tests
2.1.3 SearchResultsList (components/home/SearchResultsList.tsx)
New file: tests/components/home/SearchResultsList.test.tsx
125 lines, currently 0% coverage.
describe("SearchResultsList", () => {
describe("rendering", () => {
it("displays result items")
it("shows service name")
it("shows service address")
it("shows distance")
})
describe("result interactions", () => {
it("navigates to service detail on click")
it("shows match reason highlights")
it("displays contact button")
})
describe("verification badges", () => {
it("shows L3 badge for verified services")
it("shows L2 badge")
it("shows L1 badge")
it("hides L0 services")
})
describe("empty state", () => {
it('shows "no results" message')
it("suggests alternative searches")
})
describe("crisis results", () => {
it("prioritizes crisis services")
it("shows crisis indicator")
it("displays emergency contact prominently")
})
describe("accessibility", () => {
it("results are semantically linked")
it("keyboard navigable")
})
})
Estimated test count: 15-18 tests
2.1.4 EmergencyModal (components/ui/EmergencyModal.tsx)
New file: tests/components/ui/EmergencyModal.test.tsx
157 lines, currently 0% coverage.
describe("EmergencyModal", () => {
describe("rendering", () => {
it("displays when crisis detected")
it("hidden when not crisis")
it("shows crisis message")
it("shows emergency contacts")
})
describe("emergency contacts", () => {
it("displays crisis line phone")
it("displays 911 option")
it("shows hospital finder link")
})
describe("interactions", () => {
it("calls phone on contact click")
it("navigates to hospital finder")
it("closes on dismiss")
})
describe("accessibility", () => {
it('has role="alertdialog"')
it("is keyboard accessible")
it("announces message to screen readers")
})
})
Estimated test count: 12-15 tests
2.1.5 ClaimFlow (components/partner/ClaimFlow.tsx)
New file: tests/components/partner/ClaimFlow.test.tsx
134 lines, currently 0% coverage.
describe("ClaimFlow", () => {
describe("step 1: verification", () => {
it("displays claim instructions")
it("shows verification code input")
it("validates code format")
})
describe("step 2: ownership", () => {
it("displays identity verification form")
it("collects contact info")
it("validates required fields")
})
describe("step 3: review", () => {
it("displays claim summary")
it("shows next steps")
})
describe("flow navigation", () => {
it("moves forward on next")
it("moves backward on back")
it("submits on final step")
})
describe("error handling", () => {
it("shows error on invalid code")
it("allows retry")
it("displays server errors")
})
describe("accessibility", () => {
it("announces step number")
it("proper form labels")
it("keyboard navigable")
})
})
Estimated test count: 15-18 tests
2.2 Untested Hooks (1 priority)
2.2.1 useNetworkStatus (hooks/useNetworkStatus.ts)
New file: tests/hooks/useNetworkStatus.test.ts
78 lines, currently 0% coverage.
describe("useNetworkStatus", () => {
describe("initial state", () => {
it("returns initial online status")
it("checks navigator.onLine")
})
describe("online event", () => {
it('updates to online on "online" event')
it("triggers callback")
})
describe("offline event", () => {
it('updates to offline on "offline" event')
it("triggers callback")
})
describe("cleanup", () => {
it("removes event listeners on unmount")
})
describe("edge cases", () => {
it("handles rapid online/offline changes")
it("handles missing navigator.onLine")
})
})
Estimated test count: 10 tests
2.2.2 useShare (hooks/useShare.ts)
New file: tests/hooks/useShare.test.ts
52 lines, currently 0% coverage.
describe("useShare", () => {
describe("native share", () => {
it("uses Web Share API if available")
it("passes correct share data")
it("returns success")
})
describe("fallback", () => {
it("falls back to clipboard on error")
it("copies URL to clipboard")
it("shows success message")
})
describe("mobile detection", () => {
it("detects mobile devices")
it("uses Capacitor Share on mobile")
})
describe("error handling", () => {
it("handles share cancellation")
it("handles clipboard error")
})
})
Estimated test count: 10 tests
Phase 3: API Routes & Integration Tests (3-4 days)
3.1 Undertested API Routes
3.1.1 Admin Routes
Modify: tests/api/admin-*.test.ts
describe("POST /api/admin/save", () => {
it("requires admin role")
it("saves data to Supabase")
it("validates request body")
it("returns error on validation failure")
})
describe("POST /api/admin/push", () => {
it("requires admin role")
it("sends push notification")
it("validates OneSignal payload")
it("returns notification ID")
})
describe("POST /api/admin/reindex", () => {
it("requires admin role")
it("triggers embedding generation")
it("returns progress tracking")
})
describe("GET /api/admin/data", () => {
it("requires admin role")
it("exports all services")
it("includes embeddings")
})
Estimated test count: 12-15 tests
3.1.2 Notification Routes
Modify: tests/api/notifications-*.test.ts
describe("POST /api/v1/notifications/subscribe", () => {
it("requires authentication")
it("accepts push subscription")
it("stores in database")
it("returns success")
})
describe("POST /api/v1/notifications/unsubscribe", () => {
it("requires authentication")
it("removes subscription")
it("confirms removal")
})
Estimated test count: 6-8 tests
3.1.3 Analytics Routes
Modify: tests/api/analytics-*.test.ts
describe("POST /api/v1/analytics/search", () => {
it("records search event")
it("does not log query text")
it("buckets results")
it("returns acknowledgment")
})
Estimated test count: 4-6 tests
3.2 Integration Tests
New file: tests/integration/user-journeys.test.ts
Cross-component workflows:
describe("Search & View Service Journey", () => {
it("searches for services")
it("filters by category")
it("navigates to service detail")
it("displays complete service info")
})
describe("Offline Sync Journey", () => {
it("caches services while online")
it("searches while offline")
it("queues feedback while offline")
it("syncs when back online")
})
describe("Partner Claim Journey", () => {
it("views service as partner")
it("starts claim process")
it("completes all steps")
it("sees claimed service in dashboard")
})
describe("Crisis Response Journey", () => {
it("detects crisis keywords")
it("shows emergency modal")
it("provides immediate resources")
})
Estimated test count: 12-15 tests
Phase 4: Edge Cases & Error Scenarios (3-4 days)
4.1 Error Scenario Coverage
Modify: Multiple test files to add error cases:
describe("error handling", () => {
// Database errors
it("handles Supabase connection timeout")
it("handles database constraint violation")
it("handles IndexedDB quota exceeded")
// Network errors
it("handles offline during search")
it("handles network interrupted mid-request")
it("handles 429 rate limit")
it("handles 500 server error")
// Validation errors
it("rejects invalid email")
it("rejects empty query")
it("rejects oversized input")
// Authorization errors
it("rejects unauthorized API access")
it("rejects expired token")
it("rejects insufficient permissions")
// Data errors
it("handles malformed JSON")
it("handles corrupted cache")
it("handles stale embeddings")
// Resource limits
it("handles rate limit (60 req/min)")
it("handles max history length")
it("handles file upload size limit")
})
Estimated test count: 20-25 tests
4.2 Boundary Condition Tests
describe("boundary conditions", () => {
it("handles empty string query")
it("handles very long query (10k chars)")
it("handles unicode in query")
it("handles emoji in input")
it("handles 0 results")
it("handles 1000+ results")
it("handles service with no address")
it("handles service with 0 verification level")
it("handles distance 0 (exact location)")
it("handles distance 1000km (province-wide)")
})
Estimated test count: 10 tests
Test Infrastructure Improvements
4.1 Mocking & Fixtures
New file: tests/fixtures/services.ts
export const mockService: Service = { /* complete service object */ }
export const mockServices: Service[] = [ /* 5-10 varied services */ ]
export const createMockService = (overrides: Partial<Service>) => ({ ... })
New file: tests/fixtures/feedback.ts
Feedback mock data for consistent testing.
New file: tests/fixtures/users.ts
User and organization mocks.
4.2 Test Helpers
Modify: tests/setup.ts
Add helpers:
- Database cleanup between tests
- Mock Supabase responses
- Mock WebLLM engine
- Network mock utilities
4.3 CI/CD Configuration
Modify: .github/workflows/ci.yml
Add coverage reporting:
- name: Generate Coverage Report
run: npm run test:coverage
- name: Upload to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
flags: unittests
- name: Enforce Coverage
run: |
# Fail if coverage drops below 75%
COVERAGE=$(grep statements coverage/coverage-summary.json | grep -o '"pct": [0-9.]*' | grep -o '[0-9.]*')
if (( $(echo "$COVERAGE < 75" | bc -l) )); then
exit 1
fi
Test Execution Plan
Week 1: Libraries
# Phase 1 tests
npm test -- tests/lib/search
npm test -- tests/lib/ai
npm test -- tests/lib/offline
npm test -- tests/lib/analytics
npm test -- tests/lib/rate-limit
Week 2: Components & Hooks
Week 3: Final Coverage & Edge Cases
Coverage Target Breakdown
| Module | Current | Target | Est. Tests |
|---|---|---|---|
| lib/search | 72% | 75%+ | 50 |
| lib/ai | 66% | 85% | 35 |
| lib/offline | 53% | 75% | 65 |
| lib/analytics | 26% | 70% | 15 |
| components | 40% | 70% | 80 |
| hooks | 77% | 85% | 20 |
| API routes | 65% | 80% | 25 |
| TOTAL | 45% | 75% | ~290 tests |
Success Criteria
Coverage Metrics
- [ ] Overall coverage: 45% → 75%+ statements
- [ ] Security modules: 90%+ (
lib/auth/,lib/rate-limit/) - [ ] Critical paths: 80%+ (
lib/search/,lib/offline/) - [ ] All files have ≥50% coverage (no dark spots)
Quality Metrics
- [ ] Zero CRITICAL test failures in CI
- [ ] Zero flaky tests (0% retry rate acceptable)
- [ ] All test execution: <5min local, <10min CI
- [ ] Test naming follows convention:
it('should [behavior] when [condition]')
Automation
- [ ] Coverage report auto-generated on PR
- [ ] Coverage gate blocks PRs dropping below 70%
- [ ] Test failure notifications in Slack/Discord (optional)
Dependencies & Assumptions
- Vitest 3.x already installed with workspace support
- @testing-library/react available for component tests
- JSDOM for component testing (browser-less)
- Playwright for E2E (separate from this plan, see E2E roadmap)
- MSW (Mock Service Worker) for API mocking (recommended addition)
Test Data Management Strategy
1. Fixtures (Static Test Data)
// tests/fixtures/services.ts
export const mockServiceL3: Service = {
id: "test-service-1",
name: "Test Mental Health Service",
verification_level: 3,
// ... complete fixture
}
export const mockServices = [mockServiceL3, mockServiceL2, mockServiceL1]
// Factory function for variations
export const createMockService = (overrides: Partial<Service>): Service => ({
...mockServiceL3,
id: `test-${Date.now()}`,
...overrides,
})
2. Test Database (Integration Tests)
For tests that need real database:
// tests/setup/db.ts
import { createClient } from "@supabase/supabase-js"
export const testSupabase = createClient(process.env.SUPABASE_TEST_URL!, process.env.SUPABASE_TEST_KEY!)
// Clean up after each test
afterEach(async () => {
await testSupabase.from("services").delete().eq("id", "test-%")
})
3. Mocking External Services
// tests/mocks/supabase.ts
export const mockSupabaseClient = {
from: vi.fn().mockReturnThis(),
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockResolvedValue({ data: mockServices, error: null }),
}
// tests/mocks/webllm.ts
export const mockAIEngine = {
refineSearchQuery: vi.fn().mockResolvedValue({
expanded_query: "mental health counseling therapy",
confidence: 0.9,
}),
}
Vitest Configuration Improvements
Per-Path Coverage Thresholds
Modify: vitest.config.mts
export default defineConfig({
test: {
coverage: {
thresholds: {
// Global minimum
statements: 75,
branches: 70,
functions: 75,
lines: 75,
// Per-path overrides
"lib/auth/**": { statements: 90, branches: 85 },
"lib/search/**": { statements: 80, branches: 75 },
"lib/offline/**": { statements: 75, branches: 70 },
"lib/ai/**": { statements: 85, branches: 80 },
},
},
},
})
Rollback Plan
If coverage goals slip:
- Keep Phase 0 (security tests) - non-negotiable for production
- Focus on Phase 1 (libraries) - most value per test
- Defer Phase 2 component tests to v17.1.1
- Use E2E tests as partial coverage for UI components
- Document untested paths in
TESTING_GAPS.mdfor future work
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Flaky tests block CI | Medium | High | Quarantine + investigate immediately |
| Coverage drops on new features | High | Medium | Coverage gates on PRs |
| Test execution too slow | Low | Medium | Parallelize, use --changed flag |
| Mocking complexity | Medium | Low | Document patterns, use MSW |