Resilient UI Integration Suite
Practical 16 - Resilient UI Integration Suite
Related: Chapter 16 · Lecture slides
Objective
Build a multi-layered, resilient testing suite for an interactive civic services catalogue and application drafting workflow. The application must operate robustly across unreliable cellular connections, asynchronous race conditions, optimistic mutations, and keyboard-driven assistive technology.
Rather than relying on brittle end-to-end tests or shallow unit tests that mirror framework implementation details, you will design a testing strategy centered on risk management, accessible platform contracts, stable network boundaries, and deliberate fault injection.
The Scenario Matrix
Your test suite must validate all seven critical user experience states in the civic catalogue matrix. In Stage 4, you will intentionally inject code defects to prove that your assertions detect every failure mode.
| Scenario | Trigger / User Action | Expected Observable Outcome | Intentionally Injected Fault (Stage 4) |
|---|---|---|---|
| 1. Loading State | User initiates service search | Skeleton placeholder displayed; search button displays aria-busy="true" and is disabled | Skeleton omitted; button remains active, causing duplicate submissions |
| 2. Empty Results | User queries non-existent service ("xyz999") | Accessible status banner announced (role="status"); suggests clearing filters | Component renders blank white screen without user notification |
| 3. Server Error | Backend returns 500 Internal Server Error | Inline error alert (role="alert") appears; previous search results remain preserved; retry button displayed | Unhandled promise rejection crashes application; blank error screen |
| 4. Cancellation & Race | Rapid typing: "lic" then "license" | Query "lic" aborted via AbortController; only results for "license" render in DOM | Component ignores abort signal; slow "lic" response overwrites "license" |
| 5. Optimistic Rollback | User toggles “Bookmarked”; server rejects mutation | Star icon immediately fills; upon 500 response, icon un-fills and error toast appears | Star icon remains permanently filled despite server failure |
| 6. Form Validation | Submitting empty required email field | Field highlighted with aria-invalid="true"; error text linked via aria-describedby; focus moves to field | Plain CSS class .error applied without ARIA attributes or focus management |
| 7. Keyboard Navigation | User presses Tab, ArrowDown, Escape | Focus moves through controls in logical order; Escape dismisses modal and restores focus to trigger | Modal traps focus permanently or focus drops to document.body upon dismissal |
Workspace Setup
Initialize a modern testing workspace using Vitest, Testing Library, MSW (Mock Service Worker), and Playwright:
Configure vitest.config.ts:
Stage-by-Stage Implementation
Stage 1: Pure Logic & Parser Unit Testing
Begin at the base of the testing pyramid by validating deterministic business rules in isolation. These tests run in pure Node without DOM simulation overhead, providing sub-millisecond feedback.
- Service Fee Calculation & Formatting:
Create
src/domain/fees.tsto calculate municipal administrative charges, VAT, and fee waivers. - URL Filter Serialization:
Create
src/domain/urlParams.tsto serialize and parse search parameters (?category=transport&page=2&sort=name_asc). - Unit Test Suite (
src/domain/fees.test.ts):- Test standard fee calculation across positive, zero, and boundary values.
- Test invalid fee inputs (negative charges, non-numeric strings) ensuring they throw descriptive domain errors.
- Test URL query parameter round-trip consistency:
parseQueryParams(serializeQueryParams(filters)) === filters.
Stage 2: Component Testing with Accessible Semantics
Move up to the component boundary. Render interactive UI components into a simulated DOM (jsdom) and interact with them strictly through user-facing accessibility contracts (getByRole, getByLabelText, and @testing-library/user-event).
- Implement the Service Search Form (
src/components/ServiceSearch.ts):- Provide an input associated with
<label for="service-query">Search civic services</label>. - Provide a submit button with accessible name
"Search". - Manage keyboard focus and accessible error announcements.
- Provide an input associated with
- Write Accessible Interaction Tests (
src/components/ServiceSearch.test.ts):- Verify that form controls are queryable by role and accessible name, not by class names (
.search-input) or internal component state. - Verify that clicking submit with an empty query announces an accessible validation error linked via
aria-describedby. - Test keyboard interaction: pressing
Entersubmits the form; pressingEscapeclears the input and restores focus.
- Verify that form controls are queryable by role and accessible name, not by class names (
Stage 3: Boundary Mocking with Mock Service Worker (MSW)
Do not mock internal JavaScript modules or replace global window.fetch with simplistic stubs. Instead, intercept HTTP requests at the network transport layer using Mock Service Worker (MSW). This tests your real HTTP client, request serialisation, status code handling, and response decoding.
- Configure MSW Server (
src/test/mocks/server.ts): Define canonical handlers for/api/v1/servicesand/api/v1/services/:id/bookmark. - Simulate Edge-Case Network Boundaries:
- Happy Path: Return 200 OK with catalog items.
- Slow Network: Delay responses by 500ms to test loading spinners.
- Server Outage: Return 500 Internal Server Error to test error banners and retry actions.
- Corrupt Payloads: Return malformed JSON to test schema validation fallbacks.
Stage 4: Asynchronous Resilience & Fault Injection
Now connect your components to the MSW network boundary to test complex asynchronous flows. To ensure your tests provide genuine resilience rather than false confidence, perform deliberate fault injection.
- Test Request Cancellation & Race Conditions:
Simulate a user rapidly typing
"pas"followed by"passport". Ensure that Request 1 is aborted withAbortController, preventing a slow response from clobbering the newer search result. - Test Optimistic UI Updates with Server Rollback: When the user bookmarks a service, update the UI immediately. If the server responds with a 500 error, assert that the UI reverts to the un-bookmarked state and announces an accessible error message.
- Fault Injection Verification:
- Fault A: In
ServiceSearch.ts, comment outabortController.abort(). Run the race test and verify it fails. - Fault B: In
BookmarkButton.ts, remove the rollback logic oncatch. Run the optimistic rollback test and verify it fails.
- Fault A: In
Stage 5: Playwright Critical-Path Browser Journey
Unit and component tests verify logic and simulated DOM behavior, but they cannot prove that the layout engine, CSS stacking contexts, cookies, and real browser event loops function seamlessly together.
Create a Playwright end-to-end smoke test covering the critical citizen journey in real Chromium, Firefox, and WebKit engines:
- Create
e2e/catalogue-journey.spec.ts:- Navigate to the catalogue route.
- Perform a search and select a service.
- Fill out an application form using keyboard tab stops.
- Verify modal dialog focus trapping and dismissal via
Escape. - Trigger a simulated offline network disconnect and confirm that draft data is saved to
localStorage.
- Configure Failure Artifacts in
playwright.config.ts:- Capture full screenshots, videos, and Playwright execution traces (
trace: "on-first-retry") on test failure.
- Capture full screenshots, videos, and Playwright execution traces (
Verification & Self-Assessment
Run your full test battery and confirm all stages pass:
Observable Verification Criteria
| Verification Item | Action | Expected Pass Output |
|---|---|---|
| No Private State Inspection | Grep test files for component.state or wrapper.vm | Zero matches found; tests interact solely via DOM roles and text |
| Semantic Queries | Grep test files for .querySelector(".btn") | Zero class-based UI queries; all buttons queried via getByRole("button") |
| No Arbitrary Sleeps | Grep test files for setTimeout or sleep(1000) | Zero arbitrary timeouts; all async assertions use waitFor or findBy* |
| MSW Network Boundary | Inspect Vitest setup | Zero global fetch = vi.fn() mocks; all HTTP requests handled by MSW handlers |
| Fault Injection | Run tests against injected defects from Stage 4 | All 3 injected faults cause immediate test failure with descriptive assertions |
| Playwright Traces | Inspect test-results/ on forced E2E failure | Complete trace zip produced containing DOM snapshots, console logs, and network timeline |
Grading Rubric
| Criterion | Points | Evaluation Requirement |
|---|---|---|
| Domain Logic Isolation | 20% | Pure math, fee calculation, and URL serialization thoroughly unit-tested without DOM dependencies. |
| Accessible Component Semantics | 25% | Form controls queried strictly via getByRole and getByLabelText; validation errors associated via aria-describedby. |
| Network Boundary Mocking | 20% | Network layer intercepted via MSW; handles loading skeletons, 500 error recovery, and empty states. |
| Asynchronous Race & Rollback Resilience | 20% | Verifies AbortController cancellation under rapid typing; verifies optimistic mutation rollback upon server failure. |
| Playwright End-to-End Suite | 15% | Critical path tested in real headless browser; verifies focus trap, Escape key restoration, and trace artifacts on failure. |