What is the difference between toHaveText and toContainText?
Written by Jatin Thacker, Automation Architect · Updated
toHaveText passes only when the element's whole text matches, while toContainText passes when the expected text appears anywhere inside it. Use toHaveText by default, because toContainText also passes on a longer text, such as the wrong product whose name starts the same way.This answer is one piece of Chapter 11: Assertions.
- Web-First Assertions Cookbook
- Retry & Polling Patterns
- Visual Regression Testing
- Making a Failure Explain Itself
The quick rule: toHaveText checks all of the text, toContainText checks part of it. Both retry until the timeout, and both normalise whitespace and line breaks before comparing a string.
toHaveText | toContainText | |
|---|---|---|
| One element, a string | the whole text matches | the string appears anywhere |
| A regular expression | the text matches the pattern | the text matches the pattern |
| A list of strings | one value per element: same count, same order | an ordered subset of the elements |
ignoreCase: true | case-insensitive | case-insensitive |
Why toHaveText is the safer default. The catalog sells both 'Aurora Pour-Over Kettle' and 'Aurora Pour-Over Kettle Pro'. toContainText('Aurora Pour-Over Kettle') passes on the Pro card, so a test meant for the cheaper kettle stays green on the wrong product. toHaveText fails and prints both texts.
Lists. Pointed at the six recommended items, toHaveText needs all six, in page order. toContainText accepts two of them, as long as they are in the same order as on the page; reversed, it fails.
Use toContainText when the rest of the text genuinely changes, such as a banner with a count or a date in it. Use a regular expression when you need a pattern, for example /products$/. A regular expression is a search, not a whole-text match, even in toHaveText: /prod/ passes on 'All products', so anchor it with ^ and $ when you mean the whole text.
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/catalog.html?mock=static&seed=42&autologin=1');
});
test('one element: the whole text, or a part of it', async ({ page }) => {
const heading = page.locator('#grid-heading');
await expect(heading).toHaveText('All products');
await expect(heading).toContainText('products');
await expect(heading).toHaveText(/products$/);
await expect(heading).toHaveText('all products', { ignoreCase: true });
});
test('toContainText passes on the wrong product', async ({ page }) => {
const pro = page.getByRole('heading', { name: 'Aurora Pour-Over Kettle Pro' });
await expect(pro).toContainText('Aurora Pour-Over Kettle');
await expect(expect(pro).toHaveText('Aurora Pour-Over Kettle', { timeout: 1000 })).rejects.toThrow();
});
test('a list: toHaveText wants every item, toContainText an ordered subset', async ({ page }) => {
const picks = page.getByTestId('recommended-item');
await expect(picks).toHaveText([
'Aurora Pour-Over Kettle',
'Halcyon Burr Grinder',
'Northwind Knock Box',
'Quarry Ceramic Dripper',
'Cinder Drip Scale',
'Tidewater Cold Brew Carafe',
]);
await expect(picks).toContainText(['Aurora Pour-Over Kettle', 'Quarry Ceramic Dripper']);
// Two of six with toHaveText: fails. The right two in the wrong order: fails.
await expect(expect(picks).toHaveText(['Aurora Pour-Over Kettle', 'Quarry Ceramic Dripper'], { timeout: 1000 })).rejects.toThrow();
await expect(expect(picks).toContainText(['Quarry Ceramic Dripper', 'Aurora Pour-Over Kettle'], { timeout: 1000 })).rejects.toThrow();
});$ npx playwright test assert-text.spec.js
ok 1 [chromium] › assert-text.spec.js:7:1 › one element: the whole text, or a part of it (404ms)
ok 2 [chromium] › assert-text.spec.js:15:1 › toContainText passes on the wrong product (2.2s)
ok 3 [chromium] › assert-text.spec.js:21:1 › a list: toHaveText wants every item, toContainText an ordered subset (2.3s)
3 passed (6.1s)
# Without the .rejects wrappers, the wrong-product check fails like this:
Error: expect(locator).toHaveText(expected) failed
Locator: getByRole('heading', { name: 'Aurora Pour-Over Kettle Pro' })
Expected: "Aurora Pour-Over Kettle"
Received: "Aurora Pour-Over Kettle Pro"
# ...and toHaveText with two of the six items fails like this:
Locator: getByTestId('recommended-item')
- Expected - 0
+ Received + 4
Array [
"Aurora Pour-Over Kettle",
+ "Halcyon Burr Grinder",
+ "Northwind Knock Box",
"Quarry Ceramic Dripper",
+ "Cinder Drip Scale",
+ "Tidewater Cold Brew Carafe",
]'Order #1042 placed successfully'. Which assertion will pass?Practised in the full course: the drill The forced click.
See the chapterRelated questions
AI can write the test now. The job is knowing whether it checks anything. That's what the course teaches, one chapter at a time.
163 lessons · 226 drills · start free