Module 2: Playwright Fundamentals
Now we get hands-on. Playwright is the browser-automation framework at the heart of this course, and this module takes you from an empty folder to a passing test you wrote yourself, then shows you the tools that make debugging almost pleasant. You will install Playwright the official way, understand every file it creates, write your first test() with an expect() assertion, run it headed and headless and in the interactive UI, open a trace of a failed run, and let codegen write a first draft for you. These are the everyday motions of a Playwright engineer—by the end you will have performed all of them.
What you’ll learn
By the end of this lesson you will be able to:
- Explain what Playwright is and why it became the default for new web suites.
- Install Playwright with
npm init playwright@latestand understand what it scaffolds. - Read the standard project structure and know what each file is for.
- Write and run a first test using
test()andexpect(). - Run tests headed, headless, and in interactive UI mode.
- Open and read a trace in the trace viewer to debug a failure.
- Generate a draft test with
npx playwright codegen.
What Playwright is, and why it’s popular
Playwright is an open-source end-to-end testing framework created by Microsoft. One API drives all three major browser engines—Chromium (Chrome, Edge), Firefox, and WebKit (Safari)—so a single test suite can verify your app across the engines your users actually run. It ships with its own test runner, assertion library, parallel execution, and reporting, so you rarely need to bolt on extra tools.
It became the default choice for new projects for a handful of concrete reasons:
- Auto-waiting. Before every action, Playwright waits for the element to be present, visible, stable, and ready. This removes the single biggest source of flakiness in older tools: the manual
sleep. - Web-first assertions. Assertions like
expect(locator).toBeVisible()automatically retry until the condition is true or a timeout is reached, so timing races mostly disappear. - Isolation by default. Each test runs in a fresh browser context—like a brand-new incognito window—so tests do not leak cookies or state into one another.
- First-class tooling. The trace viewer, UI mode, and codegen make writing and debugging tests genuinely productive.
- Speed. Tests run in parallel across workers out of the box.
From the field: why teams migrate to Playwright
The most common reason teams move to Playwright is not a feature list—it is flakiness fatigue. A suite that fails one time in twenty for no real reason erodes trust until people stop reading the results. Playwright’s auto-waiting and retrying assertions attack that problem directly, and teams routinely report their “red for no reason” rate dropping sharply after migration. A trustworthy suite is one people act on; that trust is the real product here.
Installing Playwright
You need Node.js installed first (a current LTS release). Then, in an empty project folder, run the official initializer:
npm init playwright@latest
This interactive command asks a few questions—whether to use TypeScript or JavaScript (choose TypeScript; Module 3 explains why), what folder to put tests in (the default tests is fine), whether to add a GitHub Actions workflow, and whether to download browsers. Say yes to the browsers; Playwright downloads its own pinned builds of Chromium, Firefox, and WebKit so everyone on the team runs identical versions.
When it finishes you have a ready-to-run project. You can prove it immediately:
npx playwright test
That runs the example test the initializer created and prints a pass/fail summary. If it passes, your installation works.
Pitfall: “browser not found” errors
If tests fail to launch with a message about a missing browser executable, it usually means the browser binaries were not downloaded—common on CI servers or after a fresh clone. The fix is one command:
npx playwright install
On Linux CI you may also need the system libraries the browsers depend on. Playwright can install those too:
npx playwright install --with-deps
Make this part of your pipeline setup so a clean machine can always run the suite.
Project structure
After initialization, a typical project looks like this:
my-project/
tests/
example.spec.ts ← your test files live here
tests-examples/
demo-todo-app.spec.ts ← a larger example to learn from
playwright.config.ts ← central configuration (Module 5)
package.json ← dependencies and npm scripts
package-lock.json
.github/
workflows/
playwright.yml ← optional CI workflow
The two parts you touch daily are the tests/ folder, where your .spec.ts files go, and playwright.config.ts, the single place that controls how the whole suite behaves. We cover the config in depth in Module 5; for now, know that .spec.ts is the conventional suffix that tells Playwright “this file contains tests”.
Your first test
Create tests/first.spec.ts and write this:
import { test, expect } from '@playwright/test';
test('has the right page title', async ({ page }) => {
await page.goto('https://playwright.dev/');
await expect(page).toHaveTitle(/Playwright/);
});
Let us read it line by line, because every Playwright test shares this shape.
import { test, expect }brings in the two core building blocks: the function that declares a test, and the function that makes assertions.test('has the right page title', async ({ page }) => { ... })declares a test with a human-readable name. The second argument is anasyncfunction—browser actions take time, so almost everything is awaited (Module 3 coversasync/awaitfully).{ page }is a fixture: Playwright hands your test a fresh, isolated browser page to work with. You did not create it; the framework provides it.await page.goto(...)navigates the page to a URL and waits for it to load.await expect(page).toHaveTitle(/Playwright/)asserts that the page title matches the regular expression/Playwright/, retrying until it does or the timeout expires.
Here is a slightly richer example that clicks a link and checks the result—the bread and butter of UI testing:
import { test, expect } from '@playwright/test';
test('get started link navigates to the docs', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the "Get started" link.
await page.getByRole('link', { name: 'Get started' }).click();
// The Installation heading should now be visible.
await expect(
page.getByRole('heading', { name: 'Installation' })
).toBeVisible();
});
Notice again: no sleeps. Playwright waits for the link to be clickable before clicking, then toBeVisible() retries until the heading appears. You describe what should be true; the framework handles the when.
Running tests: headless, headed, and UI mode
By default Playwright runs headless—no visible browser window. This is fast and exactly what you want in CI:
npx playwright test
When you are debugging and want to watch the browser do its thing, run headed:
npx playwright test --headed
You can also target a single file or a single test by name:
# Just one file
npx playwright test tests/first.spec.ts
# Only tests whose title contains "title"
npx playwright test -g "title"
The tool you will reach for most while developing is UI mode, an interactive runner with a time-travel view of every step:
npx playwright test --ui
UI mode lets you pick tests to run, watch them execute step by step, hover over each action to see a before/after snapshot of the page, inspect locators live, and re-run on save. For day-to-day authoring it is the single most useful command in the toolkit.
Tutor tip: add npm scripts so you stop memorising flags
Put your common commands in package.json so the whole team runs them the same way:
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed",
"report": "playwright show-report"
}
Now npm run test:ui launches UI mode and npm run report opens the last HTML report. Small habit, big quality-of-life gain.
The trace viewer
When a test fails in CI—on a machine you cannot see—how do you debug it? Playwright’s answer is the trace: a complete recording of the run including a DOM snapshot before and after every action, console logs, network requests, and the source line for each step. Enable traces in playwright.config.ts (Module 5 shows the config in full); a common setting records a trace only when a test fails on retry:
// playwright.config.ts (excerpt)
export default defineConfig({
use: {
trace: 'on-first-retry',
},
});
After a failing run, open the trace with:
npx playwright show-trace trace.zip
The viewer gives you a timeline of actions. Click any step to see exactly what the page looked like at that moment, what the selector matched, and what the network was doing. Debugging a failure you never witnessed becomes a matter of scrubbing through a recording rather than guessing.
Codegen: let Playwright write a draft
You do not have to write every locator by hand. codegen opens a browser, records your clicks and typing, and emits working Playwright code as you go:
npx playwright codegen https://playwright.dev/
As you interact with the site, a window shows generated code using recommended, resilient locators (role- and text-based, which you will learn to prefer in Module 4). Copy the result into a spec file and refine it. Codegen is a brilliant starting point—but treat its output like any generated code: review it, rename the test meaningfully, remove redundant steps, and tighten the assertions. It gets you to a draft fast; the engineering judgement is still yours.
Hands-on exercise
Build a tiny suite from scratch:
- In an empty folder, run
npm init playwright@latestand choose TypeScript. Then runnpx playwright testand confirm the example passes. - Create
tests/first.spec.tswith the title-check test from this lesson. Run it headless, then with--headed, and notice the difference. - Launch
npx playwright test --ui. Step through your test and hover over each action to see the page snapshots. - Deliberately break the assertion (expect a wrong title). Run again, then open the trace with
npx playwright show-traceand find the failing step. - Run
npx playwright codegenagainst any public site, click around, and paste the generated code into a new spec. Read it—can you identify the locators and actions?
Knowledge check
Which command scaffolds a new Playwright project, and what does it set up?
npm init playwright@latest. It asks about TypeScript vs JavaScript, the tests folder, a GitHub Actions workflow, and browser downloads, then creates the tests folder, example specs, playwright.config.ts, and the dependencies—a ready-to-run project.
What are the two core functions imported in every test, and what does each do?
test() declares a test with a name and an async body; expect() makes assertions about the page or a locator. Both come from @playwright/test.
What is the { page } argument and where does it come from?
It is a built-in fixture that Playwright provides to each test: a fresh, isolated browser page. You do not create it—the framework injects it and tears it down automatically, keeping tests independent.
When would you use headed mode, UI mode, and headless?
Headless (the default) for speed and CI. Headed (--headed) to watch the browser while debugging. UI mode (--ui) for interactive authoring with step-by-step time travel and live locator inspection—the best mode for day-to-day development.
What problem does the trace viewer solve, and how do you open a trace?
It lets you debug a failure you never watched—typically from CI—by replaying a full recording with DOM snapshots, console, and network for every step. Open it with npx playwright show-trace trace.zip.
Key takeaways
- Playwright drives Chromium, Firefox, and WebKit with one API, bundling runner, assertions, parallelism, and reporting.
- Its popularity rests on auto-waiting and retrying web-first assertions, which kill most flakiness.
npm init playwright@latestscaffolds the project;npx playwright testruns it.- Every test shares one shape: import
test/expect, declare an async test, use thepagefixture, act, and assert. - Use
--headedto watch,--uito author, the trace viewer to debug, andcodegento draft.
Go deeper
A few things that pay off once the basics click.
The debug inspector. Beyond UI mode, npx playwright test --debug opens the Playwright Inspector, which pauses execution and lets you step through actions while picking locators live. You can also drop await page.pause() into a test to break at exactly that line.
- Browser channels vs bundled builds. By default Playwright uses its own pinned browser builds for reproducibility. You can instead target installed stable channels (for example, branded Google Chrome or Microsoft Edge) via the config when you need to test the exact build your users have.
- Traces are heavy—record them selectively.
trace: 'on-first-retry'is a good default: no overhead on green runs, a full recording only when something fails and retries. Recording a trace on every test slows the suite and bloats artifacts. - Codegen respects test IDs. If your app uses a consistent
data-testidattribute, codegen will prefer it, producing very stable locators. This is a strong argument for adding test IDs to your UI—more on locators in Module 4.