JS Sketchbooksee JavaScript think ✏️
← back to phase 11

11.6 — Auto-waiting & web-first assertions

This is the lesson 7.8 promised and 11.1 credited with ending an era: web-first assertions — expect(locator) checks that RETRY until the page catches up, passing the instant truth arrives. Dynamic content — spinners, late lists, toasts — stops being your problem and becomes the assertion’s.

First, honor the old enemy properly: understand exactly why sleep() can never win.

watch it happen
// the OLD world (other tools, sadder days):
//   click(); sleep(5000); read();  ← guess & pray

// Playwright:
await page.getByRole("button", { name: "Search" })
  .click();
await expect(page.getByText("12 results"))
  .toBeVisible();
// ↑ polls: check · wait · check · …
//   passes the INSTANT it's true (or times out)

await expect(page.getByRole("listitem")).toHaveCount(12);
await expect(page.getByLabel("Email"))
  .toHaveValue("ada@shop.com");
await expect(page.getByText("Saved!")).toBeHidden();

The reality every browser test lives in: pages are DYNAMIC. Click Search and the answer does not exist yet — a spinner shows, a request travels (6.7), the response renders (7.8). “12 results” appears LATER, after a delay nobody can know in advance: 200ms on a good day, 3 seconds on a bad one.

click → …the answer arrives LATERclick "Search"spinner 🌀network roundtrip (6.7)render (7.8)“12 results”appearshow long does the middle take? 200ms? 3s? NOBODY KNOWS in advancemodern pages are DYNAMIC: click search → spinner → network (6.7) → render(7.8). The answer exists LATER, at an unknowable time
under the hood

The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”

The polling loop’s internals: checks run on the browser side where possible, with gentle intervals (effectively ~100ms-ish pacing), each iteration re-resolving the locator — so even if the framework replaces the DOM node entirely (React re-renders constantly), the assertion follows the description, not a dead reference. 11.4’s design decision pays its rent here, permanently.

toHaveText has precise semantics worth knowing: it normalizes whitespace, matches the FULL text (use toContainText for substrings), and accepts arrays for lists (toHaveText(["Red", "Blue", "Green"]) asserts all three items in order — a whole list shape in one retrying assertion).

For truths that live outside the DOM — a value in your own code, an API’s answer — there’s await expect.poll(() => fn()) and expect(async …).toPass(): the same retry engine pointed at arbitrary functions. Your exercise’s pollUntil is literally this, hand-built.

Job note: you can now READ flakiness statistics: teams migrating from sleep-based suites to web-first assertions routinely report both faster runs (no padded sleeps) AND order-of-magnitude fewer flakes — the same change fixes both directions of sleep’s failure. When 11.15’s flakiness clinic lists “race conditions” as cause #1, the cure column will just say: this lesson.

your turn

⌨️ build the polling assertion

The starter fakes a dynamic page on a hand-cranked clock (deterministic — no real waiting). Build the web-first assertion: poll until truth or budget, and report WHICH.

requirements:

  • Keep the starter: tick() advances the fake clock 100ms; getResults() answers "" until 300ms have passed, then "12 results" — a page whose truth arrives later.
  • Write pollUntil(get, expected, maxChecks): up to maxChecks times — check get() === expected; on truth, return which check number succeeded; otherwise tick() and try again. Exhausted → return -1.
  • Run it: expect "12 results" with 10 checks → print passed on check 4. Then expect "99 results" with 5 checks → print timed out after 5 checks.

when you press RUN, the console must show exactly:

passed on check 4
timed out after 5 checks

✏️ Quick check 1

sleep(2000) before reading a page that takes 3s today — does the test pass, fail, or waste time?

✏️ Quick check 2

expect(locator).toBeVisible() — how does it behave between the click and the element appearing?

✏️ Quick check 3

Which family retries — expect(125).toBe(125) or expect(locator).toHaveText("Ada")?

teach it back

🗣️ Now teach it back

Explain the flaky-test killer to a friend: why dynamic pages break sleep() in BOTH directions, how a polling assertion works (freshness included), what bounds its patience, and the one honest exception to “never wait by hand.”

Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.

a few sentences, minimum — you’ve got this
to remember
Dynamic pages = answers arrive LATER at unknowable times. sleep() loses both ways by construction: too short → flaky red on correct code; too long → 10.2’s abandoned-suite slowness.
Web-first assertions POLL: fresh locator resolve (11.4) → check → wait → again — instant pass when truth arrives, honest TimeoutError (expect.timeout, 11.3) with last-seen state when it doesn’t. Family: toHaveText/Count/Value/URL, toBeVisible/Hidden (absence polls too!).
Two expect families: values check ONCE (10.4 — the past can’t change); locators RETRY (the page can). waitForTimeout = sleep in costume, never merged; the one exception is waitForResponse for wire-event choreography.
next: 11.7 Fixtures & hooks →