When a Playwright test runs, I always wonder—did the app really behave the way I expected, or did the script just move on? That doubt is exactly where Playwright assertions come in.
I use assertions to pause the test and demand proof. Is the element visible? Did the value change? Did the action actually succeed? Each check forces the application to reveal whether it’s behaving correctly.
That moment of validation decides everything. If the expectation holds, the test passes. If not, it fails instantly. With assertions in place, I’m no longer assuming correctness—I’m verifying it, step by step.
What are Playwright Assertions?
Playwright Assertions are a set of built-in functions which includes expect() functionprovided by the Playwright testing framework to validate the behavior and state of a web application during automated tests.
Playwright Assertions are used to verify whether specific conditions are met, such as checking if an element exists, contains certain text, or has a particular state. These assertions are essential for confirming that the application behaves as expected during end-to-end testing.
Playwright provides diverse assertion types:
- Element States: Check the visibility, availability, and interactivity of UI elements.
- Content Validation: Ensure elements display the correct text, values, or match specific patterns.
- Page Properties: Assertions can confirm page details like URLs, titles, or cookie presence.
- Network Interactions: Verify the outcomes of network requests and responses to ensure proper data loading and form submissions.
Playwright Expect() Function
Playwright expect is the built-in assertion API used to validate how an application behaves during a test.
It allows tests to wait for conditions to be true and then verify outcomes such as element visibility, text content, attributes, URLs, or network states. Instead of checking values instantly, expect automatically retries until the condition passes or the timeout is reached, which helps handle dynamic UI behavior.
In simple terms, expect is how Playwright confirms whether the app under test matches the expected state and decides if a test should pass or fail.
For Example:
test("Validate BrowserStack demo application site title", async ({page}) => {
await page.goto("https://bstackdemo.com/")
await expect(page).toHaveTitle("StackDemo")
})In the above code example, using BrowserStack’s Demo application in the playwright test to validate the Site Title.
Code Breakdown:
Below code will access the BrowserStack’s Demo Application
await page.goto("https://bstackdemo.com/")Below code Asserts the BrowserStack’s Website Title.
await expect(page).toHaveTitle("StackDemo")Here, Playwright waits until the element becomes visible before marking the assertion successful.
Different Types of Playwright Assertions
Assertions in Playwright is broadly classified into below types
Types of Playwright Assertions:
- Auto-retrying Assertions
- Non-retrying Assertions
- Negating Matchers
- Soft Assertions
Let’s look into each assertion type.
Auto-retrying Assertion
Auto-retrying assertions in Playwright are a vital functionality that significantly boosts the reliability and stability of test scripts by repeatedly attempting to verify assertions until they either succeed or a predefined timeout is reached.
Auto-retrying feature is especially useful in scenarios where web elements might not immediately meet expected conditions due to network delays, dynamic content loading, or client-side scripting operations.
Let’s write a test which asserts a text from BrowserStack’s Demo Application Website.
In this test, performing below steps:
- Access BrowserStack’s Demo application
- Validate the default Number of Products found
test("Validate BrowserStack default products found count", async ({page}) => {
await page.goto("https://bstackdemo.com/")
const productLocator = await page.locator(".products-found span")
await expect(productLocator).toHaveText('25 Product(s) found')
})Code Breakdown
Visit the BrowserStack’s Demo application
await page.goto("https://bstackdemo.com/")Get the locator reference
const productLocator = await page.locator(".products-found span")Assert the Number of Products found from Web page against expected value
await expect(productLocator).toHaveText('25 Product(s) found')Observe that in the above assertion you pass the locator reference and then you are using matcher toHaveText which accepts expected value as string.
If you run the above test, the test will fail in assertion with auto retry timeout.
Note that the test is timed out after retrying for 5000 milliseconds, this is because Playwright has a default timeout of 5000 milliseconds.
You can overwrite the assertion timeout at command level like below.
await expect(productLocator).toHaveText('25 Product(s) found',{timeout: 2000})Below is the full list of Auto-Retrying assertions.
| Assertion | Description |
|---|---|
| await expect(locator).toBeAttached() | Element is attached |
| await expect(locator).toBeChecked() | Checkbox is checked |
| await expect(locator).toBeDisabled() | Element is disabled |
| await expect(locator).toBeEditable() | Element is editable |
| await expect(locator).toBeEmpty() | Container is empty |
| await expect(locator).toBeEnabled() | Element is enabled |
| await expect(locator).toBeFocused() | Element is focused |
| await expect(locator).toBeHidden() | Element is not visible |
| await expect(locator).toBeInViewport() | Element intersects viewport |
| await expect(locator).toBeVisible() | Element is visible |
| await expect(locator).toContainText() | Element contains text |
| await expect(locator).toHaveAttribute() | Element has a DOM attribute |
| await expect(locator).toHaveClass() | Element has a class property |
| await expect(locator).toHaveCount() | List has exact number of children |
| await expect(locator).toHaveCSS() | Element has CSS property |
| await expect(locator).toHaveId() | Element has an ID |
| await expect(locator).toHaveJSProperty() | Element has a JavaScript property |
| await expect(locator).toHaveScreenshot() | Element has a screenshot |
| await expect(locator).toHaveText() | Element matches text |
| await expect(locator).toHaveValue() | Input has a value |
| await expect(locator).toHaveValues() | Select has options selected |
| await expect(page).toHaveScreenshot() | Page has a screenshot |
| await expect(page).toHaveTitle() | Page has a title |
| await expect(page).toHaveURL() | Page has a URL |
| await expect(response).toBeOK() | Response has an OK status |
Non-retrying Assertions
Non-retrying Assertions are only useful when web pages load data asynchronously. Test assertion will fail without any timeout or retrying when using the Non-retrying Assertion and below is an example test for the same.
test("example for non-retrying assertion", async ({page}) => {
await page.goto("https://bstackdemo.com/")
const productLocator = await page.locator(".products-found span")
const productSearchText = await productLocator.innerText()
await expect(productSearchText).toBe('25 Product(s) found')
})When you run this test, it will fail with assertion without retrying.

