End-to-end tests in real browsers, from your first click to a fast, trustworthy suite.
Playwright drives Chromium, Firefox and WebKit from code, so you can check a web app the way people use it. This course starts from what a test is and how a browser works, then builds up locators, assertions, fixtures, network stand-ins, sign-in, screenshots and accessibility checks, and ends with parallel runs in CI and the habits that keep a large suite fast and free of flaky tests.
What you’ll be able to do
Explain what end-to-end tests are for, and write and run one from an empty folder.
Find elements by role, label and text, act like a user, and check results with assertions that wait on their own.
Organise a suite with hooks, fixtures and page objects, and sign in once for many tests.
Stand in for a server with page.route, test APIs directly, and check screenshots and accessibility.
Configure browsers and phones, run in parallel and in CI, and debug any failure from its trace.
Find the root cause of flaky tests and design a suite that stays fast and reliable as it grows.
Modules
What testing is, and why a real browser· 8 slides
What a browser does when you click· 8 slides
Setting up Playwright· 8 slides
Your first test, line by line· 7 slides
Running tests and reading the results· 7 slides
Finding elements with locators· 8 slides
Narrowing and combining locators· 8 slides
Acting like a user, and auto-waiting· 8 slides
Assertions that wait· 8 slides
Organising tests: groups, hooks, steps and tags· 9 slides
Configuration, projects and devices· 8 slides
Fixtures: what a test is given· 8 slides
Page objects and reusable helpers· 8 slides
The network: stand-ins and API tests· 8 slides
Signing in once: authentication and storage state· 8 slides
Screenshots and accessibility checks· 8 slides
Parallel runs, sharding and CI· 8 slides
Flaky tests: root causes and real fixes· 9 slides
Designing a suite that scales· 8 slides
Mastery challenge· 9 slides
Final project: tests for a room booking app· 18 slides
Interactive parts (exercises, code to run, walkthroughs, flashcards, checklists) are drawn still and marked “Interactive · disabled in the PDF”. Do them in the app; the answers are at the end.
Module 1: What testing is, and why a real browser
Playwright1 / 179
Module 1 · What testing is, and why a real browser
Every app gets checked, by hand or by code
Picture a small to-do app. You type "Buy milk", press Enter, and the item appears in the list. Each time someone changes the app, somebody should check that this still works. Doing it by hand is called manual testing: open the app, type, press, look.
Manual checks are slow, boring and easy to forget. After the tenth change nobody re-checks the sign-in page, and that is when it breaks. An automated test is a small program that does the checking for you, the same way every time, in seconds.
Test
A small program that uses your app and checks the result is what you expected.
Manual testing
A person clicking through the app to see whether it works.
Automated test
A test written as code, so a computer can run it again and again.
Playwright2 / 179
Module 1 · What testing is, and why a real browser
A test does three things: prepare, act, check
Almost every test, in any language, has the same three parts. Testers call them arrange, act, assert. Read it as a recipe for one check.
Arrange: get to the starting point. Open the to-do app with an empty list.
Act: do the thing a person would do. Type "Buy milk" and press Enter.
Assert: check what should now be true. The list shows one item, "Buy milk".
If the check holds, the test passes. If it does not, the test fails and tells you what it expected and what it found. A failing test is not a disaster: it is the test doing its job, catching a problem before a real person does.
Put in orderInteractive · disabled in the PDF
Put the parts of a test for "adding a product to the cart" in order.
1.Open the shop’s product page
2.Check the cart badge now says 1
3.Click "Add to cart"
Playwright3 / 179
Module 1 · What testing is, and why a real browser
Small tests check pieces, big tests check journeys
Tests come in sizes. A unit test checks one small piece of code on its own, like a function that adds up a cart’s total. An integration test checks a few pieces working together. An end-to-end test (often written E2E) checks the whole app the way a person uses it: a real browser, real pages, real clicks.
Kind
Checks
Speed
Example
Unit
One function
Milliseconds
total([2, 3]) is 5
Integration
A few parts together
Fast
The cart saves to the database
End-to-end
The whole app in a browser
Seconds
Sign in, add a shirt, pay
Unit test
A test of one small piece of code, run without a browser.
End-to-end (E2E) test
A test that drives the real app in a real browser, start to finish.
Playwright4 / 179
Module 1 · What testing is, and why a real browser
Many small tests, fewer big ones
End-to-end tests are the most realistic, but they are slower and have more parts that can go wrong. Teams usually write many unit tests, some integration tests, and a smaller set of end-to-end tests for the journeys that matter most. People draw this as a pyramid.
End-to-enda few: sign-in, checkout, the main flows
Integrationsome: parts working together
Unitmany: each function on its own
The test pyramid: the higher up, the fewer tests, each one slower and broader.
QuizInteractive · disabled in the PDF
You want to be sure a shopper can sign in, add a mug to the cart and reach the payment page. Which kind of test fits best?
You want to be sure a shopper can sign in, add a mug to the cart and reach the payment page. Which kind of test fits best?
Playwright5 / 179
Module 1 · What testing is, and why a real browser
Some bugs only show up in a browser
Unit tests run your code without a screen. They cannot see what a browser adds on top: how the page is laid out, whether a button is hidden behind another one, whether a script fails to load. A real browser test catches the bugs people actually meet.
A unit test sees
What a function returns
Whether the logic is right
Nothing on screen
A browser test also sees
A button covered by a cookie banner
A page too wide for a phone
A form that cannot be used with the keyboard
A script that throws while the page loads
Playwright6 / 179
Module 1 · What testing is, and why a real browser
Playwright drives real browsers from code
Playwright is a free, open-source tool from Microsoft for end-to-end testing. You write tests in JavaScript or TypeScript; Playwright opens a browser, visits your pages, clicks, types, and checks what it sees. It can drive the three main browser engines: Chromium (Chrome and Edge), Firefox, and WebKit (Safari).
Playwright Test is the test runner: it finds your tests, runs them, and reports results.
It waits for the page to be ready on its own, so tests do not need pauses.
It comes with tools to record, debug and replay tests.
This course teaches Playwright 1.65. Playwright releases often, every month or two. The ideas here have been stable for years; a few newer options are marked when they appear.
QuizInteractive · disabled in the PDF
Which browser engine does Playwright use to stand in for Safari?
Which browser engine does Playwright use to stand in for Safari?
Playwright7 / 179
Module 1 · What testing is, and why a real browser
What makes a test worth having
It checks what a person cares about: "the item appears", not "a function was called".
It fails for one reason: when it breaks, you know where to look.
It gives the same answer every time: a test that passes and fails at random is called flaky, and people stop trusting it.
It is cheap to read: someone new can tell what it checks in a few seconds.
Flaky test
A test that sometimes passes and sometimes fails with no change to the code.
QuizInteractive · disabled in the PDF
A test passes on Monday, fails on Tuesday, and passes again on Wednesday. Nobody changed the app. What is it?
A test passes on Monday, fails on Tuesday, and passes again on Wednesday. Nobody changed the app. What is it?
Playwright8 / 179
Module 1 · What testing is, and why a real browser
Recap
A test prepares, acts and checks: arrange, act, assert.
Unit tests check small pieces quickly; end-to-end tests check whole journeys in a browser.
Keep many small tests and a focused set of end-to-end tests.
Playwright drives Chromium, Firefox and WebKit from code.
A good test checks what users see, fails for one reason, and never flakes.
FlashcardsInteractive · disabled in the PDF
Arrange, act, assert
Prepare the starting point, do the action, check the result.
Unit test
A fast test of one small piece of code, without a browser.
End-to-end test
A test that uses the whole app in a real browser, like a person would.
Flaky test
One that passes and fails at random with no change to the code.
WebKit
The browser engine behind Safari, one of the three Playwright drives.
Module 2: What a browser does when you click
Playwright9 / 179
Module 2 · What a browser does when you click
A web page starts as text called HTML
When you visit an address, the browser asks a server (a computer somewhere that hosts the app) for the page. The server answers with HTML: plain text made of tags that say what is on the page. <h1> is a big heading, <button> is a button, <input> is a box to type in.
todo.html
1<h1>My to-dos</h1>2<labelfor="new">What needs doing?</label>3<inputid="new"/>4<ul>5<li>Buy milk</li>6<li>Walk the dog</li>7</ul>
A heading, a labelled text box, and a list with two items.
Server
The computer that stores the app and sends pages to browsers.
HTML
The text that describes what is on a page, written as tags like <button>.
Element
One thing on the page made from a tag: a heading, a button, a list item.
Playwright10 / 179
Module 2 · What a browser does when you click
The browser turns HTML into a tree: the DOM
The browser reads the HTML and builds a tree of elements in memory, called the DOM (Document Object Model). Elements sit inside other elements, like folders in folders. The page you see is drawn from this tree, and when the tree changes, the screen changes.
body
h1My to-dos
labelWhat needs doing?
inputid="new"
ul
liBuy milk
liWalk the dog
The DOM for the to-do page above.
DOM
The live tree of elements the browser builds from HTML. Scripts change it; the screen follows.
Playwright11 / 179
Module 2 · What a browser does when you click
JavaScript changes the page while you watch
Most apps also send JavaScript, a programming language the browser runs. It listens for what you do and changes the DOM: when you press Enter in the to-do box, a script adds a new <li> to the list. Nothing reloads; the tree just grows.
This matters for testing because changes take time. The script might first ask the server to save the item, wait for the answer, and only then add it. A test that looks for the new item too early will not find it.
QuizInteractive · disabled in the PDF
A test types "Buy milk", presses Enter, and checks the list immediately, without waiting. The app saves to the server before showing the item. What can happen?
A test types "Buy milk", presses Enter, and checks the list immediately, without waiting. The app saves to the server before showing the item. What can happen?
Playwright12 / 179
Module 2 · What a browser does when you click
A click is a series of events
To a browser, a click is not one thing. The mouse moves over the element, the button goes down, the button comes up, and only then does a clickevent fire. Scripts listen for these events and react. A real user also cannot click something hidden, covered by something else, or greyed out.
What happens, in order, when someone clicks "Add to cart".
Event
A message the browser sends when something happens: a click, a key press, a page load.
Playwright13 / 179
Module 2 · What a browser does when you click
Pages talk to servers in the background
Besides the page itself, the browser makes many requests: for images, styles, scripts, and data. When the shop shows your cart, a script might request /api/cart and get back JSON, a text format for data. Each request gets a response with a status code: 200 means OK, 404 means not found, 500 means the server broke.
Request / response
The browser asks the server for something; the server answers.
JSON
A text format for data, like { "items": 2 }.
Status code
A number on every response: 200 OK, 404 not found, 500 server error.
Playwright14 / 179
Module 2 · What a browser does when you click
The accessibility tree: the page as a screen reader hears it
Some people use a screen reader, software that reads the page aloud. It does not see colours or layout. It reads a second tree the browser builds, the accessibility tree, where each thing has a role (what kind of thing it is: button, link, heading, textbox) and a name (what it is called: "Add to cart").
HTML
Role
Name
<button>Add to cart</button>
button
Add to cart
<a href="/help">Help</a>
link
Help
<h1>My to-dos</h1>
heading
My to-dos
<label for="e">Email</label><input id="e">
textbox
Email
Why a tester cares. Roles and names describe the page the way people do: "the Add to cart button". Playwright tests that find elements this way read like instructions and keep working when the styling changes. Module 6 builds on this.
Playwright15 / 179
Module 2 · What a browser does when you click
Practise: roles and names
Predict the outputInteractive · disabled in the PDF
Predict the outputInteractive · disabled in the PDF
A button shows only a trash-can picture and has no text. What does a screen reader announce as its name?
HTML
1<button><svg>…</svg></button>
A button shows only a trash-can picture and has no text. What does a screen reader announce as its name?
Playwright16 / 179
Module 2 · What a browser does when you click
Recap
The server sends HTML; the browser builds the DOM, a tree of elements.
JavaScript changes the DOM over time, so tests must wait for changes.
A click is a sequence of events, and only works on visible, enabled, uncovered elements.
Pages request data in the background; responses carry a status code.
The accessibility tree gives each element a role and a name.
FlashcardsInteractive · disabled in the PDF
DOM
The live tree of elements a browser builds from HTML.
Event
A message the browser sends when something happens, like a click.
Status code 404
The server could not find what was asked for.
Accessibility tree
The page as assistive tech sees it: each element has a role and a name.
Role
What kind of thing an element is: button, link, heading, textbox.
Accessible name
What an element is called: its text, its label, or its aria-label.
Module 3: Setting up Playwright
Playwright17 / 179
Module 3 · Setting up Playwright
The terminal: typing commands to your computer
Developers install and run tools by typing commands in a terminal: a window where you type a line, press Enter, and the computer answers in text. On macOS it is the Terminal app, on Windows PowerShell or Windows Terminal, on Linux usually just "Terminal". In this course, a line in a bash block is a command to type there.
Terminal
1cdmy-todo-app2ls
cd moves into a folder; ls (dir on Windows) lists what is in it.
Terminal
A text window where you type commands and read their output.
Command
One line typed into the terminal, like cd my-todo-app.
Folder (directory)
Where files live. A project is one folder with everything inside.
Playwright18 / 179
Module 3 · Setting up Playwright
Node.js runs JavaScript; npm installs tools
Playwright tests are JavaScript (or TypeScript, JavaScript with types). Outside a browser, JavaScript runs on Node.js, a program you install once from nodejs.org. Node comes with npm, which downloads packages: ready-made code other people publish, like Playwright.
Terminal
1node--version2npm--version
Both should print a version number. Playwright 1.65 needs a current Node: 20 or later.
Node.js
A program that runs JavaScript on your computer, outside the browser.
npm
The tool that installs packages and runs project scripts.
Package
Code someone published for others to install, like @playwright/test.
TypeScript
JavaScript with types, which catch mistakes before the code runs.
Playwright19 / 179
Module 3 · Setting up Playwright
One command sets up a project
Inside your project folder, run the Playwright starter. It asks a few questions; the defaults are fine. Choose TypeScript, keep the tests folder, and say yes to installing browsers.
Terminal
1npminitplaywright@latest
TypeScript or JavaScript? TypeScript: your editor will catch typos in locators and options.
Where to put tests?tests is the usual name.
Add a GitHub Actions workflow? A file that runs tests on GitHub on every change. Yes is handy; module 17 explains it.
Install browsers? Yes: Playwright downloads its own copies.
What `npx` and `@latest` mean.npm init playwright@latest runs the newest version of the starter without installing it for good. Later you will type npx playwright …: npx runs a tool installed in the project.
Playwright20 / 179
Module 3 · Setting up Playwright
What the starter creates
my-todo-app/
package.json# the project's packages and scripts
playwright.config.ts# how tests run: browsers, timeouts
tests/
example.spec.ts# a first test to read and run
.github/
workflows/
playwright.yml# runs the tests on GitHub
node_modules/# installed packages: never edit
Files ending in .spec.ts (or .test.ts) in tests/ are found and run.
Config file
playwright.config.ts: the settings every test shares.
Spec file
A file of tests. "Spec" is short for specification: what the app should do.
QuizInteractive · disabled in the PDF
Which file will Playwright run as tests by default?
Which file will Playwright run as tests by default?
Playwright21 / 179
Module 3 · Setting up Playwright
Playwright brings its own browsers
Each Playwright version is tested with exact builds of Chromium, Firefox and WebKit, so it downloads those builds into a shared folder on your computer. When you update Playwright, install its browsers again so the two match.
Terminal
1# all three browsers2npxplaywrightinstall34# only Chromium, plus the system libraries it needs (Linux)5npxplaywrightinstall--with-depschromium
A common first error. If a test fails with "Executable doesn’t exist" or a message asking you to run npx playwright install, the browsers for this Playwright version are missing. Run the install command and try again.
Playwright22 / 179
Module 3 · Setting up Playwright
Headless: a browser with no window
By default Playwright runs browsers headless: they load and run pages fully, but draw no window on your screen. It is faster and works on servers with no screen at all. When you want to watch, run headed.
Headless (default)
No window
Faster
Works on servers
Headed
A real window you can watch
Good for learning and debugging
--headed flag
QuizInteractive · disabled in the PDF
Is a headless browser a "real" browser?
Is a headless browser a "real" browser?
Playwright23 / 179
Module 3 · Setting up Playwright
Try it
Set up your first projectInteractive · disabled in the PDF
Fill the blanksInteractive · disabled in the PDF
Install only the Chromium browser for this Playwright version.
Terminal
1npxchromium
Playwright24 / 179
Module 3 · Setting up Playwright
Recap
Commands are typed in a terminal; cd moves between folders.
Node.js runs JavaScript; npm installs packages.
npm init playwright@latest creates the config, a tests folder and an example.
Playwright downloads browser builds that match its version: npx playwright install.
Tests run headless by default; --headed shows the window.
FlashcardsInteractive · disabled in the PDF
Node.js
Runs JavaScript on your computer, outside a browser.
npx
Runs a command-line tool installed in the project.
playwright.config.ts
The settings all tests share.
.spec.ts
The ending of a file that holds tests.
Headless
A full browser that draws no window. The default in Playwright.
Module 4: Your first test, line by line
Playwright25 / 179
Module 4 · Your first test, line by line
A whole test in nine lines
Playwright keeps a public to-do app for practice at demo.playwright.dev/todomvc, so you can run this test without building anything. Make a file tests/todo.spec.ts and type it in. The next slides go through it line by line.
tests/todo.spec.ts
1import{ test, expect }from'@playwright/test'23test('adds a to-do',async({ page })=>{4await page.goto('https://demo.playwright.dev/todomvc')5const box = page.getByPlaceholder('What needs to be done?')6await box.fill('Buy milk')7await box.press('Enter')8awaitexpect(page.getByTestId('todo-title')).toHaveText('Buy milk')9})
Terminal
1npxplaywrighttesttests/todo.spec.ts
Playwright26 / 179
Module 4 · Your first test, line by line
What each line does
WalkthroughInteractive · disabled in the PDF
tests/todo.spec.ts
1import{ test, expect }from'@playwright/test'23test('adds a to-do',async({ page })=>{4await page.goto('https://demo.playwright.dev/todomvc')5const box = page.getByPlaceholder('What needs to be done?')6await box.fill('Buy milk')7await box.press('Enter')8awaitexpect(page.getByTestId('todo-title')).toHaveText('Buy milk')9})
Lines 1:Import two things from Playwright: test, to declare a test, and expect, to check results.
Lines 3:test(name, function) declares one test. The name says what it checks; it shows up in the report. The function is what the test does.
Lines 3:{ page } asks Playwright for a fresh browser tab, called a page. Each test gets its own, so tests never see each other’s data.
Lines 4:page.goto(url) opens an address, like typing it in the address bar, and waits for it to load.
Lines 5:getByPlaceholder makes a locator: a description of how to find an element, here "the box whose grey hint text is …". It does not search yet.
Lines 6-7:fill types the text into the box; press presses a key. Both wait for the box to be ready first.
Lines 8:Assert: the item titles should read "Buy milk". expect keeps checking until it is true, or gives up after 5 seconds and fails.
Playwright27 / 179
Module 4 · Your first test, line by line
Why every line starts with await
Your test runs in Node.js; the page runs inside the browser, a separate program. Every command travels to the browser and back, which takes a moment. In JavaScript, a function that takes time is asynchronous: it hands back a promise, a placeholder for an answer that is coming. await means "wait here until the answer arrives".
A function that uses await must be marked async, which is why the test function is async ({ page }) => { … }.
Asynchronous
Takes time to finish, so the code waits for it instead of getting an answer at once.
Promise
A value that will exist later. await pauses until it does.
`async` function
A function allowed to use await inside.
Find the bugInteractive · disabled in the PDF
This test fails with "Error: Test ended." even when the to-do is added. Which line is wrong?
TypeScript
Playwright28 / 179
Module 4 · Your first test, line by line
Running it
Run npx playwright test in the terminal, from the project folder. Playwright finds your spec files, starts browsers, runs each test, and prints a summary.
Text
1Running 3 tests using 3 workers2 3 passed (2.1s)34To open last HTML report run:5 npx playwright show-report
Three tests? The starter config runs every test in Chromium, Firefox and WebKit.
Worker
A separate process that runs tests. Several workers run tests at the same time to finish sooner.
QuizInteractive · disabled in the PDF
You wrote one test, and the summary says "3 passed". Why?
You wrote one test, and the summary says "3 passed". Why?
Playwright29 / 179
Module 4 · Your first test, line by line
Reading a failure
Change the expected text to "Buy bread" and run again. The test fails, and the message says exactly what went wrong. Learn to read it top to bottom: which check, what it wanted, what it got, and where.
The first line names the browser, the file, the line and the test.
Expected is what you asked for; Received is what the page had.
Timeout shows it kept checking for 5 seconds before giving up.
The code frame points at the line that failed.
Playwright30 / 179
Module 4 · Your first test, line by line
Practise: write a check
Fill the blanksInteractive · disabled in the PDF
Complete a test that opens the to-do app and adds "Walk the dog".
TypeScript
1test('adds another to-do',async({})=>{2await page.('https://demo.playwright.dev/todomvc')3const box = page.getByPlaceholder('What needs to be done?')4await box.('Walk the dog')5await box.press('')6await(page.getByTestId('todo-title'))7.toHaveText('Walk the dog')8})
QuizInteractive · disabled in the PDF
When does page.getByPlaceholder('What needs to be done?') look for the element?
When does page.getByPlaceholder('What needs to be done?') look for the element?
Playwright31 / 179
Module 4 · Your first test, line by line
Recap
test(name, async ({ page }) => { … }) declares a test with its own fresh tab.
page.goto opens an address; locators describe how to find elements.
Actions like fill and press wait for the element first.
await expect(locator).toHaveText(…) checks, retrying for up to 5 seconds.
Every browser command is asynchronous: await it.
A failure shows the locator, expected, received, and the line.
FlashcardsInteractive · disabled in the PDF
page
A browser tab given to each test, fresh every time.
Locator
A description of how to find an element. It searches when used, not when made.
await
Wait for an asynchronous command to finish before the next line.
expect(…).toHaveText(…)
A check that retries until the text matches or the timeout passes.
Received vs expected
What the page had vs what the test asked for.
Module 5: Running tests and reading the results
Playwright32 / 179
Module 5 · Running tests and reading the results
Choosing which tests run
npx playwright test runs everything. As a suite grows you will often want just a part of it. Everything after test is an option (also called a flag) that changes what runs.
Command
Runs
npx playwright test
Every test, in every configured browser
npx playwright test tests/cart.spec.ts
One file
npx playwright test cart
Files whose path contains "cart"
npx playwright test tests/cart.spec.ts:12
The test on line 12
npx playwright test -g "empty cart"
Tests whose name matches "empty cart"
npx playwright test --project=chromium
Only the Chromium project
QuizInteractive · disabled in the PDF
Which command runs only the tests whose name mentions "sign in"?
Which command runs only the tests whose name mentions "sign in"?
Playwright33 / 179
Module 5 · Running tests and reading the results
A few flags you will use every day
Flag
What it does
--headed
Show the browser window while tests run
-x
Stop at the first failure
--last-failed
Run again only what failed last time
--workers=1
One test at a time, in order
--ui
Open UI mode (next slides)
--debug
Open the Playwright Inspector and step through the test one action at a time
Save your favourites as scripts. In package.json, a "scripts" entry like "e2e": "playwright test" lets you type npm run e2e. Extra flags go after --: npm run e2e -- --headed.
Playwright34 / 179
Module 5 · Running tests and reading the results
The HTML report
After each run Playwright writes a web page summarising every test, in the playwright-report folder. When a test fails on your machine, it opens by itself; otherwise open it with npx playwright show-report.
Every test, grouped by file, with pass, fail, flaky or skipped.
For each test, its steps and how long each took.
For failures: the error, the code that failed, and any screenshots or traces.
A search box and filters by browser and status.
Reporter
What turns results into output: a list in the terminal, an HTML page, a file for CI.
`test-results/`
The folder of files from the last run: screenshots, videos and traces of failed tests.
Playwright35 / 179
Module 5 · Running tests and reading the results
UI mode: watch, rerun, and step back in time
npx playwright test --ui opens a window listing all your tests. Click one to run it. Afterwards, every action is listed on the left; click an action to see the page as it was at that moment, before and after, with the network requests and console messages next to it.
Watch mode (the eye icon): rerun a test whenever you save its file.
Pick locator: hover over the page snapshot to get a locator for any element.
Filters: by name, browser, or status.
It is the best way to learn, because you can see what each line did.
QuizInteractive · disabled in the PDF
A test fails at the 6th step and you want to see what the page looked like just before. What do you use?
A test fails at the 6th step and you want to see what the page looked like just before. What do you use?
Playwright36 / 179
Module 5 · Running tests and reading the results
Codegen: record a test by clicking
Codegen (short for code generator) opens a browser and a recorder side by side. As you click and type, it writes the test code for you, choosing locators by role and text where it can.
Add checks with the toolbar buttons: "assert visibility", "assert text", "assert value".
Copy the code into a spec file, then tidy it: good names, fewer steps, clear checks.
--device="iPhone 13" records on a simulated phone.
A starting point, not a finished test. Codegen records what you did, including stray clicks. It cannot know what you meant to check. Treat its output as a draft and read every line.
Playwright37 / 179
Module 5 · Running tests and reading the results
Traces: a recording of a test run
A trace is a file that records a whole test: each action, a snapshot of the page before and after, network requests, console messages and errors. The starter config records one when a test is retried after failing, which is how you debug a failure that happened on another computer.
Terminal
1# record a trace for every test this time2npxplaywrighttest--traceon34# open one5npxplaywrightshow-tracetest-results/…/trace.zip
Put in orderInteractive · disabled in the PDF
A test fails. Put a sensible way to investigate in order.
1.Open the trace or UI mode and look at the page at that step
2.Fix the test or the app, then run that test again
3.Read the error: which check, expected, received
4.Open the report and look at the failed step
Playwright38 / 179
Module 5 · Running tests and reading the results
Recap
Pick tests by file, by line, by name (-g) or by project.
--headed, -x and --last-failed speed up everyday work.
The HTML report shows every test, its steps and its failures.
UI mode runs tests and lets you step back through each action.
Codegen records a draft test; traces record a run to replay later.
FlashcardsInteractive · disabled in the PDF
-g "text"
Run only tests whose name matches.
npx playwright show-report
Open the HTML report of the last run.
UI mode
--ui: run tests in a window, with a snapshot for every action.
Codegen
Records your clicks and typing as test code.
Trace
A file recording a test: actions, snapshots, network and console.
Module 6: Finding elements with locators
Playwright39 / 179
Module 6 · Finding elements with locators
A locator describes an element the way a person would
Before a test can click or check anything, it has to say which element. A locator is that description. The best descriptions are the ones a person would use: "the Sign in button", "the Email box", "the heading Your cart".
From here on, the course follows a small online shop: a sign-in page, a product list, a cart, and a checkout. This is its sign-in form.
the shop’s sign-in page
1<h1>Sign in</h1>2<form>3<labelfor="email">Email</label>4<inputid="email"type="email"/>5<labelfor="password">Password</label>6<inputid="password"type="password"/>7<label><inputtype="checkbox"/> Remember me</label>8<buttontype="submit">Sign in</button>9</form>10<ahref="/forgot">Forgot your password?</a>
Playwright40 / 179
Module 6 · Finding elements with locators
getByRole: find by what it is and what it is called
getByRole(role, { name }) looks in the accessibility tree (module 2) for an element with that role and name. It is the locator Playwright recommends first.
TypeScript
1page.getByRole('heading',{name:'Sign in'})2page.getByRole('textbox',{name:'Email'})3page.getByRole('checkbox',{name:'Remember me'})4page.getByRole('button',{name:'Sign in'})5page.getByRole('link',{name:'Forgot your password?'})
The name matches if it contains your text, ignoring case: { name: 'sign' } finds "Sign in".
{ exact: true } demands the whole name, with the same case.
A regular expression works too: { name: /sign in/i }.
Extra filters: level for headings ({ level: 1 }), checked, disabled, expanded, pressed, selected.
Regular expression
A text pattern written between slashes, like /sign in/i. The i means "ignore case".
Playwright41 / 179
Module 6 · Finding elements with locators
Why role first
Fragile
page.locator('#root > div > form > button.btn-blue')
Sturdy
page.getByRole('button',{name:'Sign in'})
It survives redesigns. New classes, a new layout, a <div> swapped for a <section>: the button is still a button called "Sign in".
It reads like the test plan. Anyone can tell what it clicks.
It checks accessibility for free. If the button loses its name, screen-reader users lose it too, and the test fails.
QuizInteractive · disabled in the PDF
A designer renames the class btn-blue to btn-primary. Which locator still works?
A designer renames the class btn-blue to btn-primary. Which locator still works?
Playwright42 / 179
Module 6 · Finding elements with locators
The other built-in locators
Locator
Finds
Good for
getByLabel('Email')
A form field by its label
Inputs, selects, checkboxes
getByPlaceholder('Search')
A field by its grey hint text
Fields with no label
getByText('Free delivery')
An element by its visible text
Paragraphs, badges, messages
getByAltText('Red mug')
An image by its alt text
Pictures
getByTitle('Close')
An element by its title
Tooltipped icons
getByTestId('cart-count')
An element by data-testid
When nothing else is stable
getByText and getByLabel match parts of the text and ignore case unless you pass { exact: true }, just like name in getByRole.
Playwright43 / 179
Module 6 · Finding elements with locators
Test ids: a contract with the developers
Some elements have no good role or text, or their text changes (a number, a translated label). For those, developers can add an attribute only tests use, and getByTestId finds it.
A different attribute name. If the app uses data-test or data-qa instead, set testIdAttribute under use in the config, and getByTestId will look for that.
Last, not first. A test id says nothing about what the user sees, so it cannot catch a missing label. Reach for it when role, label and text cannot do the job.
Playwright44 / 179
Module 6 · Finding elements with locators
CSS and XPath, when you must
page.locator(selector) accepts a CSS selector (the pattern language stylesheets use, like .price or ul > li) or an XPath (another pattern language for trees). They work, but they tie the test to how the page is built rather than what it shows.
TypeScript
1page.locator('.price')// CSS: elements with class "price"2page.locator('xpath=//footer//a')// XPath: links inside the footer
QuizInteractive · disabled in the PDF
Which locator is the best choice for the shop’s search box, <input aria-label="Search products">?
Which locator is the best choice for the shop’s search box, <input aria-label="Search products">?
Playwright45 / 179
Module 6 · Finding elements with locators
Strictness: one action, one element
If a locator matches several elements when you click or fill it, Playwright refuses with a strict mode violation rather than guessing. Clicking the wrong one would make the test check the wrong thing.
Text
1Error: strict mode violation: getByRole('button', { name: 'Add' })2resolved to 3 elements:3 1) <button>Add to cart</button> aka getByRole('button', …)4 2) <button>Add to cart</button> aka …5 3) <button>Add to wishlist</button> aka …
The fix is a more precise locator: a fuller name, exact: true, or narrowing to one product, which is the next module.
QuizInteractive · disabled in the PDF
The page has buttons "Add to cart" and "Add to wishlist". What does page.getByRole('button', { name: 'Add' }).click() do?
The page has buttons "Add to cart" and "Add to wishlist". What does page.getByRole('button', { name: 'Add' }).click() do?
Playwright46 / 179
Module 6 · Finding elements with locators
Recap
Prefer, in order: getByRole, getByLabel, getByPlaceholder, getByText, then getByTestId.
Names match part of the text, ignoring case; use exact: true or a regular expression to tighten.
CSS and XPath tie tests to markup: a last resort.
Actions need exactly one match, or they fail with a strict mode violation.
FlashcardsInteractive · disabled in the PDF
getByRole
Finds an element by its role and accessible name. The first choice.
getByLabel
Finds a form field by the text of its label.
getByTestId
Finds an element by data-testid. For when nothing user-facing is stable.
exact: true
Match the whole text, with the same case.
Strict mode violation
An action’s locator matched more than one element.
CSS selector
A pattern from stylesheets, like .price. Tied to how the page is built.
Module 7: Narrowing and combining locators
Playwright47 / 179
Module 7 · Narrowing and combining locators
Lists repeat the same buttons
The shop’s product page lists items, each with the same "Add to cart" button. A locator for "the Add to cart button" matches all of them. You need to say which product’s button.
HTML
1<ul>2<li>3<h2>Red mug</h2><p>£8</p>4<button>Add to cart</button>5</li>6<li>7<h2>Blue teapot</h2><p>£24</p><p>Sold out</p>8<buttondisabled>Add to cart</button>9</li>10<li>11<h2>Green bowl</h2><p>£12</p>12<button>Add to cart</button>13</li>14</ul>
Playwright48 / 179
Module 7 · Narrowing and combining locators
Chaining: look inside another element
Every locator has the same getBy… methods as page. Calling one on a locator searches only inside it. Read it left to right: "in the list, the item…, in that item, the button…".
`has` is more precise than `hasText`.hasText: 'mug' would also keep "Mug cosy" or a product whose description mentions mugs. has with a heading locator and exact: true pins the one product called exactly "Red mug".
Fill the blanksInteractive · disabled in the PDF
Keep only the products that are not sold out, then check there are two.
TypeScript
1const available = page2.getByRole('listitem')3.({:'Sold out'})4awaitexpect(available).(2)
Playwright50 / 179
Module 7 · Narrowing and combining locators
First, last, nth: use with care
first(), last() and nth(index) pick by position. Positions start at 0, so nth(1) is the second. They are handy, and fragile: if the shop sorts products differently, nth(1) is now another product, and the test silently checks the wrong thing.
When position is the point. If the test is about order ("newest to-do comes first"), positions are right. Even then, expect(list).toHaveText([...]) with the whole list in order is usually clearer.
Playwright51 / 179
Module 7 · Narrowing and combining locators
Checking a whole list at once
Pass an array to toHaveText to check every match, in order. It retries until all of them match, so it suits lists that load or change.
The page shows "Blue teapot, Red mug, Green bowl". Does the check above pass?
The page shows "Blue teapot, Red mug, Green bowl". Does the check above pass?
Playwright52 / 179
Module 7 · Narrowing and combining locators
or and and: two locators in one
a.or(b) matches elements that match either. It helps when the page can show one of two things, like a welcome banner or a cookie dialog. a.and(b) matches elements that match both, to add a condition.
Both may show. If both sides of an or are on screen at once, the locator matches two elements, and toBeVisible fails with a strict mode violation. Add .first() if both can appear together.
Playwright53 / 179
Module 7 · Narrowing and combining locators
Practise: the right product
Find the bugInteractive · disabled in the PDF
The test should add the Green bowl, but the cart ends up with a Red mug. Which line is wrong?
TypeScript
Predict the outputInteractive · disabled in the PDF
filter keeps matches by text (hasText, hasNotText), by contents (has, hasNot), or visible.
nth, first and last depend on order: prefer saying which one.
toHaveText([...]) checks a whole list, in order; toHaveCount checks how many.
or matches either locator; and matches both.
FlashcardsInteractive · disabled in the PDF
locator.filter({ hasText })
Keep matches that contain the text.
locator.filter({ has })
Keep matches that contain an element matching another locator.
nth(0)
The first match. Positions start at 0. Fragile if order changes.
a.or(b)
Matches elements matching either locator.
toHaveCount(n)
Check a locator matches exactly n elements, retrying.
Module 8: Acting like a user, and auto-waiting
Playwright55 / 179
Module 8 · Acting like a user, and auto-waiting
Clicking and typing
Actions are methods on a locator. They find the element, wait until it is ready, and then do what a person would do. Here is the shop’s sign-in, start to finish.
Your own computer, as an address. localhost:3000 is an app running on it, on port 3000.
Playwright56 / 179
Module 8 · Acting like a user, and auto-waiting
Pressing keys
press(key) presses one key, or a combination joined with +. Key names follow the browser’s: Enter, Tab, Escape, ArrowDown, Backspace, letters like a.
TypeScript
1const box = page.getByPlaceholder('What needs to be done?')2await box.fill('Buy milk')3await box.press('Enter')// submit4await box.press('ControlOrMeta+a')// select all, on any OS5await page.keyboard.press('Escape')// to whatever has focus
`fill` vs `pressSequentially`.fill sets the whole value at once, which is what you want almost always. pressSequentially(text) types one key at a time, for the rare field that reacts to each key, like a search box that suggests as you type.
check() and uncheck() do nothing if the box is already that way, then make sure it ended up right.
selectOption picks from a <select> by its value or its visible label.
setInputFiles fills a file upload field without opening the file picker.
hover() moves the mouse over, for menus that open on hover.
Fill the blanksInteractive · disabled in the PDF
Fill the checkout: choose standard delivery and accept the terms.
TypeScript
1await page.getByRole('',{name:'Standard delivery'})2.()3await page.getByLabel('I accept the terms').()4await page.getByRole('button',{name:'Place order'}).()
Playwright58 / 179
Module 8 · Acting like a user, and auto-waiting
Before every action, Playwright checks it could be done
A person cannot click a button that is hidden, still sliding in, greyed out, or under a popup. Before acting, Playwright waits until the element passes the same checks. This is auto-waiting, and it is why tests need no pauses.
Check
Means
Visible
It has a size and is not hidden
Stable
It has stopped moving (no animation in progress)
Receives events
Nothing else is on top of it at that spot
Enabled
It is not disabled
Editable
For fill: it is enabled and not read-only
Each action needs only the checks that apply: click needs all but editable, fill needs visible, enabled and editable. If they do not pass in time, the action fails and the error lists what it was waiting for.
Auto-waiting
Playwright waiting, on its own, until an element is ready for the action.
Actionability
The checks an element must pass before an action runs.
Playwright59 / 179
Module 8 · Acting like a user, and auto-waiting
When an action times out, the log says why
Text
1Error: locator.click: Test timeout of 30000ms exceeded.2Call log:3 - waiting for getByRole('button', { name: 'Place order' })4 - locator resolved to <button>Place order</button>5 - attempting click action6 - waiting for element to be visible, enabled and stable7 - element is visible, enabled and stable8 - <div class="cookie-banner">…</div> intercepts pointer events9 - retrying click action
QuizInteractive · disabled in the PDF
What stops the click above?
What stops the click above?
Playwright60 / 179
Module 8 · Acting like a user, and auto-waiting
Do not force it
click({ force: true }) skips the checks. It makes a red test go green while hiding a real problem: if a user cannot click the button, neither should your test. Fix the cause instead.
Popups that appear at random. For a dialog that may or may not appear (a newsletter offer, say), page.addLocatorHandler(locator, handler) runs your handler whenever that locator becomes visible before an action, then carries on. Better still, switch the popup off for tests.
Playwright61 / 179
Module 8 · Acting like a user, and auto-waiting
Clicks that change the page
When a click leads to another page, you do not need to wait for the navigation yourself. The next locator waits for its element, on whatever page is showing. To check you arrived, assert on the address.
4.Check nothing covers its middle, then press and release the mouse there
5.Return once the click is done
Playwright62 / 179
Module 8 · Acting like a user, and auto-waiting
Recap
fill, click, press, check, selectOption, setInputFiles, hover: actions on locators.
Before acting, Playwright waits for the element to be visible, stable, enabled and uncovered.
A timed-out action’s call log says what it was waiting for.
Never force: true to get past a real problem.
After a click that navigates, assert with toHaveURL and the next page’s content.
FlashcardsInteractive · disabled in the PDF
fill vs pressSequentially
fill sets the value at once; pressSequentially types key by key.
check()
Ticks a checkbox or radio and verifies it is ticked.
Auto-waiting
Actions wait until the element is ready, so tests need no sleeps.
"intercepts pointer events"
Something else is on top of the element.
force: true
Skips the checks. Hides real bugs: avoid.
toHaveURL
Checks the page’s address, retrying until it matches.
Module 9: Assertions that wait
Playwright63 / 179
Module 9 · Assertions that wait
A web-first assertion keeps checking
An assertion is the "check" part of a test. Playwright’s assertions on locators and pages are web-first: they look at the page, and if it is not right yet, they look again, over and over, until it is right or the time runs out (5 seconds by default). Pages change over time, so checks must wait for them.
Checks once: flaky
const shown =await page.getByText('Added to cart').isVisible()expect(shown).toBe(true)
Retries: reliable
awaitexpect(page.getByText('Added to cart')).toBeVisible()
On the left, isVisible() answers about this instant. If the message appears 100 milliseconds later, the test fails. On the right, the assertion waits for it.
Assertion
A check that a test makes. If it does not hold, the test fails.
Web-first assertion
An assertion that retries against the live page until it passes or times out.
Playwright64 / 179
Module 9 · Assertions that wait
The assertions you will use most
Assertion
Passes when
toBeVisible() / toBeHidden()
It is on screen / it is not
toHaveText('Red mug')
Its whole text is this
toContainText('mug')
Its text contains this
toHaveValue('sam@example.com')
A field holds this value
toBeChecked()
A checkbox or radio is ticked
toBeEnabled() / toBeDisabled()
It can or cannot be used
toHaveAttribute('href', '/help')
An attribute has this value
toHaveCount(3)
The locator matches 3 elements
toHaveURL(/checkout/) (on page)
The address matches
toHaveTitle('Cart') (on page)
The tab’s title matches
Text matching normalises spaces and line breaks, so layout whitespace does not matter. A regular expression works wherever a string does: toHaveText(/£\d+/) matches "£8" or "£24".
Playwright65 / 179
Module 9 · Assertions that wait
Saying what should not be there
Put .not before any assertion to flip it. It retries too: not.toBeVisible() waits until the element is gone. toBeHidden() means the same as not.toBeVisible(), and passes if the element does not exist at all.
Predict the outputInteractive · disabled in the PDF
The cart line reads "Red mug × 2". Which of these pass?
TypeScript
1const line = page.getByTestId('cart-line')2awaitexpect(line).toHaveText('Red mug')// A3awaitexpect(line).toContainText('Red mug')// B4awaitexpect(line).toHaveText(/Red mug/)// C
The cart line reads "Red mug × 2". Which of these pass?
Playwright66 / 179
Module 9 · Assertions that wait
How long assertions wait
Each assertion waits up to 5 seconds by default. You can change it for one check, or for all checks in the config. Raise it only where the app is really slow, like an order that takes a while to confirm.
TypeScript
1// one check2awaitexpect(page.getByText('Order confirmed'))3.toBeVisible({timeout:15_000})45// playwright.config.ts: every check6exportdefaultdefineConfig({expect:{timeout:10_000}})
Times are in milliseconds: 15_000 is 15 seconds (the underscore is just for reading).
Two different clocks. The expect timeout (5 s) limits each check. The test timeout (30 s) limits the whole test, including every action and check. A check cannot wait longer than the test has left.
Playwright67 / 179
Module 9 · Assertions that wait
Plain values do not retry
You can also expect ordinary values: numbers, strings, objects. These generic assertions (toBe, toEqual, toContain, toBeGreaterThan…) check once, because a plain value cannot change. Use them for data you already have, and locator assertions for the page.
TypeScript
1const total =8+242expect(total).toBe(32)3expect(['mug','teapot']).toContain('mug')4expect({items:2}).toEqual({items:2})
Find the bugInteractive · disabled in the PDF
This check is flaky: the to-dos load a moment after the page. Which line is the problem?
TypeScript
Playwright68 / 179
Module 9 · Assertions that wait
Soft assertions: keep going after a failure
Normally the first failed assertion stops the test. expect.soft records the failure and lets the test carry on, so one run shows every problem on a page. The test is still marked failed at the end.
Add a message. A second argument to expect labels the check in the report: expect(total, 'total includes delivery').toHaveText('£36'). It saves the reader a trip to the code.
Playwright69 / 179
Module 9 · Assertions that wait
Waiting for anything: expect.poll and toPass
Sometimes what you want to check is not on the page: a value from an API, say. expect.poll calls your function again and again until its result passes. expect(…).toPass() retries a whole block of code until none of it throws.
TypeScript
1// keep asking the API until the order is paid2await expect.poll(async()=>{3const res =await page.request.get('/api/orders/latest')4return(await res.json()).status5},{timeout:10_000}).toBe('paid')67// retry a few steps together8awaitexpect(async()=>{9await page.getByRole('button',{name:'Refresh'}).click()10awaitexpect(page.getByText('In stock'))11.toBeVisible({timeout:1000})12}).toPass({timeout:15_000})
page.request sends requests from the test (module 14). A short address like /api/… works once baseURL is set (module 11).
`toPass` has no timeout of its own. By default toPass keeps retrying until the test times out. Give it a timeout, and give the checks inside short ones, so each attempt fails fast.
QuizInteractive · disabled in the PDF
You need to wait until a text box shows "Saved". Which do you use?
You need to wait until a text box shows "Saved". Which do you use?
Playwright70 / 179
Module 9 · Assertions that wait
Recap
Assertions on locators and pages retry until they pass or time out: always await them.
toHaveText matches the whole text; toContainText a part; regular expressions match anywhere.
.not flips any assertion, and still retries.
Plain-value assertions check once: never read a value from the page and then toBe it.
expect.soft keeps going; expect.poll and toPass retry custom code.
FlashcardsInteractive · disabled in the PDF
Web-first assertion
Retries against the page until it passes or times out.
Default expect timeout
5 seconds per assertion.
toHaveText vs toContainText
Whole text vs a part of it.
expect.soft
Records a failure and keeps the test going.
expect.poll(fn)
Calls a function repeatedly until its result passes.
Why not isVisible() + toBe(true)?
It checks one instant and cannot retry: flaky.
Module 10: Organising tests: groups, hooks, steps and tags
Playwright71 / 179
Module 10 · Organising tests: groups, hooks, steps and tags
Each test stands on its own
Playwright gives every test a fresh browser context: like a new private window, with no cookies, no saved data, nothing left from the test before. A test must never rely on another having run first. Tests can then run in any order, alone, or at the same time.
Browsershared by the tests in one worker
Contexttest A: own cookies and storage
Page
Contexttest B: starts empty
Page
One browser, a fresh context per test. Launching a browser is slow; a context is cheap.
Browser context
An isolated browser session: its own cookies, storage and settings. Each test gets a new one.
QuizInteractive · disabled in the PDF
Test A adds a mug to the cart, which the shop keeps in the browser’s storage. Test B runs next and opens the cart. What does it see?
Test A adds a mug to the cart, which the shop keeps in the browser’s storage. Test B runs next and opens the cart. What does it see?
Playwright72 / 179
Module 10 · Organising tests: groups, hooks, steps and tags
test.describe groups related tests
test.describe(name, callback) puts tests in a named group. The group name is added in front of each test’s name in reports, and hooks inside apply only to the group.
tests/cart.spec.ts
1import{ test, expect }from'@playwright/test'23test.describe('cart',()=>{4test('starts empty',async({ page })=>{5await page.goto('http://localhost:3000/cart')6awaitexpect(page.getByText('Your cart is empty')).toBeVisible()7})89test('shows an added item',async({ page })=>{10// …11})12})
Reported as "cart › starts empty" and "cart › shows an added item".
Playwright73 / 179
Module 10 · Organising tests: groups, hooks, steps and tags
Hooks run code around tests
A hook is code Playwright runs at a set moment. beforeEach runs before every test in the file or group, which is the place for the shared "arrange" step.
TypeScript
1test.describe('to-do list',()=>{2 test.beforeEach(async({ page })=>{3await page.goto('http://localhost:3000/todo')4})56test('adds an item',async({ page })=>{/* … */})7test('marks an item done',async({ page })=>{/* … */})8})
Hook
Runs
beforeEach
Before every test
afterEach
After every test, even failed ones
beforeAll
Once, before the first test (in each worker)
afterAll
Once, after the last test (in each worker)
`beforeAll` has no `page`. A page belongs to one test, so beforeAll cannot use the page fixture. Use it for setup that does not touch a tab, like creating data through an API. Fixtures (module 12) are usually a better home even for that.
Playwright74 / 179
Module 10 · Organising tests: groups, hooks, steps and tags
Practise: hooks
Put in orderInteractive · disabled in the PDF
A file has a beforeAll, a beforeEach, an afterEach and two tests. Put what runs in order (one worker).
Module 10 · Organising tests: groups, hooks, steps and tags
test.step names parts of a long test
A checkout test does many things. Wrapping parts in test.step gives them names in the report, the trace and UI mode, so a failure says "failed in: pay by card" instead of pointing at line 37.
TypeScript
1test('buys a mug',async({ page })=>{2await test.step('add to cart',async()=>{3await page.goto('http://localhost:3000/products')4await page.getByRole('listitem').filter({hasText:'Red mug'})5.getByRole('button',{name:'Add to cart'}).click()6})7await test.step('check out',async()=>{8await page.getByRole('link',{name:'Checkout'}).click()9await page.getByRole('button',{name:'Place order'}).click()10})11awaitexpect(page.getByText('Thank you')).toBeVisible()12})
Playwright76 / 179
Module 10 · Organising tests: groups, hooks, steps and tags
Skipping, marking and focusing
Call
Means
test.skip(…)
Do not run this test
test.fixme(…)
Known broken; do not run it, and mark it to fix
test.fail(…)
Expected to fail: it is reported as an error if it passes
test.slow()
Give this test three times the usual timeout
test.only(…)
Run only this test (and other only tests)
TypeScript
1test('pays with Apple Pay',async({ page, browserName })=>{2 test.skip(browserName !=='webkit','Only Safari offers it')3// …4})
Skip with a condition and a reason, from inside the test.
Do not commit `test.only`. A forgotten only silently skips the rest of the suite. Set forbidOnly: !!process.env.CI in the config (the starter does) so a CI run fails if one slips in.
Playwright77 / 179
Module 10 · Organising tests: groups, hooks, steps and tags
Tags pick out groups of tests
A tag is a label starting with @. Give tests tags, then run just those with --grep. A common use is a fast smoke set: a handful of tests that show the app basically works.
What does npx playwright test --grep-invert @slow run?
What does npx playwright test --grep-invert @slow run?
Playwright78 / 179
Module 10 · Organising tests: groups, hooks, steps and tags
Avoid tests that depend on each other
test.describe.configure({ mode: 'serial' }) makes a group run in order and stop at the first failure. It exists for rare cases, but it couples tests: one failure hides the rest, and the group cannot be split across workers.
Coupled
Test 1 signs up a user
Test 2 signs in as that user
Test 3 deletes the account
Independent
Each test creates the user it needs
Shared setup lives in a hook or fixture
Any test can run alone
Playwright79 / 179
Module 10 · Organising tests: groups, hooks, steps and tags
Recap
Every test gets a fresh context; tests must not depend on each other.
test.describe groups tests; hooks run code before and after them.
test.step names parts of a test in reports and traces.
skip, fixme, fail, slow and only change how a test is run or reported.
Tags (@smoke) and --grep run chosen groups.
FlashcardsInteractive · disabled in the PDF
beforeEach
A hook that runs before every test in its file or group.
beforeAll
Runs once per worker before the tests. No page here.
test.step
Names a part of a test for reports and traces.
test.fixme
Known broken: skip it and mark it to be fixed.
forbidOnly
Config option that fails the run if a test.only is left in.
Tag
A label like @smoke on a test, selectable with --grep.
Module 11: Configuration, projects and devices
Playwright80 / 179
Module 11 · Configuration, projects and devices
One file sets how every test runs
playwright.config.ts exports one object made with defineConfig. The top level says how the runner behaves: where tests are, how long they may take, how many run at once. The use section sets options for the browser in every test.
Lines 1:defineConfig gives the editor types for every option; devices is a list of ready-made browser setups.
Lines 4-5: Tests live in tests/; each test may take 30 seconds (the default).
Lines 6:process.env.CI is an environment variable: a setting the computer passes to programs. CI services set CI, so this retries failures twice there and never on your machine.
Lines 7: Write the HTML report.
Lines 8-11:use options apply to every test’s browser. baseURL and trace are on the next slides.
Lines 12-14: Projects: each one runs the whole suite with its own settings.
Environment variable
A named setting given to a program from outside, like CI=true.
CI
Continuous integration: a service that runs the tests on every change pushed to the code.
Playwright81 / 179
Module 11 · Configuration, projects and devices
baseURL: write addresses once
With baseURL set, page.goto('/cart') means http://localhost:3000/cart. Tests get shorter, and the same tests can run against another copy of the app (a staging site, say) by changing one setting.
Read it from an environment variable, with a local default. ?? means "if that is missing, use this".
From here on. The rest of the course assumes baseURL is the shop or to-do app, and writes short addresses like /cart.
Playwright82 / 179
Module 11 · Configuration, projects and devices
Useful use options
Option
What it sets
trace: 'on-first-retry'
Record a trace when a failed test is retried
screenshot: 'only-on-failure'
Save a screenshot of failed tests
video: 'retain-on-failure'
Keep a video of failed tests
viewport: { width: 1280, height: 720 }
The page’s size in pixels
locale: 'fr-FR'
The browser’s language and number format
timezoneId: 'Europe/Paris'
The time zone the page sees
colorScheme: 'dark'
Ask the page for its dark theme
headless: false
Show the browser window
Any use option can also be set for one file or group with test.use({ … }), at the top of the file or inside a describe.
tests/prices-fr.spec.ts
1test.use({locale:'fr-FR'})23test('shows euros with a comma',async({ page })=>{4await page.goto('/products')5awaitexpect(page.getByText('8,00 €')).toBeVisible()6})
Playwright83 / 179
Module 11 · Configuration, projects and devices
Projects run the suite in several setups
A project is a named set of options. Playwright runs every test once per project. The usual reason is browsers, but a project can be anything: a phone, a language, a signed-in user.
... copies every setting from the device into use.
QuizInteractive · disabled in the PDF
With the four projects above and 10 tests, how many test runs happen with npx playwright test --project=phone?
With the four projects above and 10 tests, how many test runs happen with npx playwright test --project=phone?
Playwright84 / 179
Module 11 · Configuration, projects and devices
Phones and tablets, without a phone
devices describes popular phones, tablets and desktops: screen size, pixel density, the browser’s user agent (the name it sends to servers), and whether it has touch. Playwright emulates them: a desktop browser pretends to be the phone.
TypeScript
1test.use({...devices['iPhone 13']})23test('menu opens on a phone',async({ page })=>{4await page.goto('/')5await page.getByRole('button',{name:'Menu'}).tap()6awaitexpect(page.getByRole('navigation')).toBeVisible()7})
tap() sends a touch, which needs hasTouch; phone devices turn it on.
Emulation is not the real phone. An emulated iPhone runs WebKit on your computer with a phone-sized screen. It catches layout and touch problems, not bugs in a particular phone’s hardware or OS. For those, test on real devices too.
Emulation
One device imitating another: here, a desktop browser acting as a phone.
Viewport
The visible area of the page, in pixels.
Playwright85 / 179
Module 11 · Configuration, projects and devices
webServer: start the app before the tests
Tests need the app running. webServer tells Playwright to start it, wait until its address answers, run the tests, and stop it afterwards.
TypeScript
1webServer:{2 command:'npm run start',3 url:'http://localhost:3000',4 reuseExistingServer:!process.env.CI,5 timeout:120_000,6},
command starts the app, as you would in a terminal.
url is checked until it responds; then tests begin.
reuseExistingServer uses an app you already started yourself, locally; in CI it always starts a fresh one.
timeout is how long to wait for it to come up.
Test what you ship. Point command at the built app (a build then a production start), not the development server. Dev servers add tools and warnings the real site does not have.
Playwright86 / 179
Module 11 · Configuration, projects and devices
Practise: write a config
Fill the blanksInteractive · disabled in the PDF
Run against a local shop, keep traces on retry, and add a phone project.
The tests still open at 1280 pixels wide. Which line is in the wrong place?
TypeScript
Playwright87 / 179
Module 11 · Configuration, projects and devices
Recap
Top-level options shape the runner; use options shape each test’s browser.
baseURL lets tests write short addresses like /cart.
test.use overrides options for a file or group.
Projects run the suite once per setup: browsers, phones, languages.
devices emulates phones and tablets; tap() needs touch.
webServer starts the app for the run.
FlashcardsInteractive · disabled in the PDF
use
The browser options every test gets: baseURL, viewport, locale, trace…
Project
A named set of options. The suite runs once per project.
devices['Pixel 7']
Ready-made settings that emulate that phone.
test.use({ … })
Override use options for one file or group.
webServer
Starts the app before tests and stops it after.
process.env.CI
Set by CI services; used to change settings there.
Module 12: Fixtures: what a test is given
Playwright88 / 179
Module 12 · Fixtures: what a test is given
A fixture is something a test asks for
In async ({ page }) => …, page is a fixture: a ready-to-use thing Playwright prepares before the test and cleans up after it. The test only names what it needs; Playwright works out how to make it.
Built-in fixture
What it is
page
A fresh tab in a fresh context
context
The browser context that page lives in
browser
The browser shared by the worker
browserName
'chromium', 'firefox' or 'webkit'
request
A client for sending HTTP requests (module 14)
Fixture
Something a test asks for by name, set up before it and cleaned up after it.
Playwright89 / 179
Module 12 · Fixtures: what a test is given
Fixtures are made only when asked for
A test that does not name page gets no tab, so no time is spent opening one. Fixtures can also depend on other fixtures: page needs context, which needs browser. Playwright builds the chain in order and takes it down in reverse.
Asking for page sets up the chain below it.
QuizInteractive · disabled in the PDF
A test only asks for request, to call an API. Does Playwright open a browser tab for it?
A test only asks for request, to call an API. Does Playwright open a browser tab for it?
Playwright90 / 179
Module 12 · Fixtures: what a test is given
test.extend adds your own fixtures
Say many tests need the to-do app open with two items already in it. Make a fixture for that. test.extend returns a new test function that knows about your fixtures; tests import it instead of the built-in one.
WalkthroughInteractive · disabled in the PDF
tests/fixtures.ts
1import{ test as base }from'@playwright/test'2importtype{ Page }from'@playwright/test'34exportconst test = base.extend<{ todoPage:Page}>({5todoPage:async({ page }, use)=>{6await page.goto('/todo')7const box = page.getByPlaceholder('What needs to be done?')8for(const item of['Buy milk','Walk the dog']){9await box.fill(item)10await box.press('Enter')11}12awaituse(page)13},14})15export{ expect }from'@playwright/test'
Lines 1-2: Import the built-in test under another name, base, and the Page type.
Lines 4:extend takes an object of fixtures. <{ todoPage: Page }> tells TypeScript what each one gives.
Lines 5: A fixture is a function. It can ask for other fixtures (page), and receives use.
Lines 6-11: Setup: open the app and add two items.
Lines 12:await use(page) hands the value to the test and waits while the test runs.
Lines 15: Re-export expect, so tests import both from one place.
Finish a fixture that opens the cart page and hands it to the test.
TypeScript
1exportconst test = base.<{ cartPage:Page}>({2cartPage:async({},)=>{3await page.goto('/cart')4await(page)5},6})
Playwright92 / 179
Module 12 · Fixtures: what a test is given
Code after use is the cleanup
Everything after await use(…) runs once the test finishes, whether it passed or failed. Setup and cleanup sit together in one place, which is the big advantage over a pair of hooks.
Make a fresh test account, give it to the test, delete it after. The /api/test/… routes are ones the shop provides for tests.
Teardown
Cleanup after a test: closing, deleting, resetting.
`testInfo`
Facts about the running test: its id, title, retry number, output folder.
Playwright93 / 179
Module 12 · Fixtures: what a test is given
Fixtures or hooks?
Hooks
Run for every test in the file, needed or not
Setup and cleanup in two places
Share values through outer variables
Copied between files
Fixtures
Run only for tests that ask
Setup and cleanup together
Hand the value straight to the test, typed
Defined once, imported anywhere
Hooks are fine for a quick goto in one file. When setup is shared, has cleanup, or produces a value, use a fixture.
Find the bugInteractive · disabled in the PDF
Tests using this fixture sometimes see the cart already emptied. Which line is wrong?
TypeScript
Playwright94 / 179
Module 12 · Fixtures: what a test is given
Automatic fixtures run for every test
A fixture written as [fn, { auto: true }] runs for every test, even ones that do not name it. It suits things every test should get, like collecting browser errors.
page.on('pageerror', …) hears every uncaught error in the page’s scripts.
Listening to the page.page.on(event, handler) runs a function each time the page reports something: console for console messages, request and response for network traffic, pageerror for script crashes, dialog for alerts.
Playwright95 / 179
Module 12 · Fixtures: what a test is given
Recap
Fixtures are what tests ask for: page, context, browser, request…
They are made only when named, and can depend on each other.
test.extend adds your own; tests import the extended test.
Setup before await use(value), cleanup after it.
{ auto: true } runs a fixture for every test.
FlashcardsInteractive · disabled in the PDF
Fixture
Something a test asks for, prepared before it and cleaned up after.
await use(value)
Hands the value to the test and waits for it to finish.
test.extend
Makes a new test that knows your fixtures.
{ auto: true }
Run this fixture for every test, even if not named.
testInfo
Facts about the running test: id, title, retry, output folder.
Module 13: Page objects and reusable helpers
Playwright96 / 179
Module 13 · Page objects and reusable helpers
The same locators, in every test
After twenty to-do tests, page.getByPlaceholder('What needs to be done?') appears forty times. When the placeholder changes to "Add a task", forty lines break. A page object keeps a page’s locators and common steps in one place, so a change is one edit.
Page object
A class that describes one page (or part of one) for tests: its locators and the things a user does there.
Class
A template for objects in JavaScript: it bundles values (fields) and functions (methods) together.
Playwright97 / 179
Module 13 · Page objects and reusable helpers
A page object for the to-do app
WalkthroughInteractive · disabled in the PDF
tests/pages/todo-page.ts
1importtype{ Locator, Page }from'@playwright/test'23exportclassTodoPage{4readonly newTodo:Locator5readonly items:Locator67constructor(readonly page:Page){8this.newTodo = page.getByPlaceholder('What needs to be done?')9this.items = page.getByTestId('todo-item')10}1112asyncgoto(){13awaitthis.page.goto('/todo')14}1516asyncadd(text:string){17awaitthis.newTodo.fill(text)18awaitthis.newTodo.press('Enter')19}2021item(text:string){22returnthis.items.filter({hasText: text })23}24}
Lines 3-5: The class holds fields: locators other code can read. readonly means they are never replaced.
Lines 7-10: The constructor runs when you write new TodoPage(page). It saves the page and builds the locators. No await: locators do not search yet.
Lines 12-14: A method for opening the page.
Lines 16-19: A method for something a user does, named in the user’s words.
Lines 21-23: A method that returns a locator, so tests can act on or check one item.
Playwright98 / 179
Module 13 · Page objects and reusable helpers
Tests read like the plan
TypeScript
1test('adds and completes a to-do',async({ page })=>{2const todos =newTodoPage(page)3await todos.goto()4await todos.add('Buy milk')5await todos.add('Walk the dog')6await todos.item('Buy milk').getByRole('checkbox').check()7awaitexpect(todos.items).toHaveCount(2)8awaitexpect(todos.item('Buy milk')).toHaveClass(/completed/)9})
Keep assertions in the test. Let page objects find things and do things; let tests decide what should be true. A reader then sees every check in the test itself, and the page object stays reusable for tests that expect different results.
QuizInteractive · disabled in the PDF
The placeholder changes to "Add a task". With the page object, how many places need editing?
The placeholder changes to "Add a task". With the page object, how many places need editing?
Playwright99 / 179
Module 13 · Page objects and reusable helpers
Offer page objects as fixtures
Instead of new TodoPage(page) in every test, make it a fixture. Tests ask for todos and get one, already opened.
tests/fixtures.ts
1import{ test as base }from'@playwright/test'2import{ TodoPage }from'./pages/todo-page'3import{ CartPage }from'./pages/cart-page'45exportconst test = base.extend<{ todos:TodoPage; cart:CartPage}>({6todos:async({ page }, use)=>{7const todos =newTodoPage(page)8await todos.goto()9awaituse(todos)10},11cart:async({ page }, use)=>{12awaituse(newCartPage(page))13},14})
Fill the blanksInteractive · disabled in the PDF
Use the todos fixture in a test.
TypeScript
1test('adds a to-do',async({})=>{2await todos.('Buy milk')3awaitexpect(todos.).toHaveText(['Buy milk'])4})
Playwright100 / 179
Module 13 · Page objects and reusable helpers
Objects for parts of a page
Not everything is a whole page. A product card appears on the home page, the search page and the wishlist. A small class for the card, built from a locator instead of a page, works wherever cards appear.
TypeScript
1exportclassProductCard{2constructor(readonly root:Locator){}34getname(){returnthis.root.getByRole('heading')}5getprice(){returnthis.root.getByTestId('price')}67asyncaddToCart(){8awaitthis.root.getByRole('button',{name:'Add to cart'})9.click()10}11}1213const mug =newProductCard(14 page.getByRole('listitem').filter({hasText:'Red mug'}),15)16await mug.addToCart()
Playwright101 / 179
Module 13 · Page objects and reusable helpers
When abstraction hurts
Page objects are a tool, not a rule. Too much of them makes tests hard to read: you jump through three files to learn what a click does.
Over-built
A method for every single click
Methods that hide checks inside
Deep class hierarchies (BasePage → ShopPage → …)
Wrappers around every Playwright call
Just enough
Locators used in more than one test
Steps that take several actions
Flat classes, one per page or part
Plain Playwright calls in tests when clear
Find the bugInteractive · disabled in the PDF
TypeScript rejects this page object. Which line?
TypeScript
Playwright102 / 179
Module 13 · Page objects and reusable helpers
Small helper functions are fine too
For one shared step, a plain function is often enough. Pass it the page, and it can be used anywhere.
tests/helpers.ts
1importtype{ Page }from'@playwright/test'23exportasyncfunctionaddToCart(page:Page, product:string){4await page.goto('/products')5await page.getByRole('listitem').filter({hasText: product })6.getByRole('button',{name:'Add to cart'}).click()7}
QuizInteractive · disabled in the PDF
Only two tests share one three-line step. What is the simplest good option?
Only two tests share one three-line step. What is the simplest good option?
Playwright103 / 179
Module 13 · Page objects and reusable helpers
Recap
A page object holds a page’s locators and steps in one class.
Build locators in the constructor; put async steps in methods.
Keep assertions in tests; page objects find and do.
Offer page objects as fixtures so tests just ask for them.
Component objects cover parts that appear on many pages.
Do not over-build: plain calls and small helpers are often clearer.
FlashcardsInteractive · disabled in the PDF
Page object
A class with one page’s locators and user steps.
Why page objects?
A change to the page is one edit, and tests read like the plan.
Constructor
Runs on new. Cannot await: build locators only.
Component object
Like a page object, for a part of a page, built from a locator.
Where do assertions go?
In the tests, so every check is visible there.
Module 14: The network: stand-ins and API tests
Playwright104 / 179
Module 14 · The network: stand-ins and API tests
Why stand in for the server
The shop’s product page asks /api/products for its list. In a test you often want to decide the answer yourself: an empty list, a server error, a product with a very long name. Mocking (or stubbing) a request means answering it from the test, so the real server is never asked.
Hard-to-reach cases: errors, timeouts, odd data.
Speed and stability: no slow or flaky outside services.
Third parties: payment providers, maps and analytics you must not call from tests.
The cost. A mock can drift from the real API. If the server renames a field, mocked tests keep passing while the real app breaks. Keep some tests against the real server, and keep mock data close to what it really sends.
API
The addresses a server offers for programs (not people) to read and change data, usually in JSON.
Mock
A stand-in answer, given by the test instead of the real server.
Playwright105 / 179
Module 14 · The network: stand-ins and API tests
page.route catches requests; fulfill answers them
page.route(pattern, handler) calls your handler for every request whose address matches. Set it up before the page makes the request, so before goto.
** in a pattern matches anything, including slashes: **/api/products matches it on any host.
fulfill({ json }) answers with status 200 and the data as JSON.
fulfill({ status: 500, body: 'oops' }) answers with an error.
A regular expression or a function also works as the pattern.
QuizInteractive · disabled in the PDF
The test calls page.goto('/products') and thenpage.route('**/api/products', …). What happens?
The test calls page.goto('/products') and thenpage.route('**/api/products', …). What happens?
Playwright106 / 179
Module 14 · The network: stand-ins and API tests
Testing what happens when things fail
TypeScript
1test('says so when products fail to load',async({ page })=>{2await page.route('**/api/products',(route)=>3 route.fulfill({status:500}),4)5await page.goto('/products')6awaitexpect(page.getByRole('alert'))7.toHaveText('Something went wrong. Try again.')8})910test('copes with no network',async({ page })=>{11await page.route('**/api/products',(route)=> route.abort())12await page.goto('/products')13awaitexpect(page.getByRole('button',{name:'Retry'}))14.toBeVisible()15})
abort() makes the request fail as if the network were down.
Fill the blanksInteractive · disabled in the PDF
Answer the cart request with one item, before opening the cart.
Sometimes you want the real data with one change. route.fetch() sends the request to the real server and gives you the response; you change it and fulfill with the result. route.continue() lets a request through untouched, optionally with changed headers.
TypeScript
1await page.route('**/api/products',async(route)=>{2const response =await route.fetch()3const products =await response.json()4 products[0].name ='A very long product name that might wrap badly'5await route.fulfill({ response,json: products })6})
Real list, but the first product gets a long name, to check the layout copes.
For every tab.context.route works like page.route but for every page in the context, including popups. Use it when a flow opens new tabs.
Playwright108 / 179
Module 14 · The network: stand-ins and API tests
Waiting for a request to finish
Usually you wait for what the user sees. Sometimes the request itself matters: did saving actually send the right data? page.waitForResponse gives you a promise for the next matching response. Start waiting before the click, then await it after.
TypeScript
1const saved = page.waitForResponse('**/api/cart')2await page.getByRole('button',{name:'Add to cart'}).click()3const response =await saved4expect(response.status()).toBe(200)5expect(response.request().postDataJSON())6.toEqual({product:'red-mug',quantity:1})
Find the bugInteractive · disabled in the PDF
This test sometimes hangs until it times out. Which line is in the wrong place?
TypeScript
Playwright109 / 179
Module 14 · The network: stand-ins and API tests
Testing an API directly with request
The request fixture sends HTTP requests straight from the test, no browser involved. It uses the config’s baseURL. Tests of an API alone are much faster than browser tests and pinpoint server bugs.
tests/api/products.spec.ts
1test('lists products',async({ request })=>{2const response =await request.get('/api/products')3awaitexpect(response).toBeOK()4const products =await response.json()5expect(products.length).toBeGreaterThan(0)6expect(products[0]).toHaveProperty('price')7})89test('refuses an order with no items',async({ request })=>{10const response =await request.post('/api/orders',{11data:{items:[]},12})13expect(response.status()).toBe(400)14})
get, post, put, patch, delete, fetch: one method per kind of request.
data is sent as JSON; form as a form; headers adds headers.
toBeOK() passes for status codes 200 to 299.
Playwright110 / 179
Module 14 · The network: stand-ins and API tests
Use the API to set up, the browser to check
Clicking through five pages to put a mug in the cart is slow, and it is not what this test is about. page.request shares the page’s cookies, so a request made with it acts as the same signed-in shopper.
TypeScript
1test('removes an item from the cart',async({ page })=>{2await page.request.post('/api/cart',{3data:{product:'red-mug',quantity:1},4})5await page.goto('/cart')6await page.getByRole('button',{name:'Remove Red mug'}).click()7awaitexpect(page.getByText('Your cart is empty')).toBeVisible()8})
QuizInteractive · disabled in the PDF
The cart is tied to the browser’s session cookie. Which client should add the item?
The cart is tied to the browser’s session cookie. Which client should add the item?
Playwright111 / 179
Module 14 · The network: stand-ins and API tests
Recap
page.route catches requests; set it up before navigating.
fulfill answers, abort fails, continue passes through, fetch gets the real response to change.
Mocks reach error cases and third parties; keep some tests on the real server.
Start waitForResponse before the action that triggers it.
The request fixture tests APIs directly; page.request shares the page’s cookies.
FlashcardsInteractive · disabled in the PDF
page.route(pattern, handler)
Catch matching requests and decide how to answer.
route.fulfill({ json })
Answer with data, as if from the server.
route.abort()
Make the request fail like a network error.
page.waitForResponse
A promise for the next matching response. Start it before the action.
request fixture
Sends HTTP requests without a browser.
toBeOK()
Passes when the response status is 200 to 299.
Module 15: Signing in once: authentication and storage state
Playwright112 / 179
Module 15 · Signing in once: authentication and storage state
How a site remembers you
When you sign in to the shop, the server sends back a cookie: a small piece of text the browser stores and sends with every later request. The cookie holds a session id, which tells the server "this is Sam, already signed in". Some sites keep similar data in localStorage, the browser’s per-site storage.
Cookie
A small piece of data a site asks the browser to store and send back on every request.
Session
The server’s memory that a certain visitor is signed in, found by the id in a cookie.
`localStorage`
Storage in the browser for one site, kept between visits.
Playwright113 / 179
Module 15 · Signing in once: authentication and storage state
Signing in for every test is slow
Each test starts with an empty context, so it is signed out. If 200 tests each fill the sign-in form, that is 200 sign-ins: minutes of waiting, and 200 chances for the sign-in page to be the thing that fails. Better: sign in once, save the browser’s state, and start every test with it.
Storage state
A file with a context’s cookies and localStorage, saved with context.storageState({ path }) and loaded with the storageState option.
Playwright114 / 179
Module 15 · Signing in once: authentication and storage state
Sign in once, in a setup project
tests/auth.setup.ts
1import{ test as setup, expect }from'@playwright/test'23const authFile ='playwright/.auth/shopper.json'45setup('sign in',async({ page })=>{6await page.goto('/sign-in')7await page.getByLabel('Email').fill(process.env.SHOP_EMAIL!)8await page.getByLabel('Password').fill(process.env.SHOP_PASSWORD!)9await page.getByRole('button',{name:'Sign in'}).click()10awaitexpect(page.getByRole('heading',{name:'Your account'}))11.toBeVisible()12await page.context().storageState({path: authFile })13})
The email and password come from environment variables, never from the code. ! tells TypeScript they are set.
It waits for proof of sign-in (the account heading) before saving, so the cookie is really there.
Playwright115 / 179
Module 15 · Signing in once: authentication and storage state
The setup project runs only files ending in .setup.ts.
dependencies: ['setup'] makes it run, and pass, before any chromium test.
Every chromium test starts with the saved cookies: already signed in.
Keep the file out of the repository. The saved file holds a live session: anyone with it is signed in as that account. Add playwright/.auth to .gitignore, and only ever use test accounts.
Put in orderInteractive · disabled in the PDF
Put what happens on npx playwright test --project=chromium in order.
1.Each context is loaded with the saved cookies
2.The chromium tests start, each with a new context
3.It signs in and saves the storage state file
4.The setup project runs auth.setup.ts
5.Each test begins already signed in
Playwright116 / 179
Module 15 · Signing in once: authentication and storage state
Faster still: sign in through the API
The setup does not have to use the form. If the shop has a sign-in endpoint, call it with request and save its cookies. The form gets its own dedicated tests elsewhere.
The request client keeps the cookies the server sends, and can save them the same way.
Playwright117 / 179
Module 15 · Signing in once: authentication and storage state
Several roles, several files
A shop has shoppers and staff. Save one state file per role in setup, then pick one per file or group with test.use. To test the signed-out experience, start with an empty state.
TypeScript
1test.describe('staff',()=>{2 test.use({storageState:'playwright/.auth/staff.json'})34test('sees the orders dashboard',async({ page })=>{5await page.goto('/admin/orders')6awaitexpect(page.getByRole('heading',{name:'Orders'}))7.toBeVisible()8})9})1011test.describe('signed out',()=>{12 test.use({storageState:{cookies:[],origins:[]}})1314test('is sent to sign in',async({ page })=>{15await page.goto('/account')16awaitexpect(page).toHaveURL(/\/sign-in/)17})18})
QuizInteractive · disabled in the PDF
A chat feature needs a shopper and a staff member at the same time, in one test. How?
A chat feature needs a shopper and a staff member at the same time, in one test. How?
Playwright118 / 179
Module 15 · Signing in once: authentication and storage state
One account shared by parallel tests
All tests now sign in as the same shopper. That is fine while tests only read. When tests change the account (its address, its cart on the server), they run at the same time and trip over each other: one empties the cart while another is checking it.
One shared account
Simple to set up
Fine for read-only tests
Tests that change data collide
One account per worker
No collisions
Needs a fixture that creates accounts
Module 19 shows how
Sessions that expire. If saved sessions last an hour and a CI run takes longer, late tests fail as signed out. Sign in during each run (the setup project does), never commit a state file to reuse across runs.
Playwright119 / 179
Module 15 · Signing in once: authentication and storage state
Recap
Sites remember sign-in with cookies (and sometimes localStorage).
Sign in once in a setup project and save storageState to a file.
dependencies runs the setup first; use.storageState loads it into every context.
Keep passwords in environment variables and state files out of the repository.
Use one file per role; an empty state for signed-out tests.
FlashcardsInteractive · disabled in the PDF
Cookie
Data a site stores in the browser, sent back with every request.
Storage state
A saved file of cookies and localStorage, loaded into new contexts.
Setup project
A project other projects depend on, run first: here, for sign-in.
dependencies
Projects that must run, and pass, before this one.
Signed-out state
{ cookies: [], origins: [] }
Module 16: Screenshots and accessibility checks
Playwright120 / 179
Module 16 · Screenshots and accessibility checks
Some bugs are only visible
A test can check that the "Pay" button exists and works, and still miss that it is now white on white. Visual comparison takes a screenshot and compares it, pixel by pixel, with an approved image saved earlier, called the baseline (or golden image). Any difference fails the test and shows you where.
Checking a screenshot against an approved one, pixel by pixel.
Baseline
The approved screenshot new ones are compared with.
Playwright121 / 179
Module 16 · Screenshots and accessibility checks
Making and updating baselines
The first run has nothing to compare with: it fails and saves the screenshot as the new baseline, next to the test.
Look at it. If it is right, commit it with the code.
Later runs compare with it. A difference fails and the report shows expected, actual, and a diff image.
If the change was intended, update the baseline with --update-snapshots and review the new images before committing.
Terminal
1npxplaywrighttest--update-snapshots2npxplaywrighttest--update-snapshots=missing# only new ones
tests/
product.spec.ts
product.spec.ts-snapshots/
red-mug-chromium-linux.png# one per browser and OS
red-mug-webkit-linux.png
Baselines are kept per project and per operating system, because each draws text slightly differently.
Playwright122 / 179
Module 16 · Screenshots and accessibility checks
Keeping screenshots stable
A screenshot test is only useful if the same page gives the same pixels every time. Anything that changes between runs (a clock, an animation, an advert, a random product order) makes it flaky.
Captures the whole scrolling page, not just the viewport
maxDiffPixels / maxDiffPixelRatio
Allows a few changed pixels (a count, or a share of all)
threshold
How different a pixel’s colour may be and still count as the same
animations
'disabled' by default: CSS animations are stopped
stylePath
A stylesheet applied only for the screenshot, to hide things
Same machine, same pixels. Fonts and anti-aliasing differ between macOS, Windows and Linux, so a baseline made on your laptop will not match CI. Make and update baselines where CI runs, for example in the official Playwright Docker image, and run screenshot tests in one project only to keep the image count down.
Playwright123 / 179
Module 16 · Screenshots and accessibility checks
Practise: screenshots
QuizInteractive · disabled in the PDF
The home page shows the current time. Its screenshot test fails on every run. What is the best fix?
The home page shows the current time. Its screenshot test fails on every run. What is the best fix?
Fill the blanksInteractive · disabled in the PDF
Compare only the cart summary box, hiding the order number.
Structure snapshots: the accessibility tree as text
toMatchAriaSnapshot compares the accessibility tree of an element with a description in YAML (an indented text format). It checks structure and names, not pixels, so it ignores styling changes while catching a lost heading or a renamed button.
TypeScript
1awaitexpect(page.getByRole('navigation')).toMatchAriaSnapshot(`2 - navigation:3 - link "Shop"4 - link "Cart"5 - button "Sign in"6`)
Let Playwright write it. Codegen’s "assert snapshot" button and UI mode both produce the YAML for an element. Paste it in, then remove the parts the test should not care about.
Playwright125 / 179
Module 16 · Screenshots and accessibility checks
axe checks accessibility rules automatically
Accessibility means people can use the site whatever their abilities: with a screen reader, a keyboard only, zoomed in, or with low vision. axe is an open-source engine that checks a page against many of the published rules (WCAG, the Web Content Accessibility Guidelines). @axe-core/playwright runs it inside the page.
Terminal
1npminstall--save-dev@axe-core/playwright
TypeScript
1import AxeBuilder from'@axe-core/playwright'23test('checkout has no detectable a11y issues',async({ page })=>{4await page.goto('/checkout')5const results =awaitnewAxeBuilder({ page })6.withTags(['wcag2a','wcag2aa'])7.analyze()8expect(results.violations).toEqual([])9})
"a11y" is short for accessibility: an a, 11 letters, a y.
.withTags([...]) limits it to rules with those tags, like WCAG levels A and AA.
.include(selector) / .exclude(selector) choose which part of the page to check.
.disableRules([...]) turns off rules by id, for a known issue you are tracking.
Playwright126 / 179
Module 16 · Screenshots and accessibility checks
What a machine cannot check
Rules catch missing labels, poor contrast, buttons without names, broken ARIA. They cannot tell whether a label makes sense, whether focus moves somewhere sensible, or whether the flow works with a keyboard alone. Test those directly.
TypeScript
1test('the cart dialog works with the keyboard',async({ page })=>{2await page.goto('/products')3await page.getByRole('button',{name:'Open cart'}).focus()4await page.keyboard.press('Enter')5const dialog = page.getByRole('dialog',{name:'Your cart'})6awaitexpect(dialog).toBeVisible()7awaitexpect(dialog.getByRole('button',{name:'Close'}))8.toBeFocused()9await page.keyboard.press('Escape')10awaitexpect(dialog).toBeHidden()11})
QuizInteractive · disabled in the PDF
axe reports a contrast problem on a dialog only sometimes. What is the likely cause?
axe reports a contrast problem on a dialog only sometimes. What is the likely cause?
Playwright127 / 179
Module 16 · Screenshots and accessibility checks
Recap
toHaveScreenshot compares with a committed baseline; update with --update-snapshots and review.
Stabilise screenshots: mask moving parts, fix time and data, generate baselines where CI runs.
toMatchAriaSnapshot checks structure and names, not pixels.
Keyboard and focus need their own tests: rules cannot judge them.
FlashcardsInteractive · disabled in the PDF
Baseline
The approved screenshot that new ones are compared with.
--update-snapshots
Replace baselines with the current screenshots.
mask
Covers elements with a box before comparing.
axe
An engine that checks a page against accessibility rules.
WCAG
The Web Content Accessibility Guidelines, in levels A, AA and AAA.
toMatchAriaSnapshot
Compares the accessibility tree with a YAML description.
Module 17: Parallel runs, sharding and CI
Playwright128 / 179
Module 17 · Parallel runs, sharding and CI
Workers run tests at the same time
Playwright starts several worker processes, each with its own browser, and hands them tests. By default it uses half your computer’s CPU cores. By default, files run in parallel, but the tests inside one file run one after another, in the same worker.
Three workers sharing six files. Each worker keeps its browser open between tests.
Setting
Effect
workers: 4 or '50%'
How many workers (a number or a share of CPU cores)
fullyParallel: true
Also run tests within a file in parallel (the starter config turns it on)
test.describe.configure({ mode: 'parallel' })
Parallel for one file or group
Playwright129 / 179
Module 17 · Parallel runs, sharding and CI
A failure restarts the worker
When a test fails, Playwright throws its worker away and starts a fresh one for the next tests, so a broken browser state cannot leak into them. That is also why worker-wide setup (beforeAll, worker fixtures) may run more than once per run.
`testInfo.workerIndex`
A unique number for each worker process started; a restarted worker gets a new one.
`testInfo.parallelIndex`
A slot number from 0 to workers − 1. A restarted worker reuses its slot, so it suits per-slot data.
QuizInteractive · disabled in the PDF
A suite has 2 files of 20 tests each, and 8 workers, with default settings. How many tests can run at once?
A suite has 2 files of 20 tests each, and 8 workers, with default settings. How many tests can run at once?
Playwright130 / 179
Module 17 · Parallel runs, sharding and CI
Sharding: split the suite across machines
One machine has only so many cores. Sharding splits the suite into parts and runs each part on a different machine at the same time. Each machine runs the same command with its own shard number.
Terminal
1npxplaywrighttest--shard=1/4# on machine 12npxplaywrighttest--shard=2/4# on machine 23npxplaywrighttest--shard=3/4# on machine 34npxplaywrighttest--shard=4/4# on machine 4
Balance the shards. With fullyParallel: true, shards are split by test, so they finish at about the same time. Without it, whole files are shared out, and one huge file can make a single shard the slow one.
Shard
One slice of the suite, run on its own machine: --shard=2/4 is the second of four.
Playwright131 / 179
Module 17 · Parallel runs, sharding and CI
Merging reports from shards
Each shard only knows its own results. Have each write a blob report (a raw results file), collect the files in one place, and merge them into one HTML report.
playwright.config.ts
1reporter: process.env.CI ?'blob':'html',
Terminal
1# after downloading every shard's blob-report folder into all-blobs/2npxplaywrightmerge-reports--reporterhtml./all-blobs
Put in orderInteractive · disabled in the PDF
Put a sharded CI run in order.
1.The merged HTML report is uploaded for people to read
2.Four machines each install dependencies and browsers
3.Each runs npx playwright test --shard=N/4 with the blob reporter
4.A final job downloads all four and runs merge-reports
5.Each uploads its blob-report folder
Playwright132 / 179
Module 17 · Parallel runs, sharding and CI
A CI workflow on GitHub Actions
GitHub Actions runs workflows, written in YAML, on GitHub’s machines. This one runs the suite on every push and keeps the report when tests fail.
npm ci installs the exact package versions in the lock file.
--with-deps also installs the system libraries browsers need on Linux.
if: ${{ !cancelled() }} uploads the report even when the test step failed.
Playwright133 / 179
Module 17 · Parallel runs, sharding and CI
Settings that make CI strict and useful
playwright.config.ts
1const CI =!!process.env.CI23exportdefaultdefineConfig({4forbidOnly: CI,5retries: CI ?2:0,6workers: CI ?2:undefined,7failOnFlakyTests: CI,8use:{trace:'on-first-retry'},9})
forbidOnly: a stray test.only fails the run.
retries: a test that fails then passes on retry is marked flaky, not passed, and its first-retry trace is kept.
workers: CI machines are often small; too many workers make every test slow.
failOnFlakyTests: treat flaky as failed, so flakiness gets fixed rather than ignored.
QuizInteractive · disabled in the PDF
With retries: 2, a test fails, then passes on its first retry. How is it reported?
With retries: 2, a test fails, then passes on its first retry. How is it reported?
Playwright134 / 179
Module 17 · Parallel runs, sharding and CI
Debugging a failure that only happens in CI
Download the report artifact from the CI run, then open it locally with npx playwright show-report <folder>. Each failed test links its trace. The trace viewer shows the timeline, every action with before-and-after snapshots, the network, the console, and the test’s source.
Compare the snapshot before the action with what the test expected.
Check the Network tab for slow or failed requests around that moment.
Look at Console for script errors the user would not see.
Run only what changed.npx playwright test --only-changed=main runs only test files changed since the main branch (and tests that import changed files), for fast feedback on a branch. Keep a full run before merging.
Playwright135 / 179
Module 17 · Parallel runs, sharding and CI
Recap
Workers run files in parallel; fullyParallel also splits tests within files.
A failed test restarts its worker; per-worker setup can run more than once.
--shard=N/M splits the suite across machines; blob reports merge into one.
In CI: forbidOnly, retries with traces, a sensible worker count, and upload the report always.
Debug CI failures from the downloaded trace.
FlashcardsInteractive · disabled in the PDF
Worker
A process with its own browser that runs tests.
fullyParallel
Run tests within a file in parallel too.
--shard=2/4
Run the second of four slices of the suite.
Blob report
Raw results from one shard, merged with merge-reports.
Flaky (in a report)
Failed, then passed on retry.
failOnFlakyTests
Fail the run if any test was flaky.
Module 18: Flaky tests: root causes and real fixes
Playwright136 / 179
Module 18 · Flaky tests: root causes and real fixes
Why flakiness matters more than it seems
A flaky test is worse than no test. When red sometimes means nothing, people rerun until green and stop reading failures, and then a real bug slips through on a red that everyone ignored. Retries hide the symptom; the cause is still there, and it may be in the app.
Flaky tests often point at real bugs. A test that fails one time in fifty because two requests race may be showing a race a real user hits one time in fifty. Before blaming the test, ask whether a person could see the same thing.
Playwright137 / 179
Module 18 · Flaky tests: root causes and real fixes
First, make it fail on demand
You cannot fix what you cannot see. Run the test many times, under load, with no retries, until it fails, and keep the trace of the failure.
Terminal
1# run it 50 times, 8 at a time, no retries, trace on failures2npxplaywrighttesttests/cart.spec.ts:14\3--repeat-each=50--workers=8--retries=0\4--trace=retain-on-failure56# does order matter? run files (and parallel tests) in a random order7npxplaywrighttest--shuffle
Fails only with many workers: suspect shared data or a slow, overloaded machine.
Fails only after another test: suspect state left behind, or order dependence.
Fails only in one browser: suspect timing or a real browser difference.
Fails only in CI: compare the CI trace’s timings and network with a local one.
Playwright138 / 179
Module 18 · Flaky tests: root causes and real fixes
Cause 1: checking before the page is ready
The most common cause. Something reads the page once and decides, instead of letting an assertion retry. Fixed pauses are the same mistake in disguise: too short on a slow machine, wasted time on a fast one.
awaitexpect(page.getByRole('alert')).toHaveText('Saved')// popups that may appear:await page.addLocatorHandler(banner,()=> banner.click())
Find the bugInteractive · disabled in the PDF
This test is flaky: sometimes the coupon is not applied. Which line causes it?
TypeScript
Playwright139 / 179
Module 18 · Flaky tests: root causes and real fixes
Cause 2: clicking before the page’s scripts are ready
Many sites send ready-made HTML first, then load JavaScript that attaches the behaviour, a step called hydration. In between, a button is visible and enabled, so Playwright clicks it, but nothing is listening yet. The click is lost.
Best: have the app show it is ready, for example by enabling the button only after hydration, so auto-waiting covers it.
Or: assert something that only appears once the app is interactive before acting.
Or: retry action and check together with expect(async () => { … }).toPass().
Hydration
Scripts taking over HTML the server already sent, attaching the handlers that make it interactive.
Not `waitForLoadState('networkidle')`. Waiting for the network to be quiet for half a second is discouraged in Playwright’s own docs: pages with polling or analytics never go quiet, and quiet does not mean ready. Wait for something the user would see.
Playwright140 / 179
Module 18 · Flaky tests: root causes and real fixes
Cause 3: tests sharing data on the server
Browser contexts are isolated; the server is not. Two tests running at once against one account, one cart or one product’s stock will interfere. It passes alone and fails in the full run.
QuizInteractive · disabled in the PDF
Test A expects the cart to hold 1 item. Test B empties the same account’s cart. They run in parallel. What is the fix?
Test A expects the cart to hold 1 item. Test B empties the same account’s cart. They run in parallel. What is the fix?
Playwright141 / 179
Module 18 · Flaky tests: root causes and real fixes
Cause 4: time, randomness and the outside world
Time: "Delivery by Friday" changes every day; a test at midnight crosses a date. Fix the page’s clock (next module).
Randomness: shuffled recommendations, A/B tests. Turn them off for tests, or mock the request that decides.
Third parties: payment pages, maps, fonts from other sites. Mock them with page.route, or test them separately.
Animations: elements moving when checked. Playwright waits for stability before actions; screenshots disable animations. Prefer reducedMotion: 'reduce' if the site honours it.
QuizInteractive · disabled in the PDF
A test checks "Ordered today" and fails only on CI runs that start just before midnight. What is the cause?
A test checks "Ordered today" and fails only on CI runs that start just before midnight. What is the cause?
Playwright142 / 179
Module 18 · Flaky tests: root causes and real fixes
Cause 5: a machine with too much to do
Too many workers on a small CI machine make the app and browsers slow, and slow means timeouts. If failures rise with the worker count, lower workers, shard across more machines, or give the app more resources. Raising timeouts treats only the symptom.
Put in orderInteractive · disabled in the PDF
Put a sound way to deal with a flaky test in order.
1.Repeat the run many times to confirm it is gone
2.Fix the cause in the test or the app
3.Name the cause: timing, data, order, time, or load
4.Reproduce it with --repeat-each and no retries
5.Read the failing trace and find what differed
Playwright143 / 179
Module 18 · Flaky tests: root causes and real fixes
While you fix it: quarantine, do not ignore
If a fix will take time, keep the rest of the suite trustworthy: mark the test test.fixme with a link to the ticket, or tag it @flaky and leave it out of the blocking run with --grep-invert @flaky. Then fix it soon; a quarantine that grows is a suite that shrinks.
TypeScript
1test('applies a gift card',{2tag:'@flaky',3annotation:{type:'issue',description:'Ticket 482'},4},async({ page })=>{/* … */})
Playwright144 / 179
Module 18 · Flaky tests: root causes and real fixes
Recap
Reproduce first: --repeat-each, many workers, no retries, keep traces.
Timing: never read-then-decide or sleep; let assertions and actions wait.
Hydration: make readiness visible; avoid networkidle.
Shared server data: give each test or worker its own.
Freeze time, remove randomness, mock third parties, watch machine load.
Fix causes; quarantine briefly with a ticket.
FlashcardsInteractive · disabled in the PDF
--repeat-each=50
Run each selected test 50 times, to reproduce flakiness.
Why not waitForTimeout?
Too short on slow machines, wasted time on fast ones.
Hydration
Scripts attaching behaviour to server-sent HTML. Clicks before it can be lost.
Why not more retries?
They hide the cause, which may be a real bug users hit.
--shuffle
Run test files (and tests in parallel mode) in a random order, to find tests that depend on order.
Module 19: Designing a suite that scales
Playwright145 / 179
Module 19 · Designing a suite that scales
Each test makes the data it needs
The surest way to avoid collisions is for each test to create its own data with a name nobody else uses, through the API, and to clean it up after. Tests can then run in any order, at any parallelism, against a shared server.
TypeScript
1test('renames a list',async({ page, request }, testInfo)=>{2const name =`Groceries ${testInfo.testId}-${testInfo.retry}`3const res =await request.post('/api/lists',{data:{ name }})4const list =await res.json()56await page.goto(`/lists/${list.id}`)7await page.getByRole('button',{name:'Rename'}).click()8// …9await request.delete(`/api/lists/${list.id}`)10})
The id and retry number make the name unique across tests, projects and retries.
Put the cleanup in a fixture. Cleanup at the end of a test is skipped when the test fails before reaching it. In a fixture, code after use always runs (module 12).
Playwright146 / 179
Module 19 · Designing a suite that scales
Worker fixtures: set up once per worker
Some setup is too slow for every test, like creating and signing in a user. A worker-scoped fixture is made once per worker and shared by the tests that worker runs. Since workers never run two tests at once, each worker’s account is never used by two tests at the same moment.
The second type in extend<{}, { … }> lists worker fixtures.
Worker fixtures can only use other worker fixtures (playwright, browser), not page or request.
parallelIndex keeps the same email when a worker restarts after a failure.
Playwright147 / 179
Module 19 · Designing a suite that scales
Practise: scopes
QuizInteractive · disabled in the PDF
Why can a worker-scoped fixture not ask for page?
Why can a worker-scoped fixture not ask for page?
Fill the blanksInteractive · disabled in the PDF
Make the fixture worker-scoped.
TypeScript
1exportconst test = base.extend<{},{ shopApi:APIRequestContext}>({2shopApi:[async({}, use)=>{3const api =await playwright.request.newContext()4awaituse(api)5await api.()6},{:''}],7})
Playwright148 / 179
Module 19 · Designing a suite that scales
Option fixtures: settings projects can change
An option is a fixture with a default value that the config or test.use can override, like Playwright’s own locale. It lets one suite run, say, once per shop region.
TypeScript
1exporttypeShopOptions={ region:'uk'|'eu'}23exportconst test = base.extend<ShopOptions>({4region:['uk',{option:true}],5})67// playwright.config.ts8exportdefaultdefineConfig<ShopOptions>({9projects:[10{name:'uk',use:{region:'uk'}},11{name:'eu',use:{region:'eu'}},12],13})
Combining fixture files. Large suites keep fixtures in several files (accounts, page objects, mocks). mergeTests(a, b) combines several extended test functions into one.
Playwright149 / 179
Module 19 · Designing a suite that scales
Controlling time with page.clock
page.clock replaces the page’s clock with one the test controls: Date, timers and intervals all follow it. Install it before the page loads.
TypeScript
1test('a cart expires after 30 minutes',async({ page })=>{2await page.clock.install({time:newDate('2026-03-02T10:00:00')})3await page.goto('/cart')4awaitexpect(page.getByText('Monday 2 March')).toBeVisible()56await page.clock.fastForward('30:00')7awaitexpect(page.getByText('Your cart has expired')).toBeVisible()8})
Method
Does
install({ time })
Take over timers and start at this time
fastForward('30:00')
Jump ahead, firing due timers at most once
runFor(ms)
Advance, firing every timer due on the way
pauseAt(time)
Run to a moment and stop the clock there
setFixedTime(time)
Make Date always say this; timers still run
Playwright150 / 179
Module 19 · Designing a suite that scales
Keeping a large suite fast
Measure first: the HTML report’s Speedboard tab lists the slowest tests; fix the slowest ten before anything else.
Set up through the API, not the UI: sign-in, carts, orders.
Reuse sign-in with storage state; reuse slow setup with worker fixtures.
Mock slow third parties that are not what the test is about.
Run in parallel: fullyParallel, then shards when one machine is not enough.
Do not run everything everywhere: the full suite in one browser, a smoke set in the others.
A suite takes 40 minutes. Most tests begin by signing in through the form and adding items by clicking. What gives the biggest win first?
A suite takes 40 minutes. Most tests begin by signing in through the form and adding items by clicking. What gives the biggest win first?
Playwright151 / 179
Module 19 · Designing a suite that scales
What deserves an end-to-end test
Browser tests are the most expensive kind. Spend them where nothing cheaper gives the same confidence, and push the rest down to unit and API tests.
Test end to end
The journeys that make money or lose trust: sign in, checkout, password reset
Wiring between pages and services
What only a browser can show: layout, focus, accessibility
Test lower down
Every combination of a form’s validation rules
Price and tax calculations
Every error code an API can return
Review tests like code. In review, ask of each new test: what would make it fail, and would that failure mean something to a user? Locators by role, no sleeps, own data, one clear purpose. A suite stays healthy one review at a time.
Playwright152 / 179
Module 19 · Designing a suite that scales
Recap
Each test creates uniquely named data and cleans it up in a fixture.
Worker fixtures do slow setup once per worker; parallelIndex gives each its own data.
Option fixtures let projects and test.use change settings; mergeTests combines fixture files.
page.clock controls dates and timers.
Measure, set up through APIs, parallelise, and run the full suite in one browser.
Spend end-to-end tests on journeys and what only a browser shows.
FlashcardsInteractive · disabled in the PDF
Worker-scoped fixture
Made once per worker, shared by its tests: { scope: 'worker' }.
Option fixture
A fixture with a default that config or test.use can override: { option: true }.
mergeTests
Combines several extended test functions.
page.clock.install
Gives the test control of the page’s time.
grep on a project
Limits which tests that project runs.
Module 20: Mastery challenge
Playwright153 / 179
Module 20 · Mastery challenge
How this module works
Each slide is a problem you could meet in a real suite for the to-do app or the shop. Several have more than one plausible answer; pick the one an expert would, and read the explanation even when you are right.
A project to finish the courseInteractive · disabled in the PDF
Playwright154 / 179
Module 20 · Mastery challenge
Review: a locator
Predict the outputInteractive · disabled in the PDF
In review you see the line below. What is the best feedback?
TypeScript
1await page.locator('div.cart > ul > li:nth-child(2) button').click()
In review you see the line below. What is the best feedback?
QuizInteractive · disabled in the PDF
A test waits for either a success toast or an error alert with expect(toast.or(alert)).toBeVisible(). It sometimes fails with a strict mode violation. Why?
A test waits for either a success toast or an error alert with expect(toast.or(alert)).toBeVisible(). It sometimes fails with a strict mode violation. Why?
Playwright155 / 179
Module 20 · Mastery challenge
Puzzle: the slow save
Find the bugInteractive · disabled in the PDF
This passes locally and fails on CI about one run in ten. Which line is the cause?
TypeScript
Predict the outputInteractive · disabled in the PDF
The page has no element with the text "Loading" at all. What happens?
The page has no element with the text "Loading" at all. What happens?
Playwright156 / 179
Module 20 · Mastery challenge
Puzzle: a fixture’s cleanup
Find the bugInteractive · disabled in the PDF
Tests using order sometimes find no order to look at. Which line is wrong?
TypeScript
QuizInteractive · disabled in the PDF
A fixture seeds 500 products into a test database and takes 20 seconds. Tests only read them. Which scope?
A fixture seeds 500 products into a test database and takes 20 seconds. Tests only read them. Which scope?
Playwright157 / 179
Module 20 · Mastery challenge
Puzzle: the mock that does nothing
Find the bugInteractive · disabled in the PDF
The page still shows real products, as if the mock were not there. Which line is wrong?
TypeScript
QuizInteractive · disabled in the PDF
Every checkout test mocks the payment API. A real change to the payment API breaks production, and every test stays green. What would have caught it?
Every checkout test mocks the payment API. A real change to the payment API breaks production, and every test stays green. What would have caught it?
Playwright158 / 179
Module 20 · Mastery challenge
Puzzle: signed out halfway
QuizInteractive · disabled in the PDF
All tests share one saved sign-in. One test checks "Sign out". Afterwards, other tests running at the same time fail as signed out. Why?
All tests share one saved sign-in. One test checks "Sign out". Afterwards, other tests running at the same time fail as signed out. Why?
Put in orderInteractive · disabled in the PDF
Order the pieces of a sign-in setup for a large, parallel suite.
1.A worker fixture creates one test account per worker
2.It saves the storage state to a per-worker file
3.Each test starts signed in as its worker’s own account
4.The storageState fixture is overridden to return that file
5.It signs that account in through the API
Playwright159 / 179
Module 20 · Mastery challenge
Puzzle: the CI run
QuizInteractive · disabled in the PDF
Which settings belong in a strict, useful CI config?
Which settings belong in a strict, useful CI config?
QuizInteractive · disabled in the PDF
With --shard=1/4 to 4/4, three shards finish in 3 minutes and one takes 12. The suite is not fullyParallel. What helps most?
With --shard=1/4 to 4/4, three shards finish in 3 minutes and one takes 12. The suite is not fullyParallel. What helps most?
Playwright160 / 179
Module 20 · Mastery challenge
Final: design the test
Fill the blanksInteractive · disabled in the PDF
A complete test: mock an out-of-stock product, then check the shopper cannot buy it and sees "Sold out".
TypeScript
1test('cannot buy an out-of-stock product',async({ page })=>{2await page.('**/api/products/blue-teapot',(route)=>3 route.fulfill({json:{name:'Blue teapot',stock:0}}),4)5await page.goto('/products/blue-teapot')6const buy = page.('button',{name:'Add to cart'})7await(buy).()8awaitexpect(page.getByText('')).toBeVisible()9})
Playwright161 / 179
Module 20 · Mastery challenge
Recap: what an expert does
Finds elements the way users do, and lets the page tell them when it is ready.
Gives each test its own data and its own session where it changes them.
Puts setup and cleanup in fixtures, and sets up through APIs.
Mocks what is outside the test’s purpose, and checks the mocks against reality.
Runs strict, parallel CI with traces, and fixes flakiness at the cause.
Spends end-to-end tests on what only a browser can prove.
FlashcardsInteractive · disabled in the PDF
First locator to try
getByRole with a name.
How to wait
Web-first assertions and auto-waiting actions. Never sleeps.
Where cleanup goes
After await use(…) in a fixture.
When to register routes
Before the navigation that makes the request.
A flaky test is…
A bug to find, in the test or the app, not a reason to retry more.
Module 21: Final project: tests for a room booking app
Playwright162 / 179
Module 21 · Final project: tests for a room booking app
The project, and the starter
You will write the Playwright tests of a small app for booking meeting rooms, on your own computer, with the real tools: Chromium, the test runner, traces and UI mode. The app is written; the tests are yours. On the way you practise locators by role and label, assertions that wait, a page object offered as a fixture, page.route and reading a trace.
You need Node.js 22.12 or newer (node --version prints yours), a terminal and a code editor (module 3). Download the starter and unzip it: it makes a folder playwright-project/.
playwright-project/
README.md
package.json# Playwright 1.65, Vite 8, React 19
playwright.config.ts# how the tests run; starts the app
vite.config.ts# serves the page; /api to the server
npm install downloads Playwright, Vite and React; install chromium the browser this Playwright version was tested with.
Text
1Running 1 test using 1 worker23 ✓ 1 [chromium] › tests/example.spec.ts:3:1 › has the title (81ms)45 1 passed (1.2s)
What the last command prints. Your times will differ.
Get it runningInteractive · disabled in the PDF
Playwright163 / 179
Module 21 · Final project: tests for a room booking app
How the tests reach the app
You don’t need to start the app yourself: webServer does it (module 11). It builds the page and serves it with vite preview at http://localhost:4175, waits until that address answers, runs the tests, and stops it at the end. So the tests check the built app, the one you would ship.
playwright.config.ts
1import{ defineConfig, devices }from'@playwright/test'23exportdefaultdefineConfig({4testDir:'./tests',5fullyParallel:true,6forbidOnly:!!process.env.CI,7retries: process.env.CI ?2:0,8reporter:'list',9use:{10baseURL:'http://localhost:4175',11trace:'on-first-retry',12},13projects:[14{name:'chromium',use:{...devices['Desktop Chrome']}},15],16webServer:{17command:'npm run build && npm run preview',18url:'http://localhost:4175',19reuseExistingServer:!process.env.CI,20},21})
Vite serves the page, and a small plugin in vite.config.ts passes each request to /api/… on to the server in server/index.ts.
The server keeps each visitor’s bookings apart, found by a cookie. A visitor with no cookie starts from three sample bookings.
fullyParallel lets even the tests in one file run at the same time.
QuizInteractive · disabled in the PDF
Two tests run at once against the one server: one books a meeting, the other checks there are 3 bookings. Why does the second never see the first one’s booking?
Two tests run at once against the one server: one books a meeting, the other checks there are 3 bookings. Why does the second never see the first one’s booking?
Playwright164 / 179
Module 21 · Final project: tests for a room booking app
Step 1: check the start
Start with what everyone sees first: the heading Room bookings, and the three sample bookings in time order: Standup (09:00, Pine), Design review (10:00, Oak), then 1:1 with Sam (14:00, Oak). Every later test starts from these, so pin them down. Find things the way a person would, by role and name (module 6), and check the whole list at once (module 7). Make a new file:
tests/bookings.spec.ts
1import{ expect, test }from'@playwright/test'23test('lists the bookings in time order',async({ page })=>{4await page.goto('/')5const heading = page.getByRole('heading',{level:1})6awaitexpect(heading).toHaveText('Room bookings')78const rows = page9.getByRole('list',{name:'Bookings'})10.getByRole('listitem')11awaitexpect(rows).toHaveCount(3)12awaitexpect(rows).toContainText([13'09:00 Standup Pine',14'10:00 Design review Oak',15'14:00 1:1 with Sam Oak',16])17})
Given an array, toContainText checks part of each row’s text, in order; toHaveCount makes sure there are no others. Both wait for the list, which comes from the server a moment after the page (module 9).
Terminal
1npxplaywrighttesttests/bookings.spec.ts
Text
1Running 1 test using 1 worker23 ✓ 1 [chromium] › tests/bookings.spec.ts:3:1 › lists the bookings in time order (153ms)45 1 passed (1.4s)
Try itInteractive · disabled in the PDF
Playwright165 / 179
Module 21 · Final project: tests for a room booking app
Practise: a locator that lasts
QuizInteractive · disabled in the PDF
The next step presses the Book button. Which locator finds it the way a person would, and keeps working when the markup changes?
The next step presses the Book button. Which locator finds it the way a person would, and keeps working when the markup changes?
Playwright166 / 179
Module 21 · Final project: tests for a room booking app
Step 2: test booking a room
Now what people come for. Book Retro in Birch at 15:00 with the form, as a person would (module 8): the Meeting field, the Room and Time lists, the Book button. Then check that Retro is in the list with its time and room, and that the Meeting field is empty again. The server takes a moment to answer, so let the assertions wait; never a pause. Add this test at the end of the file:
Why `exact: true`. Labels match part of a name, and the section around the form is named Book a room. So getByLabel('Room') finds that section too, and selectOption fails: strict mode violation: getByLabel('Room') resolved to 2 elements. exact: true asks for the whole name (module 6).
Text
1$ npx playwright test tests/bookings.spec.ts2Running 2 tests using 2 workers34 ✓ 1 [chromium] › tests/bookings.spec.ts:3:1 › lists the bookings in time order (166ms)5 ✓ 2 [chromium] › tests/bookings.spec.ts:19:1 › books a room (420ms)67 2 passed (1.6s)
The tests run at the same time, so their lines can come in any order.
Try itInteractive · disabled in the PDF
Going deeper
Let codegen find a locator.npm run dev serves the app at http://localhost:5173/. With it running, npx playwright codegen http://localhost:5173/ opens it next to a recorder that writes test code as you click (module 5). It is a quick way to see which locator Playwright would choose; read every line before you keep any.
Why not expect(await rows.count()).toBe(4)? count() reads once, right after the click, before the server has answered, so the test fails at once (in one run it read 0: the list had not even loaded yet). await expect(rows).toHaveCount(4) keeps checking until it is true.
Playwright167 / 179
Module 21 · Final project: tests for a room booking app
Step 3: cancel the right booking
Cancel Design review, then check that it went and that the other two stayed: a bug could as easily remove the wrong one. Every booking has a Cancel button, so page.getByRole('button', { name: 'Cancel' }) fails with strict mode violation: … resolved to 3 elements. Start from the right row and look inside it (module 7). Add:
tests/bookings.spec.ts
1test('cancels the right booking',async({ page })=>{2await page.goto('/')3const rows = page4.getByRole('list',{name:'Bookings'})5.getByRole('listitem')6const review = rows.filter({hasText:'Design review'})7await review.getByRole('button',{name:'Cancel'}).click()89awaitexpect(review).toHaveCount(0)10awaitexpect(rows).toHaveCount(2)11awaitexpect(rows).toContainText(['Standup','1:1 with Sam'])12})
The row goes once the server has answered; toHaveCount(0) waits for that.
Text
1$ npx playwright test tests/bookings.spec.ts2Running 3 tests using 3 workers34 ✓ 3 [chromium] › tests/bookings.spec.ts:3:1 › lists the bookings in time order (163ms)5 ✓ 2 [chromium] › tests/bookings.spec.ts:19:1 › books a room (438ms)6 ✓ 1 [chromium] › tests/bookings.spec.ts:35:1 › cancels the right booking (468ms)78 3 passed (1.7s)
Try itInteractive · disabled in the PDF
Playwright168 / 179
Module 21 · Final project: tests for a room booking app
Step 4: a page object
The same locators now repeat in every test: if the list’s name changed, three tests would need the same edit. Put them in a page object (module 13), a class with the page’s locators and the steps a person takes there. Make a new file:
The constructor only builds locators: they search when they are used. alert is where the app shows the server’s errors; step 6 uses it.
Playwright169 / 179
Module 21 · Final project: tests for a room booking app
Step 4: its steps, and a fixture
Add what a person does as methods, inside the class after goto(). They find and do; the checks stay in the tests. booking() returns a locator, not an element, so it needs no await.
Then offer it as a fixture with test.extend (module 12): a test asks for bookings and gets the page object, with the app already open.
tests/fixtures.ts
1import{ test as base }from'@playwright/test'2import{ BookingsPage }from'./pages/bookings-page'34exportconst test = base.extend<{ bookings:BookingsPage}>({5bookings:async({ page }, use)=>{6const bookings =newBookingsPage(page)7await bookings.goto()8awaituse(bookings)9},10})11export{ expect }from'@playwright/test'
Playwright170 / 179
Module 21 · Final project: tests for a room booking app
Step 4: tests that ask for bookings
Now rewrite tests/bookings.spec.ts to import test and expect from ./fixtures and ask for bookings instead of page. Each test keeps its checks and loses its setup.
tests/bookings.spec.ts
1import{ expect, test }from'./fixtures'23test('lists the bookings in time order',async({ bookings })=>{4const heading = bookings.page.getByRole('heading',{level:1})5awaitexpect(heading).toHaveText('Room bookings')6awaitexpect(bookings.rows).toHaveCount(3)7awaitexpect(bookings.rows).toContainText([8'09:00 Standup Pine',9'10:00 Design review Oak',10'14:00 1:1 with Sam Oak',11])12})1314test('books a room',async({ bookings })=>{15await bookings.book('Retro','Birch','15:00')16const retro = bookings.booking('Retro')17awaitexpect(retro).toContainText('15:00 Retro Birch')18awaitexpect(bookings.rows).toHaveCount(4)19awaitexpect(bookings.meeting).toHaveValue('')20})2122test('cancels the right booking',async({ bookings })=>{23await bookings.cancel('Design review')24awaitexpect(bookings.booking('Design review')).toHaveCount(0)25awaitexpect(bookings.rows).toHaveCount(2)26awaitexpect(bookings.rows).toContainText(['Standup','1:1 with Sam'])27})
Text
1$ npx tsc --noEmit2$ npx playwright test3Running 4 tests using 4 workers45 ✓ 1 [chromium] › tests/example.spec.ts:3:1 › has the title (97ms)6 ✓ 3 [chromium] › tests/bookings.spec.ts:3:1 › lists the bookings in time order (176ms)7 ✓ 4 [chromium] › tests/bookings.spec.ts:14:1 › books a room (448ms)8 ✓ 2 [chromium] › tests/bookings.spec.ts:22:1 › cancels the right booking (481ms)910 4 passed (1.7s)
tsc prints nothing when the types are right, which catches a misspelt method before a test runs.
Try itInteractive · disabled in the PDF
Playwright171 / 179
Module 21 · Final project: tests for a room booking app
Step 5: survive a reload
The server keeps the bookings, so they should outlast a reload: page.reload() loads the page again, as the reload button does. Cancel 1:1 with Sam, book Planning in Oak at 16:00, wait until Planning shows (then the server has it), reload, and check both changes are still there. Make a new file:
tests/reload.spec.ts
1import{ expect, test }from'./fixtures'23test('keeps the bookings after a reload',async({ bookings })=>{4await bookings.cancel('1:1 with Sam')5awaitexpect(bookings.booking('1:1 with Sam')).toHaveCount(0)6await bookings.book('Planning','Oak','16:00')7awaitexpect(bookings.booking('Planning')).toBeVisible()89await bookings.page.reload()10awaitexpect(bookings.booking('Planning')).toBeVisible()11awaitexpect(bookings.booking('1:1 with Sam')).toHaveCount(0)12})
QuizInteractive · disabled in the PDF
After the reload, why check that Planning is visible before checking that 1:1 with Sam is gone?
After the reload, why check that Planning is visible before checking that 1:1 with Sam is gone?
Playwright172 / 179
Module 21 · Final project: tests for a room booking app
Step 5: the room you chose
The page keeps the room you choose under Show in the browser’s storage, so that should outlast a reload too. Add a method to BookingsPage that presses one of those buttons:
Then add a test to tests/reload.spec.ts. The chosen room is the pressed button, aria-pressed="true"; Birch has no bookings, so the list says so.
tests/reload.spec.ts
1test('remembers the room you chose',async({ bookings })=>{2await bookings.show('Birch')3const empty = bookings.page.getByText('No bookings in Birch.')4awaitexpect(empty).toBeVisible()56await bookings.page.reload()7const birch = bookings.page.getByRole('button',{name:'Birch'})8awaitexpect(birch).toHaveAttribute('aria-pressed','true')9awaitexpect(empty).toBeVisible()10})
Text
1$ npx playwright test tests/reload.spec.ts2Running 2 tests using 2 workers34 ✓ 1 [chromium] › tests/reload.spec.ts:14:1 › remembers the room you chose (259ms)5 ✓ 2 [chromium] › tests/reload.spec.ts:3:1 › keeps the bookings after a reload (873ms)67 2 passed (2.1s)
Try itInteractive · disabled in the PDF
Playwright173 / 179
Module 21 · Final project: tests for a room booking app
Step 6: test what goes wrong
A good suite checks the refusals too. The server refuses a booking with no name (or only spaces), and a room that is taken: Oak at 10:00 is Design review’s. The form shows its answer in an element with the role alert, adds nothing, and keeps what you typed. Make a new file:
tests/errors.spec.ts
1import{ expect, test }from'./fixtures'23test('refuses a booking with no name',async({ bookings })=>{4await bookings.book(' ','Pine','11:00')5awaitexpect(bookings.alert).toHaveText('Give the meeting a name.')6awaitexpect(bookings.rows).toHaveCount(3)7})89test('refuses a room that is taken',async({ bookings })=>{10await bookings.book('Retro','Oak','10:00')11awaitexpect(bookings.alert).toHaveText(12'Oak is already booked at 10:00.',13)14awaitexpect(bookings.rows).toHaveCount(3)15awaitexpect(bookings.meeting).toHaveValue('Retro')16})
Check the alert first: it shows only once the server has answered, so the count after it is checked with the answer in.
Playwright174 / 179
Module 21 · Final project: tests for a room booking app
Step 6: make the server fail with page.route
Some failures the real server never gives on demand, like an error while cancelling. So answer that request yourself (module 14): page.route catches the requests whose address matches, and fulfill answers them, here with a 500. Then check the page says so and keeps the booking. Add to tests/errors.spec.ts:
tests/errors.spec.ts
1test('keeps a booking when cancelling fails',async({ bookings })=>{2await bookings.page.route('**/api/bookings/*',(route)=>3 route.fulfill({status:500}),4)5await bookings.cancel('Design review')6awaitexpect(bookings.alert).toHaveText('The server answered 500.')7awaitexpect(bookings.booking('Design review')).toBeVisible()8})
* stops at a slash, so the pattern matches the cancel request, DELETE /api/bookings/b1, but not the list at /api/bookings.
The fixture has already opened the page, which is fine: the route only has to be there before the click that makes the request.
An empty answer has no error message, so the app says The server answered 500. (see src/api.ts).
Text
1$ npx playwright test tests/errors.spec.ts2Running 3 tests using 3 workers34 ✓ 2 [chromium] › tests/errors.spec.ts:18:1 › keeps a booking when cancelling fails (239ms)5 ✓ 3 [chromium] › tests/errors.spec.ts:3:1 › refuses a booking with no name (305ms)6 ✓ 1 [chromium] › tests/errors.spec.ts:9:1 › refuses a room that is taken (324ms)78 3 passed (1.5s)
Try itInteractive · disabled in the PDF
Playwright175 / 179
Module 21 · Final project: tests for a room booking app
Practise: a test that cannot fail
Predict the outputInteractive · disabled in the PDF
This passes. Would it fail if the server had a bug and booked Oak at 10:00 a second time?
TypeScript
1test('refuses a room that is taken',async({ bookings })=>{2await bookings.book('Retro','Oak','10:00')3awaitexpect(bookings.rows).toHaveCount(3)4})
This passes. Would it fail if the server had a bug and booked Oak at 10:00 a second time?
Playwright176 / 179
Module 21 · Final project: tests for a room booking app
Step 7: make a test fail, on purpose
Tests are there to fail one day, and what matters then is how fast you find out why. Plant a bug: in src/bookings.ts, make inTimeOrder forget to sort. Then run with a trace, a recording of each test (module 5).
On the left, every step: the bookings fixture’s Navigate, then each Expect. Click the failed Expect "toContainText" to see the page as it was then.
The Network tab has GET /api/bookings, and its answer lists Design review first, in the order the bookings were made. The server is fine; the page did not sort them.
npx playwright test --ui shows the same, test by test, every time you run one: often quicker while you work.
In CI the config retries a failing test and records a trace of the first retry (on-first-retry, module 17). On your computer there are no retries, so ask with --trace on.
Try itInteractive · disabled in the PDF
Playwright178 / 179
Module 21 · Final project: tests for a room booking app
Step 8: run it all, many times
A suite earns trust by passing every time, not once. With the bug undone, check the types, then run every test ten times over, as module 18 does to hunt flaky tests. Any failure now is worth its trace.
Terminal
1npxtsc--noEmit2npxplaywrighttest--repeat-each=10
Text
1Running 90 tests using 6 workers2 …3 ✓ 90 [chromium] › tests/reload.spec.ts:14:1 › remembers the room you chose (228ms)4 ✓ 89 [chromium] › tests/reload.spec.ts:3:1 › keeps the bookings after a reload (862ms)56 90 passed (10.0s)
Nine tests, ten times each. The number of workers depends on your computer: by default, half its CPU cores.
Try itInteractive · disabled in the PDF
Playwright179 / 179
Module 21 · Final project: tests for a room booking app
What you built, and where to go next
Nine tests in four files, which run in parallel and pass run after run, against the built app that webServer starts.
Locators by role, label and text, narrowed to one row with filter, and assertions that wait instead of pauses.
A page object, BookingsPage, offered as the bookings fixture.
Tests of what goes wrong: the server’s refusals, and a failure made with page.route.
A failure read from its trace, and a suite checked for flakiness with --repeat-each.
Where to go next, in the same project:
Run the suite in Firefox and WebKit too: add projects (module 11) and npx playwright install their browsers.
Add an accessibility check with axe, and a test that books a room with the keyboard only (module 16).
Run it in CI on every push, with retries and traces (module 17).
Test the API directly with request (module 14): that a taken room answers 409, without a browser.
Answer key
1. What testing is, and why a real browser · A test does three things: prepare, act, check · Put in orderPut the parts of a test for "adding a product to the cart" in order.1. Open the shop’s product page 2. Click "Add to cart" 3. Check the cart badge now says 1Arrange (open the page), act (click), assert (check the badge). Checking before acting would test nothing, because the badge has not had a reason to change yet.
1. What testing is, and why a real browser · Many small tests, fewer big ones · QuizYou want to be sure a shopper can sign in, add a mug to the cart and reach the payment page. Which kind of test fits best?B. An end-to-end testIt is a journey across several pages, so it needs a real browser driving the whole app: an end-to-end test. A unit test could check the cart total, but not that the pages link up. Checking by hand once does not protect the next change.
1. What testing is, and why a real browser · Playwright drives real browsers from code · QuizWhich browser engine does Playwright use to stand in for Safari?C. WebKitSafari is built on WebKit, so Playwright runs WebKit to show how pages behave there. Chromium is the engine behind Chrome and Edge; Firefox is its own engine.
1. What testing is, and why a real browser · What makes a test worth having · QuizA test passes on Monday, fails on Tuesday, and passes again on Wednesday. Nobody changed the app. What is it?B. A flaky testNothing changed, yet the result did: that is flakiness. Later modules show where it comes from (timing, shared data, the network) and how Playwright helps avoid it.
2. What a browser does when you click · JavaScript changes the page while you watch · QuizA test types "Buy milk", presses Enter, and checks the list immediately, without waiting. The app saves to the server before showing the item. What can happen?B. The check sometimes fails, because the item is not there yetThe check races the app. When the server is fast it wins, when it is slow it loses: a flaky test. Playwright solves this by retrying checks until they pass or time runs out, which module 9 covers.
2. What a browser does when you click · Practise: roles and names · QuizWhat is the role and name of the text box here?B. Role "textbox", name "Password"An <input> that takes text has the role textbox, and the <label> pointing at it (by for="pw") gives it the name "Password". The id is for the code, not for people, so it is not the name. A password box is still a textbox to the accessibility tree.
2. What a browser does when you click · Practise: roles and names · QuizA button shows only a trash-can picture and has no text. What does a screen reader announce as its name?B. Nothing useful: it has no nameThe browser cannot guess what a picture means. Without text or an aria-label="Delete", the button has no name: a real accessibility bug, and one a role-based test catches because it cannot find the button by name.
3. Setting up Playwright · What the starter creates · QuizWhich file will Playwright run as tests by default?B. tests/cart.spec.tsBy default Playwright looks for files whose names end in .spec or .test followed by a JavaScript or TypeScript extension. cart.ts could hold helper code; it is not run as tests on its own.
3. Setting up Playwright · Headless: a browser with no window · QuizIs a headless browser a "real" browser?B. Yes: it runs scripts, styles and layout; it just does not show a windowHeadless mode skips putting pixels in a window, not the work of a browser. Layout, scripts, events and screenshots all happen. That is why tests pass the same way headed or headless.
3. Setting up Playwright · Try it · Fill the blanksInstall only the Chromium browser for this Playwright version.1: playwright, 2: installnpx playwright runs the Playwright command line installed in the project; install downloads browsers. Naming one browser saves time and disk space when that is all you test.
4. Your first test, line by line · Why every line starts with await · Find the bugThis test fails with "Error: Test ended." even when the to-do is added. Which line is wrong?Line 6: await expect(page.getByTestId('todo-title')).toHaveText('Buy milk')Without await, the test does not wait for the check: the function ends while the check is still running, so Playwright stops it and fails the test with "Test ended.", whether the page is right or not. Higher up in a test, a missing await lets the next lines run before the check has finished. Playwright’s checks on locators always need await.
4. Your first test, line by line · Running it · QuizYou wrote one test, and the summary says "3 passed". Why?B. The config runs each test once per browser: Chromium, Firefox and WebKitThe starter playwright.config.ts defines three projects, one per browser engine, and every test runs in each. Module 11 shows how to choose which ones run.
4. Your first test, line by line · Practise: write a check · Fill the blanksComplete a test that opens the to-do app and adds "Walk the dog".1: page, 2: goto, 3: fill, 4: Enter, 5: expectAsk for page, open the address with goto, type with fill, press Enter to submit, then expect the title. The same shape works for nearly every test you will write.
4. Your first test, line by line · Practise: write a check · QuizWhen does page.getByPlaceholder('What needs to be done?') look for the element?B. Only when you use it: in an action like fill or a check like expectA locator is a recipe, not a found element. It searches each time you act on it, so it always finds the current element, even if the page has redrawn it. That is why creating one needs no await.
5. Running tests and reading the results · Choosing which tests run · QuizWhich command runs only the tests whose name mentions "sign in"?B. npx playwright test -g "sign in"-g (short for --grep) filters by test name. A bare word after test filters by file path, so sign-in matches a file like tests/sign-in.spec.ts, not a test name. --project picks a browser setup.
5. Running tests and reading the results · UI mode: watch, rerun, and step back in time · QuizA test fails at the 6th step and you want to see what the page looked like just before. What do you use?B. UI mode: click the 6th action to see the snapshot before and afterUI mode records a snapshot for every action, so you travel back to any moment. Pauses only slow the test down and still do not show you the page’s inside.
5. Running tests and reading the results · Traces: a recording of a test run · Put in orderA test fails. Put a sensible way to investigate in order.1. Read the error: which check, expected, received 2. Open the report and look at the failed step 3. Open the trace or UI mode and look at the page at that step 4. Fix the test or the app, then run that test againStart with the cheapest information, the error message, and only dig into the trace when you need to see the page. Rerunning the single test (by file and line) keeps the loop short.
6. Finding elements with locators · Why role first · QuizA designer renames the class btn-blue to btn-primary. Which locator still works?B. page.getByRole('button', { name: 'Sign in' })The CSS selector names a class, which is a styling detail, so it breaks. The role and name are what the user perceives, and the rename did not change those.
6. Finding elements with locators · CSS and XPath, when you must · QuizWhich locator is the best choice for the shop’s search box, <input aria-label="Search products">?C. page.getByRole('textbox', { name: 'Search products' })A plain <input> (no type="search") has the role textbox, and aria-label gives it its name. The role searchbox is only for type="search" or an explicit role="searchbox". locator('input') would match every input on the page.
6. Finding elements with locators · Strictness: one action, one element · QuizThe page has buttons "Add to cart" and "Add to wishlist". What does page.getByRole('button', { name: 'Add' }).click() do?B. Fails with a strict mode violation: it matches bothNames match on a part of the text by default, so "Add" matches both buttons, and actions refuse to choose. Use the full name, or { name: 'Add', exact: true } if a button is really called just "Add".
7. Narrowing and combining locators · filter keeps the matches you want · Fill the blanksKeep only the products that are not sold out, then check there are two.1: filter, 2: hasNotText, 3: toHaveCountfilter with hasNotText drops the teapot, which says "Sold out". toHaveCount checks how many elements a locator matches, and retries until it is right, like every expect on a locator.
7. Narrowing and combining locators · Checking a whole list at once · QuizThe page shows "Blue teapot, Red mug, Green bowl". Does the check above pass?B. No: the array must match in orderAn array checks each element against the same position. It also fails if there are more or fewer matches than items in the array. To ignore order, sort or check each item separately; to check only part of each text, use toContainText.
7. Narrowing and combining locators · Practise: the right product · Find the bugThe test should add the Green bowl, but the cart ends up with a Red mug. Which line is wrong?Line 4: await bowl.getByRole('button', { name: 'Add to cart' })Line 4 searches inside all products, not inside bowl, and first() then picks the mug. Chain from bowl so the search stays inside the right item, and the first() is no longer needed.
7. Narrowing and combining locators · Practise: the right product · QuizWhat is the difference between these two?B. A finds the list item that contains "Sold out"; B finds the "Sold out" text itselffilter keeps the outer elements (the items) that contain a match. Chaining moves into them and returns the inner match (the paragraph). Use A to then click that item’s button; use B to check the label itself.
8. Acting like a user, and auto-waiting · Pressing keys · QuizWhy write ControlOrMeta+a instead of Control+a?B. It presses Control on Windows and Linux, and Command on macOSShortcuts use Command on a Mac and Control elsewhere. ControlOrMeta picks the right one for the computer running the test, so one test works for both.
8. Acting like a user, and auto-waiting · Checkboxes, menus, and files · Fill the blanksFill the checkout: choose standard delivery and accept the terms.1: radio, 2: check, 3: check, 4: clickRadio buttons have the role radio. check() works for both radios and checkboxes, and it verifies the box really is checked afterwards, which click() does not.
8. Acting like a user, and auto-waiting · When an action times out, the log says why · QuizWhat stops the click above?B. A cookie banner covers the button"intercepts pointer events" means another element is on top, at the spot Playwright would click. A real person could not click it either. Close the banner first in the test, or set a cookie so it never shows (module 15).
8. Acting like a user, and auto-waiting · Clicks that change the page · Put in orderPut what happens during button.click() in order.1. Find the element the locator describes 2. Wait until it is visible, stable and enabled 3. Scroll it into view 4. Check nothing covers its middle, then press and release the mouse there 5. Return once the click is doneFinding comes first, then the actionability checks, then scrolling and the real mouse events. If the element changes during the checks (redrawn, say), Playwright starts again with the fresh element.
9. Assertions that wait · Saying what should not be there · QuizThe cart line reads "Red mug × 2". Which of these pass?B. B and CA string in toHaveText must match the whole text, so A fails on the "× 2". toContainText checks for a part (B), and a regular expression matches anywhere unless anchored with ^ and $ (C).
9. Assertions that wait · Plain values do not retry · Find the bugThis check is flaky: the to-dos load a moment after the page. Which line is the problem?Line 3: await expect(items).toHaveCount(3)count() reads the number once, right now, and the plain toBe cannot retry. If the list has not loaded, it sees 0. await expect(items).toHaveCount(3) keeps checking until there are three.
9. Assertions that wait · Waiting for anything: expect.poll and toPass · QuizYou need to wait until a text box shows "Saved". Which do you use?B. await expect(box).toHaveValue('Saved')There is a built-in web-first assertion for exactly this, with a clearer error. Reach for poll and toPass only when no locator assertion fits.
10. Organising tests: groups, hooks, steps and tags · Each test stands on its own · QuizTest A adds a mug to the cart, which the shop keeps in the browser’s storage. Test B runs next and opens the cart. What does it see?B. An empty cart: B has a new contextBrowser storage belongs to a context, and B gets a fresh one. (If the shop saved the cart on its server, B could see it: that is shared data, which module 19 deals with.)
10. Organising tests: groups, hooks, steps and tags · Practise: hooks · Put in orderA file has a beforeAll, a beforeEach, an afterEach and two tests. Put what runs in order (one worker).1. beforeAll 2. beforeEach, then test 1 3. afterEach for test 1 4. beforeEach, then test 2 5. afterEach for test 2beforeAll runs once, first. beforeEach and afterEach wrap each test. If tests are split across workers, each worker runs its own beforeAll, which is why it must not assume it runs only once in total.
10. Organising tests: groups, hooks, steps and tags · Practise: hooks · Fill the blanksOpen the shop’s product page before each test.1: beforeEach, 2: pagetest.beforeEach takes the same fixtures as a test, so it can ask for page and open the address.
10. Organising tests: groups, hooks, steps and tags · Tags pick out groups of tests · QuizWhat does npx playwright test --grep-invert @slow run?B. Every test except those tagged @slow--grep keeps matching tests; --grep-invert drops them. Tags are matched like test names, so the same flags work for both.
11. Configuration, projects and devices · Projects run the suite in several setups · QuizWith the four projects above and 10 tests, how many test runs happen with npx playwright test --project=phone?B. 10Without the flag each of the 10 tests runs in all 4 projects: 40 runs. --project=phone keeps only one project, so 10.
11. Configuration, projects and devices · Practise: write a config · Fill the blanksRun against a local shop, keep traces on retry, and add a phone project.1: baseURL, 2: on-first-retry, 3: projects, 4: devicesbaseURL and trace go under use, since they shape the browser; projects sits at the top level and each project has its own use.
11. Configuration, projects and devices · Practise: write a config · Find the bugThe tests still open at 1280 pixels wide. Which line is in the wrong place?Line 3: use: { baseURL: 'http://localhost:3000', viewport: { width: 390, height: 844 } },viewport is a browser option, so it belongs inside use. At the top level it is ignored (TypeScript flags it, too, which is one reason to write the config in TypeScript).
12. Fixtures: what a test is given · Fixtures are made only when asked for · QuizA test only asks for request, to call an API. Does Playwright open a browser tab for it?B. No: fixtures are only set up when a test names themFixtures are lazy. Pure API tests that never name page run without a tab, which makes them very fast.
12. Fixtures: what a test is given · Using a custom fixture · Fill the blanksFinish a fixture that opens the cart page and hands it to the test.1: extend, 2: page, 3: use, 4: useThe fixture asks for page, does its setup, then passes the value to use. Leave out the use(…) call and the test fails with "use() was not called in fixture"; leave out only its await, and any cleanup after it runs while the test is still going.
12. Fixtures: what a test is given · Fixtures or hooks? · Find the bugTests using this fixture sometimes see the cart already emptied. Which line is wrong?Line 5: await use(page)Without await, the fixture does not wait for the test. It goes straight on to the cleanup and empties the cart while the test is still running.
13. Page objects and reusable helpers · Tests read like the plan · QuizThe placeholder changes to "Add a task". With the page object, how many places need editing?A. One: the constructorEvery test reaches the box through todos.newTodo, which is defined once. That single edit is the main reason page objects exist.
13. Page objects and reusable helpers · Offer page objects as fixtures · Fill the blanksUse the todos fixture in a test.1: todos, 2: add, 3: itemsThe test names the fixture, calls the page object’s method, and asserts on one of its locators. No setup lines are left in the test.
13. Page objects and reusable helpers · When abstraction hurts · Find the bugTypeScript rejects this page object. Which line?Line 4: // move navigation into an async goto() methodA constructor cannot be async, so it cannot await. Keep constructors to building locators, and put navigation in an async goto() method (or in the fixture).
13. Page objects and reusable helpers · Small helper functions are fine too · QuizOnly two tests share one three-line step. What is the simplest good option?B. A small helper functionA function names the step and keeps it in one place, without a class. Promote it to a page object if more steps for that page pile up. Copying is fine for two lines, but steps tend to grow.
14. The network: stand-ins and API tests · page.route catches requests; fulfill answers them · QuizThe test calls page.goto('/products') and thenpage.route('**/api/products', …). What happens?B. The page may already have asked the real server before the route existedRoutes only catch requests made after they are set up. The page asks for products while it loads, so register routes first, then navigate.
14. The network: stand-ins and API tests · Testing what happens when things fail · Fill the blanksAnswer the cart request with one item, before opening the cart.1: route, 2: fulfill, 3: json, 4: gotoRegister the route, answer with fulfill({ json }), and only then navigate so the page’s request is caught.
14. The network: stand-ins and API tests · Waiting for a request to finish · Find the bugThis test sometimes hangs until it times out. Which line is in the wrong place?Line 2: const ordered = page.waitForResponse('**/api/orders') // before the clickIf the response arrives before waitForResponse starts listening, it is missed, and the wait never ends. Create the promise first, click, then await it.
14. The network: stand-ins and API tests · Use the API to set up, the browser to check · QuizThe cart is tied to the browser’s session cookie. Which client should add the item?B. page.request: it shares the page’s cookiespage.request (and context.request) use the browser context’s cookies, so the server sees the same session. The request fixture has its own, separate cookies: the item would land in another cart.
15. Signing in once: authentication and storage state · Wire it up in the config · Put in orderPut what happens on npx playwright test --project=chromium in order.1. The setup project runs auth.setup.ts 2. It signs in and saves the storage state file 3. The chromium tests start, each with a new context 4. Each context is loaded with the saved cookies 5. Each test begins already signed inProject dependencies run first, even when you choose only the dependent project. Each test still gets its own context; the storage state just pre-fills it.
15. Signing in once: authentication and storage state · Several roles, several files · QuizA chat feature needs a shopper and a staff member at the same time, in one test. How?B. Make two contexts with browser.newContext({ storageState }), one per roleEach context is a separate session, so two contexts in one test can be two different people. Close them at the end. Signing out and in would never have both online at once.
16. Screenshots and accessibility checks · Practise: screenshots · QuizThe home page shows the current time. Its screenshot test fails on every run. What is the best fix?B. Mask the clock, or fix the time with page.clockThe difference is real but irrelevant, so remove its cause: mask that element, or freeze the page’s time (module 19). A higher tolerance would also hide real changes; updating every run would compare nothing.
16. Screenshots and accessibility checks · Practise: screenshots · Fill the blanksCompare only the cart summary box, hiding the order number.1: summary, 2: toHaveScreenshot, 3: masktoHaveScreenshot also works on a locator, capturing just that element. Smaller screenshots fail for fewer unrelated reasons.
16. Screenshots and accessibility checks · What a machine cannot check · Quizaxe reports a contrast problem on a dialog only sometimes. What is the likely cause?B. The check sometimes runs while the dialog is still fading in, half transparentMid-animation, the text really is faint for a moment. Wait until the page is settled (assert the dialog is visible and finished, or turn off animations with reducedMotion: 'reduce' if the site honours it) before running axe. Disabling the rule would hide real problems.
17. Parallel runs, sharding and CI · A failure restarts the worker · QuizA suite has 2 files of 20 tests each, and 8 workers, with default settings. How many tests can run at once?B. 2By default a file’s tests run in order in one worker, so only the two files run side by side and six workers sit idle. fullyParallel: true lets all 8 workers share the 40 tests.
17. Parallel runs, sharding and CI · Merging reports from shards · Put in orderPut a sharded CI run in order.1. Four machines each install dependencies and browsers 2. Each runs npx playwright test --shard=N/4 with the blob reporter 3. Each uploads its blob-report folder 4. A final job downloads all four and runs merge-reports 5. The merged HTML report is uploaded for people to readShards run independently and in parallel; only the merge waits for all of them. Upload blobs even when tests fail, or failed shards vanish from the report.
17. Parallel runs, sharding and CI · Settings that make CI strict and useful · QuizWith retries: 2, a test fails, then passes on its first retry. How is it reported?B. FlakyPlaywright marks it flaky: it passed in the end, but not reliably. The run still succeeds unless failOnFlakyTests is on. Either way, the trace from the retry is there to find out why.
18. Flaky tests: root causes and real fixes · Cause 1: checking before the page is ready · Find the bugThis test is flaky: sometimes the coupon is not applied. Which line causes it?Line 3: // no branch: await field.fill(…) waits for the fieldisVisible() answers immediately. If the field renders a moment late, the branch is skipped and the coupon never applied. A test should know what the page will show; drop the if and let fill wait.
18. Flaky tests: root causes and real fixes · Cause 3: tests sharing data on the server · QuizTest A expects the cart to hold 1 item. Test B empties the same account’s cart. They run in parallel. What is the fix?B. Give each test, or each worker, its own account or dataSerial mode hides the problem by giving up parallelism, and retries only lower the odds. Separate data removes the collision entirely. The next module shows per-test and per-worker data fixtures.
18. Flaky tests: root causes and real fixes · Cause 4: time, randomness and the outside world · QuizA test checks "Ordered today" and fails only on CI runs that start just before midnight. What is the cause?B. The order is placed before midnight and checked after, so it now says "yesterday"The test depends on the real clock. Freezing time with page.clock makes "today" the same throughout, whatever time the run starts.
18. Flaky tests: root causes and real fixes · Cause 5: a machine with too much to do · Put in orderPut a sound way to deal with a flaky test in order.1. Reproduce it with --repeat-each and no retries 2. Read the failing trace and find what differed 3. Name the cause: timing, data, order, time, or load 4. Fix the cause in the test or the app 5. Repeat the run many times to confirm it is goneGuessing and adding waits rarely works. A trace of the actual failure shows what the page was doing; the repeat run afterwards proves the fix.
19. Designing a suite that scales · Practise: scopes · QuizWhy can a worker-scoped fixture not ask for page?B. page belongs to one test; a worker fixture outlives many testsA fixture can depend only on fixtures that live at least as long as it does. page is made and thrown away per test; a worker fixture lives for the whole worker. Use browser or playwright there.
19. Designing a suite that scales · Practise: scopes · Fill the blanksMake the fixture worker-scoped.1: playwright, 2: dispose, 3: scope, 4: workerWorker fixtures are written as [fn, { scope: 'worker' }] and depend on worker-level fixtures like playwright. Disposing the request context after use frees its resources.
19. Designing a suite that scales · Keeping a large suite fast · QuizA suite takes 40 minutes. Most tests begin by signing in through the form and adding items by clicking. What gives the biggest win first?B. Signing in once with storage state and adding items through the APIRepeated setup through the UI is usually the largest cost, and it is not what those tests check. Lower timeouts make no test faster, only fail sooner; fewer assertions make tests weaker, not quicker.
20. Mastery challenge · Review: a locator · QuizIn review you see the line below. What is the best feedback?B. Use getByRole('listitem').filter({ hasText: 'Blue teapot' }).getByRole('button', { name: 'Remove' })The CSS path depends on markup and on order, and says nothing about which product or which button. The role-based version names both, survives redesigns and re-sorting, and fails if the button loses its accessible name. force would hide real problems; nth(1) is still order-based.
20. Mastery challenge · Review: a locator · QuizA test waits for either a success toast or an error alert with expect(toast.or(alert)).toBeVisible(). It sometimes fails with a strict mode violation. Why?B. Both appeared at once, so the locator matched two elementsAssertions like toBeVisible are strict: they need one element. When both show, or matches two. Use .first() if both are legitimate, but here it also hints at a real bug: success and error at the same time.
20. Mastery challenge · Puzzle: the slow save · Find the bugThis passes locally and fails on CI about one run in ten. Which line is the cause?Line 4: await expect(page.getByRole('status')).toHaveText('Saved')The half-second sleep guesses how long saving takes. On a slow CI machine it is not enough, the reload happens first, and the old name comes back. Wait for the app’s own confirmation (or the save response) before reloading.
20. Mastery challenge · Puzzle: the slow save · QuizThe page has no element with the text "Loading" at all. What happens?B. It passes straight awaytoBeHidden passes when no element matches or the match is not visible. That is handy for spinners, but also a trap: a typo in the text makes it pass for the wrong reason. Pair it with a positive check of what should appear.
20. Mastery challenge · Puzzle: a fixture’s cleanup · Find the bugTests using order sometimes find no order to look at. Which line is wrong?Line 6: // move the delete below: await use(order), then deleteCleanup belongs afterawait use(order). Here the order is deleted before the test ever runs. (It "sometimes" works only if the delete is slow to take effect.)
20. Mastery challenge · Puzzle: a fixture’s cleanup · QuizA fixture seeds 500 products into a test database and takes 20 seconds. Tests only read them. Which scope?B. Worker scope, or a setup project run once before all testsRead-only data can be shared safely, so make it once: per worker, or once for the whole run in a setup project with dependencies. Per test would add 20 seconds to every test.
20. Mastery challenge · Puzzle: the mock that does nothing · Find the bugThe page still shows real products, as if the mock were not there. Which line is wrong?Line 1: // register the route first, then: await page.goto('/products')The page fetched its products while loading, before the route existed. Set up routes first, then navigate. (If the app requests /api/products?page=1, the pattern must allow for the query too: a regular expression like /\/api\/products/ does.)
20. Mastery challenge · Puzzle: the mock that does nothing · QuizEvery checkout test mocks the payment API. A real change to the payment API breaks production, and every test stays green. What would have caught it?B. A few tests, or an API test, against the real (test-mode) payment serviceMocks only check that the app works with the answers you wrote. Something has to check those answers still match reality: a small set of tests against the real service, run regularly.
20. Mastery challenge · Puzzle: signed out halfway · QuizAll tests share one saved sign-in. One test checks "Sign out". Afterwards, other tests running at the same time fail as signed out. Why?B. Signing out ended the session on the server, and every test was using that same sessionContexts are separate, but they all carry a cookie for the same server session. Signing out ends it for everyone. Give that test its own fresh sign-in (its own account, or its own session) instead of the shared state.
20. Mastery challenge · Puzzle: signed out halfway · Put in orderOrder the pieces of a sign-in setup for a large, parallel suite.1. A worker fixture creates one test account per worker 2. It signs that account in through the API 3. It saves the storage state to a per-worker file 4. The storageState fixture is overridden to return that file 5. Each test starts signed in as its worker’s own accountThis is the per-worker variant of the setup project: no shared session, no collisions, and sign-in still happens only once per worker. Playwright’s authentication guide shows the same pattern.
20. Mastery challenge · Puzzle: the CI run · QuizWhich settings belong in a strict, useful CI config?A. forbidOnly: true; B. trace: 'on-first-retry' with a few retries; D. Upload the report even when tests failA stray only, missing traces or a lost report all make failures harder to deal with. Ten retries turns a flaky suite green and hides the problem; keep retries low and treat flaky results as work to do.
20. Mastery challenge · Puzzle: the CI run · QuizWith --shard=1/4 to 4/4, three shards finish in 3 minutes and one takes 12. The suite is not fullyParallel. What helps most?B. Turn on fullyParallel (or split the huge file) so shards are balanced by testWithout fullyParallel, whole files are shared out, so one giant file lands on one shard. Splitting by test balances the load; more shards would leave that file just as slow.
20. Mastery challenge · Final: design the test · Fill the blanksA complete test: mock an out-of-stock product, then check the shopper cannot buy it and sees "Sold out".1: route, 2: getByRole, 3: expect, 4: toBeDisabled, 5: Sold outRoute before navigating; find the button by role and name; use web-first assertions for both the disabled button and the message the shopper sees. No sleeps, no CSS, no shared data.
21. Final project: tests for a room booking app · How the tests reach the app · QuizTwo tests run at once against the one server: one books a meeting, the other checks there are 3 bookings. Why does the second never see the first one’s booking?B. Each test gets a new browser context with no cookies, so the server gives each test its own bookingsThey really do run at once, and nothing undoes a booking. A fresh context per test (module 10) has no cookie, so the server starts it from the samples, apart from everyone else. If the server kept one shared list, contexts alone would not help: that is the shared data of module 18, solved by giving each test its own data (module 19).
21. Final project: tests for a room booking app · Practise: a locator that lasts · QuizThe next step presses the Book button. Which locator finds it the way a person would, and keeps working when the markup changes?C. page.getByRole('button', { name: 'Book' })A role and a name are what a person, or a screen reader, goes by, and they survive a redesign. getByText('Book') matches part of any text, ignoring case: here the headings Room bookings and Book a room as well as the button, so a click fails with a strict mode violation. form button breaks when the form gains a second button, and nth(0) presses whichever button comes first.
21. Final project: tests for a room booking app · Step 5: survive a reload · QuizAfter the reload, why check that Planning is visible before checking that 1:1 with Sam is gone?B. Right after a reload the list is still loading and has no rows, so “1:1 with Sam is gone” would pass at once, whatever the server keptCounted straight after reload(), the rows number 0: the page is fetching the list again. toHaveCount(0) does wait, but it is satisfied by that empty moment. Once Planning shows, the list has loaded, and “gone” then means the server really has no 1:1 with Sam.
21. Final project: tests for a room booking app · Practise: a test that cannot fail · QuizThis passes. Would it fail if the server had a bug and booked Oak at 10:00 a second time?B. No: there are 3 rows before the server has even answered, so the check passes at once and the test endsWith that bug planted in the server, this test still passes, while the step 6 test fails: it waits for the alert, which never comes. A count that should stay the same is true from the start. Wait for the sign that the server has answered first, then check what should not have changed.