SoftwareTestPilot
Module 01 · Lesson 3beginner 11 min read Playwright

Your First Playwright Test — Line by Line

Write a real end-to-end test in TypeScript, understand every line, and learn the debug workflow professional QA teams use every day.

Quick answer

A Playwright test is an async function inside test('name', async ({ page }) => {...}). Inside it you navigate, interact with locators, and assert with web-first expect(). Here's the smallest real test:

tests/first.spec.ts
ts
import { test, expect } from '@playwright/test';

test('Playwright.dev homepage links to docs', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await page.getByRole('link', { name: 'Get started' }).click();
  await expect(page).toHaveURL(/docs\/intro/);
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

Prerequisites

  • Playwright installed — see Lesson 2.
  • A code editor (VS Code recommended).
  • Basic idea of JavaScript syntax — variables and functions. That's it.

1. Anatomy of a Playwright test

tests/anatomy.spec.ts
ts
import { test, expect } from '@playwright/test';   // 1. import fixtures

test.describe('Sign up flow', () => {                // 2. group
  test('shows success banner', async ({ page }) => { // 3. one test
    await page.goto('/signup');                      // 4. arrange
    await page.getByLabel('Email').fill('a@b.co');   // 5. act
    await page.getByRole('button', { name: 'Sign up' }).click();
    await expect(page.getByRole('status')).toHaveText('Welcome!'); // 6. assert
  });
});
  • test.describe — groups related tests in the report.
  • page — a fresh browser tab, provided as a fixture. New tab per test, so tests are isolated.
  • await before every Playwright call — non-negotiable.
  • getByRole / getByLabel — user-facing locators that mirror how screen readers see the page.
  • expect(...).toBe... — web-first assertion, auto-retries for up to 5 s.

2. Write your first test from scratch

  1. 1

    Create the file

    Inside the Playwright project from Lesson 2, create tests/todo.spec.ts.

  2. 2

    Paste this test

    tests/todo.spec.ts
    ts
    import { test, expect } from '@playwright/test';
    
    test('adds a todo to the demo app', async ({ page }) => {
      await page.goto('https://demo.playwright.dev/todomvc');
    
      const input = page.getByPlaceholder('What needs to be done?');
      await input.fill('Write my first Playwright test');
      await input.press('Enter');
    
      const todo = page.getByTestId('todo-title');
      await expect(todo).toHaveText('Write my first Playwright test');
      await expect(page.getByTestId('todo-count')).toHaveText('1 item left');
    });
  3. 3

    Run it in UI Mode

    npx playwright test tests/todo.spec.ts --ui
  4. 4

    Watch it turn green

    Use the time-travel slider on the left to scrub through every step and see the DOM at that moment. This is what makes Playwright dramatically faster to debug than Selenium.

3. Ways to run your test

npx playwright test                              # all specs, all browsers
npx playwright test tests/todo.spec.ts           # one file
npx playwright test -g "adds a todo"             # match by test name
npx playwright test --project=chromium           # one browser
npx playwright test --headed                     # see the browser
npx playwright test --ui                         # UI Mode (recommended)
npx playwright test --debug                      # Playwright Inspector

4. Web-first assertions (kill the sleeps)

Web-first assertions poll the DOM until they pass or hit their timeout. They replace every waitForTimeoutyou would otherwise write.

// ❌ Flaky — sleep, then guess
await page.waitForTimeout(3000);
expect(await page.locator('.badge').textContent()).toBe('1');

// ✅ Auto-retrying, deterministic
await expect(page.locator('.badge')).toHaveText('1');

Most-used assertions

  • toBeVisible() / toBeHidden()
  • toHaveText(str | regex)
  • toHaveValue(str)
  • toHaveURL(regex) / toHaveTitle()
  • toHaveCount(n) — count of matching locators
  • toBeEnabled() / toBeChecked()

5. The debug workflow real teams use

  1. 1

    Reproduce in UI Mode

    Open npx playwright test --ui, run the failing test, use the time-travel slider to find the first step that looks wrong.

  2. 2

    Pin the browser open with page.pause()

    await page.getByLabel('Email').fill('a@b.co');
    await page.pause();                     // Inspector opens here
    await page.getByRole('button', { name: 'Sign up' }).click();
  3. 3

    Use the Locator picker

    Inside the Inspector or UI Mode, click "Pick locator" and hover the element. Playwright suggests the most stable selector — usually getByRole.

  4. 4

    Save a trace on CI

    playwright.config.ts
    ts
    export default defineConfig({
      use: { trace: 'on-first-retry', screenshot: 'only-on-failure' },
    });

6. Common beginner mistakes

Forgetting await
// ❌ Test passes even when broken — expect() ran before the click resolved
page.getByRole('button', { name: 'Sign in' }).click();
expect(page).toHaveURL('/dashboard');
Using CSS selectors from the DOM

Selectors like div.card > button.btn-primary break the moment a designer touches the class. Always prefer getByRole('button', { name: /submit/i }).

Sleeping for network

Replace waitForTimeout() with await page.waitForResponse(u => u.url().includes('/api/orders')) or a web-first assertion on the DOM change it produces.

7. Hands-on task (15 minutes)

  1. 1

    Automate a todo app end-to-end

    Extend the demo test above so it: adds 3 todos, marks the second as complete, deletes the first, and asserts the remaining count is 1.

  2. 2

    Add a failing assertion on purpose

    Change one expected value. Run --ui and use the time-travel slider to identify the exact step that mismatches.

  3. 3

    Commit

    git add tests/todo.spec.ts
    git commit -m "test(todo): first e2e — add, complete, delete"

8. What's next

Green tests are easy. Reading a red one — quickly — is the real skill. Continue to Module 1 / Lesson 4: How to Read a Failing Playwright Test.

Docs: Writing tests · Assertions reference

Frequently asked questions

1.Do I need to write async/await in every Playwright test?
Yes. Every Playwright action returns a Promise — clicks, fills, navigations, assertions. Forgetting `await` is the #1 beginner bug: the test passes 'accidentally' because the assertion runs before the action completes.
2.What's the difference between `page.click()` and `page.getByRole('button').click()`?
`page.click(selector)` is the legacy API. `page.getByRole()` returns a Playwright Locator with auto-waiting, better error messages and re-tryable assertions. Use locators for every new test.
3.Why does my test pass sometimes and fail sometimes?
Almost always a race condition. You're asserting before the UI updates. Replace `await page.waitForTimeout(2000)` with web-first assertions like `await expect(locator).toBeVisible()` — they auto-retry for up to 5 seconds.
4.How do I debug a failing Playwright test?
Three tools, in this order: (1) `npx playwright test --ui` for time-travel debugging, (2) `--debug` flag to step through with the Playwright Inspector, (3) `page.pause()` inside the test to freeze the browser at that line.
5.Should tests share state or be independent?
Independent. Each `test()` gets a fresh browser context and clean cookies. Sharing state between tests is the fastest way to flaky suites and impossible-to-debug failures.
6.What is a web-first assertion?
An assertion that polls the DOM until the condition is true or a timeout is hit — e.g. `await expect(page.getByText('Success')).toBeVisible()`. It removes the need for manual waits and eliminates 90% of flakiness.

Related lessons