| Assertion | Description |
|---|---|
| expect(value).toBe() | Value is the same |
| expect(value).toBeCloseTo() | Number is approximately equal |
| expect(value).toBeDefined() | Value is not undefined |
| expect(value).toBeFalsy() | Value is falsy, e.g. false, 0, null, etc. |
| expect(value).toBeGreaterThan() | Number is more than |
| expect(value).toBeGreaterThanOrEqual() | Number is more than or equal |
| expect(value).toBeInstanceOf() | Object is an instance of a class |
| expect(value).toBeLessThan() | Number is less than |
| expect(value).toBeLessThanOrEqual() | Number is less than or equal |
| expect(value).toBeNaN() | Value is NaN |
| expect(value).toBeNull() | Value is null |
| expect(value).toBeTruthy() | Value is truthy, i.e. not false, 0, null, etc. |
| expect(value).toBeUndefined() | Value is undefined |
| expect(value).toContain() | String contains a substring |
| expect(value).toContain() | Array or set contains an element |
| expect(value).toContainEqual() | Array or set contains a similar element |
| expect(value).toEqual() | Value is similar – deep equality and pattern matching |
| expect(value).toHaveLength() | Array or string has length |
| expect(value).toHaveProperty() | Object has a property |
| expect(value).toMatch() | String matches a regular expression |
| expect(value).toMatchObject() | Object contains specified properties |
| expect(value).toStrictEqual() | Value is similar, including property types |
| expect(value).toThrow() | Function throws an error |
| expect(value).any() | Matches any instance of a class/primitive |
| expect(value).anything() | Matches anything |
| expect(value).arrayContaining() | Array contains specific elements |
| expect(value).closeTo() | Number is approximately equal |
| expect(value).objectContaining() | Object contains specific properties |
| expect(value).stringContaining() | String contains a substring |
| expect(value).stringMatching() | String matches a regular expression |
Negating Matchers
Negating Matchers are used when we want to check that a certain condition does not hold true. It essentially reverses the condition you’re checking for, enabling you to assert the absence of a condition or element. Negating Matchers are especially helpful for ensuring that a web page or application is free from errors, incorrect states, or unwanted elements.
Below is the example test. The test will assert for filter count not matching the value 3
test("example for negating matcher", async ({page}) => {
await page.goto("https://bstackdemo.com/")
const filter = await page.locator(".filters .filters-available-size")
const filterCount = await filter.count()
await expect(filterCount).not.toEqual(3)
})Soft Assertions
By default Assertion will abort the test as soon as the expected result is not matched with the actual result. There are cases where we have to check multiple assertions and at the end of the test throw the assertion error.
Soft assertion is good for cases where we want to assert multiple cases and then fail the test at the end.
Below is the example test. The test has two assertions and both will execute and fail.
test("example for soft assertion", async ({page}) => {
await page.goto("https://bstackdemo.com/")
const filter = await page.locator(".filters .filters-available-size")
const filterCount = await filter.count()
await expect.soft(filterCount).not.toEqual(4)
await expect.soft(page).toHaveTitle("StackDemo!")
})If you don’t use soft assertion, then the test will fail at the first assertion check and doesn’t continue.
Running the above test will show two assertion errors.
Playwright Custom Matchers with Examples
Playwright provides users to create their own Custom Matchers, which can be chained with expect() for Assertions
To create Custom Matchers, we need to extend the expect() function from Playwright and then add our Custom Matchers within the extend function like below
import { expect as baseExpect } from '@playwright/test';
import type { Page, Locator } from '@playwright/test';
export { test } from '@playwright/test';
export const expect = baseExpect.extend({
async toHavePrice(locator: Locator, expected: number, options?: { timeout?: number }) {
const assertionName = 'toHavePrice';
let pass: boolean;
let matcherResult: any;
try {
await baseExpect(locator).toHaveText(String(expected), options);
pass = true;
} catch (e: any) {
matcherResult = e.matcherResult;
pass = false;
}
const message = pass
? () => this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
'\n\n' +
`Locator: ${locator}\n` +
`Expected: ${this.isNot ? 'not' : ''}${this.utils.printExpected(expected)}\n` +
(matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '')
: () => this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
'\n\n' +
`Locator: ${locator}\n` +
`Expected: ${this.utils.printExpected(expected)}\n` +
(matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '');
return {
message,
pass,
name: assertionName,
expected,
actual: matcherResult?.actual,
};
},
});We can create a new file called fixture.ts and add the above code in that file. Once the code is added we can then write a test like below.
import { test, expect } from '../fixtures/fixtures';
test("example for custom matcher", async ({page}) => {
await page.goto("https://bstackdemo.com/")
const phone = await page.locator(".shelf-item__title", {hasText:"iPhone 12 Mini", })
const phoneParent = await phone.locator("..")
const phonePrice = phoneParent.locator(".shelf-item__price .val b")
await expect(phonePrice).toHavePrice("699")
})Handling Dynamic UI States with Assertions
Modern web applications rarely stay still. Elements load asynchronously, UI states change based on user actions, and content updates after network calls. Assertions in Playwright are designed to handle this dynamism without relying on manual waits or fragile timing logic.
Playwright’s expect() assertions automatically wait for the expected condition to be met before failing. This makes them effective for validating UI states that appear, disappear, or update over time.
Common dynamic scenarios handled with assertions include:
- Elements becoming visible after an API response
- Buttons enabling or disabling based on form input
- Text or attributes updating after user interaction
- Loaders or spinners disappearing before content renders
For example:
await expect(page.locator('.loader')).toBeHidden();
await expect(page.locator('#status')).toHaveText('Completed');Here, the assertions wait until the UI reaches the expected state instead of checking immediately. This approach reduces flakiness and keeps tests aligned with how real users experience the application.
Assertions for Network Requests and Responses
UI validation alone is often not enough. Many user-visible changes depend on network activity, and Playwright allows assertions to validate those interactions directly.
Assertions can be combined with network interception to verify:
- API response status codes
- Request payloads sent by the application
- Response bodies affecting UI behavior
- Completion of critical backend operations
A common pattern involves waiting for a response and then asserting on its outcome:
const response = await page.waitForResponse(
resp => resp.url().includes('/api/order') && resp.status() === 200
);
await expect(response.ok()).toBeTruthy();By asserting on network behavior, tests confirm not just that the UI changed, but that it changed for the right reason. This strengthens test coverage for data-driven workflows and reduces blind spots where UI-only assertions might pass despite backend issues.
Common Mistakes with Playwright Assertions
Even with powerful defaults, assertions can introduce instability when used incorrectly. Many flaky tests trace back to assertion misuse rather than application defects.
Frequent mistakes include:
- Using manual waits instead of relying on auto-retrying assertions
- Asserting too early on elements that depend on async rendering
- Overusing hard assertions where soft assertions are more appropriate
- Validating implementation details instead of user-visible behavior
For example, checking text immediately after navigation:
// Fragile approach
await page.click('#submit');
expect(await page.textContent('#msg')).toBe('Success');A more stable approach:
await page.click('#submit');
await expect(page.locator('#msg')).toHaveText('Success');This allows Playwright to wait until the UI is truly ready. Clear, user-focused assertions combined with built-in retries result in tests that fail for real issues, not timing quirks or transient states
Best Practices to use Playwright Expect()
The expect() function is a cornerstone of assertions in Playwright, offering the ability to assert on elements, network responses, and other test conditions.
Below are the few Best Practices for Playwright Expect() :
1. Leverage Built-In Retry Mechanism
Understand the automatic retry feature in Playwright’s expect():
- Utilize this feature for dynamic content where elements may appear or change state over time.
- Avoid excessive reliance which might conceal performance issues or complex race conditions.
2. Employ Semantic Locators
Opt for Playwright’s advanced selectors like role and text selectors to improve both the readability and maintainability of your tests:
await expect(page.locator('text=Sign In')).toBeVisible();
await expect(page.locator('role=button', {name: 'Send'})).toBeEnabled();These selectors enhance the semantic clarity and accessibility focus of your tests.
3. Integrate Actions with Verification
Simultaneously perform user actions and verify outcomes to mimic real user flows:
await page.click('button#save');
await expect(page.locator('text=Saved successfully')).toBeVisible();This method validates user interactions in real-time.
4. Customize Timeouts
Adjust timeouts in expect() when the default settings do not align with specific test requirements:
- Modify the timeout parameter to suit specific waiting needs without overextending test durations.
5. Assert Non-Presence
Assert the non-presence of elements or messages, particularly useful in validating error handling and user feedback:
await expect(page.locator('text=Error')).not.toBeVisible();6. Utilize State-Specific Assertions
Make full use of Playwright’s state-specific assertions to directly assess the user interface:
await expect(page.locator('input[type="checkbox"]')).toBeChecked();7. Assert Network Interactions
Capture and assert network responses to ensure backend integration is functioning as expected:
const [response] = await Promise.all([
page.waitForResponse(resp => resp.url().includes('/api/submit') && resp.status() === 200),
page.click('button#submit')
]);
await expect(response).toBeOK();8. Validate Accessibility Features
Assert on accessibility features to ensure your application is accessible:
await expect(page.locator(‘role=button’, {name: ‘Confirm’})).toHaveAttribute(‘aria-live’, ‘polite’);
9. Enhance Assertion Failures
Incorporate diagnostic tools like screenshots or logs to investigate why assertions fail:
test.fail(async ({ page }) => {
await page.screenshot({ path: 'error-snapshot.png' });
});Why run Playwright Tests on Real Device Cloud?
Here’s why you should run Playwright tests on real browsers & devices using a cloud-based testing platform like BrowserStack Automate:
- Accurate UI behavior validation: Assertions related to visibility, text rendering, focus states, and animations can behave differently across real browsers and devices. Running them on BrowserStack Automate ensures these checks hold true in real environments.
- Reduced false positives in test results: Assertions may pass locally but fail for users due to OS-level differences, browser engines, or device-specific timing. Real device testing helps catch these gaps early.
- Consistent assertion timing under real conditions: Real devices expose performance variations that impact assertion retries and timeouts. This makes assertions more reliable and aligned with real-world load and rendering behavior.
- Scalable validation across environments: BrowserStack Automate enables running assertion-heavy Playwright tests in parallel across multiple browser–OS combinations, ensuring broader coverage without increasing execution time.
- Better debugging when assertions fail: Screenshots, videos, and logs from real device runs provide clear context for why an assertion failed, making root cause analysis faster and more accurate.
Conclusion
The expect() function of Playwright is a critical asset for automation testers, delivering powerful and adaptable assertions crucial for verifying the integrity and functionality of web applications. Its inherent retry capability, coupled with its proficiency in handling complex assertions on elements, network interactions, and accessibility attributes, is particularly effective for modern web applications characterized by dynamic and asynchronous behavior.
Utilizing expect() adeptly within Playwright tests enables testers to confirm that their applications not only adhere to specific requirements but also deliver a stable user experience across diverse scenarios. This function’s ability to precisely adjust assertion conditions, such as timeouts, and to assess a broad spectrum of criteria—from the visibility of elements to the correctness of API responses—increases the precision and dependability of tests.
Running your Playwright Tests on BrowserStack’s Real Device Cloud helps you get access to 3500+ real device and browser combinations for maximum test coverage. It allows you to test under real user conditions, which will help identify the bottlenecks in the real user experience and rectify them. BrowserStack’s Automate allows you to run Playwright tests on the cloud and across browsers and devices simultaneously by leveraging parallel testing for faster testing with a vast coverage.

