An action does something. An assertion checks whether the expected result became true. Without assertions, a script cannot decide whether behavior was correct.
An assertion is the referee that checks the result after the play.
arrange state → act → assert meaningful result
Series: Playwright Zero to Expert | Demo app used throughout this series: saucedemo.com
Here’s a question worth actually sitting with before diving into syntax: a test that logs in, clicks around, and never once checks whether anything was actually correct — is it really testing anything at all? It isn’t. It’s just performing actions. The entire point of a test, the thing that actually catches bugs, is the moment it compares what actually happened against what was supposed to happen. That comparison is an assertion, and this part is about doing it properly.
Action vs. Assertion — a Distinction Worth Being Precise About
Every line of Playwright code you’ve written so far falls into one of exactly two categories, and it’s worth being able to name which is which without hesitating:
- An action does something —
.click(),.fill(),.check(),.goto(). It changes the state of the page. - An assertion checks something — it makes a claim about the current state of the page, and either confirms that claim is true, or fails the test because it isn’t.
await page.getByRole("button", { name: "Login" }).click(); // ACTION — does something
await expect(page.getByText("Products")).toBeVisible(); // ASSERTION — checks something
A test built entirely out of actions, with no assertions, might run successfully to completion and still be worthless — it could click a login button that silently failed to actually log anyone in, and the test would report “passed,” because nothing ever checked whether the login genuinely worked.
This is a real, common early mistake — writing tests that faithfully perform a sequence of steps without ever actually verifying an outcome — and it’s worth treating “what is this test actually proving?” as a question to ask about every test you write, not just an afterthought at the end.
expect() and Web-First Assertions
Playwright’s assertion syntax is built around expect():
await expect(page.getByText("Products")).toBeVisible();
Read this exactly as it sounds: “I expect the locator matching text ‘Products’ to be visible.” The specific check being performed — .toBeVisible() — is called a matcher. A few of the most commonly used ones:
await expect(page.getByText("Products")).toBeVisible();
await expect(page.getByText("Out of stock")).toBeHidden();
await expect(page.getByRole("button", { name: "Add to cart" })).toBeEnabled();
await expect(page.getByRole("checkbox")).toBeChecked();
await expect(page.getByPlaceholder("Username")).toHaveValue("standard_user");
await expect(page.getByText("Sauce Labs Backpack")).toHaveText(
"Sauce Labs Backpack",
);
await expect(page).toHaveURL(/inventory\.html/);
await expect(page).toHaveTitle("Swag Labs");
await expect(page.locator(".inventory_item")).toHaveCount(6);
Now, here is the single most important thing to understand about these, and the thing that most cleanly separates modern Playwright assertions from older-style testing: these are called web-first assertions, and every single one of them is await-ed and automatically retries.
Analogy: The Oven Inspector vs. Static Measurements Imagine baking a cake and verifying its height:
- Static Check (Generic Assertion / expect(x).toBe(y)): You slide the batter into the oven. The instant the door clicks shut, you slide a ruler inside and measure. The batter is still flat liquid. You panic, declare “The cake has failed to rise!”, and abort the bake.
- Polling Check (Web-First Assertion / await expect(x).toBeVisible()): You slide the batter into the oven. You know baking takes time. You pull up a chair and check the height through the oven window every 10 seconds. If it’s still flat at second 10, 30, or 60, you wait. The moment it rises to the target line (say, at minute 15), you mark the test as “Passed” and pull the cake out immediately. Only if the entire 30-minute timer runs out and the cake remains flat liquid do you declare a failure.
📊 Visual Flowchart: Web-First Assertion Polling Loop
Here is the polling cycle Playwright executes when resolving a web-first assertion:
graph TD
Start["Assertion Initiated:<br>await expect(locator).toBeVisible()"] --> Check{"Does element state match<br>expected condition?"}
Check -->|Yes| Pass["Assertion Passes Immediately<br>(Test continues to next line)"]
Check -->|No| Timeout{"Has assertion timeout<br>(default 5s) elapsed?"}
Timeout -->|No| Wait["Wait 100ms (polling interval)"]
Wait --> Check
Timeout -->|Yes| Fail["Fail Test<br>(Raise AssertionError & Stop execution)"]
Auto-Retry — Why await Belongs in Front of Every Assertion
Recall Part 2’s honest warning about forgetting await in front of asynchronous actions causing confusing, inconsistent failures. Assertions carry an extra, specific reason await matters here, worth understanding precisely.
When you write await expect(locator).toBeVisible(), Playwright doesn’t check exactly once, immediately, and give up if it’s not true yet. It polls — checking repeatedly, at short intervals, for up to a configurable timeout (5 seconds, by default) — and the assertion only actually fails if the condition is still false once that entire timeout has elapsed. The moment the condition becomes true, the assertion passes immediately, without waiting out the rest of the timeout unnecessarily.
Assertion starts
↓
Check condition → false → wait a moment → check again
↓
Check condition → false → wait a moment → check again
↓
Check condition → true → PASS, immediately, don't wait any longer
Think about why this matters enormously for real applications, not just as an abstract nicety. Click SauceDemo’s Login button, and the redirect to the inventory page doesn’t happen instantaneously — there’s a real, if often small, delay while the application processes the login and navigates.
An assertion checking exactly once, the instant after the click resolves, might check before that redirect has actually finished — and would then incorrectly report failure, even though the login was about to succeed a fraction of a second later.
Auto-retry is precisely what makes it safe and correct to write await expect(page.getByText('Products')).toBeVisible() immediately after a click, with no separate, manually-written wait step in between — the assertion itself is the wait, built directly into the check.
This is worth contrasting explicitly with generic assertions, which don’t retry at all:
// Web-first — retries automatically, correct choice for checking page state
await expect(page.getByText("Products")).toBeVisible();
// Generic — checks exactly once, no retry, appropriate for plain JS values, not page state
expect(cartItems.length).toBe(2);
Generic assertions (no await, no automatic retry) exist for checking plain values that don’t depend on the page’s asynchronous state at all — comparing two numbers you’ve already computed, checking an array’s length, verifying a string matches a pattern. The moment you’re asserting something about the actual, live page — visibility, text content, count of elements, URL — you want the web-first, retrying version, await-ed. Mixing these up is a genuinely common early source of flaky, timing-sensitive test failures.
Soft Assertions
By default, the moment an assertion fails, the test stops immediately — no further lines of that test run at all. Sometimes, though, you genuinely want to collect multiple failures in one run before stopping, rather than fixing one, re-running, discovering the next failure, fixing that, re-running again. Soft assertions let you do exactly this:
await expect.soft(page.getByText("Products")).toBeVisible();
await expect.soft(page.locator(".inventory_item")).toHaveCount(6);
await expect
.soft(page.getByRole("button", { name: "Add to cart" }).first())
.toBeEnabled();
// The test continues past a soft assertion failure and only actually
// fails, reporting every soft failure together, at the very end of the test
This is genuinely useful for something like verifying an entire page’s contents at once — instead of the test dying at the very first mismatch and hiding whatever else might also be wrong, a soft-assertion-based check reports the complete picture of everything that’s actually broken in a single run, which is often far more useful information for whoever’s diagnosing the failure afterward.
Timeouts
Every assertion’s retry-and-poll behavior operates within a timeout — 5 seconds by default, configurable globally in playwright.config.ts (recall Part 6) or per-assertion:
await expect(page.getByText("Order confirmed")).toBeVisible({ timeout: 10000 }); // 10 seconds
It’s worth resisting an instinct beginners commonly have here: when a test occasionally fails, the reflexive fix is often to just raise the timeout, over and over, hoping the problem eventually goes away.
Sometimes a genuinely slow operation does warrant a longer timeout — but a timeout that keeps needing to be raised is very often a sign of something else entirely: a genuinely broken or inconsistent feature, a race condition, or a locator matching the wrong thing intermittently.
We’ll return to this exact judgment call properly once we reach Part 12 (waiting) and Part 28 (flaky tests) — for now, treat “just increase the timeout” as a reasonable first check, never an automatic, unquestioned fix.
How It Works in a Real Test Run
A web-first assertion repeatedly resolves its locator and checks the expected condition until it passes or the assertion timeout expires. This is different from reading a value once and applying a synchronous assertion to that frozen result.
A useful test sentence is action → observable outcome: submit login → URL becomes inventory and heading becomes Products. Assertions should prove behavior important to the user, not every incidental implementation detail.
Interview Questions
Q: What is the difference between an action and an assertion in a Playwright test?
Ans: An action changes the state of the page — clicking, filling a field, checking a box. An assertion checks the current state of the page against an expected outcome, and either confirms it or fails the test. A test made up only of actions with no assertions can technically pass every time without actually verifying that anything worked correctly, since nothing in it ever makes a real claim that gets checked.
Q: What does it mean that Playwright’s web-first assertions “auto-retry,” and why does this matter?
Ans: Instead of checking a condition exactly once and immediately failing if it isn’t true yet, a web-first assertion polls repeatedly over a configurable timeout, passing the moment the condition becomes true and only failing if it’s still false once the timeout fully elapses. This matters because real applications often have a short, genuine delay between an action (like a click) and its visible result (like a page navigating), and auto-retry means the assertion itself correctly waits for that result instead of needing a separate, manually-written wait step beforehand.
Q: When would you use a generic assertion (like expect(value).toBe(...) without await) instead of a web-first assertion?
Ans: Generic assertions are appropriate for checking plain, already-known JavaScript values that don’t depend on the page’s live, asynchronous state — comparing two numbers, checking an array’s length, verifying a string’s format. Web-first assertions, which are await-ed and retry automatically, are for anything that depends on the actual current state of the page itself, like visibility, text content, or element count, since those can change over a short delay after an action.
Q: What is a soft assertion, and when would you use one instead of a regular assertion?
Ans: A soft assertion doesn’t stop the test immediately when it fails — the test keeps running, and all soft assertion failures are collected and reported together at the end. This is useful when you want a complete picture of everything wrong on a page in one test run — for example, checking several unrelated pieces of a page’s content at once — rather than the test stopping at the very first failure and hiding whatever else might also be broken.
Q: A test occasionally fails with a timeout on an assertion, and a teammate suggests simply increasing the timeout value. What would you want to investigate before agreeing to that fix?
Ans: I’d want to understand why the assertion is occasionally taking longer than expected in the first place — whether it’s a genuinely slow but legitimate operation that just needs more time, or a sign of something else, like a race condition, an inconsistent feature, or a locator that intermittently matches the wrong element. Simply raising the timeout can mask a real, underlying problem rather than fix it, and it also makes every failure of that test take longer to actually report, which slows down the whole suite over time if applied too liberally.
Q: Why doesn’t expect(locator).toBeVisible() need a separate, explicit wait statement written before it, the way some older automation tools might require?
Ans: Because the assertion itself performs the waiting — it’s await-ed and polls automatically for up to its configured timeout, rather than checking exactly once. This built-in retry behavior means the assertion effectively is the wait, checking repeatedly until the condition becomes true or the timeout is reached, without needing a manually-written wait step placed before it.
Exercises — Part 9
Understand: Without looking back, explain in your own words why a test with only actions and no assertions can still “pass” every time, and why that’s a problem worth actively avoiding.
Simple Practice:
Write a Playwright test that logs into SauceDemo and asserts, using web-first assertions, on at least three separate things: that the products heading is visible, that exactly six inventory items are present (toHaveCount), and that the page URL matches the inventory page.
Real-World Scenario: Imagine a checkout confirmation page that takes a genuinely variable amount of time to load — sometimes 200ms, sometimes 4 seconds, depending on backend load. Write, in your own words, an explanation of why a single, non-retrying check performed immediately after clicking “Place Order” would be an unreliable way to verify the order succeeded, and why a web-first assertion handles this correctly without you writing any extra waiting logic.
Challenge:
Rewrite a small multi-assertion test (like the one from the Simple Practice exercise above) using expect.soft(...) for each check instead of regular expect(...). Deliberately make one of the assertions fail (assert an incorrect count, for instance), run the test, and observe in the output whether the other, correct assertions still ran and were reported, rather than the test stopping at the first failure.
Next: Part 10 — Navigation and Pages
— goto, working with multiple tabs, popups, and windows, and everything involved in moving around a real, multi-page application.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed