level 02 / assertions

Assertions

expect() in depth — visible/hidden, text, URL, count, attribute, and soft assertions that don't abort the test on first failure.

practice site: SauceDemo

How expect() works in Playwright

Playwright’s expect() is not Jest’s expect. It is locator-aware and retries automatically until the assertion passes or the timeout expires.

// This retries for up to 5s (configurable) checking visibility
await expect(page.getByText('Products')).toBeVisible();

// NOT like Jest: does not throw immediately on failure
// It polls the DOM until the condition is met

This means you can write assertions immediately after an action without manual waits — the assertion’s retry covers the async UI update.

Visibility

// Element is visible (in DOM + visible in viewport)
await expect(page.getByRole('button', { name: 'Checkout' })).toBeVisible();

// Element is hidden or detached from DOM
await expect(page.getByTestId('loading-spinner')).toBeHidden();

// Element is in DOM (even if hidden)
await expect(page.locator('.modal')).toBeAttached();

// Element is not in DOM at all
await expect(page.locator('.old-element')).not.toBeAttached();

Text content

// Contains this text (substring match)
await expect(page.getByRole('heading')).toContainText('Welcome');

// Exact text match
await expect(page.getByRole('heading')).toHaveText('Welcome back, Alice');

// Match with regex
await expect(page.getByTestId('item-count')).toHaveText(/\d+ items?/);

// Inner text (all text nodes concatenated)
const text = await page.getByRole('heading').innerText();
expect(text).toBe('Welcome back, Alice'); // synchronous — value already retrieved

URL and navigation

// Current URL matches string exactly
await expect(page).toHaveURL('https://saucedemo.com/inventory.html');

// URL matches regex
await expect(page).toHaveURL(/inventory/);

// Page title
await expect(page).toHaveTitle('Swag Labs');
await expect(page).toHaveTitle(/Swag/);

Count

// Exactly N elements match the locator
await expect(page.locator('.inventory_item')).toHaveCount(6);

// At least one match
await expect(page.locator('.notification')).not.toHaveCount(0);

Attributes and properties

// Element has attribute with value
await expect(page.getByRole('textbox')).toHaveAttribute('placeholder', 'Username');
await expect(page.getByRole('textbox')).toHaveAttribute('disabled', '');

// CSS class
await expect(page.getByRole('button', { name: 'Selected' })).toHaveClass(/selected/);

// Input value
await expect(page.getByLabel('Username')).toHaveValue('standard_user');

// Checkbox state
await expect(page.getByRole('checkbox', { name: 'Remember me' })).toBeChecked();

Negation

Every assertion has a .not counterpart:

await expect(page.getByText('Error')).not.toBeVisible();
await expect(page.locator('.spinner')).not.toBeAttached();
await expect(page).not.toHaveURL(/login/);

Soft assertions

Soft assertions do not abort the test on failure. All soft assertions are collected and reported together at the end of the test.

test('checkout page summary', async ({ page }) => {
  await page.goto('/checkout-step-two.html');

  // These collect failures but don't stop the test
  await expect.soft(page.getByTestId('subtotal')).toHaveText('Item total: $29.99');
  await expect.soft(page.getByTestId('tax')).toHaveText('Tax: $2.40');
  await expect.soft(page.getByTestId('total')).toHaveText('Total: $32.39');

  // This hard assertion runs regardless of the soft failures above
  await expect(page.getByRole('button', { name: 'Finish' })).toBeVisible();
});
// If soft assertions fail, the test is marked failed after the test body completes

Reference

