The most important thing to understand: locator assertions auto-retry. expect(locator).toBeVisible() polls until the element appears — you don't write any waiting loops. Value assertions like expect(someString).toBe('x') are instant and can be flaky on async UIs.
Locator assertions — they wait automatically
Locator assertions poll the DOM in a loop — no manual waitFor needed
Every assertion that takes a Locator retries until the condition is met or the timeout expires (default: 5 seconds). Playwright re-queries the element and checks the condition in a loop. You don't write waitFor manually — the assertion does it.
The most common ones I use every day:
ts
// Видимість — найпоширеніша перевіркаawaitexpect(page.getByRole('heading', { name: 'Orders' })).toBeVisible()
awaitexpect(page.getByTestId('error-banner')).not.toBeVisible()
awaitexpect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
// Текст — підрядок або повний збігawaitexpect(page.getByTestId('status-badge')).toHaveText('Shipped')
awaitexpect(page.getByTestId('order-count')).toContainText('24 orders')
// URL і заголовокawaitexpect(page).toHaveURL('/dashboard')
awaitexpect(page).toHaveURL(/\/orders\/\d+/)
awaitexpect(page).toHaveTitle('Orders | CRM')
// Поля вводуawaitexpect(page.getByLabel('Email')).toHaveValue('admin@example.com')
awaitexpect(page.getByLabel('Status')).toHaveValue('pending')
// Кількість рядків у таблиціawaitexpect(page.getByRole('row')).toHaveCount(11) // 1 header + 10 rows// Checkboxawaitexpect(page.getByRole('checkbox', { name: 'Notify client' })).toBeChecked()
Value assertions — instant, no retry
These assert plain JavaScript values — strings, numbers, arrays, objects. They run once and fail immediately if the condition isn't met. Use them for data you've already extracted from the page, not for UI state that might still be loading.
ts
// ✅ Правильно — витягуємо значення, потім перевіряємоconst count = await page.getByRole('row').count()
expect(count).toBeGreaterThan(0)
const title = await page.title()
expect(title).toContain('Orders')
// ✅ Перевірка об'єктів і масивів (не пов'язана з DOM)const ids = ['ORD-001', 'ORD-002', 'ORD-003']
expect(ids).toHaveLength(3)
expect(ids).toContain('ORD-002')
expect(ids[0]).toMatch(/^ORD-\d+/)
// ❌ Небезпечно — значення може ще не завантажитисяconst text = await page.locator('.status').textContent()
expect(text).toBe('Shipped') // краще: await expect(locator).toHaveText('Shipped')
Negating with .not
Any assertion can be negated with .not. It works on both locator and value assertions. For locator assertions, .not also waits — it retries until the condition becomes false.
ts
// Після логауту — форма входу видима, dashboard — ніawaitexpect(page.getByRole('form', { name: 'Login' })).toBeVisible()
awaitexpect(page.getByTestId('dashboard')).not.toBeVisible()
// Кнопка Submit вимкнена поки поля не заповненіawaitexpect(page.getByRole('button', { name: 'Submit' })).not.toBeEnabled()
// Значення не порожнєexpect(orderId).not.toBeUndefined()
expect(orderId).not.toBe('')
Soft assertions — fail later, not immediately
A normal assertion stops the test immediately when it fails. A soft assertion (expect.soft) marks the test as failed but lets it continue running. I use this when I want to check multiple things on a page and see all failures in one test run.
ts
test('order confirmation page is complete', async ({ page }) => {
await page.goto('/orders/42/confirmation')
// Перевіряємо всі елементи — не зупиняємося при першій помилціawaitexpect.soft(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible()
awaitexpect.soft(page.getByTestId('order-number')).toHaveText('ORD-042')
awaitexpect.soft(page.getByTestId('total-amount')).toContainText('$')
awaitexpect.soft(page.getByRole('link', { name: 'View all orders' })).toBeVisible()
// Наприкінці — явно перевірити що не було soft-падінь// (опціонально, тест все одно зафейлиться якщо були)expect(test.info().errors).toHaveLength(0)
})
expect.poll — retry any async check
expect.poll runs a function repeatedly until a value assertion passes. Use it when you need to check something that isn't a locator — like an API response, a database value, or a count you computed yourself.
ts
// Polling API поки не повернеться 200awaitexpect.poll(async () => {
const response = await page.request.get('/api/orders/42/status')
return response.status()
}, {
message: 'order export should eventually succeed',
timeout: 15000,
}).toBe(200)
// Polling поки кількість рядків не збіжитьсяawaitexpect.poll(async () => {
returnawait page.getByRole('row').count()
}, { timeout: 5000 }).toBe(11)
Add a message to assertions
Pass a second argument to expect() to label the assertion in reports. When the test fails, the message appears in the error output — much easier to find which assertion failed than reading a locator description.
ts
awaitexpect(
page.getByTestId('status-badge'),
'order should be in shipped state after submit'
).toHaveText('Shipped')
// Якщо впаде — в репорті побачиш:// Error: order should be in shipped state after submit// Expected: "Shipped"// Received: "Pending"