Tutorial October 12, 2026 16 min read

Playwright Automation Tutorial 2026: Build a Real Test Suite

Most tutorials stop at one test against a homepage. In this one, you'll build a complete Playwright automation suite for a working online store: login tests, data-driven negative cases, a Page Object Model, saved login sessions, a full checkout flow, cross-browser runs and a CI pipeline. Every code block on this page was run against the live site before publishing.

Playwright automation means using Microsoft's open-source Playwright framework to drive real browsers (Chromium, Firefox and WebKit) from code. Your tests click, type and navigate like a user would, then check that the application did the right thing. Playwright waits for elements automatically, runs tests in parallel and records a full trace when something fails. Those features have made it the go-to framework for many new test automation projects.

This tutorial is project-based. Instead of explaining every API in isolation, we'll automate a real application from start to finish. If you've never installed Playwright, skim how to install Playwright first. If you just want the basics of a single test, how to use Playwright is a shorter read.


What You Can Automate with Playwright

  • End-to-end UI flows: logins, sign-ups, carts, checkouts, dashboards.
  • APIs: REST calls with the built-in request fixture, with no extra tool needed (API testing guide).
  • Network behaviour: mock responses, simulate errors, block third-party scripts.
  • Visual and accessibility checks: screenshot comparison and ARIA snapshots.
  • Mobile layouts: emulate phones and tablets with real device profiles.
  • Non-test automation: generating PDFs, filling repetitive forms, collecting data.

Want short, standalone examples of each? See 20 Playwright scripts you can copy and run. To try them without installing anything, use one of these ways to run Playwright online.


What You'll Build

We'll automate Sauce Demo (saucedemo.com), a free online store built specifically for automation practice. It has public test users, a product list, a cart and a checkout, which covers almost every pattern you'll meet on a real project.

By the end you'll have

  • A configured Playwright project
  • Positive and negative login tests
  • Data-driven tests from one loop
  • Page Object Model classes
  • Login once, reuse the session
  • An end-to-end checkout test
  • Runs on 3 browsers in parallel
  • A GitHub Actions CI pipeline

Here's the final project structure, so you know where each file goes:

Project structure
playwright-automation-tutorial/
├── pages/
│   ├── LoginPage.js           // Page Object: login screen
│   ├── InventoryPage.js       // Page Object: product list
│   └── CheckoutPage.js        // Page Object: checkout steps
├── tests/
│   ├── auth.setup.js          // Logs in once, saves session
│   ├── login.spec.js          // Login tests (logged out)
│   └── checkout.spec.js       // Cart, sort and checkout tests
├── playwright/.auth/          // Saved session (git-ignored)
├── playwright.config.js
└── .github/workflows/playwright.yml

Step 1: Create the Project

You need Node.js 20 or newer. Create a folder and run the Playwright installer:

Terminal
mkdir playwright-automation-tutorial
cd playwright-automation-tutorial
npm init playwright@latest

# Answer the prompts:
# ✔ TypeScript or JavaScript? → JavaScript
# ✔ Where to put your end-to-end tests? → tests
# ✔ Add a GitHub Actions workflow? → true
# ✔ Install Playwright browsers? → true

Delete the generated tests/example.spec.js and the tests-examples/ folder. You'll replace them with your own tests. Then create the pages/ folder and add the saved-session folder to .gitignore so login cookies never get committed:

Terminal
rm -rf tests/example.spec.js tests-examples
mkdir pages
echo "playwright/.auth" >> .gitignore

Step 2: Configure Playwright for the App

Replace playwright.config.js with the version below. Three settings do the heavy lifting:

  • baseURL lets tests call page.goto('/') instead of repeating the full URL.
  • testIdAttribute: 'data-test': Sauce Demo marks elements with data-test, while Playwright's getByTestId() looks for data-testid by default. This one line makes getByTestId() work on this app.
  • A setup project runs before the browser projects and logs in once. We'll write it in Step 6.
playwright.config.js
// @ts-check
const { defineConfig, devices } = require('@playwright/test');

module.exports = defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  reporter: 'html',
  use: {
    baseURL: 'https://www.saucedemo.com',
    testIdAttribute: 'data-test',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    // Runs first: logs in and saves the session
    { name: 'setup', testMatch: /.*\.setup\.js/ },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'], storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'], storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
    },
  ],
});

Step 3: Build the Page Objects

A Page Object is a class that holds the locators and actions for one screen. Tests then read like a description of user behaviour, and when the UI changes you fix one file instead of twenty. Here's what to use for each element on Sauce Demo:

ElementLocatorWhy this one
Username / password inputsgetByPlaceholder('Username')The inputs have no visible label, but the placeholder is stable
Login, Checkout, Finish buttonsgetByRole('button', { name })Role locators match what users and screen readers see
Error box, cart badge, page titlegetByTestId('error')Purpose-built test hooks that don't change with styling
One product's "Add to cart"getByTestId('inventory-item').filter({ hasText })Narrows to the right card first, then finds its button

