skip to content

Assertions

Checks that keep re-testing the page until it agrees, plus the snapshot matchers built on them. Interviewers probe it because an assertion that waits removes most explicit waiting from a suite.

on this pageshow

explore

questions

29

In Playwright, what does expect(locator).toMatchAriaSnapshot() compare, and what does it ignore?

level: juniorimportance: must knowfreq 45%

answer

  1. Structure, not appearance
  2. Serialised to YAML, line per node
  3. Roles, accessible names, a few states
  4. Hidden nodes never appear
  5. Retries like other web-first assertions

basics

~20 s

It compares an element's accessibility tree, meaning the roles, accessible names and a few ARIA states of its non-hidden nodes, against a YAML template. It ignores pixels, CSS, tag names, class names and hidden nodes.

solid answer

~40 s

`expect(locator).toMatchAriaSnapshot()` serialises the accessibility tree under a locator into YAML and compares it with an expected template, either inline or stored in a file such as `main.aria.yml`. Each line carries a node's role and accessible name, plus optional state such as `[level=1]`, nested by indentation. Because the tree holds roles and names rather than markup or pixels, a restyled or relocated export button passes, while a renamed one, a demoted heading, or a balance widget that stopped rendering fails. Nodes hidden from assistive technology never appear. It is an auto-retrying assertion, so it re-reads the tree until it matches or the expect timeout expires. A pass means the structure matches what you recorded, not that the page is accessible.

code

typescript · 13 lines
typescript
import { expect, test } from '@playwright/test';

test('statement page keeps its structure', async ({ page }) => {
  await page.goto('/accounts/12345/statement');

  await expect(page.getByRole('main')).toMatchAriaSnapshot(`
    - heading "October statement" [level=1]
    - region "Balance":
      - text: /Closing balance/
    - table "Transactions"
    - button "Export CSV"
  `);
});

go deeper

for a junior

Remember the one-line version: it compares roles and accessible names from the accessibility tree against a YAML template, not pixels or markup. Know that the template can be inline or in a file.

for a middle

Be able to explain why a restyle passes and a rename fails, what an accessible name is, and why hidden nodes are absent. Mention that the assertion retries until the expect timeout.

for a senior

Show judgment about scope: the wider the locator, the more unrelated edits rewrite the template. Explain what a snapshot buys over a handful of element assertions and where it costs you review time.

for a principal

Frame it as a cheap, broad structural net whose real cost is churn and review fatigue, and set the policy for which surfaces earn one and who reviews regenerated templates.

