Without notes, state yesterday’s main idea and one unresolved question.
Playwright end-to-end tests
Testing, Security, Docker, and Deployment
Objective
Automate a few critical user journeys through the browser.
Production readiness resembles validation before field deployment: repeatable setup, protective limits, test evidence, monitoring, and rollback plans.
- locators by user-visible meaning
- stable test data
- screenshots and traces on failure
Why this matters
Yesterday's integration tests proved your API behaves. They say nothing about whether a human can actually log in, because they never opened a browser. Today you automate the one journey that must never break — log in, create equipment, add a maintenance record, log out — in a real browser, and you set it up so that when it fails at 7am you get a screenshot and a replayable trace instead of a shrug. Two of these tests are worth having. Twenty would be a second job.
What an end-to-end test does
Playwright starts a real browser, navigates to your running app, and drives it the way a person does: click, type, read. Nothing is faked — real HTTP, real cookies, real React rendering, real database. That is the value: an E2E test is the only test that can prove the parts you assembled in Week 10 still work together.
It is also the reason E2E tests are slow (seconds each) and fragile (anything on the path can break them). So you write few of them, and only for critical paths: journeys where a failure means the product is unusable. Login is critical. The colour of a tooltip is not.
The field trial
Bench measurements and subassembly tests tell you the parts meet spec. Before you ship, someone still powers the finished unit on, in its enclosure, and runs it through the operating sequence. That trial is slow and you only run a few, but it is the only test that includes the wiring harness, the enclosure fit, and the thermal reality. E2E tests are that trial.
Locators by user-visible meaning
A locator is how the test finds an element on the page. This choice decides whether your test survives next month.
Resilient locators use what a user can perceive: the element's role, its accessible name, its label, its visible text.
page.getByRole("button", { name: "Save equipment" });
page.getByLabel("Serial number");
page.getByText("Pump 3");
Brittle locators use what a build tool produced: generated class hashes, deep CSS paths, or positions:
page.locator(".css-1a2b3c4 > div:nth-child(3) > button"); // breaks on any restyle
The class hash changes when the styling changes, and the nth-child index changes when someone
adds a wrapper div. Neither change affects a single user, yet both turn your suite red. That is
the definition of a bad test.
There is a second, quieter benefit: if you cannot find a control by its role and name, a screen
reader user cannot find it either. A test that is hard to write with getByRole is usually telling
you the markup has an accessibility problem — a button with no text, an input with no label.
When there is genuinely nothing user-visible to grab, add an explicit test hook in your markup —
data-testid="equipment-row" — and use page.getByTestId("equipment-row"). It is a deliberate,
stable contract rather than an accident of styling.
Directions that survive the redecoration
"The door marked Fire Exit" still works after the walls are repainted. "The third door past the blue poster" does not. Both get you there today; only one is worth writing down.
Waiting, without sleeping
The most common beginner instinct is to add a delay: "wait two seconds for the page to load." Two seconds is simultaneously too long on a fast machine and too short on a slow one, so the suite is both slow and flaky.
Playwright's assertions retry automatically until they pass or the timeout expires:
await expect(page.getByRole("heading", { name: "Equipment" })).toBeVisible();
That line polls until the heading appears, then continues immediately. It replaces every sleep you
were about to write. Use page.waitForTimeout() only while debugging by hand, never in a committed
test.
Stable test data
An E2E test that depends on data already sitting in your database is a test that passes on your machine and fails on everyone else's. Two rules:
- Create what you need inside the test, with a unique value so parallel runs cannot collide:
const serial = `SN-${Date.now()}`. - Point the tests at the test database, never a database with real records — the test writes rows, and it will write them wherever you aim it.
Failure artifacts
When an E2E test fails you were not watching, so Playwright can save evidence:
- A screenshot of the page at the moment of failure.
- A trace: a recorded timeline you can replay afterwards, with the DOM at every step, the network requests, and the console output. It is the single most useful debugging artifact in this entire course.
Walkthrough: install, write one journey, read one failure
From your web project folder:
npm init playwright@latest
It asks where to put tests (accept tests or use e2e) and whether to add a GitHub Actions
workflow, then downloads the browsers. That download is a few hundred megabytes and happens once.
Open playwright.config.js and set the base URL, the artifacts, and a webServer block so
Playwright starts your app itself:
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
use: {
baseURL: "http://localhost:5173",
screenshot: "only-on-failure",
trace: "on-first-retry",
},
webServer: {
command: "npm run dev",
url: "http://localhost:5173",
reuseExistingServer: true,
},
});
Now e2e/critical-path.spec.js:
import { test, expect } from "@playwright/test";
test("technician can log in, add equipment, and log out", async ({ page }) => {
const serial = `SN-${Date.now()}`;
await page.goto("/login");
await page.getByLabel("Email").fill("[email protected]");
await page.getByLabel("Password").fill("correct-horse-battery");
await page.getByRole("button", { name: "Log in" }).click();
await expect(page.getByRole("heading", { name: "Equipment" })).toBeVisible();
await page.getByRole("link", { name: "Add equipment" }).click();
await page.getByLabel("Name").fill("Pump 3");
await page.getByLabel("Serial number").fill(serial);
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByText(serial)).toBeVisible();
await page.getByRole("button", { name: "Log out" }).click();
await expect(page.getByRole("heading", { name: "Log in" })).toBeVisible();
});
Run it:
npx playwright test
Running 1 test using 1 worker
✓ 1 [chromium] › e2e/critical-path.spec.js:3:1 › technician can log in, add equipment, and log out (4.2s)
1 passed (5.1s)
Now break it on purpose: change the button label in your React app from "Log in" to "Sign in" and run again.
1) [chromium] › e2e/critical-path.spec.js:3:1 › technician can log in, add equipment, and log out
Error: locator.click: Timeout 30000ms exceeded.
Call log:
- waiting for getByRole('button', { name: 'Log in' })
9 | await page.getByRole("button", { name: "Log in" }).click();
Read it: the action that failed, what it was waiting for, and the line. Then open the report:
npx playwright show-report
The HTML report opens in your browser with the failure screenshot attached. Restore the label and confirm green again.
Checkpoint
You can point at the failure output and say three things: which step failed, what locator it was waiting for, and where the screenshot is. That is the whole skill.
Your turn
- Install Playwright and configure
baseURL,screenshot: "only-on-failure", andtrace. - Write the full journey: log in → create equipment → add a maintenance record → log out, in
one test. Use
getByRoleandgetByLabelthroughout; if a control cannot be found that way, fix the markup — add the missing<label>or button text — rather than reaching for a CSS path. - Generate a unique serial per run so repeated runs cannot collide.
- Add one assertion after each step, so a failure names the step rather than the journey.
- Run
npx playwright test. Then break one label deliberately, re-run, and opennpx playwright show-report. Save the screenshot path indocs/test-plan.md. - Set
retries: 1in the config and re-run the broken version so a trace is recorded, then replay it:npx playwright show-trace test-results/*/trace.zip. Step through the timeline once. - Add
test-results/andplaywright-report/to.gitignore. Artifacts are evidence, not source.
Never point E2E tests at production
These tests create records, and a Date.now() serial makes them hard to spot and clean up. Aimed
at a live system they write junk into real data — and a login test will lock a real account if
you add a failed-password case. Set baseURL to your local app and DATABASE_URL to the test
database, and confirm both before the first run.
Reviewer mode — after your test passes
"Review this Playwright test for brittle selectors, unnecessary waits, and overlong scope."
Ask for specific findings with evidence — the exact locator, the exact line — not praise and not
a rewrite. Expect it to flag any waitForTimeout and any CSS-path locator. Verify each finding by
making the change and re-running before you accept it.
You are done when
One critical-path test passes reliably twice in a row, and a deliberate break produces a screenshot and a trace you have actually opened.
Common pitfalls
- Forgetting
await. Almost every Playwright call returns a promise. A missingawaitmakes the test pass instantly while doing nothing. - Sleeping instead of asserting.
waitForTimeout(2000)is the leading cause of flaky suites. Useexpect(...).toBeVisible()and let it retry. - One giant test covering everything. When it fails you learn only "the app is broken". Keep a journey to one coherent path, and assert after each step.
- Relying on data that already exists. "Pump 3 is in the list" is true on your machine only. Create the row in the test.
Verify it yourself
Open today's reference, Playwright's Installation and first test guide.
- Find its section on locators and confirm the recommended priority order. Where does
getByTestIdsit relative togetByRole— and does that match how this lesson told you to use it? - Find what
trace: "on-first-retry"means precisely, and what you would set instead to record a trace on every run. Note why you would not want that on by default.
The hour
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Automate login, create equipment, add maintenance, and logout.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
One passing critical-path test and useful failure artifacts.
Working with AI today
Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.
Review this Playwright test for brittle selectors, unnecessary waits, and overlong scope.
References
End-of-day quiz
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.