# Introduction to Playwright

Course: [Playwright](https://www.devtutorials.ai/learn/playwright) · Section: Playwright Foundations
Source: https://www.devtutorials.ai/learn/playwright/foundations/introduction-to-playwright

> What Playwright is and why it was made, its history and features, the browsers, languages and platforms it supports, how it works (architecture, browser contexts, the protocol, auto-waiting), its tools and its three npm packages.

## Playwright Introduction

- **Playwright** is a free tool that controls a web browser with code.
- You use it to test websites. A test opens a page, clicks and types like a person, then checks the result.
- It works in Chrome, Edge, Firefox and Safari's engine.
- In this lesson, you will learn what Playwright is, how it works and which tools it has.

**Simple lesson:** Playwright is a robot user that tests your website for you.

### A Real-World Example

- Priya builds a to-do list app.
- Every time she changes the code, she opens the app, types a task and presses Enter.
- Then she checks that the task appears and the counter says **1 item left**.
- This takes two minutes. She does it ten times a day. Sometimes she forgets a step.
- With Playwright, she writes these steps once as a **test**. The test runs in about one second, every time, in every browser.

The test below does exactly what Priya does by hand. It uses **TodoMVC**, the practice app the Playwright team uses in its own docs. Press **▶ Run tests**, then open the **Browser** and **Steps** tabs.

- **What happened:** the test opened the app, typed **Buy milk**, pressed Enter and checked two things.
- Both checks passed, so the terminal says **1 passed**.
- Do not worry about every word yet. **Your First Playwright Test** explains each line.

### What You Will Learn

1. What Playwright is and what an end-to-end test is.
2. Why Playwright was created, and a short history.
3. Its main features.
4. The browsers, languages and platforms it supports.
5. How it works: architecture, browser contexts, the protocol and auto-waiting.
6. The tools in the Playwright family.
7. The three npm packages, and which one you need.

## What Is Playwright?

- **Playwright** = an open-source tool from Microsoft for **browser automation** and **end-to-end testing**.
- **Browser automation** = code that drives a browser: open pages, click, type, read text.
- **End-to-end test (E2E test)** = a test that uses the whole app, from the screen down to the server, like a real user.
- **Open source** = the code is public and free to use. Playwright uses the Apache 2.0 license.
- Playwright starts a browser, sends it commands, and reports what passed or failed.

<figure className="plw-fig">
<svg viewBox="0 0 640 210" role="img" aria-labelledby="fig-plw-run" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-run">Animation: a test file sends steps to Playwright, Playwright drives the browser, and the result 1 passed comes back</title>
  <defs><marker id="plw-arrow1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--plw-slate)" }} /></marker></defs>
  <rect x="15" y="40" width="170" height="120" rx="12" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)", strokeWidth: 1.5 }} />
  <text x="100" y="64" textAnchor="middle" style={{ fill: "var(--plw-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>example.spec.ts</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fill: "var(--plw-blue)" }}>
    <text x="30" y="90">goto(app)</text>
    <text x="30" y="110">fill('Buy milk')</text>
    <text x="30" y="130">press('Enter')</text>
    <text x="30" y="150">expect(...)</text>
  </g>
  <rect x="30" y="78" width="140" height="17" rx="4" style={{ fill: "var(--plw-orange)", opacity: 0.18 }}>
    <animate attributeName="y" values="78;78;98;98;118;118;138;138;78" keyTimes="0;0.15;0.2;0.35;0.4;0.55;0.6;0.9;1" dur="8s" repeatCount="indefinite" />
  </rect>
  <rect x="235" y="65" width="160" height="70" rx="12" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)", strokeWidth: 1.5 }} />
  <text x="315" y="96" textAnchor="middle" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 18, fontWeight: 700 }}>Playwright</text>
  <text x="315" y="116" textAnchor="middle" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 11 }}>sends each step</text>
  <rect x="445" y="30" width="180" height="140" rx="12" style={{ fill: "var(--plw-win)", stroke: "var(--plw-slate)", strokeWidth: 1.5 }} />
  <rect x="445" y="30" width="180" height="24" rx="12" style={{ fill: "var(--plw-winbar)" }} />
  <text x="535" y="47" textAnchor="middle" style={{ fill: "var(--plw-slate)", fontFamily: "var(--sans)", fontSize: 11 }}>Browser</text>
  <text x="535" y="80" textAnchor="middle" style={{ fill: "var(--plw-red)", fontFamily: "var(--sans)", fontSize: 22, fontWeight: 300 }}>todos</text>
  <rect x="460" y="92" width="150" height="22" rx="3" style={{ fill: "none", stroke: "var(--plw-slate)", strokeWidth: 1 }} />
  <text x="468" y="108" style={{ fill: "var(--plw-slate)", fontFamily: "var(--sans)", fontSize: 11 }} opacity="0">Buy milk<animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.25;0.28;0.4;0.42;1" dur="8s" repeatCount="indefinite" /></text>
  <text x="468" y="138" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12 }} opacity="0">○ Buy milk<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.42;0.45;0.95;1" dur="8s" repeatCount="indefinite" /></text>
  <text x="468" y="158" style={{ fill: "var(--plw-slate)", fontFamily: "var(--sans)", fontSize: 10 }} opacity="0">1 item left<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.42;0.45;0.95;1" dur="8s" repeatCount="indefinite" /></text>
  <path d="M185 100 H232" style={{ stroke: "var(--plw-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#plw-arrow1)" />
  <path d="M395 100 H442" style={{ stroke: "var(--plw-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#plw-arrow1)" />
  <circle r="6" cy="100" style={{ fill: "var(--plw-orange)" }}>
    <animate attributeName="cx" values="185;445;185;445;185;445;185;445" keyTimes="0;0.12;0.15;0.3;0.4;0.5;0.6;0.72" dur="8s" repeatCount="indefinite" />
  </circle>
  <g opacity="0">
    <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.75;0.78;0.95;1" dur="8s" repeatCount="indefinite" />
    <rect x="235" y="160" width="160" height="34" rx="17" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)" }} />
    <text x="315" y="182" textAnchor="middle" style={{ fill: "var(--plw-green)", fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700 }}>✓ 1 passed</text>
  </g>
</svg>
<figcaption>Your test lists the steps. Playwright sends each one to the browser. At the end, you get a result.</figcaption>
</figure>

### Why Test in a Real Browser?

- Users do not read your code. They use your site in a browser.
- A real browser runs your HTML, CSS and JavaScript together, like users see them.
- So an E2E test catches problems that small code tests miss: a button hidden behind a banner, a broken link, a form that never sends.

**Test** = code that checks your app works. A test **passes** when every check is true. It **fails** when one check is false or a step cannot finish.

**Simple lesson:** Playwright drives a real browser like a person, and tells you what broke.

## Why Playwright Was Created

- Testing in browsers used to be painful. Testers had three big problems.
- **Flaky tests:** a test passed once and failed the next time, with no code change. Usually the test clicked before the page was ready.
- **One browser only:** many tools worked well in Chrome only. Safari and Firefox bugs reached users.
- **Slow tests:** starting a new browser for every test took seconds.
- Playwright was built to fix these three problems. It waits for the page by itself, it drives three browser engines with one API, and it gives every test a fresh, fast browser context.

**Simple lesson:** Playwright was made to make browser tests reliable, fast and cross-browser.

## History of Playwright

- In **2017**, a team at Google built **Puppeteer**, a tool that controls Chrome.
- Several people from that team moved to **Microsoft** and started Playwright.
- The first public version came out in **January 2020**. Version **1.0** followed in **May 2020**.
- Python, Java and .NET versions came next, so more teams could use it.
- In **2021**, Playwright got its own test runner, **Playwright Test**, with the **Trace Viewer**.
- In **2023**, **UI Mode** arrived for watching and fixing tests.
- In **2025**, Playwright added tools for AI assistants: **Playwright MCP** and the **Test Agents**.
- Today, a new version comes out about once a month.

<figure className="plw-fig">
<svg viewBox="0 0 640 170" role="img" aria-labelledby="fig-plw-history" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-history">Timeline that appears step by step: 2017 Puppeteer, 2020 Playwright 1.0, 2021 Playwright Test and Trace Viewer, 2023 UI Mode, 2025 MCP and Test Agents</title>
  <line x1="40" y1="85" x2="600" y2="85" style={{ stroke: "var(--plw-slate)", strokeWidth: 2 }} />
  <g style={{ fontFamily: "var(--sans)" }}>
    <g><circle cx="70" cy="85" r="9" style={{ fill: "var(--plw-slate)" }} /><text x="70" y="62" textAnchor="middle" style={{ fill: "var(--ink)", fontSize: 14, fontWeight: 700 }}>2017</text><text x="70" y="115" textAnchor="middle" style={{ fill: "var(--plw-slate)", fontSize: 11 }}>Puppeteer</text><text x="70" y="130" textAnchor="middle" style={{ fill: "var(--plw-slate)", fontSize: 11 }}>(Google)</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.15;0.2;0.94;1" dur="10s" repeatCount="indefinite" /><circle cx="190" cy="85" r="9" style={{ fill: "var(--plw-green)" }} /><text x="190" y="62" textAnchor="middle" style={{ fill: "var(--ink)", fontSize: 14, fontWeight: 700 }}>2020</text><text x="190" y="115" textAnchor="middle" style={{ fill: "var(--plw-green)", fontSize: 11 }}>Playwright 1.0</text><text x="190" y="130" textAnchor="middle" style={{ fill: "var(--plw-green)", fontSize: 11 }}>(Microsoft)</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.33;0.38;0.94;1" dur="10s" repeatCount="indefinite" /><circle cx="310" cy="85" r="9" style={{ fill: "var(--plw-blue)" }} /><text x="310" y="62" textAnchor="middle" style={{ fill: "var(--ink)", fontSize: 14, fontWeight: 700 }}>2021</text><text x="310" y="115" textAnchor="middle" style={{ fill: "var(--plw-blue)", fontSize: 11 }}>Playwright Test</text><text x="310" y="130" textAnchor="middle" style={{ fill: "var(--plw-blue)", fontSize: 11 }}>+ Trace Viewer</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.51;0.56;0.94;1" dur="10s" repeatCount="indefinite" /><circle cx="430" cy="85" r="9" style={{ fill: "var(--plw-purple)" }} /><text x="430" y="62" textAnchor="middle" style={{ fill: "var(--ink)", fontSize: 14, fontWeight: 700 }}>2023</text><text x="430" y="115" textAnchor="middle" style={{ fill: "var(--plw-purple)", fontSize: 11 }}>UI Mode</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.69;0.74;0.94;1" dur="10s" repeatCount="indefinite" /><circle cx="550" cy="85" r="9" style={{ fill: "var(--plw-orange)" }} /><text x="550" y="62" textAnchor="middle" style={{ fill: "var(--ink)", fontSize: 14, fontWeight: 700 }}>2025</text><text x="550" y="115" textAnchor="middle" style={{ fill: "var(--plw-orange)", fontSize: 11 }}>MCP and</text><text x="550" y="130" textAnchor="middle" style={{ fill: "var(--plw-orange)", fontSize: 11 }}>Test Agents</text></g>
  </g>
</svg>
<figcaption>From a Chrome-only tool to a full testing toolkit.</figcaption>
</figure>

**Simple lesson:** Playwright grew from Puppeteer's ideas into Microsoft's full browser testing toolkit.

## Features of Playwright

- **Cross-browser:** one test runs in Chromium, Firefox and WebKit.
- **Auto-waiting:** actions wait until an element is ready. You do not add sleeps.
- **Web-first assertions:** checks like `toHaveText()` keep retrying until they pass or time runs out.
- **Isolation:** every test gets a clean browser context. Tests do not affect each other.
- **Parallel runs:** many tests run at the same time to save time.
- **Strong locators:** find elements the way users see them, by role, label or text.
- **Tools:** Codegen, UI Mode, Trace Viewer and a VS Code extension.
- **More than clicks:** network mocking, API tests, screenshots, mobile emulation and accessibility checks.

### One Test, Three Browsers

- In a real project, the config file lists **projects**. Each project is one browser.
- Then `npx playwright test` runs every test once per project.
- Run this test. The report shows it three times, once for each browser.
- **In this lab,** all three rows run in your own browser. A real project starts real Chromium, Firefox and WebKit.

- Look at the `[chromium]`, `[firefox]` and `[webkit]` labels in the terminal. **Playwright Configuration** shows how to set up projects.

**Simple lesson:** Playwright gives you reliable waits, clean tests and many browsers out of the box.

## Supported Browsers

- Playwright drives three **browser engines**.
- **Browser engine** = the part of a browser that reads HTML and CSS and draws the page.
- **Chromium** powers Google Chrome and Microsoft Edge.
- **Firefox** is Mozilla's browser.
- **WebKit** powers Apple Safari.
- Playwright downloads its own tested build of each engine when you install it.

WebKit is why many teams pick Playwright. You can check Safari-like behaviour on Windows or Linux, with no Mac.

**Simple lesson:** three engines, Chromium, Firefox and WebKit, cover almost every browser your users have.

## Supported Languages

- You can write Playwright tests in four languages.

| Language | Package | Test runner |
|---|---|---|
| JavaScript and TypeScript | `@playwright/test` (npm) | Playwright Test (built in) |
| Python | `playwright` (pip) | pytest, with `pytest-playwright` |
| Java | `com.microsoft.playwright` (Maven) | JUnit or TestNG |
| .NET (C#) | `Microsoft.Playwright` (NuGet) | MSTest, NUnit or xUnit |

- The ideas are the same in every language. Only the spelling changes.
- Here is the same check in Python, for comparison:

```python
expect(page.get_by_test_id("todo-title")).to_have_text("Buy milk")
```

- This course uses **TypeScript** and **JavaScript**, because the Node.js version has the most features. The test runner, UI Mode and Test Agents are all built for it.

Pick **TypeScript** or **JavaScript** in the top bar. Every code box in this course switches. TypeScript is the default for new Playwright projects.

**Simple lesson:** four languages, one set of ideas. This course teaches the TypeScript and JavaScript version.

## Supported Platforms

- Playwright runs on the three big desktop systems.
- **Windows:** Windows 11 and later, Windows Server 2019 and later, or WSL.
- **macOS:** macOS 14 (Sonoma) and later.
- **Linux:** Debian 12 or 13, and Ubuntu 22.04, 24.04 or 26.04.
- It also runs on CI servers and in Docker. **CI/CD and Scaling** shows how.
- These versions change over time. Always check the system requirements in the docs.

**Simple lesson:** Playwright runs on Windows, macOS and Linux, on your laptop and on CI.

## How Playwright Works

### Playwright Architecture

- Your test runs in **Node.js**, not inside the browser.
- Playwright opens **one connection** to the browser and keeps it open.
- It sends small messages over that connection: "go to this page", "click this button".
- The browser sends messages back: "done", "page loaded", "a request was sent".
- Chromium speaks the **Chrome DevTools Protocol (CDP)**. For Firefox and WebKit, Playwright uses its own builds that speak a similar protocol.

<figure className="plw-fig">
<svg viewBox="0 0 640 220" role="img" aria-labelledby="fig-plw-arch" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-arch">Animation: the test in Node.js talks to Playwright, which keeps one open connection to each browser. A click message goes out, and a done message comes back</title>
  <rect x="15" y="60" width="150" height="100" rx="12" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)", strokeWidth: 1.5 }} />
  <text x="90" y="98" textAnchor="middle" style={{ fill: "var(--plw-blue)", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>Your test</text>
  <text x="90" y="118" textAnchor="middle" style={{ fill: "var(--plw-blue)", fontFamily: "var(--sans)", fontSize: 11 }}>runs in Node.js</text>
  <rect x="215" y="60" width="150" height="100" rx="12" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)", strokeWidth: 1.5 }} />
  <text x="290" y="98" textAnchor="middle" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>Playwright</text>
  <text x="290" y="118" textAnchor="middle" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 11 }}>keeps the connection</text>
  <g style={{ fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>
    <rect x="470" y="20" width="155" height="44" rx="10" style={{ fill: "var(--plw-win)", stroke: "var(--plw-blue)" }} /><text x="547" y="47" textAnchor="middle" style={{ fill: "var(--plw-blue)" }}>Chromium (CDP)</text>
    <rect x="470" y="88" width="155" height="44" rx="10" style={{ fill: "var(--plw-win)", stroke: "var(--plw-orange)" }} /><text x="547" y="115" textAnchor="middle" style={{ fill: "var(--plw-orange)" }}>Firefox</text>
    <rect x="470" y="156" width="155" height="44" rx="10" style={{ fill: "var(--plw-win)", stroke: "var(--plw-purple)" }} /><text x="547" y="183" textAnchor="middle" style={{ fill: "var(--plw-purple)" }}>WebKit</text>
  </g>
  <path d="M165 110 H215" style={{ stroke: "var(--plw-slate)", strokeWidth: 2 }} />
  <path d="M365 110 C420 110 420 42 470 42" style={{ stroke: "var(--plw-slate)", strokeWidth: 2, fill: "none" }} />
  <path d="M365 110 H470" style={{ stroke: "var(--plw-slate)", strokeWidth: 2, fill: "none" }} />
  <path d="M365 110 C420 110 420 178 470 178" style={{ stroke: "var(--plw-slate)", strokeWidth: 2, fill: "none" }} />
  <g>
    <rect width="54" height="20" rx="10" y="88" style={{ fill: "var(--plw-orange)" }}>
      <animate attributeName="x" values="140;400;400" keyTimes="0;0.4;1" dur="5s" repeatCount="indefinite" />
      <animate attributeName="opacity" values="1;1;0;0" keyTimes="0;0.4;0.42;1" dur="5s" repeatCount="indefinite" />
    </rect>
    <text y="102" textAnchor="middle" style={{ fill: "#fff", fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>click<animate attributeName="x" values="167;427;427" keyTimes="0;0.4;1" dur="5s" repeatCount="indefinite" /><animate attributeName="opacity" values="1;1;0;0" keyTimes="0;0.4;0.42;1" dur="5s" repeatCount="indefinite" /></text>
    <rect width="54" height="20" rx="10" y="122" opacity="0" style={{ fill: "var(--plw-green)" }}>
      <animate attributeName="x" values="400;400;140;140" keyTimes="0;0.5;0.9;1" dur="5s" repeatCount="indefinite" />
      <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.49;0.5;0.9;0.92" dur="5s" repeatCount="indefinite" />
    </rect>
    <text y="136" textAnchor="middle" opacity="0" style={{ fill: "#fff", fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>done<animate attributeName="x" values="427;427;167;167" keyTimes="0;0.5;0.9;1" dur="5s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.49;0.5;0.9;0.92" dur="5s" repeatCount="indefinite" /></text>
  </g>
</svg>
<figcaption>Messages travel both ways on one open connection. That is why Playwright is fast and hears every page event.</figcaption>
</figure>

### Browser Contexts and Pages

- Playwright organizes the browser in three levels.
- **Browser** = one running browser program.
- **Browser context** = a fresh, private profile inside that browser, like a new incognito window. It has its own cookies and storage.
- **Page** = one tab inside a context.
- Making a new context takes milliseconds. Starting a browser takes much longer.
- So Playwright starts the browser once, and gives **every test its own new context**.

- Test this yourself. The first test adds a task. The second test opens the same app.
- Does the second test see the task from the first one? Run it and look at the **Steps** tab.

- **What happened:** both tests passed. The second test saw an empty list.
- Each test had its own context, so nothing leaked from the first test.
- **Browser Contexts, Pages, and Emulation** shows how to open several contexts in one test, for example a buyer and a seller.

### The Playwright Protocol

- **Protocol** = an agreed set of messages two programs use to talk.
- Older tools often send a new web request for each command, and then ask again and again: "is it ready yet?"
- Playwright keeps one connection open. The browser **tells** Playwright when things happen.
- So Playwright knows the moment a page loads, a button appears or a request finishes.
- You never write protocol messages yourself. Playwright's API does it for you.

You do not need to learn CDP to use Playwright. It is useful to know the name, because error messages and advanced tools sometimes mention it. **Playwright Library and Advanced Browser Control** shows the rare cases where you use it directly.

### Auto-Waiting Overview

- Web pages load in pieces. A button may appear half a second after the page.
- **Auto-waiting** = before an action, Playwright waits until the element is ready.
- For a click, Playwright checks that the element is **visible**, **stable** (not moving), **enabled** and **receives events** (nothing covers it).
- When every check passes, it clicks. If time runs out, the test fails with a clear message.

<figure className="plw-fig">
<svg viewBox="0 0 640 230" role="img" aria-labelledby="fig-plw-wait" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-wait">Animation: before a click, Playwright ticks four checks one by one: visible, stable, enabled, receives events. Then it clicks</title>
  <rect x="20" y="30" width="250" height="170" rx="12" style={{ fill: "var(--plw-win)", stroke: "var(--plw-slate)", strokeWidth: 1.5 }} />
  <rect x="20" y="30" width="250" height="24" rx="12" style={{ fill: "var(--plw-winbar)" }} />
  <rect x="85" y="110" width="120" height="40" rx="8" style={{ fill: "var(--plw-blue)" }} opacity="0">
    <animate attributeName="opacity" values="0;0;0.4;1;1" keyTimes="0;0.08;0.2;0.3;1" dur="7s" repeatCount="indefinite" />
    <animate attributeName="y" values="140;140;125;110;110" keyTimes="0;0.08;0.2;0.3;1" dur="7s" repeatCount="indefinite" />
  </rect>
  <text x="145" y="135" textAnchor="middle" style={{ fill: "#fff", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }} opacity="0">Place order<animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.25;0.3;1" dur="7s" repeatCount="indefinite" /></text>
  <circle cx="145" cy="130" r="0" style={{ fill: "none", stroke: "var(--plw-orange)", strokeWidth: 3 }}>
    <animate attributeName="r" values="0;0;8;30;0" keyTimes="0;0.8;0.82;0.92;1" dur="7s" repeatCount="indefinite" />
  </circle>
  <g style={{ fontFamily: "var(--sans)", fontSize: 15 }}>
    <text x="320" y="62" style={{ fill: "var(--ink)", fontWeight: 700 }}>locator.click() waits for:</text>
    <g opacity="0.35"><animate attributeName="opacity" values="0.35;0.35;1;1;0.35" keyTimes="0;0.32;0.35;0.97;1" dur="7s" repeatCount="indefinite" /><text x="330" y="96" style={{ fill: "var(--plw-green)" }}>✓ Visible</text></g>
    <g opacity="0.35"><animate attributeName="opacity" values="0.35;0.35;1;1;0.35" keyTimes="0;0.44;0.47;0.97;1" dur="7s" repeatCount="indefinite" /><text x="330" y="126" style={{ fill: "var(--plw-green)" }}>✓ Stable (not moving)</text></g>
    <g opacity="0.35"><animate attributeName="opacity" values="0.35;0.35;1;1;0.35" keyTimes="0;0.56;0.59;0.97;1" dur="7s" repeatCount="indefinite" /><text x="330" y="156" style={{ fill: "var(--plw-green)" }}>✓ Enabled</text></g>
    <g opacity="0.35"><animate attributeName="opacity" values="0.35;0.35;1;1;0.35" keyTimes="0;0.68;0.71;0.97;1" dur="7s" repeatCount="indefinite" /><text x="330" y="186" style={{ fill: "var(--plw-green)" }}>✓ Receives events (not covered)</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.8;0.82;0.97;1" dur="7s" repeatCount="indefinite" /><text x="330" y="216" style={{ fill: "var(--plw-orange)", fontWeight: 700 }}>→ Click!</text></g>
  </g>
</svg>
<figcaption>The button fades in and slides into place. Playwright waits until all four checks pass, then clicks.</figcaption>
</figure>

- Assertions wait too. `expect(...).toHaveText()` keeps checking until the text matches, up to **5 seconds** by default.
- On the practice shop page below, the order is confirmed **1.2 seconds** after the click. There is no sleep in the test. Run it.

- **What happened:** the status said "Placing your order…" first. The assertion kept retrying. It passed when the text changed.
- Look at the time in the terminal. The test took a bit more than one second, the time the page really needed.

<figure className="plw-fig">
<svg viewBox="0 0 640 170" role="img" aria-labelledby="fig-plw-sleep" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-sleep">Animation: a fixed one second sleep checks too early and fails. Auto-waiting checks the moment the page is ready at 1.2 seconds and passes</title>
  <g style={{ fontFamily: "var(--sans)", fontSize: 12 }}>
    <text x="20" y="20" style={{ fill: "var(--plw-slate)" }}>0 s</text><text x="300" y="20" style={{ fill: "var(--plw-slate)" }}>1 s</text><text x="370" y="20" style={{ fill: "var(--ink)", fontWeight: 700 }}>page ready (1.2 s)</text><text x="580" y="20" style={{ fill: "var(--plw-slate)" }}>2 s</text>
  </g>
  <line x1="384" y1="28" x2="384" y2="160" style={{ stroke: "var(--plw-green)", strokeWidth: 2, strokeDasharray: "5 4" }} />
  <text x="20" y="62" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>waitForTimeout(1000)</text>
  <rect x="20" y="72" height="22" rx="4" style={{ fill: "var(--plw-red-soft)", stroke: "var(--plw-red)" }}>
    <animate attributeName="width" values="0;290;290" keyTimes="0;0.4;1" dur="6s" repeatCount="indefinite" />
  </rect>
  <text x="320" y="89" opacity="0" style={{ fill: "var(--plw-red)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>✘ checked too early<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.4;0.42;0.95;1" dur="6s" repeatCount="indefinite" /></text>
  <text x="20" y="122" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>expect(...).toHaveText(...)</text>
  <rect x="20" y="132" height="22" rx="4" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)" }}>
    <animate attributeName="width" values="0;364;364" keyTimes="0;0.5;1" dur="6s" repeatCount="indefinite" />
  </rect>
  <text x="395" y="149" opacity="0" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>✓ passed the moment it was ready<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.5;0.52;0.95;1" dur="6s" repeatCount="indefinite" /></text>
</svg>
<figcaption>A fixed sleep guesses. On a slow day the guess is wrong. Auto-waiting checks again and again, and moves on as soon as the page is ready.</figcaption>
</figure>

Do not add `page.waitForTimeout()` to make a test pass. A fixed sleep is too short on a slow computer and wastes time on a fast one. Let Playwright's auto-waiting do the job. **Actions and Auto-Waiting** covers every check in detail.

**Simple lesson:** Playwright talks to the browser on one open line, gives each test a clean context, and waits for the page so you do not have to.

## The Playwright Ecosystem

- Playwright is a family of tools. Click each one to see what it does.

### Playwright Test Runner

- `@playwright/test` finds your test files, runs them in parallel in each browser, retries if you ask, and writes a report.
- You will use it in almost every lesson.

### Playwright Library

- The library is the browser control part on its own, with no test runner.
- You use it in scripts: take screenshots, save PDFs, fill a form every morning.

### Codegen

- `npx playwright codegen` opens a browser. As you click, it writes the test code for you.
- It picks good locators. You still clean up the code and add checks. **Your First Playwright Test** shows how.

### UI Mode and Trace Viewer

- **UI Mode** is a window to run tests, watch each step and jump back in time.
- **Trace Viewer** replays a finished run: actions, page snapshots, network calls and console logs.
- **Debugging and Tracing** teaches both.

### VS Code Extension

- The **Playwright Test for VS Code** extension adds run and debug buttons next to each test.
- It can also pick a locator when you click on the page.

### Test Agents, MCP and CLI

- These tools let AI assistants work with Playwright.
- **Test Agents:** a planner, a generator and a healer that plan, write and fix tests.
- **Playwright MCP:** lets an AI assistant drive a real browser.
- **Playwright CLI (`playwright-cli`):** browser commands made for AI coding agents.
- **AI-Assisted Testing with Agents, MCP, and CLI** covers them. Learn the basics first, so you can judge what an AI writes.

**Simple lesson:** the test runner is the heart. The other tools help you write, watch and fix tests.

## Playwright Packages

- On npm, Playwright comes in three packages. Each one holds the one below it.

| Package | What it gives you | Use it for |
|---|---|---|
| `@playwright/test` | Test runner, `test`, `expect`, fixtures, reports | Writing tests (most people) |
| `playwright` | Browser control API, and downloads browsers | Scripts that are not tests |
| `playwright-core` | Browser control API only, no browser download | Using a browser you already have |

- For testing, install `@playwright/test`. It already includes the browser control API.
- Every test file starts by importing `test` and `expect` from it:

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

Do not import `test` from `playwright`. That package has no test runner, so `test` and `expect` do not exist there. Import them from `@playwright/test`.

**Simple lesson:** for tests, you need `@playwright/test`. It includes everything else.

## Reading a Failing Test

- A test that fails is not bad news. It tells you exactly what broke.
- The test below has a mistake on purpose. Run it, open the **Terminal** tab and read the error.

- **Locator** tells you which element Playwright checked: the level 1 heading.
- **Expected** is what the test wanted: `"Todos"`.
- **Received** is what the page had: `"todos"`, with a small t.
- `toHaveText()` with a string is case-sensitive. Change `'Todos'` to `'todos'` and run it again.
- The test waited 5 seconds before it failed. It kept retrying, in case the text changed.

**Simple lesson:** read Locator, Expected and Received. They tell you what went wrong.

## Try It in Your Project

- These steps take the lesson from the lab to your own app.
- Pick one important flow in your app, like log in or search, and write down its steps as a user would.
- List which browsers your users really use. That decides your projects later.
- Bookmark the Playwright release notes. You will update often.

**Simple lesson:** use one idea from this lesson on your own app this week.

## TL;DR

- Playwright is a free Microsoft tool that drives real browsers for end-to-end tests.
- It was built to fix flaky, slow, one-browser tests.
- It supports Chromium, Firefox and WebKit, four languages, and Windows, macOS and Linux.
- Your test runs in Node.js and talks to the browser over one open connection.
- Each test gets its own browser context, so tests never share state.
- Actions and assertions wait automatically. Do not add sleeps.
- For tests, install and import from `@playwright/test`.

## Common mistakes

| Mistake | What happens | Fix |
|---|---|---|
| Adding `waitForTimeout(3000)` before a click | The test is slow, and still fails on a slow day | Remove it. Actions and `expect()` wait by themselves |
| Expecting one test to see data from another | The second test fails, because each test gets a fresh context | Make every test set up what it needs |
| Importing `test` from `playwright` | `test` is undefined and nothing runs | Import `test` and `expect` from `@playwright/test` |
| Testing only in Chromium | Safari and Firefox users meet bugs you never saw | Add Firefox and WebKit projects |
| Thinking a failing test is broken | You delete a test that found a real bug | Read Locator, Expected and Received first |

## Check Yourself

## Exercises

## What to Remember

- Playwright drives Chromium, Firefox and WebKit to test sites like a real user.
- Every test gets a clean browser context.
- Actions and assertions wait by themselves. Never add fixed sleeps.
- Read Locator, Expected and Received when a test fails.
- For tests, use `@playwright/test`.

Next: **Installation and Project Setup**