## What the matcher compares `expect(locator).toMatchAriaSnapshot(template)` builds the **accessibility tree** of the element the locator points at, serialises it to YAML, and compares that YAML against the template you supply. The accessibility tree is the structure the browser exposes to assistive technology: every node that is not hidden, described by its **role** (`heading`, `button`, `table`, `link`, `region`), its **accessible name** (the text that identifies it), and a small set of ARIA states such as a heading `level` or a checkbox being checked. Aria snapshots landed in Playwright 1.49 and the matcher is one of the auto-retrying assertions, so it re-reads the tree until it matches or the `expect` timeout expires. A template for a bank statement page reads like the page's outline: ```yaml - heading "October statement" [level=1] - region "Balance": - text: /Closing balance/ - table "Transactions" - button "Export CSV" ``` ## What it deliberately ignores - **Pixels, colours and layout.** Restyling the export button, moving it, or changing its font does not move the tree, so the assertion stays green. - **Tag names, classes, ids and data attributes.** The tree records that a node *is* a button, not that it is a `<button class="btn-primary">`. - **Hidden nodes.** Anything removed from the accessibility tree, such as a container marked `aria-hidden`, never appears in the snapshot at all. - **Properties you did not write.** The template asserts what it names and nothing else: a bare `- button` line matches a button whatever its accessible name happens to be. ## What that makes it good at catching 1. A control renamed from "Export CSV" to "Download", which no visual or CSS check treats as a regression but which every user of the label notices. 2. A heading demoted from `[level=1]` to `[level=2]`, flattening the document outline. 3. A whole region that stopped rendering, for example the balance widget vanishing above the transaction table. 4. A control that lost its label entirely and now exposes an empty name. ## How it differs from asserting one element | Change on the statement page | Aria snapshot of `main` | A single assertion on the export button | | --- | --- | --- | | Export button renamed | Fails | Fails | | Balance widget removed | Fails | Passes, nothing looked at it | | Heading level changed | Fails | Passes | | Button restyled or moved | Passes | Passes | | A new unrelated banner added | Depends on the template's scope | Passes | One snapshot therefore acts as a broad structural net over a surface, where element assertions are narrow and deliberate. That breadth is the whole point and also the cost: the wider the element you point it at, the more unrelated edits will rewrite it. ## Writing and storing the expectation Two storage forms exist, and both compare the same way: - **Inline**, as a template literal passed straight to the matcher, which keeps the expectation next to the test that owns it. - **File-backed**, via `toMatchAriaSnapshot({ name: 'main.aria.yml' })`, which stores the YAML in the test file's snapshot directory. Hand-writing a template is the common beginner mistake, because indentation and nesting must reflect the real tree. Generate it instead: `await page.getByRole('main').ariaSnapshot()` returns the YAML for the live page, and you paste in the part you actually want to assert, deleting the volatile rest. ## What a pass does not prove A green snapshot means the tree still matches what you recorded. It says nothing about whether that tree is a *good* one; judging whether the page is usable by assistive technology is a separate discipline with its own criteria and its own manual checks. Recording a page whose structure is already wrong simply freezes the wrongness.

  • How would you produce the template in the first place instead of typing it by hand?
    Call `ariaSnapshot()` on the locator you intend to assert, for example `await page.getByRole('main').ariaSnapshot()`, and print or log the YAML it returns. Paste the part you care about into the test and delete the volatile lines. Hand-written templates usually fail on nesting, because the indentation has to reflect the real tree.
  • Does a green aria snapshot mean the statement page is accessible?
    No. It means the tree matches the one you recorded. If the recorded tree already had an unlabelled export button, the snapshot preserves that defect happily. Judging whether the structure serves assistive technology is a separate review with its own criteria, and this matcher only guards against unnoticed change.
  • Why does the matcher retry rather than compare once?
    It is one of Playwright's auto-retrying assertions, so it re-reads the accessibility tree until it matches or the expect timeout runs out. That absorbs a statement page whose transaction table renders a moment after the balance widget, without a hand-written wait before the assertion.

It checks a floor plan rather than a photograph of the lobby: the rooms and their labels must match, but repainting the walls changes nothing.

saying these in an interview costs you the question

  • Calls it a screenshot comparison stored as text
  • Thinks it asserts CSS classes, ids or tag names
  • Expects aria-hidden content to appear in the snapshot
  • Believes a passing snapshot proves the page is accessible
  • Assumes it compares once with no retry
  • Says every property of every node must be listed
open as a page

In Playwright, how does expect(locator).toBeHidden() differ from expect(locator).not.toBeVisible()?

level: juniorimportance: must knowfreq 58%

basics

~20 s

In outcome they are the same. Both retry until the element is hidden or absent from the DOM, and both pass when it never existed at all. The difference is readability and the failure message, not semantics.

open as a page

In Playwright, what happens the first time expect(page).toHaveScreenshot() runs and no reference image exists?

level: juniorimportance: must knowfreq 74%

basics

~20 s

Playwright captures the page, writes that capture to the snapshots folder as the new reference image, and still fails the test. A later run, or an automatic retry, compares against the file just written and passes.

open as a page

In Playwright, what does expect.soft() do that a plain expect() does not?

level: juniorimportance: must knowfreq 56%

basics

~10 s

A soft assertion records its failure and lets the test keep running, while a plain expect throws and stops the test at that line. Both end with the test reported as failed.

open as a page

In Playwright, what happens if you forget to await expect(locator).toBeVisible()?

