How to fix false positives when using `locator.count()` without assertions?
Playwrightlocator.count() returns a number immediately — it does not assert, does not retry, and does not fail if the count is wrong. Calling count() without wrapping the result in an assertion means the check is silent: even if the count is 0 when you expected 10, the test passes. This is a common pattern that creates tests which appear to run but provide no actual coverage. The fix is to replace bare count() calls with expect(locator).toHaveCount(n), which retries and fails on mismatch.
Common mistake
test('shows 10 search results', async ({ page }) => {
await page.goto('https://app.example.com/search?q=widget');
const results = page.getByRole('listitem');
const count = await results.count();
// This line does nothing useful — logs the count but doesn't fail if it's wrong
console.log('Results found:', count);
// Test passes whether count is 10 or 0
});
Also common: using count() in a conditional instead of asserting:
if (await page.getByRole('alert').count() > 0) {
// Looks like error handling but doesn't fail the test if an alert unexpectedly appears
}
The fix
Replace count-based logic with proper Playwright assertions:
import { test, expect } from '@playwright/test';
test('shows 10 search results', async ({ page }) => {
await page.goto('https://app.example.com/search?q=widget');
// toHaveCount retries until count matches or timeout expires
await expect(page.getByRole('listitem')).toHaveCount(10);
});
test('shows results within expected range', async ({ page }) => {
await page.goto('https://app.example.com/search?q=widget');
// For variable counts, assert a minimum
const results = page.getByRole('listitem');
await expect(results).not.toHaveCount(0); // At least one result
const count = await results.count();
expect(count).toBeGreaterThan(0);
expect(count).toBeLessThanOrEqual(20); // API page size limit
});
For asserting an element is absent (count is zero):
test('no error messages shown', async ({ page }) => {
await page.goto('https://app.example.com/form');
await page.getByRole('button', { name: 'Submit' }).click();
// Prefer toBeHidden or not.toBeVisible for single elements
await expect(page.getByRole('alert')).toHaveCount(0);
// Or more readably:
await expect(page.getByRole('alert')).not.toBeVisible();
});
Why it works
expect(locator).toHaveCount(n) uses Playwright's assertion retry loop, polling the locator every ~100ms until the element count matches n or the timeout expires. If the count doesn't match within the timeout, the test fails with a clear message showing the expected vs actual count. This auto-retry behavior is also why it handles async rendering — if the list renders from an API call, toHaveCount waits for the list to populate rather than catching it empty on first check.
Tips
- Treat locator.count() like a raw DOM query — it's useful for reading a value, but reading a value and not asserting on it is a test smell that produces false confidence.
- When you do need the count as a value (e.g., to iterate), call toHaveCount first to establish the assertion, then call count() to get the value.
- toHaveCount(0) and not.toBeVisible() are often interchangeable for single elements, but toHaveCount is necessary when multiple elements with the same locator should all be absent.
- For tables specifically, toHaveCount on row locators is a clean way to assert pagination — await expect(page.getByRole('row')).toHaveCount(11) (10 data rows + 1 header row).