Capstone: Testing a Real Feature

Course 1 · Ch 9
Capstone: Testing a Real Feature
A complete test suite for a LoginForm, using every tool this course has built up

One feature, one complete test suite, every chapter's tool making an appearance: rendering, real user interaction, async mocking, conditional rendering, and an accessibility check.

The Feature Under Test

LoginForm: an email field and a password field, both required. Submitting calls POST /api/login. While the request is pending, the button reads "Signing in...". A failed login (401) shows an error message. A successful login shows a welcome message.

Setting Up MSW for the Capstone

const server = setupServer( http.post("/api/login", () => HttpResponse.json({ name: "Ada" })) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); // Ch.6's leaked-handler gotcha, avoided afterAll(() => server.close());

The Full Test Suite

describe("LoginForm", () => { it("renders labeled email and password fields", () => { // Ch.3 render(<LoginForm />); expect(screen.getByLabelText("Email")).toBeInTheDocument(); expect(screen.getByLabelText("Password")).toBeInTheDocument(); }); it("shows a validation message when submitted empty", async () => { // Ch.4 render(<LoginForm />); await userEvent.click(screen.getByRole("button", { name: "Log In" })); expect(await screen.findByText("Email and password are required")).toBeInTheDocument(); }); it("shows a pending state, then a welcome message, on success", async () => { // Ch.5 + Ch.6 render(<LoginForm />); await userEvent.type(screen.getByLabelText("Email"), "ada@example.com"); await userEvent.type(screen.getByLabelText("Password"), "hunter2"); await userEvent.click(screen.getByRole("button", { name: "Log In" })); expect(screen.getByRole("button", { name: "Signing in..." })).toBeInTheDocument(); expect(await screen.findByText("Welcome, Ada!")).toBeInTheDocument(); }); it("shows an error message on a failed login", async () => { // Ch.6 server.use(http.post("/api/login", () => new HttpResponse(null, { status: 401 }))); render(<LoginForm />); await userEvent.type(screen.getByLabelText("Email"), "ada@example.com"); await userEvent.type(screen.getByLabelText("Password"), "wrong"); await userEvent.click(screen.getByRole("button", { name: "Log In" })); expect(await screen.findByText("Invalid email or password")).toBeInTheDocument(); }); it("has no accessibility violations", async () => { // Ch.8 const { container } = render(<LoginForm />); expect(await axe(container)).toHaveNoViolations(); }); });

What This Capstone Demonstrates

TestChapter's Tool Applied
Labeled fields renderCh.3 — query by role/label
Empty-submission validationCh.4 — userEvent + conditional rendering
Pending → success flowCh.5 + Ch.6 — findBy/async + MSW
Failed-login errorCh.6 — server.use() error override
Accessibility checkCh.8 — jest-axe

Chapter 2's runner mechanics (describe/it/expect) sit underneath every single one of these, and Chapter 7's point stands too — this exact same suite, translated to @testing-library/vue or @testing-library/svelte, would look nearly identical.

A Good Test Suite Reads Like a Specification
A new developer could read this suite's five test names — with no access to LoginForm's actual implementation — and know exactly what it's supposed to do: labeled fields, required-field validation, a pending state, a success path, a failure path, and an accessibility baseline. That's the real, practical payoff of everything this course has built toward: tests that document behavior, not just verify it.
Honest Scope: This Isn't an E2E Test
This suite tests LoginForm in relative isolation — no real routing, no real backend, no verification that a successful login actually navigates to a dashboard page. That's Chapter 1's E2E layer, deliberately out of this course's scope from the very first chapter. A real production app would layer Playwright/Cypress tests on top of exactly this kind of component-level suite, not instead of it.

Coding Challenges

Challenge 1

Add a sixth test to this suite: confirm that the "Log In" button is disabled while the login request is pending, to prevent a double-submit, using the same MSW setup as the success-path test.

📄 View solution
Challenge 2

This suite's empty-submission test doesn't use MSW at all. Explain why that's correct — what makes that specific behavior different from the login-success and login-failure tests that do need it.

📄 View solution
Challenge 3

Write a short reflection: pick one habit from this course (e.g. query priority, queryBy for absence, resetting MSW handlers, coverage skepticism) that you think you'd have gotten wrong without this course, and explain what would have gone wrong in practice.

📄 View solution

Chapter 9 Quick Reference

  • A complete feature test suite combines: rendering + query priority (Ch.3), userEvent + conditional rendering (Ch.4), async findBy (Ch.5), MSW + error simulation (Ch.6), and jest-axe (Ch.8)
  • Ch.2's describe/it/expect mechanics underpin every test regardless of what's being tested
  • Ch.7's cross-framework convergence means this same suite structure transfers to Vue/Svelte with minimal changes
  • A well-named test suite documents behavior — readable as a spec, not just a pass/fail gate
  • This suite is intentionally not an E2E test — Chapter 1 scoped that layer out from day one

✅ Frontend Testing Complete — 9 / 9 chapters

From the testing pyramid through Jest/Vitest mechanics, the Testing Library philosophy, real component and hook testing, mocking, cross-framework testing, coverage honesty, and accessibility — to a complete, realistic feature test suite. Every habit this course built is meant to transfer directly to real code, in React or otherwise.