What it is
A test that opens a real browser, clicks buttons and checks that a person sees the right thing. Essentially a robot repeating a user's actions.
Such tests are slower and costlier than unit tests but catch what nothing else can: a broken button, a form that doesn't submit, a break between interface and server.
For a learning project, two or three on the main flows is enough.
Installation
npm init playwright@latest
The installer asks a couple of questions and creates a structure with an example.
A first test
import { test, expect } from "@playwright/test";
test("a visitor sees the notes list", async ({ page }) => {
await page.goto("http://localhost:3000/notes");
await expect(page.getByRole("heading", { name: "Notes" })).toBeVisible();
await expect(page.getByRole("listitem")).toHaveCount(3);
});
Run it:
npx playwright test
With a visible browser, to watch what happens:
npx playwright test --headed
Locating elements
The most important topic: it decides whether your tests are robust or start failing on every styling change.
Good — search the way a person does, by role and label.
page.getByRole("button", { name: "Save" })
page.getByLabel("Email")
page.getByText("Note saved")
The benefit is twofold: the test survives restyling, and it incidentally checks accessibility — if an element can't be found by role, a screen reader won't find it either.
Bad — searching by generated class names. Restyle and every test fails, although nothing broke.
A compromise — dedicated test attributes. Use them where there's no human-readable way to find the element.
Waiting
The main cause of flakiness is checking before the page has updated.
Don't insert fixed pauses: they're either too short or slow everything down. The correct approach waits for a state:
await expect(page.getByText("Saved")).toBeVisible();
That assertion waits for the element to appear and fails only after a reasonable timeout.
Filling a form
test("a note can be added", async ({ page }) => {
await page.goto("http://localhost:3000/notes");
await page.getByLabel("Title").fill("Buy bread");
await page.getByRole("button", { name: "Add" }).click();
await expect(page.getByText("Buy bread")).toBeVisible();
});
That's a ready main-flow test worth having in any project with forms.
Why such tests get flaky
They depend on everything at once: the network, machine speed, database state, random delays. Hence the "passes sometimes" disease.
What helps:
- Don't rely on data from a previous run. Each test prepares its own.
- Don't depend on order. Tests must pass in any sequence.
- Wait for state, not for time.
- Don't test through the interface what can be checked more cheaply. Calculations belong in unit tests, not a browser.
What it adds to a portfolio
A couple of end-to-end tests in a learning project reads very well: it shows you think not only about writing something but about it continuing to work.
And for a tester it's the direct route into automation — a discipline where demand is consistently higher than for manual checking.
Хватит читать — пора делать
На CohortX можно найти команду под пет-проект и получить тот самый опыт, о котором спрашивают на собеседовании.
Похожие статьи
- Автотесты интерфейса: знакомство с PlaywrightЧто такое сквозные тесты, как написать первый, как искать элементы устойчиво и почему такие тесты бывают капризными.