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:
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
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
Create the file
Inside the Playwright project from Lesson 2, create
tests/todo.spec.ts. - 2
Paste this test
tests/todo.spec.tstsimport { 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
Run it in UI Mode
npx playwright test tests/todo.spec.ts --ui - 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 Inspector4. 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 locatorstoBeEnabled()/toBeChecked()
5. The debug workflow real teams use
- 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
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
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
Save a trace on CI
playwright.config.tstsexport default defineConfig({ use: { trace: 'on-first-retry', screenshot: 'only-on-failure' }, });
6. Common beginner mistakes
// ❌ Test passes even when broken — expect() ran before the click resolved
page.getByRole('button', { name: 'Sign in' }).click();
expect(page).toHaveURL('/dashboard');Selectors like div.card > button.btn-primary break the moment a designer touches the class. Always prefer getByRole('button', { name: /submit/i }).
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
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
Add a failing assertion on purpose
Change one expected value. Run
--uiand use the time-travel slider to identify the exact step that mismatches. - 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