Default assertion timeout is 5 seconds. That is fine for most elements. But what about a report that takes 15 seconds to generate? A file that takes 10 seconds to upload? A search that hits a slow API? You need custom timeouts. And when assertions fail in CI, you need custom messages that tell you exactly what went wrong.
import { test, expect } from '@playwright/test';
test('slow operations need longer timeouts', async ({ page }) => {
await page.goto('/banking/reports');
// Default 5s timeout -- fine for quick elements
await expect(page.getByRole('heading', { name: 'Reports' })).toBeVisible();
// Generate a report -- takes up to 15 seconds
await page.getByRole('button', { name: 'Generate Report' }).click();
await expect(page.getByText('Report ready')).toBeVisible({ timeout: 15000 });
// File upload -- might take 10 seconds
await expect(page.getByText('Upload complete')).toBeVisible({ timeout: 10000 });
// Search with slow backend
await page.getByPlaceholder('Search...').fill('annual report');
await expect(page.getByTestId('search-results')).toHaveCount(5, {
timeout: 8000,
});
});import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
// Change default assertion timeout for ALL tests
timeout: 10000, // 10 seconds instead of 5
},
});Default assertion errors say "expected element to be visible." Helpful, but not great in CI logs when you have 200 tests. Custom messages tell you exactly what failed and why.
import { test, expect } from '@playwright/test';
test('custom error messages for clarity', async ({ page }) => {
await page.goto('/banking/dashboard');
// Custom message as second argument to expect()
await expect(
page.getByTestId('balance'),
'Account balance should be visible after login'
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Transfer' }),
'Transfer button should be enabled for active accounts'
).toBeEnabled();
await expect(
page.getByRole('row'),
'Transaction table should show exactly 10 recent transactions'
).toHaveCount(10);
await expect(
page.getByTestId('status'),
'Account status should show Active for verified users'
).toHaveText('Active');
});// Without custom message:
Error: expect(locator).toHaveCount(10)
Expected: 10
Received: 0
Locator: getByRole('row')
// With custom message:
Error: Transaction table should show exactly 10 recent transactions
Expected: 10
Received: 0
Locator: getByRole('row')See the difference? The second error tells you exactly what is wrong without reading the test code. In CI with 200 tests, this saves 10 minutes of debugging per failure.
import { test, expect } from '@playwright/test';
test('report generation with clear errors', async ({ page }) => {
await page.goto('/banking/reports');
await page.getByRole('button', { name: 'Generate' }).click();
// Custom message + custom timeout
await expect(
page.getByTestId('report-download-link'),
'Download link should appear after report generation completes'
).toBeVisible({ timeout: 20000 });
});Write custom messages that explain the business context, not the technical check. Not "balance div should be visible" but "Account balance should be visible after login." The message should make sense to a product manager reading the CI report.
Do not set timeout to 60000 everywhere to "avoid flaky tests." If an element takes 60 seconds to appear, your application has a performance bug. Fix the app, do not mask it with long timeouts. 10-15 seconds is a reasonable upper limit for most operations.
Q: How do you handle assertions for slow-loading elements in Playwright?
A: Playwright assertions auto-retry for 5 seconds by default. For operations that genuinely take longer -- report generation, file uploads, complex searches -- I pass a custom timeout: expect(locator).toBeVisible({ timeout: 15000 }). I also add custom error messages so CI failures are immediately understandable: expect(locator, "Report download link should appear within 15s").toBeVisible({ timeout: 15000 }). For project-wide changes, I adjust the global expect.timeout in playwright.config.ts. But I am careful not to set huge timeouts everywhere -- that usually masks performance bugs in the application.
Key Point: Pass { timeout: ms } for slow operations. Pass a custom message string as the second argument to expect() for better CI debugging. Do not mask performance bugs with long timeouts.