Playwright Basics
A comprehensive, hands-on guide to end-to-end testing with Playwright and TypeScript. From installation to CI/CD, learn everything you need to write reliable browser tests for modern web applications.
Introduction
What is Playwright?
Playwright is an open-source end-to-end testing framework developed by Microsoft. It allows you to automate Chromium, Firefox, and WebKit browsers with a single API. Unlike older tools, Playwright was built from the ground up for the modern web — it handles single-page applications, iframes, shadow DOM, and multiple tabs natively.
Why Choose Playwright?
- Cross-browser — One API works with Chromium (Chrome, Edge), Firefox, and WebKit (Safari) on Windows, macOS, and Linux.
- Auto-waiting — Playwright automatically waits for elements to be actionable before performing operations. No more manual sleep calls or flaky waits.
- Web-first assertions — Assertions retry until the condition is met or a timeout is reached, eliminating most race-condition flakiness.
- Test isolation — Every test gets a fresh browser context (cookies, storage, cache) out of the box. Tests cannot interfere with each other.
- Powerful tooling — Built-in test generator (Codegen), Trace Viewer for post-mortem debugging, and an HTML reporter for rich test reports.
- TypeScript first — Full TypeScript support with auto-complete, type checking, and excellent IDE integration.
How Does Playwright Compare?
| Feature | Playwright | Cypress | Selenium |
|---|---|---|---|
| Cross-browser | Chromium, Firefox, WebKit | Chromium, Firefox, WebKit (limited) | All major browsers |
| Language support | TypeScript, JavaScript, Python, Java, C# | JavaScript / TypeScript only | Java, Python, C#, Ruby, JS |
| Auto-waiting | Built-in, comprehensive | Built-in | Manual waits required |
| Multi-tab / multi-origin | Full support | Limited / workarounds | Supported |
| iframes | First-class API | Supported (some limitations) | Supported |
| Parallel execution | Built-in, file-level & test-level | Via Cypress Cloud or plugins | Via Selenium Grid |
| Trace viewer | Built-in with DOM snapshots | Video only (screenshots in Cloud) | Not built-in |
| Network interception | Full API mocking & modification | cy.intercept() | Limited |
Installation
The fastest way to get started is the official init command, which scaffolds an entire project with configuration, example tests, and optionally a CI workflow.
1# Create a new Playwright project from scratch
2npm init playwright@latest
3
4# You will be prompted:
5# Do you want to use TypeScript or JavaScript? -> TypeScript
6# Where to put your end-to-end tests? -> tests
7# Add a GitHub Actions workflow? -> true
8# Install Playwright browsers? -> trueThe command installs @playwright/test (the test runner) and downloads browser binaries for Chromium, Firefox, and WebKit. After it finishes you will have a project structure like this:
1my-playwright-project/
2├── node_modules/
3├── tests/
4│ └── example.spec.ts # Sample test
5├── tests-examples/
6│ └── demo-todo-app.spec.ts # Full demo test
7├── .github/
8│ └── workflows/
9│ └── playwright.yml # CI workflow
10├── playwright.config.ts # Main configuration
11├── package.json
12└── tsconfig.jsonnpm install -D @playwright/test and then run npx playwright install to download the browsers.Installing Specific Browsers
By default all three browsers are installed, which takes some disk space. If you only need Chromium, you can install selectively:
1# Install only Chromium
2npx playwright install chromium
3
4# Install Chromium and Firefox, skip WebKit
5npx playwright install chromium firefox
6
7# Install browsers along with OS-level dependencies (useful in Docker / CI)
8npx playwright install --with-deps chromiumConfiguration
The heart of every Playwright project is playwright.config.ts. This file controls which browsers to test, timeouts, retries, reporters, and much more. Here is a thoroughly annotated example:
1import { defineConfig, devices } from '@playwright/test';
2
3export default defineConfig({
4 // Directory that contains your test files
5 testDir: './tests',
6
7 // Maximum time a single test can run before it is forcibly stopped
8 timeout: 30_000,
9
10 // Assertion-level timeout (how long expect() waits before failing)
11 expect: {
12 timeout: 5_000,
13 },
14
15 // Run tests in parallel across files
16 fullyParallel: true,
17
18 // Fail the entire suite immediately if any test fails (useful in CI)
19 forbidOnly: !!process.env.CI,
20
21 // Retry failed tests once in CI, never locally
22 retries: process.env.CI ? 2 : 0,
23
24 // Limit parallel workers in CI to avoid resource contention
25 workers: process.env.CI ? 1 : undefined,
26
27 // Which reporter to use
28 reporter: [
29 ['html', { open: 'never' }],
30 ['list'],
31 ],
32
33 // Shared settings applied to every test in every project
34 use: {
35 // Base URL lets you write relative paths: page.goto('/login')
36 baseURL: 'http://localhost:3000',
37
38 // Capture a trace on the first retry of a failed test
39 trace: 'on-first-retry',
40
41 // Capture a screenshot when a test fails
42 screenshot: 'only-on-failure',
43
44 // Record video only when retrying
45 video: 'on-first-retry',
46 },
47
48 // Define browser projects to test against
49 projects: [
50 {
51 name: 'chromium',
52 use: { ...devices['Desktop Chrome'] },
53 },
54 {
55 name: 'firefox',
56 use: { ...devices['Desktop Firefox'] },
57 },
58 {
59 name: 'webkit',
60 use: { ...devices['Desktop Safari'] },
61 },
62
63 // Mobile viewports
64 {
65 name: 'Mobile Chrome',
66 use: { ...devices['Pixel 5'] },
67 },
68 {
69 name: 'Mobile Safari',
70 use: { ...devices['iPhone 13'] },
71 },
72 ],
73
74 // Automatically start your dev server before running tests
75 webServer: {
76 command: 'npm run dev',
77 url: 'http://localhost:3000',
78 reuseExistingServer: !process.env.CI,
79 timeout: 120_000,
80 },
81});Key Configuration Options Explained
- testDir — The directory where Playwright looks for
*.spec.tsfiles. Defaults to the project root if omitted. - timeout — Maximum time per test. If a test exceeds this, Playwright terminates it and marks it failed. Default is 30 seconds.
- fullyParallel — When true, tests inside a single file can run in parallel (not just across files).
- retries — How many times to retry a failed test. Great for CI where transient failures may occur. Keep it at 0 locally so you catch real bugs.
- projects — Each project represents a browser configuration. You can also use projects for different environments (staging vs production) or test suites (smoke vs full regression).
- webServer — Tells Playwright to start your development server before running tests. It waits for the URL to respond before proceeding.
forbidOnly: true locally. This option is designed for CI and will cause any test with test.only() to fail the entire suite, which is disruptive during development.Writing Your First Test
Playwright test files use the .spec.ts extension by convention. Each file imports test and expect from @playwright/test. The test function receives a destructured page object, which represents a single browser tab.
1import { test, expect } from '@playwright/test';
2
3// A test block: "test" accepts a name and an async callback.
4// The callback receives a "page" object that represents a browser tab.
5test('homepage has correct title and link', async ({ page }) => {
6 // Navigate to the application
7 await page.goto('/');
8
9 // Assert the page title
10 await expect(page).toHaveTitle(/My Application/);
11
12 // Find a link by its visible text and click it
13 await page.getByRole('link', { name: 'Get Started' }).click();
14
15 // Assert the URL changed after clicking
16 await expect(page).toHaveURL(/.*getting-started/);
17});Grouping Tests with describe
Use test.describe() to group related tests and share setup logic with test.beforeEach(). This is similar to describe/beforeEach in Jest or Mocha.
1import { test, expect } from '@playwright/test';
2
3test.describe('User Authentication', () => {
4 // Runs once before all tests in this describe block
5 test.beforeEach(async ({ page }) => {
6 await page.goto('/login');
7 });
8
9 test('should display login form', async ({ page }) => {
10 await expect(page.getByRole('heading', { name: 'Sign In' })).toBeVisible();
11 await expect(page.getByLabel('Email')).toBeVisible();
12 await expect(page.getByLabel('Password')).toBeVisible();
13 });
14
15 test('should show error for invalid credentials', async ({ page }) => {
16 await page.getByLabel('Email').fill('wrong@example.com');
17 await page.getByLabel('Password').fill('badpassword');
18 await page.getByRole('button', { name: 'Sign In' }).click();
19
20 await expect(page.getByText('Invalid email or password')).toBeVisible();
21 });
22
23 test('should redirect to dashboard on success', async ({ page }) => {
24 await page.getByLabel('Email').fill('user@example.com');
25 await page.getByLabel('Password').fill('correctpassword');
26 await page.getByRole('button', { name: 'Sign In' }).click();
27
28 await expect(page).toHaveURL(/.*dashboard/);
29 await expect(page.getByText('Welcome back')).toBeVisible();
30 });
31});Running Your Tests
Use the CLI to run tests. Playwright provides many useful flags:
1# Run all tests
2npx playwright test
3
4# Run in headed mode (see the browser)
5npx playwright test --headed
6
7# Run a single file
8npx playwright test tests/auth.spec.ts
9
10# Run tests matching a name
11npx playwright test -g "should redirect"
12
13# Run only Chromium
14npx playwright test --project=chromium
15
16# Debug mode: steps through each action with Inspector
17npx playwright test --debugLocators
Locators are the way you find elements on the page. Playwright provides multiple locator strategies, but the recommended approach is to use role-based locators whenever possible. These mirror how users and assistive technology interact with your app, making tests more resilient and accessible.
Role-based Locators (Recommended)
1// ----- By Role (preferred - most resilient to DOM changes) -----
2// Buttons
3await page.getByRole('button', { name: 'Submit' });
4await page.getByRole('button', { name: /submit/i }); // regex, case-insensitive
5
6// Links
7await page.getByRole('link', { name: 'Documentation' });
8
9// Headings
10await page.getByRole('heading', { name: 'Welcome', level: 1 });
11
12// Form elements
13await page.getByRole('textbox', { name: 'Email' });
14await page.getByRole('checkbox', { name: 'Accept terms' });
15await page.getByRole('combobox', { name: 'Country' });
16
17// Navigation, dialog, etc.
18await page.getByRole('navigation');
19await page.getByRole('dialog');Text, Label, Placeholder, and TestID
1// ----- By Text Content -----
2await page.getByText('Welcome to the site');
3await page.getByText(/welcome/i); // partial, case-insensitive
4await page.getByText('Welcome', { exact: true }); // exact match only
5
6// ----- By Label (for form inputs) -----
7await page.getByLabel('Email address');
8await page.getByLabel('Password');
9
10// ----- By Placeholder -----
11await page.getByPlaceholder('Search...');
12
13// ----- By Alt Text (images) -----
14await page.getByAltText('Company logo');
15
16// ----- By Title Attribute -----
17await page.getByTitle('Close dialog');
18
19// ----- By Test ID (escape hatch when nothing else works) -----
20await page.getByTestId('submit-button');
21// Matches: <button data-testid="submit-button">Send</button>Chaining and Filtering
Real-world pages often have multiple elements with the same role or text. Chaining and filtering let you narrow your search scope to find exactly the right element.
1// Chaining narrows the search scope to children of the parent locator
2const productCard = page.locator('.product-card').filter({ hasText: 'Laptop' });
3await productCard.getByRole('button', { name: 'Add to cart' }).click();
4
5// .nth() selects by index (0-based)
6await page.getByRole('listitem').nth(2).click();
7
8// .first() and .last()
9await page.getByRole('listitem').first().click();
10await page.getByRole('listitem').last().click();
11
12// Filter by another locator
13await page
14 .getByRole('listitem')
15 .filter({ has: page.getByRole('heading', { name: 'Premium' }) })
16 .getByRole('button', { name: 'Buy' })
17 .click();
18
19// CSS and XPath (use sparingly - they break more easily)
20await page.locator('css=div.card >> text=Details').click();
21await page.locator('xpath=//div[@class="card"]//button').click();getByRole — (2) getByLabel / getByPlaceholder — (3) getByText — (4) getByTestId — (5) CSS / XPath as a last resort. The higher the priority, the more resilient the locator is to DOM refactoring.Interactions
Once you have located an element, you need to interact with it. Playwright provides methods for clicking, typing, selecting, dragging, and more. All interactions auto-wait for the element to be visible, enabled, and stable before acting.
Click Actions
1// Standard click
2await page.getByRole('button', { name: 'Submit' }).click();
3
4// Double click
5await page.getByText('Select this text').dblclick();
6
7// Right click (context menu)
8await page.getByText('Show options').click({ button: 'right' });
9
10// Shift + Click (multi-select)
11await page.getByText('Item 3').click({ modifiers: ['Shift'] });
12
13// Click at specific position relative to the element center
14await page.locator('#canvas').click({ position: { x: 100, y: 200 } });
15
16// Force click (bypasses actionability checks - use as last resort)
17await page.getByRole('button', { name: 'Hidden' }).click({ force: true });Form Interactions
1// Type into text fields
2await page.getByLabel('Username').fill('john_doe');
3await page.getByLabel('Bio').fill('Hello, I am a tester.');
4
5// Clear a field first, then type character by character (simulates real typing)
6await page.getByLabel('Search').clear();
7await page.getByLabel('Search').pressSequentially('Playwright', { delay: 50 });
8
9// Select from dropdown
10await page.getByLabel('Country').selectOption('US');
11await page.getByLabel('Country').selectOption({ label: 'United States' });
12await page.getByLabel('Colors').selectOption(['red', 'green']); // multi-select
13
14// Check / uncheck
15await page.getByLabel('Accept terms').check();
16await page.getByLabel('Notifications').uncheck();
17
18// Upload a file
19await page.getByLabel('Upload resume').setInputFiles('resume.pdf');
20await page.getByLabel('Upload photos').setInputFiles(['photo1.png', 'photo2.png']);
21// Clear file selection
22await page.getByLabel('Upload resume').setInputFiles([]);Keyboard and Mouse
1// Press individual keys
2await page.keyboard.press('Enter');
3await page.keyboard.press('Escape');
4await page.keyboard.press('Tab');
5
6// Key combinations
7await page.keyboard.press('Control+a'); // Select all
8await page.keyboard.press('Control+c'); // Copy
9await page.keyboard.press('Meta+v'); // Paste (Mac)
10
11// Type a string as keyboard input
12await page.keyboard.type('Hello World');
13
14// Hold and release keys manually
15await page.keyboard.down('Shift');
16await page.keyboard.press('ArrowDown');
17await page.keyboard.press('ArrowDown');
18await page.keyboard.up('Shift');
19
20// Hover
21await page.getByText('Menu').hover();
22
23// Drag and drop
24await page.locator('#source').dragTo(page.locator('#target'));{ force: true } unless you have a very specific reason. Forcing clicks bypasses Playwright's actionability checks, which means your test might interact with elements a real user could never reach. If you find yourself needing force frequently, the application likely has accessibility issues worth fixing.Assertions
Assertions verify that the application is in the expected state. Playwright uses web-first assertions that automatically retry until the condition is satisfied or a timeout is reached. This is fundamentally different from immediate assertions in unit testing frameworks and is key to eliminating flakiness.
Common Assertions
1import { test, expect } from '@playwright/test';
2
3test('assertions showcase', async ({ page }) => {
4 await page.goto('/products');
5
6 // ----- Visibility -----
7 await expect(page.getByText('Product Catalog')).toBeVisible();
8 await expect(page.getByText('Loading...')).toBeHidden();
9
10 // ----- Text Content -----
11 await expect(page.getByRole('heading')).toHaveText('Product Catalog');
12 await expect(page.getByRole('heading')).toContainText('Product');
13
14 // ----- Page Title & URL -----
15 await expect(page).toHaveTitle(/Products/);
16 await expect(page).toHaveURL(/\/products$/);
17
18 // ----- Element Count -----
19 await expect(page.getByRole('listitem')).toHaveCount(12);
20
21 // ----- CSS Classes & Attributes -----
22 await expect(page.locator('.nav-link.active')).toHaveClass(/active/);
23 await expect(page.getByRole('link', { name: 'Docs' }))
24 .toHaveAttribute('href', '/docs');
25
26 // ----- Input Values -----
27 await expect(page.getByLabel('Search')).toHaveValue('');
28 await page.getByLabel('Search').fill('laptop');
29 await expect(page.getByLabel('Search')).toHaveValue('laptop');
30
31 // ----- Enabled / Disabled -----
32 await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
33 await expect(page.getByRole('button', { name: 'Delete' })).toBeDisabled();
34
35 // ----- Checked State -----
36 await expect(page.getByLabel('Remember me')).not.toBeChecked();
37 await page.getByLabel('Remember me').check();
38 await expect(page.getByLabel('Remember me')).toBeChecked();
39});Waiting and Advanced Assertions
While Playwright auto-waits for most operations, there are scenarios where you need explicit waiting — for example, waiting for a network response or for an element to disappear.
1// Playwright assertions auto-retry by default (up to expect.timeout).
2// You can also configure the timeout per assertion:
3await expect(page.getByText('Data loaded'))
4 .toBeVisible({ timeout: 10_000 }); // wait up to 10 seconds
5
6// Wait for a specific condition using page.waitForSelector
7await page.waitForSelector('.results-table', { state: 'visible' });
8
9// Wait for a network response
10const responsePromise = page.waitForResponse('**/api/products');
11await page.getByRole('button', { name: 'Load Products' }).click();
12const response = await responsePromise;
13expect(response.status()).toBe(200);
14
15// Wait for navigation
16await Promise.all([
17 page.waitForURL('**/dashboard'),
18 page.getByRole('button', { name: 'Login' }).click(),
19]);
20
21// Wait for element to be detached from DOM
22await expect(page.locator('.spinner')).toHaveCount(0);
23
24// Soft assertions - don't stop the test on failure
25await expect.soft(page.getByText('Beta')).toBeVisible();
26await expect.soft(page.getByText('v2.0')).toBeVisible();
27// Test continues even if the above fail; failures are collected at the endexpect.soft() when you want to check multiple independent conditions without stopping the test at the first failure. All soft assertion failures are reported at the end. This is great for verifying a dashboard where multiple widgets should all render correctly.Screenshots and Videos
Visual evidence is invaluable for debugging test failures, especially in CI environments where you cannot watch the browser in real time. Playwright supports on-demand screenshots, visual comparison testing, and automatic video recording.
Taking Screenshots
1import { test, expect } from '@playwright/test';
2
3test('capture screenshots', async ({ page }) => {
4 await page.goto('/dashboard');
5
6 // Full page screenshot
7 await page.screenshot({ path: 'screenshots/dashboard-full.png', fullPage: true });
8
9 // Viewport only (what the user sees)
10 await page.screenshot({ path: 'screenshots/dashboard-viewport.png' });
11
12 // Screenshot of a specific element
13 const chart = page.locator('.revenue-chart');
14 await chart.screenshot({ path: 'screenshots/revenue-chart.png' });
15
16 // Visual comparison (golden file testing)
17 // First run creates the reference image, subsequent runs compare against it
18 await expect(page).toHaveScreenshot('dashboard.png', {
19 maxDiffPixelRatio: 0.01, // Allow 1% pixel difference
20 });
21
22 // Element-level visual comparison
23 await expect(page.locator('.sidebar')).toHaveScreenshot('sidebar.png');
24});Video Recording Configuration
Configure video recording in your playwright.config.ts file. The recommended approach is to record only on failure or first retry to save disk space.
1// In playwright.config.ts
2import { defineConfig } from '@playwright/test';
3
4export default defineConfig({
5 use: {
6 // Record video for every test (generates large files)
7 // video: 'on',
8
9 // Record only when retrying a failed test (recommended)
10 video: 'on-first-retry',
11
12 // Record only for failed tests
13 // video: 'retain-on-failure',
14
15 // Video resolution
16 video: {
17 mode: 'on-first-retry',
18 size: { width: 1280, height: 720 },
19 },
20
21 // Screenshot options
22 screenshot: 'only-on-failure',
23 },
24});toHaveScreenshot() method compares against a golden reference image. On the first run it creates the reference. On subsequent runs it compares pixel by pixel. Use npx playwright test --update-snapshots to refresh reference images after intentional visual changes.maxDiffPixelRatio or maxDiffPixels to account for minor rendering differences across platforms.Reporting
Playwright ships with several built-in reporters and supports custom reporters. The HTML reporter and Trace Viewer are particularly powerful for diagnosing failures.
Reporter Configuration
1// playwright.config.ts - Reporter configuration
2import { defineConfig } from '@playwright/test';
3
4export default defineConfig({
5 reporter: [
6 // Built-in terminal reporter for live output
7 ['list'],
8
9 // HTML report with full details, screenshots, traces
10 ['html', {
11 open: 'never', // 'always' | 'never' | 'on-failure'
12 outputFolder: 'playwright-report',
13 }],
14
15 // JUnit XML for CI/CD systems (Jenkins, GitLab, etc.)
16 ['junit', { outputFile: 'results/junit.xml' }],
17
18 // JSON reporter for custom processing
19 ['json', { outputFile: 'results/results.json' }],
20 ],
21
22 use: {
23 // Trace captures a timeline of every action, network request, and DOM snapshot
24 trace: 'on-first-retry',
25 // Options: 'on' | 'off' | 'on-first-retry' | 'retain-on-failure'
26 },
27});Useful CLI Commands
1# Run tests
2npx playwright test
3
4# Open the HTML report after running tests
5npx playwright show-report
6
7# View a trace file (download from HTML report or CI artifacts)
8npx playwright show-trace trace.zip
9
10# Run with specific reporter override
11npx playwright test --reporter=dot
12
13# Run tests in headed mode (see the browser)
14npx playwright test --headed
15
16# Run a single test file
17npx playwright test tests/login.spec.ts
18
19# Run tests matching a name pattern
20npx playwright test -g "should display error"Trace Viewer
The Trace Viewer is one of Playwright's most powerful debugging tools. It records a complete timeline of your test including:
- Every action performed (clicks, fills, navigations)
- DOM snapshots before and after each action
- Network requests and responses
- Console logs and errors
- Screenshots at each step
You can open traces in the browser at trace.playwright.dev by dragging and dropping the trace zip file, or use the CLI command npx playwright show-trace trace.zip.
CI/CD Integration
Playwright works with all major CI/CD platforms. Below is a complete GitHub Actions workflow that runs tests, uploads the HTML report, and preserves trace files for failed tests:
1# .github/workflows/playwright.yml
2name: Playwright Tests
3
4on:
5 push:
6 branches: [main]
7 pull_request:
8 branches: [main]
9
10jobs:
11 test:
12 timeout-minutes: 15
13 runs-on: ubuntu-latest
14 steps:
15 - uses: actions/checkout@v4
16
17 - uses: actions/setup-node@v4
18 with:
19 node-version: 20
20
21 - name: Install dependencies
22 run: npm ci
23
24 - name: Install Playwright Browsers
25 run: npx playwright install --with-deps
26
27 - name: Run Playwright tests
28 run: npx playwright test
29
30 - name: Upload HTML report
31 uses: actions/upload-artifact@v4
32 if: always()
33 with:
34 name: playwright-report
35 path: playwright-report/
36 retention-days: 14
37
38 - name: Upload test traces
39 uses: actions/upload-artifact@v4
40 if: failure()
41 with:
42 name: playwright-traces
43 path: test-results/
44 retention-days: 7npx playwright install --with-deps to install OS-level dependencies (fonts, libraries) that browsers need. Without these, you will get cryptic browser launch errors.Full Example: E-commerce Checkout Test
Let us put everything together with a realistic test suite for an e-commerce application. This example demonstrates login helpers, search, form filling, API response interception, visual verification, and grouped tests with shared setup.
1import { test, expect, type Page } from '@playwright/test';
2
3// ---------------------------------------------------------------------------
4// Real-world E2E test: Online store checkout flow
5// ---------------------------------------------------------------------------
6
7// Helper function to log in before tests that need authentication
8async function loginAsCustomer(page: Page) {
9 await page.goto('/login');
10 await page.getByLabel('Email').fill('customer@example.com');
11 await page.getByLabel('Password').fill('SecurePass123!');
12 await page.getByRole('button', { name: 'Sign In' }).click();
13 await expect(page).toHaveURL(/.*account/);
14}
15
16test.describe('E-commerce Checkout Flow', () => {
17 test.beforeEach(async ({ page }) => {
18 await loginAsCustomer(page);
19 });
20
21 test('should search for a product and add it to cart', async ({ page }) => {
22 // Use the search bar
23 await page.getByPlaceholder('Search products...').fill('Wireless Headphones');
24 await page.getByPlaceholder('Search products...').press('Enter');
25
26 // Verify search results appeared
27 await expect(page.getByRole('heading', { name: 'Search Results' })).toBeVisible();
28 await expect(page.getByRole('listitem')).toHaveCount(3);
29
30 // Click the first product
31 await page
32 .getByRole('listitem')
33 .first()
34 .getByRole('link', { name: /Wireless Headphones/i })
35 .click();
36
37 // Verify product detail page
38 await expect(page).toHaveURL(/.*\/products\/\d+/);
39 await expect(page.getByRole('heading', { level: 1 })).toContainText('Wireless Headphones');
40
41 // Select options and add to cart
42 await page.getByLabel('Color').selectOption('Black');
43 await page.getByRole('button', { name: 'Add to Cart' }).click();
44
45 // Verify cart badge updated
46 await expect(page.getByTestId('cart-count')).toHaveText('1');
47 });
48
49 test('should complete checkout with valid payment', async ({ page }) => {
50 // Assume the product is already in the cart (from a previous action or fixture)
51 await page.goto('/cart');
52 await expect(page.getByRole('heading', { name: 'Shopping Cart' })).toBeVisible();
53
54 // Verify cart has items
55 const cartItems = page.getByRole('listitem').filter({
56 has: page.locator('[data-testid="cart-item"]'),
57 });
58 await expect(cartItems).toHaveCount(1);
59
60 // Proceed to checkout
61 await page.getByRole('button', { name: 'Proceed to Checkout' }).click();
62 await expect(page).toHaveURL(/.*\/checkout/);
63
64 // Fill shipping information
65 await page.getByLabel('Full Name').fill('Jane Doe');
66 await page.getByLabel('Address').fill('123 Test Street');
67 await page.getByLabel('City').fill('San Francisco');
68 await page.getByLabel('State').selectOption('CA');
69 await page.getByLabel('ZIP Code').fill('94102');
70 await page.getByRole('button', { name: 'Continue to Payment' }).click();
71
72 // Fill payment information
73 await page.getByLabel('Card Number').fill('4242424242424242');
74 await page.getByLabel('Expiration').fill('12/28');
75 await page.getByLabel('CVC').fill('123');
76
77 // Place order and wait for API response
78 const orderPromise = page.waitForResponse(
79 (resp) => resp.url().includes('/api/orders') && resp.status() === 201
80 );
81 await page.getByRole('button', { name: 'Place Order' }).click();
82 const orderResponse = await orderPromise;
83 const orderData = await orderResponse.json();
84
85 // Verify confirmation page
86 await expect(page).toHaveURL(/.*\/order-confirmation/);
87 await expect(page.getByRole('heading', { name: 'Order Confirmed' })).toBeVisible();
88 await expect(page.getByText(orderData.orderId)).toBeVisible();
89
90 // Screenshot the confirmation for records
91 await page.screenshot({
92 path: `screenshots/order-${orderData.orderId}.png`,
93 fullPage: true,
94 });
95 });
96
97 test('should show validation errors for empty checkout form', async ({ page }) => {
98 await page.goto('/checkout');
99
100 // Click submit without filling anything
101 await page.getByRole('button', { name: 'Continue to Payment' }).click();
102
103 // Verify all required field errors appear
104 const requiredFields = ['Full Name', 'Address', 'City', 'ZIP Code'];
105 for (const fieldName of requiredFields) {
106 await expect(
107 page.getByText(`${fieldName} is required`)
108 ).toBeVisible();
109 }
110
111 // The URL should NOT have changed
112 await expect(page).toHaveURL(/.*\/checkout/);
113 });
114});What This Example Demonstrates
- Reusable helper functions — The
loginAsCustomerfunction is extracted so multiple test suites can share it. - test.beforeEach — Every test starts logged in, reducing duplication.
- Diverse locator strategies — The tests use
getByPlaceholder,getByRole,getByLabel,getByText, andgetByTestIddepending on context. - Network response interception — The checkout test waits for the actual API response and uses the returned order ID in assertions.
- Chaining and filtering — Cart items are located by filtering list items that contain a specific test ID.
- Dynamic screenshots — The confirmation screenshot uses the order ID in the file name for traceability.
- Form validation testing — The third test verifies client-side validation without needing any backend interaction.
request context, component testing, authentication state reuse via storageState, page object model patterns, and parameterized tests with test.describe.configure.Recommended Courses
Take your Playwright skills to the next level with this hand-picked course.
Playwright with TypeScript
A comprehensive course covering Playwright fundamentals, advanced patterns, and real-world test automation strategies with TypeScript.
View Course on Udemy