# Your First Playwright Test

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

> The parts of a test (import, test(), the page fixture, page.goto()), writing a test that acts and checks, running tests from the command line in one file or one browser, headless and headed mode, reading results (passed, failed, flaky, skipped), the HTML report, and recording tests with Codegen.

## Your First Playwright Test

- Now you will write a test yourself, line by line.
- Every Playwright test has the same small shape. Once you know it, you can read any test.
- Then you will run tests in different ways, read the results and record a test with Codegen.

**Simple lesson:** a test opens a page, does what a user does, and checks the result.

### A Real-World Example

- Priya adds a "mark as done" feature to her to-do app.
- She wants a test for it: add a task, tick it, and check that the counter says **0 items left**.
- Here is her test. Press **▶ Run tests**, then open the **Steps** tab to see each action.

- **What happened:** the test typed a task, ticked its checkbox and checked the counter.
- The Steps tab lists each action with its time.

### What You Will Learn

1. The parts of a test.
2. How to write a test that acts and checks.
3. How to run all tests, one file, or one browser.
4. Headless and headed mode.
5. How to read the results and the HTML report.
6. How to record a test with Codegen.

## Anatomy of a Test

- Every test file has the same building blocks. The animation points at each one.

<figure className="plw-fig">
<svg viewBox="0 0 640 230" role="img" aria-labelledby="fig-plw-anatomy" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-anatomy">Animation: the parts of a test are highlighted one by one: the import line, test with its title, the page fixture, page.goto, an action, and an assertion</title>
  <rect x="10" y="10" width="400" height="210" rx="10" style={{ fill: "var(--plw-term-bg)" }} />
  <g style={{ fontFamily: "var(--mono)", fontSize: 12, fill: "#e6e6e6" }}>
    <text x="24" y="40">import {"{"} test, expect {"}"} from '@playwright/test';</text>
    <text x="24" y="76">test('has a title', async ({"{"} page {"}"}) =&gt; {"{"}</text>
    <text x="40" y="104">await page.goto('https://…');</text>
    <text x="40" y="132">await page.getByRole('link').click();</text>
    <text x="40" y="160">await expect(page).toHaveTitle(/Shop/);</text>
    <text x="24" y="188">{"}"});</text>
  </g>
  <rect x="18" y="24" width="385" height="24" rx="4" style={{ fill: "none", stroke: "var(--plw-orange)", strokeWidth: 2 }} opacity="0"><animate attributeName="opacity" values="0;1;1;0;0" keyTimes="0;0.02;0.15;0.17;1" dur="12s" repeatCount="indefinite" /></rect>
  <rect x="18" y="60" width="160" height="24" rx="4" style={{ fill: "none", stroke: "var(--plw-orange)", strokeWidth: 2 }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.17;0.19;0.32;0.34;1" dur="12s" repeatCount="indefinite" /></rect>
  <rect x="210" y="60" width="78" height="24" rx="4" style={{ fill: "none", stroke: "var(--plw-orange)", strokeWidth: 2 }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.34;0.36;0.49;0.51;1" dur="12s" repeatCount="indefinite" /></rect>
  <rect x="34" y="88" width="230" height="24" rx="4" style={{ fill: "none", stroke: "var(--plw-orange)", strokeWidth: 2 }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.51;0.53;0.66;0.68;1" dur="12s" repeatCount="indefinite" /></rect>
  <rect x="34" y="116" width="295" height="24" rx="4" style={{ fill: "none", stroke: "var(--plw-orange)", strokeWidth: 2 }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.68;0.7;0.83;0.85;1" dur="12s" repeatCount="indefinite" /></rect>
  <rect x="34" y="144" width="310" height="24" rx="4" style={{ fill: "none", stroke: "var(--plw-orange)", strokeWidth: 2 }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.85;0.87;0.98;1" dur="12s" repeatCount="indefinite" /></rect>
  <g style={{ fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>
    <text x="430" y="110" opacity="0" style={{ fill: "var(--plw-blue)" }}>1. Import test and expect<animate attributeName="opacity" values="0;1;1;0;0" keyTimes="0;0.02;0.15;0.17;1" dur="12s" repeatCount="indefinite" /></text>
    <text x="430" y="110" opacity="0" style={{ fill: "var(--plw-blue)" }}>2. test(title, body)<animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.17;0.19;0.32;0.34;1" dur="12s" repeatCount="indefinite" /></text>
    <text x="430" y="110" opacity="0" style={{ fill: "var(--plw-blue)" }}>3. The page fixture<animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.34;0.36;0.49;0.51;1" dur="12s" repeatCount="indefinite" /></text>
    <text x="430" y="110" opacity="0" style={{ fill: "var(--plw-blue)" }}>4. Open a page<animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.51;0.53;0.66;0.68;1" dur="12s" repeatCount="indefinite" /></text>
    <text x="430" y="110" opacity="0" style={{ fill: "var(--plw-blue)" }}>5. Act like a user<animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.68;0.7;0.83;0.85;1" dur="12s" repeatCount="indefinite" /></text>
    <text x="430" y="110" opacity="0" style={{ fill: "var(--plw-blue)" }}>6. Check the result<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.85;0.87;0.98;1" dur="12s" repeatCount="indefinite" /></text>
  </g>
</svg>
<figcaption>Six parts: import, test, page, goto, an action, and an assertion.</figcaption>
</figure>

### Importing test and expect

- `import { test, expect } from '@playwright/test';` is the first line of almost every test file.
- `test` declares a test. `expect` checks a result.

### The test() Function

- `test('title', async ({ page }) => { ... })` declares one test.
- The **title** is a short sentence about what the test proves. It appears in every report.
- The **body** is an `async` function. Playwright calls it when the test runs.
- **async** = a function that can wait. Inside it, `await` waits for each step to finish.

### The page Fixture (First Look)

- `{ page }` asks Playwright for a **page**: a fresh browser tab, made just for this test.
- **Fixture** = something Playwright prepares for your test and cleans up after it.
- You never start or close the browser yourself. **Fixtures** explains how this works.

### Navigating with page.goto()

- `await page.goto(url)` opens a web address and waits for the page to load.
- Always `await` it. Without `await`, the test moves on before the page is there.

### Test File Naming

- Playwright runs files whose names end in `.spec.ts` or `.test.ts` (or `.js`).
- Name the file after the feature it tests: `login.spec.ts`, `cart.spec.ts`.
- A file called `login.ts` is **not** a test file. Playwright skips it.

**await** = "wait until this step is done". Every Playwright action and every web-first assertion returns a promise. Put `await` in front of each one.

**Simple lesson:** import, `test()`, `{ page }`, `page.goto()`, then actions and checks.

## Writing a Test

### Writing a Simple Test

- The smallest useful test opens a page and checks one thing.
- This one opens the Practice Shop and checks the page title.

- `/Practice Shop/` is a **regular expression**. It means "the title contains Practice Shop".
- A plain string, `'Home | Practice Shop'`, would need the whole title to match.

### Interacting with the Page and Adding an Assertion

- A real test does something, then checks the result.
- Below, the test searches the product list, then checks how many products are left.

- **Assertion** = a check that must be true. If it is false, the test fails.
- Good tests follow three steps: **arrange** (open the page), **act** (click, type), **assert** (check).

**Simple lesson:** open the page, act like a user, then assert what the user should see.

## Running Tests

### npx playwright test

- `npx playwright test` runs every test file, in every browser listed in the config.
- In the labs, the command line sits under the editor. You can edit it like a real terminal.

- Two files, two tests, three browsers: six results.
- Now change the command and run it again:

| Command | What it runs |
|---|---|
| `npx playwright test` | Every test, in every project |
| `npx playwright test cart` | Only files whose name contains `cart` |
| `npx playwright test tests/cart.spec.ts` | One file |
| `npx playwright test --project=chromium` | Every test, in Chromium only |
| `npx playwright test cart --project=firefox` | One file, in Firefox only |

- Each browser in the config is a **project**. **Playwright Configuration** explains projects.

### Headless vs Headed Mode

- **Headless** = the browser runs with no window. This is the default. It is fast and works on servers.
- **Headed** = you see the browser window. Add `--headed` to watch your test run.
- In the lab, the **Browser** tab always shows the page, so you can watch every run.

### Reading Terminal Output

- Each line of the output is one test in one browser:

```text
  ✓  1 [chromium] › tests/cart.spec.ts:3:1 › adding a product updates the cart count (612ms)
```

- `✓` = the result. `1` = the order. `[chromium]` = the project. Then the file, the line and column, the title and the time.

### Passed, Failed, Flaky and Skipped

- Run this file. It has one test of each kind. The command adds `--retries=1`, so a failed test gets one more try.

| Result | Mark | Meaning |
|---|---|---|
| Passed | ✓ | Every step worked and every check was true |
| Failed | ✘ | A step or a check failed, on every try |
| Flaky | ✘ then ✓ | It failed first, then passed on a retry. Something is unreliable |
| Skipped | - | The test did not run, because you told Playwright to skip it |

A flaky test is a warning, not a success. Find out why it failed the first time. **Playwright Configuration** shows how retries work.

**Simple lesson:** `npx playwright test` runs everything. Add a file name or `--project` to run less.

## Viewing Results

### Opening the HTML Report

- A new project uses the **HTML reporter**. After each run, it writes `playwright-report/index.html`.
- `npx playwright show-report` opens it in your browser.
- When a test fails, the report opens by itself.

<figure className="plw-fig">
<svg viewBox="0 0 640 210" role="img" aria-labelledby="fig-plw-report" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-report">Sketch of an HTML report: filter tabs All, Passed, Failed, Flaky, Skipped, then a list of tests with their browser and time</title>
  <rect x="10" y="10" width="620" height="190" rx="10" style={{ fill: "var(--plw-win)", stroke: "var(--plw-slate)" }} />
  <rect x="24" y="24" width="250" height="26" rx="6" style={{ fill: "var(--panel2)", stroke: "var(--line)" }} />
  <text x="36" y="42" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>Search tests</text>
  <g style={{ fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>
    <text x="300" y="42" style={{ fill: "var(--ink)" }}>All 6</text>
    <text x="350" y="42" style={{ fill: "var(--plw-green)" }}>Passed 4</text>
    <text x="420" y="42" style={{ fill: "var(--plw-red)" }}>Failed 1</text>
    <text x="485" y="42" style={{ fill: "var(--plw-orange)" }}>Flaky 1</text>
    <text x="545" y="42" style={{ fill: "var(--muted)" }}>Skipped 0</text>
  </g>
  <g style={{ fontFamily: "var(--sans)", fontSize: 12 }}>
    <rect x="24" y="64" width="592" height="30" rx="6" style={{ fill: "var(--plw-red-soft)" }} />
    <text x="36" y="83" style={{ fill: "var(--plw-red)", fontWeight: 700 }}>✘ checkout shows the total</text><text x="420" y="83" style={{ fill: "var(--ink2)" }}>chromium · cart.spec.ts:12</text><text x="580" y="83" textAnchor="end" style={{ fill: "var(--ink2)" }}>5.2s</text>
    <rect x="24" y="100" width="592" height="30" rx="6" style={{ fill: "var(--plw-orange-soft)" }} />
    <text x="36" y="119" style={{ fill: "var(--plw-orange)", fontWeight: 700 }}>± sync completes</text><text x="420" y="119" style={{ fill: "var(--ink2)" }}>firefox · sync.spec.ts:3</text><text x="580" y="119" textAnchor="end" style={{ fill: "var(--ink2)" }}>1.6s</text>
    <rect x="24" y="136" width="592" height="30" rx="6" style={{ fill: "var(--plw-green-soft)" }} />
    <text x="36" y="155" style={{ fill: "var(--plw-green)", fontWeight: 700 }}>✓ home page has the shop name</text><text x="420" y="155" style={{ fill: "var(--ink2)" }}>webkit · home.spec.ts:3</text><text x="580" y="155" textAnchor="end" style={{ fill: "var(--ink2)" }}>0.9s</text>
  </g>
  <text x="320" y="190" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>Click a test to see its steps, its error and its trace.</text>
</svg>
<figcaption>A sketch of the HTML report. Filter by result, then open a test to see what happened.</figcaption>
</figure>

- The lab shows the same facts in the **Terminal** and **Steps** tabs. **Reporting** covers the HTML report in depth.

**Simple lesson:** `npx playwright show-report` opens a clickable report of the last run.

## Recording Tests with Codegen

- **Codegen** = a tool that writes test code while you click around a page.
- In a real project, `npx playwright codegen https://example.com` opens two windows: a browser and the **Inspector**, which shows the code.
- Try the practice version below. Add two tasks, tick one, then click **Assert text** and click the counter.

### Recording Actions and Assertions

- Every click, fill and key press becomes one line of code.
- The assertion buttons add checks: **Assert visibility**, **Assert text** and **Assert value**.
- Codegen picks good locators for you: roles, labels and test ids first.

### Copying Generated Locators

- The Inspector also has a **Pick locator** button. Click any element, and it shows the best locator for it.
- **Locators** has a practice picker.

### Codegen Options

| Option | Example | What it does |
|---|---|---|
| `--device` | `npx playwright codegen --device="iPhone 13" <url>` | Records on an emulated phone |
| `--viewport-size` | `npx playwright codegen --viewport-size="800,600" <url>` | Sets the window size |
| `--target` | `npx playwright codegen --target=python <url>` | Writes code in another language |
| `--output` | `npx playwright codegen -o tests/new.spec.ts <url>` | Saves the code to a file |

### Cleaning Up Generated Tests

- Recorded code is a draft. Before you keep it:
1. Give the test a real title. Codegen calls every test `'test'`.
2. Delete extra clicks you made by accident.
3. Add assertions. A test with no `expect()` proves almost nothing.
4. Move repeated steps into a helper. **Page Object Model** shows how.

### Limits of Generated Code

- Codegen records **what** you did, not **why**. It cannot know what result matters.
- It may pick a weak locator, such as one based on text that changes.
- Use it to start fast. Then edit the test like any code you write.

Codegen is great for learning. Record a flow, then read the code. You will learn locators quickly.

**Simple lesson:** Codegen writes a first draft. You add a title, assertions and clean-up.

## Try It in Your Project

- These steps take the lesson from the lab to your own app.
- Record one flow of your app with `npx playwright codegen <your-url>`.
- Clean the recording: rename the test, replace weak locators, add a real assertion at the end.
- Run it headed once, then headless, and open the HTML report.

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

## TL;DR

- A test has an import, `test()`, the `page` fixture, `page.goto()`, actions and assertions.
- Put `await` before every action and web-first assertion.
- Test files end in `.spec.ts` or `.test.ts`.
- `npx playwright test` runs everything. Add a file name or `--project` to run less.
- Results are passed, failed, flaky or skipped.
- `npx playwright show-report` opens the HTML report.
- `npx playwright codegen` records a draft test.

## Common mistakes

| Mistake | What happens | Fix |
|---|---|---|
| Forgetting `await` before `page.goto()` | The next step runs before the page loads | Put `await` before every Playwright call |
| Naming the file `login.ts` | Playwright finds no tests in it | Name it `login.spec.ts` |
| A test with no `expect()` | It passes even when the feature is broken | Add at least one assertion |
| Keeping Codegen's `test('test', ...)` | Reports are full of tests called "test" | Give each test a clear title |
| Ignoring flaky tests | Real bugs hide behind retries | Fix the cause of the first failure |

## Check Yourself

## Exercises

## What to Remember

- A test is: import, `test()`, `{ page }`, `goto`, act, assert.
- `await` every Playwright call.
- Run less with a file name or `--project`.
- Flaky means "unreliable", even if it passed in the end.
- Codegen gives you a draft. You add the meaning.

Next: **Locators**