level: juniorimportance: must knowfreq 84%

basics

~20 s

Playwright's web-first matchers return a promise. Without await, the test body runs on and the test finishes before the matcher decides anything, so the assertion can never fail it — the error is dropped or blamed on a later test.

open as a page

Your Playwright check that the export spinner is gone passes even when the statement page fails to load — why?

level: middleimportance: must knowfreq 63%

basics

~10 s

A negated web-first assertion stops retrying the moment its condition is true, and it is already true on a page that never rendered the spinner. The check passes at time zero and proves nothing.

open as a page

In Playwright, when should you reach for `expect.poll` instead of wrapping assertions in `toPass`?

level: middleimportance: must knowfreq 68%

basics

~20 s

Use expect.poll when the check reduces to one value and one matcher. Use toPass when a whole block must be retried together: several assertions, or code that can throw. Poll retries a value, toPass retries statements.

open as a page

In Playwright, what path does toHaveScreenshot() use for its reference image, and what controls it?

level: middleimportance: must knowfreq 61%

basics

~20 s

By default the reference lands beside the test in a folder named after the spec file, with the project name and platform in the file name. The snapshotPathTemplate config option, built from tokens, changes that layout.

open as a page

How does Playwright's expect(locator).toHaveText() retry until the text matches?

level: middleimportance: must knowfreq 70%

basics

~20 s

Each attempt re-runs the locator query from scratch, reads the element's text, normalizes whitespace and compares it. Playwright repeats until the condition holds or the expect timeout (5 seconds by default in 1.63) expires, then reports the last value seen.

open as a page

In Playwright, what does running the tests with -u actually rewrite, and what does it leave untouched?

level: seniorimportance: must knowfreq 57%

basics

~20 s

It rewrites reference images only for the tests that command actually executed, and only for the projects and the platform that ran. Everything filtered out, every other project, and every orphaned file stays exactly as it was.

open as a page

How do you use Playwright's `expect.poll` to assert that an async function eventually returns an expected value?

level: juniorimportance: should knowfreq 46%

basics

~10 s

Hand expect.poll the function, not the value: await expect.poll(() => readRowCount()).toBe(42). Playwright re-invokes the callback and re-applies the matcher until it passes or the timeout expires.

open as a page

A Playwright aria snapshot fails after you intentionally rename the Export button; how do you get it green without weakening the check?

level: middleimportance: should knowfreq 41%

basics

~20 s

Read the YAML diff in the failure, confirm every changed line is intended, then re-run that spec with --update-snapshots so the stored template is regenerated. Commit the regenerated template with the code change and review it as a normal text diff.

open as a page

In a Playwright aria snapshot template, how do you express a node's role, accessible name and heading level?

level: middleimportance: should knowfreq 32%

basics

~20 s

Each node is one YAML list item: the role, then the accessible name in double quotes, then state in square brackets, as in a heading line reading role heading, name October statement, level=1. Indentation nests children.

open as a page

In Playwright, what happens to expect(locator).not.toHaveText('Pending') when the element is missing?

level: middleimportance: should knowfreq 37%

basics

~20 s

It does not pass. Playwright only treats a missing element as success for visibility and attachment checks, so the content negation keeps retrying and fails at the timeout with a log showing no element was ever found.

open as a page

What probe intervals and timeout defaults do Playwright's `expect.poll` and `toPass` use?

level: middleimportance: should knowfreq 52%

basics

~20 s

Both probe on intervals of 100, 250, 500 then 1000 milliseconds, with the last value repeating. In Playwright 1.63 expect.poll uses the expect timeout, five seconds by default, while toPass defaults to timeout 0 and has none of its own.

open as a page

In Playwright, what does the mask option of toHaveScreenshot() accept, and what does it do to the image?

level: middleimportance: should knowfreq 52%

basics

~20 s

It accepts an array of locators. Each matched element is painted over with a solid box covering its bounding box, bright pink by default, changeable with maskColor. The box is applied while capturing, so both the reference and the actual carry it.

