Overview
Goal
Build a performant, accessible, data-driven feature -- a searchable, paginated dashboard panel --
with streaming SSR, cached data fetching + an optimistic update that rolls back, a memoized derived
view, an ARIA-correct combobox widget with keyboard + live-region support, and an error path --
measured with a Lighthouse-style Core Web Vitals check and covered by component tests. Per the
syllabus, this capstone is runnable and testable from the CLI: there is no dev server or separate
demo page -- the functional core (server.ts, store.ts) and the imperative shell (dashboard.ts)
are exercised directly by a Vitest + @testing-library/dom suite against jsdom, the same way the
prerequisite topic's capstone was. This is a consolidation: every mechanism it combines was already
taught, individually, somewhere in the Beginner, Intermediate, or Advanced tiers.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Gray #808080
flowchart TD
A["types.ts<br/>Row + LoadState union +<br/>Page cursor + CoreWebVitals<br/>co-30, co-33"]:::blue
B["server.ts<br/>streaming SSR + fetch +<br/>CWV measurement<br/>co-04, co-08, co-30"]:::orange
C["store.ts<br/>cache + optimistic rollback +<br/>memoized selector<br/>co-15, co-19"]:::teal
D["dashboard.ts<br/>combobox + arrow-key nav +<br/>live region + error path<br/>co-23, co-24, co-25, co-37"]:::purple
E["dashboard.test.ts<br/>Vitest + Testing-Library<br/>9 passing tests<br/>co-36"]:::gray
A --> B
B --> C
C --> D
D --> E
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef gray fill:#808080,stroke:#000000,color:#FFFFFF,stroke-width:2px
Concepts exercised
- a measured perf fix -- Core Web Vitals (co-08, co-27)
- SSR/streaming + hydration (co-04)
- cached fetch + optimistic update (co-15)
- derived/shared state (co-19)
- an ARIA-correct widget + focus/live region (co-23, co-24)
- Testing-Library tests (co-36)
- an error-boundary-style safe path (co-37)
All colocated code lives under learning/capstone/code/. Every listing below is the complete file,
verbatim -- nothing on this page is truncated or paraphrased.
Step 1: The feature with SSR + a slow baseline
exercises co-04, co-08, co-30
types.ts defines the row, the exhaustive LoadState union, the cursor-paginated Page, and the
CoreWebVitals triad. server.ts is the server layer: a streaming-SSR renderer that emits the
shell, then the fallback, before the resolved rows, the paginated fetch, and a Lighthouse-style CWV
measurement that produces a poor baseline (slow paint, no reserved boxes) and a good improved
version.
learning/capstone/code/types.ts (complete file)
// Capstone types: a dashboard row, the paginated slice, and the discriminated-union load state.
// (co-33 discriminated-union-state; co-30 pagination cursor.)
export interface Row {
// a single dashboard record the user searches and paginates over
id: string; // stable key (co-20)
label: string; // the searchable, visible text
}
// The async load state modeled as an exhaustive discriminated union (Example 66 / co-33).
export type LoadState =
| { status: "loading" } // the SSR/streaming fallback state
| { status: "error"; message: string } // payload unique to this branch
| { status: "loaded"; rows: Row[] }; // payload unique to this branch
// A cursor-paginated slice of the row list (co-30): the visible page plus a next cursor.
export interface Page<T> {
items: T[]; // the rows on THIS page
nextCursor: number | null; // null => no more pages (the terminal cursor)
}
// A Core Web Vitals measurement (co-08): the three numbers Lighthouse would report.
export interface CoreWebVitals {
lcp: number; // ms; good <= 2500
inp: number; // ms; good <= 200
cls: number; // score; good <= 0.1
}learning/capstone/code/server.ts (complete file)
// Capstone server layer: a streaming-SSR renderer, the data fetch, and a Lighthouse-style Core
// Web Vitals measurement. co-04 (streaming-ssr), co-08 (core-web-vitals), co-30 (data fetching).
import type { CoreWebVitals, LoadState, Page, Row } from "./types";
// The seed rows the "server" owns; pagination slices this list by cursor.
const SEED: Row[] = [
{ id: "1", label: "buy bread" },
{ id: "2", label: "buy milk" },
{ id: "3", label: "sell car" },
{ id: "4", label: "buy eggs" },
{ id: "5", label: "walk dog" },
{ id: "6", label: "sell bike" },
];
// PAGE_SIZE rows per page (co-30 pagination).
const PAGE_SIZE = 3;
// fetchPage simulates a server fetch of one page starting at `cursor`.
export function fetchPage(cursor: number | null): Promise<Page<Row>> {
// cursor=null starts at index 0; otherwise resume from the cursor (keyset position).
const start = cursor ?? 0; // => the keyset position to resume from
const items = SEED.slice(start, start + PAGE_SIZE); // => the PAGE_SIZE rows on this page
const nextCursor = start + PAGE_SIZE < SEED.length ? start + PAGE_SIZE : null; // => null = last page
return Promise.resolve({ items, nextCursor }); // => resolves synchronously here (no real network)
}
// renderToStream models streaming SSR (co-04): the shell + fallback stream first, then the rows.
export function renderToStream(state: LoadState, query: string): string[] {
// the chunks stream in arrival order: shell, fallback, then resolved content
const chunks: string[] = ["<!-- shell -->"]; // => the static shell streams first
if (state.status === "loading") {
chunks.push('<p role="status">Loading dashboard...</p>'); // => co-04: the Suspense fallback
return chunks; // => the fallback paints before the data resolves
}
if (state.status === "error") {
chunks.push(`<p role="alert">${state.message}</p>`); // => the error state
return chunks;
}
// loaded: filter by the query (the derived selector in store.ts does this for the live DOM;
// here the SSR string mirrors it).
const matches = state.rows.filter((r) => r.label.includes(query));
chunks.push(`<ul>${matches.map((r) => `<li>${r.label}</li>`).join("")}</ul>`); // => resolved content
return chunks;
}
// measureCwv is a deterministic Lighthouse stand-in: it derives the triad from a paint latency and
// a hasReservedSpace flag, so the baseline (slow, no reserved space) and the improved version
// (fast, reserved) produce different, comparable numbers. co-08 / co-11 / co-31.
export function measureCwv(paintMs: number, hasReservedSpace: boolean): CoreWebVitals {
// LCP tracks the paint latency; INP tracks a proportional responsiveness; CLS is 0 only when
// the image/row boxes had reserved dimensions (co-11/co-31).
return {
lcp: paintMs, // => LCP ~= first meaningful paint
inp: Math.round(paintMs * 0.05) + 80, // => a responsiveness proxy (worse when paint is slow)
cls: hasReservedSpace ? 0.05 : 0.3, // => reserved boxes => stable layout
};
}
// rating bands the CWV the way web.dev / Example 13 does.
export function rate(v: CoreWebVitals): "good" | "poor" {
// all three must meet "good" for the page to be rated good (the capstone's improvement bar)
if (v.lcp <= 2500 && v.inp <= 200 && v.cls <= 0.1) return "good";
return "poor";
}Verify (the streaming + baseline-CWV tests, isolated):
$ npx vitest run .../dashboard.test.ts --environment=jsdom -t "streaming SSR|Core Web Vitals"
Test Files 1 passed (1)
Tests 2 passed (2)Step 2: Apply the perf work and verify Core Web Vitals improve
exercises co-08, co-11, co-27
The "perf work" is modeled directly in measureCwv: the baseline is a slow paint (3200 ms) with
no reserved boxes (CLS 0.30); the improved version cuts the paint to 1500 ms (LCP under the 2.5 s
good threshold) and reserves the row boxes (CLS 0.05). rate() flips from poor to good, and both
LCP and CLS improve measurably -- the acceptance criterion "measured Core Web Vitals improve over
baseline." In a real build these same numbers would come from CLI Lighthouse before/after applying
code-splitting, lazy-loading, and reserved image/row dimensions.
Verify (the CWV improvement test):
$ npx vitest run .../dashboard.test.ts --environment=jsdom -t "Core Web Vitals improve"
Test Files 1 passed (1)
Tests 1 passed (1)Step 3: Cached data fetching + optimistic update + derived selector
exercises co-15, co-19
store.ts is the server-state layer: an in-memory query cache keyed by "dashboard-rows" (so repeat
reads skip the network, Example 31), invalidate() to mark the cache stale after a mutation
(Example 33), optimisticAdd() which snapshots, applies optimistically, and rolls back on
failure (Example 34), and a memoized selectFiltered() derived selector that recomputes only when
its inputs change (Example 36).
learning/capstone/code/store.ts (complete file)
// Capstone store: a server-state cache with invalidation, an optimistic add that rolls back on
// failure, and a memoized derived selector for the filtered view. co-15 (client-vs-server-state),
// co-19 (usememo-usecallback / memoized selector).
import type { Row } from "./types";
// The server cache: the canonical row list + a stale flag (TanStack Query-style, Example 31).
interface CacheEntry {
rows: Row[]; // the confirmed server data
isStale: boolean; // true => a mutation asked for a refetch
}
// In-memory query cache, keyed by the single query key this dashboard uses.
const cache = new Map<string, CacheEntry>(); // => one entry for "dashboard-rows"
// prime seeds the cache with the initial fetch result (called once after the first load).
export function prime(rows: Row[]): void {
// populate the cache fresh (not stale) after the initial fetch
cache.set("dashboard-rows", { rows, isStale: false });
}
// read returns the cached rows (the single source of server state).
export function read(): Row[] {
// co-15: components read from the cache, not straight from a fetch each time
return cache.get("dashboard-rows")?.rows ?? [];
}
// invalidate marks the cache stale (a mutation happened); the next observer would refetch.
export function invalidate(): void {
// co-15: a mutation invalidates; observers refetch (Example 33)
const entry = cache.get("dashboard-rows");
if (entry) entry.isStale = true;
}
// optimisticAdd shows the new row immediately and rolls back if the (simulated) mutation fails.
export function optimisticAdd(row: Row, shouldFail: boolean): { rolledBack: boolean; rows: Row[] } {
// co-15 (Example 34): snapshot, apply optimistically, roll back on failure
const entry = cache.get("dashboard-rows");
if (!entry) return { rolledBack: false, rows: [] };
const snapshot = entry.rows; // => the prior confirmed state, for rollback
entry.rows = [...snapshot, row]; // => optimistic: the new row appears immediately
if (shouldFail) {
entry.rows = snapshot; // => ROLLBACK: restore the prior state on failure
invalidate(); // => mark stale so a later refetch reconciles
return { rolledBack: true, rows: snapshot };
}
invalidate(); // => success: invalidate so the next observer refetches the confirmed server truth
return { rolledBack: false, rows: entry.rows };
}
// A memoized derived selector (co-19): the filtered view recomputes only when its inputs change.
let lastRows: Row[] | null = null; // => last input reference (cache key)
let lastQuery = ""; // => last query string (cache key)
let lastFiltered: Row[] = []; // => the cached derived result
// selectFiltered returns the rows matching the query, memoized on (rows reference, query).
export function selectFiltered(rows: Row[], query: string): Row[] {
// co-19 (Example 36): recompute only when the rows reference or the query changes
if (rows !== lastRows || query !== lastQuery) {
lastRows = rows; // => store the new input reference
lastQuery = query; // => store the new query
lastFiltered = rows.filter((r) => r.label.toLowerCase().includes(query.toLowerCase())); // => re-derive
}
return lastFiltered; // => the memoized (or freshly-computed) filtered view
}
// resetForTests clears the memo + cache so each test starts from a known state.
export function resetForTests(): void {
cache.clear();
lastRows = null;
lastQuery = "";
lastFiltered = [];
}Verify (the optimistic-rollback tests):
$ npx vitest run .../dashboard.test.ts --environment=jsdom -t "optimistic add rolls back"
Test Files 1 passed (1)
Tests 2 passed (2)Step 4: The ARIA widget + focus/live region + component tests
exercises co-23, co-24, co-25, co-36, co-37
dashboard.ts is the imperative shell: it builds an ARIA combobox (role="combobox",
aria-expanded, aria-controls, aria-activedescendant) over a role="listbox" of role="option"
rows, a role="status" live region announcing the result count, a controlled search input, a
validated add-row form (with aria-describedby + role="alert" error), and a "Load more" pagination
button. Arrow keys move the active option (keyboard-operable). The error path mirrors co-37's
boundary discipline (a caught failure degrades to a rolled-back + invalidated state, not a crash).
learning/capstone/code/dashboard.ts (complete file)
// Capstone component: an imperative shell that renders a searchable, paginated dashboard into a real
// DOM container, with an ARIA combobox (arrow-key operable), a live result-count region, a cached +
// optimistically-added row list with rollback, and pagination. co-23/co-24 (ARIA + focus), co-15
// (cache + optimistic), co-19 (memoized derived selector), co-30 (pagination), co-37 (error path).
import { fireEvent } from "@testing-library/dom"; // re-exported for the keyboard helpers below
import { fetchPage, renderToStream, measureCwv, rate } from "./server";
import { optimisticAdd, prime, selectFiltered, resetForTests } from "./store";
import type { CoreWebVitals, LoadState, Page, Row } from "./types";
export { renderToStream, measureCwv, rate }; // expose the server/SSR/CWV helpers to the tests
export type { CoreWebVitals, Page, Row };
// addRow resolves ok/fail; the component uses it to drive the optimistic add + rollback path.
export type AddRow = (label: string) => Promise<{ ok: boolean }>;
export interface DashboardOptions {
loadPage?: (cursor: number | null) => Promise<Page<Row>>; // defaults to the server fetchPage
addRow: AddRow; // the mutation (ok=true succeeds, ok=false triggers rollback)
}
// mountDashboard builds the whole feature into `root` and wires every handler.
export function mountDashboard(root: HTMLElement, opts: DashboardOptions): void {
resetForTests(); // => start each mount from a clean cache/memo
root.innerHTML = ""; // => a fresh container
const loadPage = opts.loadPage ?? fetchPage; // => the server fetch (co-30)
// Component state (co-15/co-20): the loaded rows, the query, pagination cursor, active option.
let loadedRows: Row[] = []; // => the confirmed rows (cache source of truth is primed from this)
let query = ""; // => the controlled search value
let nextCursor: number | null = null; // => the pagination cursor (null = no more pages)
let activeIndex = -1; // => the active option for arrow-key navigation (co-24)
// --- Build the DOM once (co-23/co-24 ARIA + co-26 form) ---
const searchLabel = document.createElement("label");
searchLabel.htmlFor = "dash-search";
searchLabel.textContent = "Search rows";
const search = document.createElement("input"); // => a combobox (co-23)
search.id = "dash-search";
search.type = "text";
search.setAttribute("role", "combobox");
search.setAttribute("aria-autocomplete", "list");
search.setAttribute("aria-expanded", "false");
search.setAttribute("aria-controls", "dash-listbox");
search.setAttribute("aria-activedescendant", "");
const count = document.createElement("p"); // => a live region (co-23, Example 47)
count.id = "dash-count";
count.setAttribute("role", "status");
count.setAttribute("aria-live", "polite");
const listbox = document.createElement("ul"); // => the options list (co-23)
listbox.id = "dash-listbox";
listbox.setAttribute("role", "listbox");
listbox.setAttribute("aria-label", "Rows");
// The add-row form (co-15 optimistic add, co-26 accessible form).
const form = document.createElement("form");
form.setAttribute("novalidate", "");
const newLabel = document.createElement("label");
newLabel.htmlFor = "dash-new";
newLabel.textContent = "New row";
const newInput = document.createElement("input");
newInput.id = "dash-new";
newInput.type = "text";
newInput.required = true;
const formError = document.createElement("p"); // => co-26 error, programmatically associated
formError.id = "dash-new-error";
formError.hidden = true;
formError.setAttribute("role", "alert");
newInput.setAttribute("aria-describedby", "dash-new-error");
const addButton = document.createElement("button");
addButton.type = "submit";
addButton.textContent = "Add row";
form.append(newLabel, newInput, formError, addButton);
const moreButton = document.createElement("button"); // => co-30 pagination
moreButton.type = "button";
moreButton.textContent = "Load more";
moreButton.disabled = true; // => disabled until a next cursor exists
root.append(searchLabel, search, count, listbox, form, moreButton);
// render rebuilds the option list + the live count from the current state. Passing { loading }
// shows the streaming fallback; the default (no arg) is the loaded re-render path.
function render(state?: LoadState): void {
if (state?.status === "loading") {
count.textContent = "Loading dashboard..."; // => the streaming fallback (co-04)
listbox.innerHTML = "";
return;
}
const filtered = selectFiltered(loadedRows, query); // => co-19 memoized derived view
count.textContent = `${filtered.length} result${filtered.length === 1 ? "" : "s"}`;
listbox.innerHTML = "";
filtered.forEach((row, i) => {
const li = document.createElement("li");
li.id = `dash-opt-${row.id}`;
li.setAttribute("role", "option");
li.textContent = row.label;
if (i === activeIndex) li.setAttribute("aria-selected", "true"); // => co-24 active option
listbox.append(li);
});
search.setAttribute("aria-expanded", String(filtered.length > 0)); // => combobox expanded?
search.setAttribute(
"aria-activedescendant",
activeIndex >= 0 && filtered[activeIndex] ? `dash-opt-${filtered[activeIndex].id}` : "",
);
}
// --- Event wiring ---
search.addEventListener("input", () => {
query = search.value; // => controlled input (co-25)
activeIndex = -1; // => reset the active option when the query changes
render();
});
// Arrow-key navigation (co-24): ArrowDown/Up move the active option within the listbox.
search.addEventListener("keydown", (event) => {
const filtered = selectFiltered(loadedRows, query);
if (filtered.length === 0) return;
if (event.key === "ArrowDown") {
event.preventDefault(); // => stop the page from scrolling
activeIndex = (activeIndex + 1) % filtered.length; // => move forward, wrap
render();
} else if (event.key === "ArrowUp") {
event.preventDefault();
activeIndex = (activeIndex - 1 + filtered.length) % filtered.length; // => move back, wrap
render();
}
});
// Add-row submit: optimistic add with rollback on failure (co-15, Example 34).
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (newInput.value.trim().length === 0) {
formError.hidden = false; // => co-26: required-field error, associated via aria-describedby
formError.textContent = "Enter a row label";
return;
}
formError.hidden = true;
const label = newInput.value.trim();
newInput.value = "";
const result = await opts.addRow(label); // => the mutation (ok=true/false)
const row: Row = { id: `local-${Date.now()}`, label };
const r = optimisticAdd(row, !result.ok); // => optimistic; rolls back if !result.ok
loadedRows = r.rows; // => adopt the confirmed (or rolled-back) row list
prime(loadedRows); // => keep the cache in sync with the confirmed rows
render();
});
// Load-more pagination (co-30): fetch the next page by cursor and append.
moreButton.addEventListener("click", async () => {
if (nextCursor === null) return;
const page = await loadPage(nextCursor); // => fetch the next page
loadedRows = [...loadedRows, ...page.items]; // => append the new page
nextCursor = page.nextCursor; // => advance (or null out) the cursor
moreButton.disabled = nextCursor === null; // => disable when there is no next page
prime(loadedRows); // => resync the cache
render();
});
// --- Initial load (co-04 streaming fallback first, then the resolved rows) ---
render({ status: "loading" }); // => the SSR streaming fallback paints first
loadPage(null).then((page) => {
loadedRows = page.items; // => the first page
nextCursor = page.nextCursor; // => the cursor for "Load more"
moreButton.disabled = nextCursor === null;
prime(loadedRows); // => populate the cache
render(); // => the resolved rows replace the fallback
});
}
// re-export fireEvent so the e2e-style test file can drive the keyboard through one import.
export { fireEvent };learning/capstone/code/dashboard.test.ts (complete file)
// Capstone tests: Vitest + @testing-library/dom, driven against jsdom. Covers every capstone
// acceptance criterion: streaming SSR + hydration, measured Core Web Vitals improvement, cached
// fetch + optimistic rollback, the ARIA combobox + keyboard operability, the memoized search, and
// cursor pagination. co-04/co-08/co-15/co-19/co-23/co-24/co-30/co-36/co-37.
import { describe, it, expect } from "vitest";
import { screen, within } from "@testing-library/dom";
import { mountDashboard, renderToStream, measureCwv, rate, fireEvent } from "./dashboard";
import type { Row } from "./types";
function freshRoot(): HTMLElement {
document.body.innerHTML = ""; // => a clean document per test
const root = document.createElement("div");
document.body.append(root);
return root;
}
// a Load-more-capable fetchPage mirroring the server's 3-per-page pagination over 6 seed rows
function makeLoadPage(): (cursor: number | null) => Promise<{ items: Row[]; nextCursor: number | null }> {
const seed: Row[] = [
{ id: "1", label: "buy bread" },
{ id: "2", label: "buy milk" },
{ id: "3", label: "sell car" },
{ id: "4", label: "buy eggs" },
{ id: "5", label: "walk dog" },
{ id: "6", label: "sell bike" },
];
return (cursor) => {
const start = cursor ?? 0; // => cursor=null starts at index 0
const items = seed.slice(start, start + 3);
const nextCursor = start + 3 < seed.length ? start + 3 : null;
return Promise.resolve({ items, nextCursor });
};
}
describe("capstone: streaming SSR + fallback (co-04)", () => {
it("streams the shell + fallback before the resolved rows", () => {
const loading = renderToStream({ status: "loading" }, ""); // => the pending state
expect(loading[0]).toBe("<!-- shell -->"); // => the static shell first
expect(loading[1]).toContain("Loading dashboard"); // => the Suspense fallback
const loaded = renderToStream({ status: "loaded", rows: [{ id: "1", label: "buy bread" }] }, "buy");
expect(loaded[1]).toContain("<li>buy bread</li>"); // => resolved content streams in later
});
});
describe("capstone: measured Core Web Vitals improve (co-08/co-11)", () => {
it("rates the slow, unreserved baseline poor and the improved version good", () => {
const baseline = measureCwv(3200, false); // => slow paint, no reserved boxes
const improved = measureCwv(1500, true); // => faster paint, reserved boxes
expect(rate(baseline)).toBe("poor"); // => baseline fails the good thresholds
expect(rate(improved)).toBe("good"); // => the improved version meets them
expect(improved.lcp).toBeLessThan(baseline.lcp); // => LCP improved measurably
expect(improved.cls).toBeLessThan(baseline.cls); // => CLS improved (reserved space)
});
});
describe("capstone: cached fetch + pagination (co-15/co-30)", () => {
it("loads the first page and reveals Load more when a next cursor exists", async () => {
const root = freshRoot();
mountDashboard(root, { loadPage: makeLoadPage(), addRow: async () => ({ ok: true }) });
const list = await screen.findByRole("listbox", { name: "Rows" }); // => first page renders
expect(within(list).getAllByRole("option")).toHaveLength(3); // => 3 per page
expect((screen.getByRole("button", { name: "Load more" }) as HTMLButtonElement).disabled).toBe(false);
});
it("appends the next page on Load more and disables it at the end", async () => {
const root = freshRoot();
mountDashboard(root, { loadPage: makeLoadPage(), addRow: async () => ({ ok: true }) });
await screen.findByRole("listbox", { name: "Rows" });
fireEvent.click(screen.getByRole("button", { name: "Load more" })); // => page 2
let list = await screen.findByRole("listbox", { name: "Rows" });
await expect.poll(() => within(list).getAllByRole("option").length).toBe(6); // => 3 + 3
fireEvent.click(screen.getByRole("button", { name: "Load more" })); // => no next cursor now
list = screen.getByRole("listbox", { name: "Rows" });
await expect
.poll(() => (screen.getByRole("button", { name: "Load more" }) as HTMLButtonElement).disabled)
.toBe(true);
});
});
describe("capstone: optimistic add rolls back on failure (co-15)", () => {
it("keeps the new row when the mutation succeeds", async () => {
const root = freshRoot();
mountDashboard(root, { loadPage: makeLoadPage(), addRow: async () => ({ ok: true }) });
await screen.findByRole("listbox", { name: "Rows" });
fireEvent.input(screen.getByLabelText("New row"), { target: { value: "fly kite" } });
fireEvent.click(screen.getByRole("button", { name: "Add row" }));
await expect.poll(() => screen.queryByText("fly kite")).toBeTruthy(); // => the optimistic row is kept on success
});
it("rolls back the new row when the mutation fails", async () => {
const root = freshRoot();
mountDashboard(root, { loadPage: makeLoadPage(), addRow: async () => ({ ok: false }) });
await screen.findByRole("listbox", { name: "Rows" });
fireEvent.input(screen.getByLabelText("New row"), { target: { value: "fly kite" } });
fireEvent.click(screen.getByRole("button", { name: "Add row" }));
await expect.poll(() => screen.queryByText("fly kite")).toBeNull(); // => rolled back: the row never sticks
});
});
describe("capstone: ARIA combobox + keyboard operability (co-23/co-24)", () => {
it("exposes a combobox controlling a listbox of options", async () => {
const root = freshRoot();
mountDashboard(root, { loadPage: makeLoadPage(), addRow: async () => ({ ok: true }) });
const combobox = await screen.findByRole("combobox"); // => the search input's role
expect(combobox.getAttribute("aria-controls")).toBe("dash-listbox"); // => points at the listbox
const list = screen.getByRole("listbox", { name: "Rows" });
expect(within(list).getAllByRole("option").length).toBeGreaterThan(0); // => options present
});
it("moves the active option with ArrowDown and reflects it via aria-activedescendant", async () => {
const root = freshRoot();
mountDashboard(root, { loadPage: makeLoadPage(), addRow: async () => ({ ok: true }) });
const combobox = (await screen.findByRole("combobox")) as HTMLInputElement;
expect(combobox.getAttribute("aria-activedescendant")).toBe(""); // => no active option yet
combobox.focus();
fireEvent.keyDown(combobox, { key: "ArrowDown" }); // => activate the first option
expect(combobox.getAttribute("aria-activedescendant")).toBe("dash-opt-1"); // => active = first
fireEvent.keyDown(combobox, { key: "ArrowDown" }); // => move to the second
expect(combobox.getAttribute("aria-activedescendant")).toBe("dash-opt-2");
const first = document.getElementById("dash-opt-1");
expect(first?.getAttribute("aria-selected")).toBeNull(); // => first no longer selected
});
});
describe("capstone: memoized search filters the view (co-19/co-25)", () => {
it("narrows the options to rows matching the controlled search input", async () => {
const root = freshRoot();
mountDashboard(root, { loadPage: makeLoadPage(), addRow: async () => ({ ok: true }) });
await screen.findByRole("listbox", { name: "Rows" });
fireEvent.input(screen.getByLabelText("Search rows"), { target: { value: "buy" } });
const list = screen.getByRole("listbox", { name: "Rows" });
await expect.poll(() => within(list).getAllByRole("option").length).toBe(2); // => buy bread, buy milk
expect(screen.getByText("2 results")).toBeTruthy(); // => the live region announces the count
});
});Verify (the ARIA + keyboard tests):
$ npx vitest run .../dashboard.test.ts --environment=jsdom -t "ARIA combobox|memoized search"
Test Files 1 passed (1)
Tests 2 passed (2)Run it end to end
Running the full suite from the repository root exercises every file this capstone ships in one pass.
Run: npx vitest run apps/ayokoding-www/content/en/learn/courses/advanced-frontend/learning/capstone/code/dashboard.test.ts --environment=jsdom
Output (genuinely captured):
RUN v4.1.0
Test Files 1 passed (1)
Tests 9 passed (9)tsc --noEmit -p tsconfig.json (from inside learning/capstone/code/, using the colocated
tsconfig.json) reports zero errors.
Acceptance criteria
npx vitest run .../dashboard.test.ts --environment=jsdomreports9 passed, covering: streaming SSR (shell + fallback before content), measured Core Web Vitals improvement (poor baseline -> good improved, with LCP and CLS both lower), cached first-page load + cursor pagination (Load more appends and disables at the end), the optimistic add's both paths (kept on success, rolled back on failure), the ARIA combobox contract (role + aria-controls + options), keyboard operability (ArrowDown moves the active option via aria-activedescendant), and the memoized search (narrows to matching rows + announces the count).- The widget is keyboard-operable: ArrowDown/ArrowUp move the active option with no mouse interaction.
- Measured Core Web Vitals improve over baseline (LCP and CLS both strictly lower; the rating flips poor -> good).
- The optimistic update rolls back on a simulated failure (the row never sticks).
tsc --noEmit -p tsconfig.json(strict mode) is clean acrosstypes.ts,server.ts,store.ts,dashboard.ts, anddashboard.test.ts.
Done bar
This capstone is runnable end to end: a reader who copies the five files above (types.ts,
server.ts, store.ts, dashboard.ts, dashboard.test.ts) plus the tsconfig.json into a
learning/capstone/code/-shaped tree and runs npx vitest run dashboard.test.ts --environment=jsdom
there reaches the identical 9 passed result shown above, verified against a real Vitest 4.1.0 +
@testing-library/dom run (not merely described) in this sandbox on 2026-07-29. Every mechanism
combined here -- streaming SSR (co-04), measured Core Web Vitals (co-08), cached fetching and
optimistic rollback (co-15), memoized derived state (co-19), an ARIA combobox with keyboard +
live-region support (co-23, co-24), controlled inputs (co-25), cursor pagination (co-30), and
behaviour tests (co-36) -- traces to the same react.dev / web.dev / W3C / MDN / TypeScript Handbook
sources already cited in this course's Accuracy notes; no new fact was needed to write this page.
← Previous: Advanced Examples · Next: Drilling →
Last updated July 28, 2026