Lesson 10

E2E Testing & Playwright

What is E2E · Tools landscape · Playwright setup · Selectors · First test

→ next slide  |  ESC overview

Lesson Plan
Lesson 10: E2E Testing & Playwright

1. The testing pyramid — where E2E fits, trade-offs

2. Tools landscape — Playwright vs Cypress vs Selenium

3. Playwright setup — install, config, project structure

4. Selectors & auto-wait — writing stable tests

5. First test together — assertions, UI mode, codegen

Homework: Fork playwright-kriso. Start exploring Kriso.ee and write Task 1 (flat tests) — teacher walks through it next lesson, Task 2 (POM + CI) the lesson after.
Testing Levels

Where Does E2E Fit?

Unit Tests Single function or class. No browser, no network, no DB.

Does calculatePrice() return the right number?

Fast · Cheap · Many
Integration / API Tests Multiple components together. Tests real DB or API.

Does POST /register save a user to the database?

Medium speed · Some
E2E / UI Tests Entire app in a real browser. Real user perspective.

Can a user search, add to cart, and pay?

Slow · Expensive · Few
E2E — few, slow, critical paths only
Integration / API — some
Unit — many, fast, cheap
E2E Testing

What Does E2E Actually Test?

E2E tests drive a real browser through a real user journey — from the first click to the final confirmation.

What E2E catches that unit tests miss:
  • Frontend ↔ backend integration failures
  • Routing and navigation bugs
  • JavaScript errors in the browser
  • UI state issues (forms, modals, loaders)
  • Cross-service data flow problems
  • Cookie, session, and auth token issues
E2E limitations — know these:
  • Slow (10–60 seconds per test)
  • Flaky — timing, animations, network
  • Expensive to maintain (UI changes break them)
  • Hard to debug failures
  • Can't cover every edge case efficiently
E2E tests are the most realistic tests you can write — and the most expensive.
Cover the 3–5 flows that, if broken, would hurt users the most.
Tools

Playwright vs Cypress vs Selenium

Feature Playwright Cypress Selenium
Browser support Chromium, Firefox, WebKit Chrome, Firefox, Edge All + Safari
Language support JS/TS, Python, Java, C# JS/TS only JS, Python, Java, C#, Ruby
Auto-wait Built in — no sleeps needed Built in Manual — you manage waits
Parallel execution Native, cross-browser Paid tier for cross-browser Via Grid setup
Setup complexity One command One command Drivers, env config
Debugging Trace viewer, UI mode, video Time-travel in dashboard Screenshots only
Industry trend (2024–25) Growing fast — new standard Established, slowing Legacy — still common in Java
Playwright is the current industry favourite for new projects. Selenium remains dominant in Java enterprise shops.
Setup

Getting Started with Playwright

# Install Playwright — one command does everything
npm init playwright@latest

# Choose:
# ✔ TypeScript or JavaScript?   → TypeScript (recommended)
# ✔ Where to put tests?         → tests/
# ✔ Add GitHub Actions?         → Yes
# ✔ Install browsers?           → Yes

# Run all tests (headless)
npx playwright test

# Run with interactive UI mode (recommended for development)
npx playwright test --ui

# Run in visible browser window
npx playwright test --headed

# Record test actions in a browser → generates test code
npx playwright codegen https://www.kriso.ee
Project Structure

What Gets Created

my-project/
  tests/
    example.spec.ts ← your tests go here
  pages/ ← POM classes (you create)
  .github/workflows/
    playwright.yml ← CI pipeline
  playwright.config.ts ← all settings
  package.json
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,     // 30s per test
  retries: 1,          // retry flaky tests once
  reporter: 'html',    // HTML report at end

  use: {
    baseURL: 'https://www.kriso.ee',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'retain-on-failure',
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
  ],
});
Playwright UI Mode

Your Best Friend: --ui

Playwright UI mode gives you a visual test runner — see exactly what the browser does at each step.

Time-travel debugging Click any step and see a screenshot of exactly what the browser looked like at that moment.
Watch mode Tests re-run automatically when you save a file. Instant feedback as you write tests.
Trace viewer After a failure, open the trace file and step through the test frame by frame.
Filter & search Run specific tests, filter by status (passed/failed), search by name.
Locator picker Hover over any element in the snapshot to get the recommended Playwright selector.
Network log See all network requests made during each test step.
Use --ui while writing tests. Switch to headless npx playwright test for CI.
Codegen

