QA plan: Wire UI to API (#18)
Story: Wire UI to API; loading and error states (#18)
Capability: Web UI (#16)
Spec: specs/ui.md
Auth dependency: RAG009 — SPA sends Authorization: Bearer <key> on every /query request
Test scope
Section titled “Test scope”| Type | Coverage |
|---|---|
| Unit (Vitest + RTL) | Submitting the form calls POST /query with question body and Authorization: Bearer header |
| Unit | Loading indicator appears while request is in flight; disappears on response |
| Unit | Successful response renders answer and sources (delegates rendering to QAPair from #17) |
| Unit | HTTP error response renders error message in answer area; page does not crash |
| Unit | 401 specifically renders “Key invalid, expired, or out of requests — request a new one” |
| Unit | Health indicator polls /healthz every 30 seconds; renders DB state from response |
| Unit | Health indicator does not block submit (non-blocking side effect) |
| e2e (Playwright, QA env) | User types question, submits, receives answer with sources from live API |
| e2e | 401 flow: expired key shows the exact 401 message from the spec |
| e2e | Health indicator shows green/red state reflecting actual DB connectivity |
| Security | Authorization header value is read from localStorage; no key is hardcoded or logged client-side |
| Security | VITE_API_URL is a public URL; no secrets appear in the built bundle (grep on dist/ in CI) |
Entry criteria
Section titled “Entry criteria”- Story #17 (React chat UI) merged and passing
- Story #63 (SPA key intake) merged — key is stored in
localStoragebefore wire-up tests run - QA environment deployed for e2e tests
Exit criteria
Section titled “Exit criteria”- Unit tests pass: API call with correct headers, loading state, success render, error render, 401-specific message
- Health poll unit test:
vi.useFakeTimers()advances 30s; confirms second fetch fired - e2e against QA: full happy-path smoke — question in, answer+sources out
- No secrets in the built bundle:
npm run build && grep -r "rks_" dist/exits 1 (no matches) - TypeScript:
tsc --noEmitexits 0
Environments
Section titled “Environments”Unit: Vitest headless. e2e: Playwright against QA CloudFront URL (requires deployed QA env). The e2e step runs on merge to main after the QA deploy step in the CI pipeline (#29), not on PR.
Tooling
Section titled “Tooling”Vitest, React Testing Library, MSW for API stubs, Playwright, tsc.
Fixtures and data
Section titled “Fixtures and data”- MSW handler returning
{ answer: "stub", sources: [...] }for the happy path - MSW handler returning HTTP 401 for the auth-error path
- MSW handler with 2s artificial delay for loading-state test
How tests are written
Section titled “How tests are written”test("401 response shows key-renewal message", async () => { // Given the API returns 401 server.use(http.post("/query", () => new HttpResponse(null, { status: 401 }))); render(<App />); // When user submits a question await userEvent.type(screen.getByRole("textbox"), "What is RAG?"); await userEvent.keyboard("{Enter}"); // Then the 401-specific message is shown await screen.findByText("Key invalid, expired, or out of requests — request a new one");});- Health poll every 30 seconds from every open tab may hit Aurora cold-resume if the DB has been idle. Confirm
/healthzdoes not touch Bedrock. It sits onANY /{proxy+}, which thePOST /queryroute-level throttle does not cover. - CloudFront cache may serve a stale SPA after deploy. Add a cache-invalidation step to the CI pipeline for the e2e run.
Capability acceptance criteria link
Section titled “Capability acceptance criteria link”Spec: submit calls /query, loading indicator shows, answer and sources render, 401 renders specific message, health indicator polls every 30s. All are direct exit criteria above.
© 2026 Benjamin Arunski