How to Use Playwright’s not.toBeEmpty Assertion
Learn the correct Playwright syntax for asserting that a locator contains content, with retries, timeouts, examples, troubleshooting, and CI guidance.
not.toBeEmpty() is the negated form of Playwright Test’s toBeEmpty() locator assertion. Use it with an awaited locator assertion:
import { test, expect } from '@playwright/test';
test('warning has content', async ({ page }) => {
const warning = page.locator('div.warning');
await expect(warning).not.toBeEmpty();
});
toBeEmpty() checks that a locator points to an empty editable element or to a DOM node with no text. Adding .not asserts the opposite. Because this is a web-specific asynchronous assertion, Playwright re-fetches the locator and retries until the condition passes or the assertion timeout is reached. Always await it.
See the official Playwright assertion guide and the LocatorAssertions API reference for the current API details.
1. Install and import the right expect
Install Playwright Test in your project, then import both test and expect from @playwright/test:
npm install --save-dev @playwright/test
npx playwright install
import { test, expect } from '@playwright/test';
Do not substitute a separate expect package. Playwright’s integrated expect is connected to the test runner, fixtures, reporting, and retrying web assertions. If your project has custom fixtures, use a re-export of Playwright’s own expect.
2. Basic patterns
Assert that a status message is populated
test('status message is not empty', async ({ page }) => {
await page.goto('https://example.com/status');
const status = page.getByRole('status');
await expect(status).not.toBeEmpty();
});
Use a CSS locator
const warning = page.locator('div.warning');
await expect(warning).not.toBeEmpty();
Use a test id
const result = page.getByTestId('search-result');
await expect(result).not.toBeEmpty();
Check content rendered after an action
test('submit renders a validation message', async ({ page }) => {
await page.goto('https://example.com/form');
await page.getByRole('button', { name: 'Submit' }).click();
const message = page.getByRole('alert');
await expect(message).not.toBeEmpty();
});
The assertion itself waits for the content. You usually do not need a fixed sleep before it.
3. What the matcher means
The Playwright API reference defines toBeEmpty() as ensuring that a Locator points to an empty editable element or to a DOM node that has no text. Therefore:
await expect(locator).toBeEmpty()expects the documented empty state.await expect(locator).not.toBeEmpty()expects the documented non-empty state.
This matcher is about the element’s text or editable value as defined by Playwright. It is not a general visual-emptiness test. It does not, by itself, prove that an element is visible, that it has a particular child structure, or that a page loaded successfully. Use separate assertions such as toBeVisible(), toContainText(), or toHaveCount() when those are the actual requirements.
4. Retry behavior and timeouts
Playwright’s web assertions re-fetch the locator and retry until the expected condition is true or the timeout expires. The assertion guide lists a default assertion timeout of five seconds. Configure a project-wide value in your Playwright config:
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
timeout: 10_000,
},
});
Override the timeout for one assertion with the options object:
await expect(page.getByRole('status')).not.toBeEmpty({
timeout: 15_000,
});
Choose a timeout that covers the slowest legitimate rendering path. A very large timeout can hide a real failure and make a suite slow; a value that is too small creates flaky failures when the application is legitimately loading.
Abort a retrying assertion
The LocatorAssertions reference documents an optional AbortSignal for this matcher, added in Playwright v1.62. If the signal is already aborted or becomes aborted while Playwright is retrying, the assertion fails without continuing to retry:
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3_000);
try {
await expect(page.getByRole('status')).not.toBeEmpty({
timeout: 10_000,
signal: controller.signal,
});
} finally {
clearTimeout(timer);
}
Use this when a surrounding operation has its own cancellation budget. For ordinary tests, the normal assertion timeout is simpler.
5. Choosing a reliable locator
- Identify the exact element whose content matters.
- Prefer stable, user-facing locators such as
getByRole,getByLabel, or a dedicated test id. - Keep the locator in a variable so the assertion and any later diagnostics refer to the same target.
- Use a CSS selector when the page has no better stable contract.
const emptyState = page.getByTestId('results-empty');
await expect(emptyState).not.toBeEmpty();
Do not assert against a broad container when a nested status, alert, or result element is the real contract. A broad locator can pass because unrelated text exists elsewhere in the container.
6. Complete example
import { test, expect } from '@playwright/test';
test('search results contain rendered content', async ({ page }) => {
await page.goto('https://example.com/search');
await page.getByRole('textbox', { name: 'Search' }).fill('playwright');
await page.getByRole('button', { name: 'Search' }).click();
const results = page.getByTestId('search-results');
await expect(results).not.toBeEmpty({ timeout: 10_000 });
});
The test waits for the results locator to become non-empty. If the locator never gets text, Playwright reports an assertion timeout with the locator details.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
expect(...).not.toBeEmpty is not a function |
The imported expect is from another package. |
Import expect from @playwright/test, or from your fixture module that re-exports it. |
| The assertion times out | The locator is wrong, the page never renders text, or the timeout is too short. | Inspect the locator, wait for the action that triggers rendering, and set a justified per-assertion or project timeout. |
| The test fails immediately | The locator does not resolve to the intended element, or an abort signal was triggered. | Check the selector and cancellation path. Remove or extend the signal deadline when appropriate. |
| The test is flaky | A fixed sleep races the application, or the locator targets an unstable container. | Remove sleeps, use the locator assertion’s retrying behavior, and choose a stable semantic locator. |
| The element looks populated but the assertion fails | What is visible may be generated by styling, pseudo-elements, descendants outside the target, or a different element. | Assert the actual text-bearing node, or use a matcher that expresses the requirement such as toContainText. |
8. Related assertions
toBeEmpty(): require the documented empty state.toContainText('...'): require specific text while allowing additional text.toHaveText('...'): require exact text according to that matcher.toBeVisible(): require visibility separately from content.toHaveCount(1): verify that the locator resolves to the expected number of elements.
Combining focused assertions often communicates the behavior better:
const alert = page.getByRole('alert');
await expect(alert).toBeVisible();
await expect(alert).not.toBeEmpty();
await expect(alert).toContainText('Email is required');
9. Performance, reliability, and CI notes
- Locator assertions retry without requiring polling code in your test.
- Keep assertion timeouts close to the application’s real rendering budget so failures surface promptly.
- Prefer one precise assertion over repeated loops that query the DOM manually.
- Use the same configuration in local and CI runs, with a documented increase only when CI infrastructure is slower.
- When diagnosing a timeout, inspect the locator and the preceding action before increasing the timeout.
10. Or skip the browser setup
If your goal is to capture a page for a visual check, documentation, or an agent workflow rather than run an in-browser assertion, ScreenshotNeo returns a screenshot or PDF from one request. Read the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account.
11. FAQ
Does not.toBeEmpty() check visibility?
No. It checks the documented empty or non-empty content condition. Add toBeVisible() when visibility is part of the requirement.
When was toBeEmpty() added?
The LocatorAssertions API reference marks it as added in Playwright v1.20.
Must I await the assertion?
Yes. It is an asynchronous web assertion that retries until it passes or times out.
Can I change the five-second default?
Yes. Set testConfig.expect globally or pass a per-assertion timeout in milliseconds.
Is whitespace-only content guaranteed to count as non-empty?
The cited documentation defines the matcher in terms of an empty editable element or a DOM node with no text. For whitespace-sensitive behavior, use a more explicit text assertion that matches your application’s contract.


