Skip to content

Latest commit

 

History

History
253 lines (178 loc) · 8.75 KB

File metadata and controls

253 lines (178 loc) · 8.75 KB

Section 6 (Stretch): End-to-End Testing with Playwright

What you'll build: Browser-level tests that launch a real Chromium instance, navigate the PawFinder UI, and assert that complete user journeys work correctly.

Time: 60 minutes (optional — for groups who finish Section 5 early)
Prerequisites: Section 4 complete (both the web app AND the animals API must be running)


The big idea

Unit and component tests run fast and in isolation — but they can't catch problems that only appear when everything runs together: the API, the routing, the browser, and the full component tree.

End-to-end (E2e) tests fill that gap. They run against the real running app in a real browser.

The trade-off:

Unit / Component E2e
Speed Very fast (milliseconds) Slow (seconds per test)
Reliability High (isolated) Lower (real network, real browser)
What it catches Logic bugs, rendering bugs Integration bugs, browser quirks
When to write Always For critical user journeys

The rule of thumb: few, focused E2e tests for the journeys that absolutely must work — searching, filtering, viewing a pet, submitting an enquiry. Don't test every variant; that's what component tests are for.


Step 0 — Install browsers and start the app

Install Playwright's browser binaries (one-time setup):

npx playwright install chromium

Make sure both servers are running:

# Terminal 1:
cd services/animals-api && npm start

# Terminal 2:
npm run dev

Then run the tests in UI mode to see what's happening:

npm run test:e2e:ui

This opens Playwright's visual test runner. You'll see it's empty — let's fix that.


Step 1 — Your first test

Create e2e/home.spec.ts:

import { test, expect } from '@playwright/test'

test('home page shows the PawFinder heading', async ({ page }) => {
  await page.goto('/')
  await expect(page.getByRole('heading', { name: 'Find Your Perfect Pet' })).toBeVisible()
})

test('home page loads animals from the API', async ({ page }) => {
  await page.goto('/')
  // Wait for at least one animal card to appear
  await expect(page.locator('article').first()).toBeVisible()
  // The API has 28 available animals
  await expect(page.getByText('28 animals found')).toBeVisible()
})

Run this test — you should see a real Chromium window flash open, load your app, and the tests pass.

page.goto('/') — Playwright's baseURL is configured to http://localhost:5173, so / resolves to your dev server. The playwright.config.ts also starts the dev server automatically if it's not already running.

page.locator vs page.getByRole — Both query the DOM. getByRole is preferred because it tests accessibility-compatible markup, just like Testing Library. locator('article') is a CSS selector — useful when there's no accessible role to query by.


Step 2 — Test the search feature

test('searching for an animal name filters the list', async ({ page }) => {
  await page.goto('/')
  // Wait for animals to load first
  await expect(page.locator('article').first()).toBeVisible()

  // Type in the search box
  await page.getByRole('searchbox').fill('golden')

  // Only Max should be visible (Golden Retriever)
  await expect(page.getByRole('heading', { name: 'Max' })).toBeVisible()
  await expect(page.getByText('1 animal found')).toBeVisible()
})

test('clearing the search restores all animals', async ({ page }) => {
  await page.goto('/')
  await expect(page.locator('article').first()).toBeVisible()

  await page.getByRole('searchbox').fill('golden')
  await expect(page.getByText('1 animal found')).toBeVisible()

  await page.getByRole('searchbox').fill('')
  await expect(page.getByText('28 animals found')).toBeVisible()
})

fill vs typepage.fill() sets the input value directly (fast). page.type() simulates keystrokes one at a time (slow but more realistic for testing autocomplete behaviour). Use fill unless you specifically need character-by-character simulation.


Step 3 — Test the species filter

test('clicking Cats filter shows only cats', async ({ page }) => {
  await page.goto('/')
  await expect(page.locator('article').first()).toBeVisible()

  await page.getByRole('button', { name: 'Cats' }).click()

  // Wait for the filter to apply
  await expect(page.getByText(/animals found/)).toBeVisible()

  // All visible cards should be cats — check the first one
  // Cats have breeds like "Domestic Shorthair", "Siamese", "Maine Coon" etc.
  // The simplest check: no dog breeds should be visible
  await expect(page.getByText('Golden Retriever')).not.toBeVisible()
})