For a deeper comparison of locator types, see the Playwright locators guide. Now create the three page objects:

pages/LoginPage.js
class LoginPage {
  constructor(page) {
    this.page = page;
    this.username = page.getByPlaceholder('Username');
    this.password = page.getByPlaceholder('Password');
    this.loginButton = page.getByRole('button', { name: 'Login' });
    this.error = page.getByTestId('error');
  }

  async goto() {
    await this.page.goto('/');
  }

  async login(username, password) {
    await this.username.fill(username);
    await this.password.fill(password);
    await this.loginButton.click();
  }
}

module.exports = { LoginPage };
pages/InventoryPage.js
class InventoryPage {
  constructor(page) {
    this.page = page;
    this.title = page.getByTestId('title');
    this.cartBadge = page.getByTestId('shopping-cart-badge');
    this.cartLink = page.getByTestId('shopping-cart-link');
    this.sortSelect = page.getByTestId('product-sort-container');
    this.prices = page.getByTestId('inventory-item-price');
  }

  async goto() {
    await this.page.goto('/inventory.html');
  }

  async addToCart(productName) {
    const card = this.page.getByTestId('inventory-item').filter({ hasText: productName });
    await card.getByRole('button', { name: 'Add to cart' }).click();
  }

  async sortBy(label) {
    await this.sortSelect.selectOption({ label });
  }

  async getPrices() {
    const texts = await this.prices.allTextContents();
    return texts.map((t) => parseFloat(t.replace('$', '')));
  }
}

module.exports = { InventoryPage };
pages/CheckoutPage.js
class CheckoutPage {
  constructor(page) {
    this.page = page;
    this.checkoutButton = page.getByRole('button', { name: 'Checkout' });
    this.firstName = page.getByPlaceholder('First Name');
    this.lastName = page.getByPlaceholder('Last Name');
    this.postalCode = page.getByPlaceholder('Zip/Postal Code');
    this.continueButton = page.getByRole('button', { name: 'Continue' });
    this.finishButton = page.getByRole('button', { name: 'Finish' });
    this.completeHeader = page.getByTestId('complete-header');
  }

  async fillShipping(first, last, zip) {
    await this.firstName.fill(first);
    await this.lastName.fill(last);
    await this.postalCode.fill(zip);
    await this.continueButton.click();
  }
}

module.exports = { CheckoutPage };

Don't guess locators. Run npx playwright codegen https://www.saucedemo.com, click around, and Playwright writes the locator for each element you touch. Copy the good ones into your page objects. The codegen tutorial covers this in detail.

Step 4: Automate the Login Tests

Login is the first thing to automate on almost any app, because every other test depends on it. Start with the happy path:

tests/login.spec.js (part 1)
const { test, expect } = require('@playwright/test');
const { LoginPage } = require('../pages/LoginPage');

// These tests need a logged-OUT browser, so clear the saved session
test.use({ storageState: { cookies: [], origins: [] } });

test('valid user lands on the Products page', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('standard_user', 'secret_sauce');

  await expect(page).toHaveURL(/inventory\.html/);
  await expect(page.getByTestId('title')).toHaveText('Products');
});

Notice there's no waitForTimeout() or sleep. Every expect() on a page or locator retries until it passes or times out (5 seconds by default). That auto-waiting is the main reason Playwright tests are less flaky than older Selenium suites.

Why test.use({ storageState: ... })? In Step 6 every browser project will start already logged in. Login tests need a clean, logged-out browser, so this line overrides the saved session for this file only.

Step 5: Data-Driven Negative Tests

Testing only the happy path is the most common gap in beginner suites. Bugs live in the edge cases: locked accounts, wrong passwords, empty fields. Rather than writing four near-identical tests, describe the cases as data and generate a test for each one with a loop. Add this to the same file:

tests/login.spec.js (part 2)
const badLogins = [
  { user: 'locked_out_user', pass: 'secret_sauce', error: 'Sorry, this user has been locked out.' },
  { user: 'standard_user', pass: 'wrong_password', error: 'Username and password do not match' },
  { user: '', pass: 'secret_sauce', error: 'Username is required' },
  { user: 'standard_user', pass: '', error: 'Password is required' },
];

for (const { user, pass, error } of badLogins) {
  test(`login fails for "${user || 'empty user'}" / "${pass || 'empty password'}"`, async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login(user, pass);

    await expect(loginPage.error).toContainText(error);
    await expect(page).not.toHaveURL(/inventory/);
  });
}

Each test gets a unique, readable name, so the report tells you exactly which case failed. To cover a new scenario, add one line to the array.

Step 6: Log In Once, Reuse the Session

