✅ Writing Good Assertions (expect)

Writing Good Assertions (expect): Playwright's expect() function works like a calibrated quality-control measurement instrument: it takes a reading, checks it against the toleran

Playwright's expect() function works like a calibrated quality-control measurement instrument: it takes a reading, checks it against the tolerance range, re-measures at set intervals until the condition is met, and if it still isn't met at timeout, reports both the expected value and the actual value obtained. What makes this different from assertEquals() in TestNG or assertThat() in JUnit? In Java, assertions are evaluated at a single instant: if the condition isn't satisfied at that exact millisecond the test goes red immediately, and the error is as plain as "AssertionError: expected X but was Y" — with no information about which element, what page state, or what led up to it. In Playwright, expect(page.locator(".toast")).toBeVisible() uses polling: it checks every 100ms for up to 5000ms; if it fails, the report includes a screenshot, the DOM state at the time of failure, and the full polling history. The QA reality: in async UIs (SPA, AJAX, WebSocket), a single-shot assertion silently produces false PASSes — the "not visible" assertion passes before a modal opens, the "removed" assertion passes after it closes, but neither actually verified the state the user saw. expect()'s polling mechanism systematically prevents this.

expect() is Playwright's assertion function. But it works differently from JUnit's assertEquals() or the classic asserts you might have used in Selenium projects: "web-first" assertions called on a locator (like toBeVisible(), toHaveText()) auto-retry for 5 seconds by default. If the condition isn't true the instant it's checked, it doesn't blow up right away — it waits until the page state changes.

In JUnit, Assert.assertEquals("Expected", element.getText()) fails the INSTANT that line runs if the element's text isn't that value yet — even if the text would be correct 200ms later. In Playwright, await expect(locator).toHaveText("Expected") re-reads that text for up to 5 seconds; if it becomes correct after 200ms, the test passes. Think of it as a built-in version of the Awaitility pattern: await().atMost(5, SECONDS).until(...).

Web-First Assertions vs Generic Assertions

The most common web-first assertions: toBeVisible() / toBeHidden(), toHaveText() / toContainText(), toHaveValue(), toBeChecked(), toBeEnabled() / toBeDisabled(), toHaveAttribute(), toHaveClass(), toHaveCSS(), toHaveCount(), toHaveURL(), toHaveTitle(). Prefix any of them with .not. to assert the opposite: expect(locator).not.toBeVisible().

How expect() Auto-Retries

Classic Assert (JUnit)

assertEquals looks at the DOM at the exact instant it runs. If a spinner is still spinning or the text hasn't updated yet, the test FAILS immediately.

Checking something that will be correct in 300ms, right now, is the most common cause of flaky tests.

Playwright — Checking

The moment await expect(locator).toHaveText(...) runs, Playwright performs the first check. If it's not correct yet, it doesn't throw — it enters a polling loop.

Playwright re-checks the condition roughly every ~100ms. This silently continues until the page state changes (spinner disappears, message appears) — you write zero extra code for this.

Condition True → Passed

The moment the condition becomes true (before the 5s budget runs out), the assertion passes immediately and execution moves to the next line. The total wait is exactly as long as needed — no wasted fixed Thread.sleep(5000).