test('filter button shows as active when selected', async ({ page }) => {
  await page.goto('/')
  await page.getByRole('button', { name: 'Cats' }).click()

  const catsButton = page.getByRole('button', { name: 'Cats' })
  // The active button has aria-pressed="true"
  await expect(catsButton).toHaveAttribute('aria-pressed', 'true')
})

Step 4 — Test the animal detail page

test('clicking an animal card navigates to the detail page', async ({ page }) => {
  await page.goto('/')
  await expect(page.locator('article').first()).toBeVisible()

  // Click Max's card
  await page.getByRole('heading', { name: 'Max' }).click()

  // We should be on the detail page
  await expect(page).toHaveURL(/\/animals\/max-001/)
  await expect(page.getByRole('heading', { name: 'Max' })).toBeVisible()
})

test('animal detail page shows the story and rehoming advice', async ({ page }) => {
  await page.goto('/animals/max-001')

  await expect(page.getByText('Their Story')).toBeVisible()
  await expect(page.getByText('Rehoming Advice')).toBeVisible()
  // Max's story contains this phrase
  await expect(page.getByText(/relocated abroad/)).toBeVisible()
})

test('back link returns to the listing', async ({ page }) => {
  await page.goto('/animals/max-001')
  await page.getByRole('link', { name: 'Back to all animals' }).click()
  await expect(page).toHaveURL('/')
  await expect(page.getByText('Find Your Perfect Pet')).toBeVisible()
})

Step 5 — Bonus: Test the enquiry form

If you built the enquiry form in Section 4's "Going further":

test('submitting an enquiry shows a success message', async ({ page }) => {
  await page.goto('/animals/max-001')

  await page.getByLabel('Your name').fill('Test User')
  await page.getByLabel('Email address').fill('test@example.com')
  await page.getByLabel('Message').fill('I would love to adopt Max!')

  await page.getByRole('button', { name: 'Submit enquiry' }).click()

  await expect(page.getByText(/enquiry submitted/i)).toBeVisible()
})

Running tests and viewing results

Run all E2e tests headlessly (faster, good for CI):

npm run test:e2e

View the HTML report after a run:

npx playwright show-report

Run with a visible browser (great for debugging):

npx playwright test --headed

Record a new test by interacting with the browser:

npx playwright codegen http://localhost:5173

Check your work

All tests should pass with:

npm run test:e2e

Try making a test fail deliberately — comment out the fill('') in the "clearing the search" test. The 28 animals found assertion will fail and Playwright will show you exactly which line failed and what the page looked like at that moment.


Going further

  1. Visual regression — Playwright can take screenshots and compare them to a baseline. Add await expect(page).toHaveScreenshot('home-page.png') and see what happens on the second run
  2. API mocking in E2e — Playwright can intercept network requests with page.route(). Try mocking the API to return a specific set of animals so your E2e tests don't depend on the dev database
  3. Test multiple viewports — add a mobile viewport test: page.setViewportSize({ width: 375, height: 667 }). Does the filter bar still work?
  4. Trace viewer — run tests with --trace on and then open npx playwright show-trace to see a timeline of every action, network request, and DOM snapshot

The day in review

You've built:

  • A component library following Atomic Design (Badge, Avatar, PetCard, PetList, SearchPanel)
  • A Storybook that documents every atom and molecule
  • A real-data app with loading states, error handling, and routing
  • Unit tests, component tests, and end-to-end tests

What comes next (future hackday): Adding a local LLM to answer questions about animals and rehoming. The story, healthNotes, and rehomingAdvice fields in the API data were designed with this in mind — they contain rich enough context for an LLM to answer questions like "which dog would suit a flat?" or "tell me about Max's history."

Well done — go adopt a rescue animal.