Logging in through the UI before every test wastes time and adds a point of failure to tests that aren't about login. The fix is a setup project: it logs in once, saves cookies and local storage to a file, and every browser project starts from that file. The dependencies: ['setup'] lines in the config guarantee it runs first.

tests/auth.setup.js
const { test: setup, expect } = require('@playwright/test');
const { LoginPage } = require('../pages/LoginPage');

const authFile = 'playwright/.auth/user.json';

setup('log in once and save session', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('standard_user', 'secret_sauce');
  await expect(page).toHaveURL(/inventory/);

  await page.context().storageState({ path: authFile });
});

On a real project, read credentials from environment variables (process.env.TEST_USER) instead of hard-coding them. Sauce Demo's credentials are public, so they're fine inline here.

Step 7: The End-to-End Checkout Test

This is the test that earns its keep: it walks the money path a real customer takes. Because of Step 6 it starts already logged in and goes straight to the product list:

tests/checkout.spec.js
const { test, expect } = require('@playwright/test');
const { InventoryPage } = require('../pages/InventoryPage');
const { CheckoutPage } = require('../pages/CheckoutPage');

test('user can buy two products end to end', async ({ page }) => {
  const inventory = new InventoryPage(page);
  const checkout = new CheckoutPage(page);

  await inventory.goto();
  await inventory.addToCart('Sauce Labs Backpack');
  await inventory.addToCart('Sauce Labs Bike Light');
  await expect(inventory.cartBadge).toHaveText('2');

  await inventory.cartLink.click();
  await expect(page.getByTestId('inventory-item')).toHaveCount(2);

  await checkout.checkoutButton.click();
  await checkout.fillShipping('Asim', 'Noaman', '54000');

  // Overview page: item total = 29.99 + 9.99
  await expect(page.getByTestId('subtotal-label')).toContainText('$39.98');

  await checkout.finishButton.click();
  await expect(checkout.completeHeader).toHaveText('Thank you for your order!');
});

test('sorting by price low to high orders the products', async ({ page }) => {
  const inventory = new InventoryPage(page);
  await inventory.goto();
  await inventory.sortBy('Price (low to high)');

  const prices = await inventory.getPrices();
  const sorted = [...prices].sort((a, b) => a - b);
  expect(prices).toEqual(sorted);
});

The checkout test checks the business result (correct subtotal, order confirmed), not just that pages loaded. The sort test shows a second pattern: read data off the page, compute what it should be, and compare. That catches logic bugs a purely visual check would miss.

Step 8: Run the Suite Across Browsers

Run everything:

Terminal
npx playwright test --reporter=list

Playwright runs the setup project first, then all 7 tests in Chromium, Firefox and WebKit in parallel. This is the actual output from running this exact suite:

Running 22 tests using 5 workers

  ✓  [setup] › tests/auth.setup.js › log in once and save session
  ✓  [chromium] › tests/login.spec.js › valid user lands on the Products page
  ✓  [chromium] › tests/login.spec.js › login fails for "locked_out_user" / "secret_sauce"
  ✓  [chromium] › tests/checkout.spec.js › user can buy two products end to end
  ✓  [chromium] › tests/checkout.spec.js › sorting by price low to high orders the products
  ✓  [firefox] › tests/checkout.spec.js › user can buy two products end to end
  ✓  [webkit] › tests/checkout.spec.js › user can buy two products end to end
  ...

  22 passed (19.6s)

That's 1 setup test plus 7 tests × 3 browsers = 22 runs in about 20 seconds. Commands you'll use every day:

Terminal: everyday commands
# One browser only (fastest feedback while writing tests)
npx playwright test --project=chromium

# One file, or tests whose name matches a phrase
npx playwright test tests/checkout.spec.js
npx playwright test -g "locked_out"

# Watch the browser while tests run
npx playwright test --headed --project=chromium

# Open the HTML report from the last run
npx playwright show-report

Want more speed on a large suite? The parallel testing guide covers workers and sharding, and the cross-browser testing guide adds mobile viewports.

Step 9: Debug a Failing Test

Every automation suite breaks eventually. What matters is how fast you find out why. Make a test fail on purpose (change '$39.98' to '$40.00'), then try the two tools you'll rely on most:

Terminal
# UI mode: time-travel through every step, with DOM snapshots
npx playwright test --ui

# Record a full trace, then open it
npx playwright test --trace on --project=chromium
npx playwright show-report

In the report, click the failed test and open its Trace. You'll see every action, the page before and after it, network calls and console logs, so you can usually spot the cause in under a minute. The Trace Viewer tutorial walks through it panel by panel.

Step 10: Run It in CI with GitHub Actions

Automation pays off when it runs on every push without anyone remembering to start it. If you said yes to the GitHub Actions prompt in Step 1, you already have this file. Otherwise, create it:

.github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