open as a page

Which capture defaults does Playwright's toHaveScreenshot() apply that page.screenshot() does not?

level: middleimportance: should knowfreq 45%

basics

~20 s

The matcher defaults to animations disabled and scale css, where a plain page screenshot allows animations and captures at device resolution. Both hide the text caret. The matcher also re-captures until the image matches or the assertion times out.

open as a page

How can a Playwright test tell mid-run that an earlier expect.soft() check already failed?

level: middleimportance: should knowfreq 34%

basics

~20 s

Read test.info().errors, the array of errors the runner has recorded for the running test. Soft failures land there as they happen, so asserting the array is empty stops the test at the point you choose.

open as a page

In Playwright, when do you use expect(locator).toHaveText() versus toContainText()?

level: middleimportance: should knowfreq 58%

basics

~20 s

Both retry and normalize whitespace, but toHaveText requires the element's entire text to match while toContainText only requires a substring. Use the exact form on tightly-scoped elements and the containment form when surrounding text is volatile.

open as a page

A Playwright aria snapshot of a bank statement page fails every run because the balance figures change, so how do you stabilize it?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Relax the volatile nodes instead of regenerating the template: write their text or name as a regular expression between slashes, or drop the value entirely, and scope the assertion to the region the test is about rather than the whole page body.

open as a page

In Playwright, how do you check that a deleted transaction row is really gone and not merely never rendered?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Bracket the delete with positive assertions. Assert the table and the target row are visible and the row count is known, delete, then assert the count dropped by one and the row is no longer attached.

open as a page

A Playwright `toPass` block clicks Refresh then asserts the balance, and it times out only in CI. What's wrong?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Everything inside a toPass block re-runs on each probe, so the Refresh click keeps firing and restarting the load being awaited. With toPass defaulting to no timeout, the test timeout ends the run instead of the assertion.

open as a page

How would you frame a Playwright screenshot baseline for a bank statement page that scrolls for thousands of rows?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Assert on a locator rather than the page: expect(page.getByTestId('balance-widget')).toHaveScreenshot() scrolls that element into view and captures only its box. Full page and clip are the alternatives when a region cannot be located.

open as a page

How do you add a custom matcher to Playwright's expect, and what must that matcher return?

level: seniorimportance: should knowfreq 41%

basics

~20 s

Import expect as baseExpect from @playwright/test, call baseExpect.extend with your matcher functions, and export the result as expect. Each matcher returns an object with a boolean pass and a message function that builds the failure text.

open as a page

On a bank statement page, which Playwright checks would you make soft and which must stay hard?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Make a check soft when nothing later in the test depends on it, such as the balance format or the export button's label. Keep it hard when the following steps are meaningless or misleading if it fails.

open as a page

Why does asserting Playwright's locator.textContent() with expect(value).toBe() flake where toHaveText() does not?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Reading with textContent() takes one snapshot of the text, and comparing it with a generic matcher happens once. Nothing retries, so a value that arrives moments later fails. toHaveText re-reads the element until it matches or the timeout expires.

open as a page

What does Playwright's expect.configure() return, and which options can you preset on it?

level: middleimportance: nice to knowfreq 29%

basics

~20 s

It returns a new expect instance carrying preset options - timeout, soft and message - and leaves the imported expect untouched. You call the returned instance exactly like expect, and its matchers inherit those defaults.

open as a page

Across a large Playwright suite, how do you decide which pages get an aria snapshot without creating constant template churn?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Put snapshots on high-value, stable surfaces whose structure is a contract, scope each one to the region the test is about, and set an update policy: regeneration scoped to the changed spec, templates reviewed as code, shrink any template that churns.

open as a page

When is asserting with Playwright's toHaveCSS or toHaveClass worth the coupling it creates?

level: principalimportance: nice to knowfreq 34%

basics

~20 s

Only when the styling itself is the requirement, such as an overdraft that must be visually marked, or when the class is a documented state contract. Otherwise assert user-visible meaning or a stable state attribute instead.

open as a page