expect(locator).toBeVisible()
Element visible in viewport
expect(locator).toBeHidden()
Element hidden or not in DOM
expect(locator).toHaveText(str)
Exact text match
expect(locator).toContainText(str)
Substring match
expect(locator).toHaveValue(str)
Input current value
expect(locator).toHaveCount(n)
Number of matching elements
expect(locator).toHaveAttribute(k, v)
HTML attribute value
expect(locator).toBeChecked()
Checkbox/radio is checked
expect(page).toHaveURL(url)
Current page URL
expect(page).toHaveTitle(str)
Page text</div> </div><div class="cmdrow" data-astro-cid-5t5il5ja> <div class="c" data-astro-cid-5t5il5ja>expect.soft(...)</div> <div class="d" data-astro-cid-5t5il5ja>Non-aborting assertion</div> </div> </div> <h2 id="try-it-assertions-simulation">Try it: Assertions simulation</h2> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="ZrJTr3" component-url="/playwright-typescript-guide/_astro/PwRunner.DxZTvWnK.js" component-export="default" renderer-url="/playwright-typescript-guide/_astro/client.Bh93nMUV.js" props="{"mockPage":[0,"inventory.html"],"label":[0,"Assertions — inventory page checks"],"height":[0,280],"starter":[0,"import { test, expect } from '@playwright/test';\n\ntest('inventory page assertions', async ({ page }) => {\nawait page.goto('/inventory');\nawait expect(page.getByRole('heading')).toBeVisible();\nawait expect(page.locator('[data-testid^=\"result-\"]')).not.toHaveCount(0);\n});\n"],"steps":[1,[[0,{"description":[0,"Navigate to inventory practice page"],"action":[0,"navigate"],"value":[0,"inventory.html"]}],[0,{"description":[0,"Expect heading visible"],"action":[0,"expect-visible"],"selector":[0,"h2"]}],[0,{"description":[0,"Expect PASS badge present"],"action":[0,"expect-visible"],"selector":[0,"[data-status=\"PASS\"]"]}],[0,{"description":[0,"Expect FAIL badge present"],"action":[0,"expect-visible"],"selector":[0,"[data-status=\"FAIL\"]"]}],[0,{"description":[0,"Expect results table visible"],"action":[0,"expect-visible"],"selector":[0,"table"]}]]]}" ssr client="only" opts="{"name":"PwRunner","value":"react"}"></astro-island> <h2 id="interview-questions">Interview questions</h2> <astro-island uid="Z9Adx6" component-url="/playwright-typescript-guide/_astro/QuizCard.DKWiMbE5.js" component-export="default" renderer-url="/playwright-typescript-guide/_astro/client.Bh93nMUV.js" props="{"storageKey":[0,"pw-assertions"],"questions":[1,[[0,{"q":[0,"How does Playwright's expect() differ from Jest's expect()?"],"a":[0,"Playwright's expect() is locator-aware and retries automatically. It polls the DOM condition until it passes or the assertionTimeout (default 5s) expires. Jest's expect() evaluates synchronously at the moment of call. This means in Playwright you do not need to await the element state before asserting — the assertion itself handles the waiting."],"senior":[0,"The retry mechanism is implemented via polling with exponential backoff. The default assertionTimeout is 5s but can be configured globally in playwright.config.ts or per-assertion with expect(locator, { timeout: 10_000 }). Understanding this distinction prevents a common anti-pattern: adding page.waitForSelector() before expect() — it's redundant and adds latency without benefit."]}],[0,{"q":[0,"What is the difference between toHaveText() and toContainText()?"],"a":[0,"toHaveText() matches the full text of the element (or an exact match against the list for multi-element locators). toContainText() matches a substring. For most assertions, toContainText() is more resilient — a heading that reads 'Products (6 items)' passes toContainText('Products') even if the item count changes."],"senior":[0,"A common mistake: using toHaveText() on a locator that resolves to multiple elements. With multiple elements, toHaveText() takes an array and checks each element's text against the array in order. This is useful for asserting list content but produces confusing errors when the length doesn't match. Use toHaveCount() first, then toHaveText() with an array, to get clear failure messages."]}],[0,{"q":[0,"When would you use soft assertions and what is their trade-off?"],"a":[0,"Soft assertions are useful when you want to validate multiple related conditions in a single test run without the first failure masking the rest — e.g., checking all items in a price summary, verifying all form field errors, auditing an entire page for required elements. The trade-off: a soft-assertion failure still marks the test as failed, but the test continues running, potentially causing misleading subsequent failures."],"senior":[0,"Overusing soft assertions is an anti-pattern. If your test needs 10 soft assertions to verify a page, you likely have a test that is doing too much. The correct model: one assertion per test scenario. Reserve soft assertions for cases where multiple values must be correct together (financial totals, form validation messages) and the combined assertion forms a single logical check."]}]]]}" ssr client="only" opts="{"name":"QuizCard","value":"react"}"></astro-island> <nav class="pn" data-astro-cid-tj3kbygk> <a class="pn-link" href="/playwright-typescript-guide/levels/2/actions/" data-astro-cid-tj3kbygk>← Actions</a> <a class="pn-link next" href="/playwright-typescript-guide/levels/2/waiting/" data-astro-cid-tj3kbygk>Waiting Strategies →</a> </nav> </article> <aside class="toc" data-astro-cid-tj3kbygk> <div class="toc-title" data-astro-cid-tj3kbygk>On this page</div> <a href="#how-expect-works-in-playwright" data-astro-cid-tj3kbygk>How expect() works in Playwright</a><a href="#visibility" data-astro-cid-tj3kbygk>Visibility</a><a href="#text-content" data-astro-cid-tj3kbygk>Text content</a><a href="#url-and-navigation" data-astro-cid-tj3kbygk>URL and navigation</a><a href="#count" data-astro-cid-tj3kbygk>Count</a><a href="#attributes-and-properties" data-astro-cid-tj3kbygk>Attributes and properties</a><a href="#negation" data-astro-cid-tj3kbygk>Negation</a><a href="#soft-assertions" data-astro-cid-tj3kbygk>Soft assertions</a><a href="#reference" data-astro-cid-tj3kbygk>Reference</a><a href="#try-it-assertions-simulation" data-astro-cid-tj3kbygk>Try it: Assertions simulation</a><a href="#interview-questions" data-astro-cid-tj3kbygk>Interview questions</a> </aside> </div> </main> <footer data-astro-cid-lejjx2fa> <div class="wrap" data-astro-cid-lejjx2fa> pw<b style="color:var(--ember)" data-astro-cid-lejjx2fa>·handbook</b> — production-grade Playwright engineering, level by level.<br data-astro-cid-lejjx2fa> <a href="/playwright-typescript-guide/" data-astro-cid-lejjx2fa>Home</a> · <a href="https://playwright.dev" rel="noopener" data-astro-cid-lejjx2fa>Playwright Docs</a> · MIT License </div> </footer> </body></html>