On CI the config turns on 2 retries and blocks accidental test.only. The HTML report, including traces for any retried test, is uploaded as an artifact you can download from the run page. For caching, sharding and Slack alerts, see Playwright CI/CD with GitHub Actions.

6 Mistakes That Make Automation Flaky

  1. Fixed waits. page.waitForTimeout(3000) is either too slow or too short. Use web-first assertions like expect(locator).toBeVisible(), which wait exactly as long as needed.
  2. Brittle CSS/XPath selectors. div > div:nth-child(3) > button breaks on the next redesign. Prefer role, label, placeholder and test-id locators.
  3. Forgetting await. A missing await lets the next line run before the action finishes, which causes intermittent failures. The missing-playwright-await rule in eslint-plugin-playwright catches it.
  4. Tests that depend on each other. Each test gets a fresh browser context, so never rely on state left by a previous test. Use setup projects or fixtures instead.
  5. Logging in through the UI everywhere. It's slow and multiplies failure points. Reuse a saved session (Step 6).
  6. Only testing the happy path. The data-driven pattern in Step 5 makes negative cases nearly free to add.

Finished the tutorial? Check how much of it stuck. The Playwright practice tests ask exam-style questions on page objects, storage state, locators and CI, with an explanation for every answer.

Speed Up Playwright Automation with AI

Once you understand the structure above, AI can do much of the typing. There are two main approaches in 2026:

  • Playwright Test Agents. Run npx playwright init-agents --loop=claude in your project to add three agents: a planner that explores the app and writes a test plan, a generator that turns the plan into test files, and a healer that repairs failing tests. See the Test Agents guide.
  • Playwright MCP + Claude. The MCP server lets Claude drive a real browser, read the live page and write tests that use real locators. Setup takes about five minutes: Playwright MCP server setup guide.
Example prompt to Claude (with Playwright MCP)
Open https://www.saucedemo.com and log in as standard_user.
Add the cheapest product to the cart, check out, and write a
Playwright JavaScript test for this flow. Reuse my existing
page objects in pages/ and add any methods that are missing.

The catch: AI writes tests fast, but it can't tell you whether a test checks the right thing. The skills in this tutorial (good locators, page objects, real assertions, session reuse) are what let you review AI output and keep a suite maintainable.

Want to go further with Playwright + Claude AI?

Hands-on Udemy course: a full JavaScript framework, API testing, CI/CD, and Claude AI with the MCP Server generating and debugging your tests.

Enroll on Udemy →

Frequently Asked Questions

What is Playwright automation?

Playwright automation means using Microsoft's open-source Playwright framework to control real browsers (Chromium, Firefox and WebKit) from code, so you can test web applications automatically. Tests click, type and navigate like a user, then assert that the page behaves correctly.

How long does it take to learn Playwright automation?

If you know basic JavaScript, you can write and run your first test in under an hour. Building a structured suite like this one, with a Page Object Model, saved sessions and CI, usually takes a few days of practice. Becoming job-ready typically takes a few weeks of regular work on real projects.

Which language is best for Playwright automation?

JavaScript and TypeScript are the most popular, because the Playwright Test runner is built for Node.js and gets new features first. Playwright also supports Python, Java and C#. This tutorial uses plain JavaScript, so there's no TypeScript setup to learn first.

Can I practice Playwright automation on a free website?

Yes. This tutorial uses saucedemo.com, a free demo store built for automation practice. It has public test users like standard_user and locked_out_user with the password secret_sauce.

How do I avoid logging in before every Playwright test?

Use a setup project that logs in once and saves the session with page.context().storageState({ path }). Other projects depend on it and load the file via the storageState option, so every test starts logged in.

Can AI write Playwright automation tests for me?

Yes. Playwright's Test Agents (planner, generator, healer) are added with npx playwright init-agents --loop=claude, and the Playwright MCP server lets Claude drive a real browser to generate tests. You still need the fundamentals in this tutorial to review and maintain what the AI produces.


Asim Noaman - Playwright and Claude AI course instructor

Asim Noaman

Senior QA Automation Engineer & AI Testing Specialist

With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.

Udemy InstructorPublished course author
Playwright + AI ExpertSpecialized in AI-powered QA
Production ExperienceEnterprise-grade frameworks
Connect on LinkedIn

Playwright + Claude AI Course

Turn This Tutorial Into a Job-Ready Framework

You've built a working suite. The course takes it the rest of the way: JavaScript from scratch, a full Page Object Model framework, API testing, CI/CD, and Claude AI with the Playwright MCP Server generating and debugging tests for you. You'll finish with a framework you can show to employers.

  • JavaScript and Playwright from absolute zero, no prior experience needed
  • Page Object Model and test architecture used on real teams
  • Claude AI generates your tests; you learn by reviewing and refining
  • GitHub Actions CI/CD so your tests run on every code push
Start Learning on Udemy →