Skip to content

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

TypeCoverage
Unit (Vitest + RTL)Submitting the form calls POST /query with question body and Authorization: Bearer header
UnitLoading indicator appears while request is in flight; disappears on response
UnitSuccessful response renders answer and sources (delegates rendering to QAPair from #17)
UnitHTTP error response renders error message in answer area; page does not crash
Unit401 specifically renders “Key invalid, expired, or out of requests — request a new one”
UnitHealth indicator polls /healthz every 30 seconds; renders DB state from response
UnitHealth indicator does not block submit (non-blocking side effect)
e2e (Playwright, QA env)User types question, submits, receives answer with sources from live API
e2e401 flow: expired key shows the exact 401 message from the spec
e2eHealth indicator shows green/red state reflecting actual DB connectivity
SecurityAuthorization header value is read from localStorage; no key is hardcoded or logged client-side
SecurityVITE_API_URL is a public URL; no secrets appear in the built bundle (grep on dist/ in CI)
  • Story #17 (React chat UI) merged and passing
  • Story #63 (SPA key intake) merged — key is stored in localStorage before wire-up tests run
  • QA environment deployed for e2e tests
  • 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 --noEmit exits 0

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.

Vitest, React Testing Library, MSW for API stubs, Playwright, tsc.

  • 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
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 /healthz does not touch Bedrock. It sits on ANY /{proxy+}, which the POST /query route-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.

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.