Recording Tests with Codegen

npx playwright codegen https://www.kriso.ee

What happens:

  1. A browser window opens
  2. A Playwright Inspector panel opens alongside it
  3. Every action you take (click, type, navigate) is recorded as test code
  4. Copy the generated code into your test file
  5. Edit and clean it up — codegen is a starting point, not the final test
Codegen generates:
await page.goto('https://www.kriso.ee/');
await page.getByRole('searchbox').click();
await page.getByRole('searchbox').fill('Python');
await page.getByRole('button', { name: 'Otsi' }).click();
await expect(page.getByRole('heading'))
  .toContainText('Python');
Clean up after codegen: Remove redundant .click() before .fill().
Add meaningful assertions.
Replace fragile selectors with semantic ones.
Selectors

Choosing the Right Selector

The selector you choose determines how fragile your test is. Prefer semantic selectors that survive styling changes.

Fragile — avoid these ❌
// CSS class — breaks when design changes
page.locator('.btn-primary-lg-v2')

// XPath — brittle, hard to read
page.locator('//div[3]/ul/li[1]/a')

// Position-based — meaningless
page.locator('nth=0')
Robust — use these ✅
// By ARIA role + name (best)
page.getByRole('button', { name: 'Sign In' })

// By visible text
page.getByText('Forgot password?')

// By form placeholder
page.getByPlaceholder('Email address')

// By test ID (teams control this)
page.getByTestId('submit-btn')

// By label text
page.getByLabel('Password')
Priority: getByRolegetByLabelgetByPlaceholdergetByTextgetByTestId
First Test

Your First Playwright Test

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

// test.describe groups related tests
test.describe('Playwright Demo Site', () => {

  test('page has correct title', async ({ page }) => {
    await page.goto('https://playwright.dev');
    await expect(page).toHaveTitle(/Playwright/);
  });

  test('get started link navigates to docs', async ({ page }) => {
    await page.goto('https://playwright.dev');

    // Find the "Get started" link by its visible text
    await page.getByRole('link', { name: 'Get started' }).click();

    // The URL should change to /docs/intro
    await expect(page).toHaveURL(/.*intro/);
  });

  test('search works', async ({ page }) => {
    await page.goto('https://playwright.dev');

    await page.getByRole('button', { name: 'Search' }).click();
    await page.getByRole('searchbox').fill('assertions');

    // Results should appear
    await expect(page.getByRole('option').first()).toBeVisible();
  });

});
Assertions

Playwright Assertion Cheatsheet

// ── PAGE ───────────────────────────────
// URL exact or regex
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveURL(/checkout/);

// Page title
await expect(page).toHaveTitle(/Kriso/i);

// ── ELEMENT VISIBILITY ─────────────────
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeAttached(); // in DOM

// ── ELEMENT STATE ──────────────────────
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toBeFocused();
// ── CONTENT ────────────────────────────
// Exact text
await expect(locator).toHaveText('Hello');
// Contains text
await expect(locator).toContainText('Hell');
// Attribute value
await expect(locator).toHaveAttribute('href', '/home');
// CSS value
await expect(locator).toHaveCSS('color', 'red');

// ── COUNTS ─────────────────────────────
await expect(locator).toHaveCount(5);

// ── NEGATION ───────────────────────────
// Add .not before any assertion
await expect(locator).not.toBeVisible();
await expect(page).not.toHaveURL('/error');

// ── SOFT ASSERTIONS ────────────────────
// Don't stop on failure — collect all errors
await expect.soft(locator).toHaveText('A');
await expect.soft(locator2).toHaveText('B');

Homework — Kriso.ee Test Automation

The next 3 lessons all build toward the same assignment.
Fork the repo and start exploring now — we'll walk through the solutions together week by week.

📁 github.com/tanjaq/playwright-kriso

Task 1 — UI Tests   Write all 3 test suites in tests/kriso.spec.ts
Next week: I will make one example in class.
Task 2 — Page Object Model   Refactor to tests/kriso-pom.spec.ts using page classes.
Add CI pipeline.
Push — confirm it goes green.
A week later: I will refactor one example in class.

Start now:
Fork the repo, install dependencies, explore Kriso manually.
Run npx playwright codegen https://www.kriso.ee