playwright expect(page).toHaveURL() timed out after navigation only in CI - how to fix
Fixes Playwright toHaveURL timeouts that happen only in CI after navigation. Use when expect(page).toHaveURL() times out in CI but passes locally. Covers checking what the page was doing, ensuring the webServer is ready before tests, and waiting for the load state before asserting. Not for wrong expected URLs.
TL;DR
toHaveURL timed out because the navigation itself is slower in CI: a cold browser, a slow webServer boot, or a baseURL pointing at a service that is not ready yet. The assertion races the navigation and loses. Make the webServer's readiness a precondition of the job, wait for the navigation's load state explicitly, and raise the assertion timeout only after the page is provably loading.
The query
playwright expect(page).toHaveURL() timed out after navigation only in CI - how to fixUse this when
- expect(page).toHaveURL() times out only in CI
- navigations are visibly slower in CI than locally
- the app uses a webServer or service containers in CI
Not for
- the expected URL is wrong in every environment (the app navigates somewhere unexpected; fix the app or the expectation)
- the navigation is never attempted (the click or goto that should trigger it failed first)
- the URL is correct but the assertion is checked before a client-side redirect finishes (wait for the redirect explicitly)
Steps
1. Check what the page was doing when the assertion timed out
The trace or video shows whether the page was loading slowly, stuck blank, or already there. Each points at a different fix.
npx playwright show-trace test-results/trace.zipExpected output: you see the page state at timeout: slow load, blank page, or wrong page.
2. Ensure the webServer is ready before tests start
A server still booting when tests begin makes every navigation slow. Playwright's webServer config with a reuse guard, or a CI service healthcheck, makes readiness explicit.
// playwright.config.ts
export default defineConfig({
webServer: {
command: 'npm run dev',
url: 'https://example.com/health',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});Expected output: tests only start after the server responds to the health check.
3. Wait for the load state, then assert the URL
Separate the navigation wait from the URL assertion so the timeout message tells you which one failed.
await page.goto('/login');
await page.waitForLoadState('domcontentloaded');
await expect(page).toHaveURL(/.*\/dashboard/, { timeout: 15000 });Expected output: the navigation completes before the assertion runs, and the assertion has its own documented timeout.
4. Verify against the real CI service topology
If baseURL points at a service that is slow to become reachable in CI, a service container healthcheck or a readiness probe in the workflow, the navigation will keep racing.
services:
web:
image: myapp:test
options: >-
--health-cmd "curl -f https://example.com/health"
--health-interval 10sExpected output: the service is reachable before the first navigation, and the timeout is gone.
Tool compatibility
- Playwright 1.40 - 1.55
- any browser Playwright supports
Variant phrasings
- toHaveURL() timed out after navigation in CI
- playwright expect toHaveURL timeout CI only
- waiting for page to have URL timed out in CI
Why it happens
toHaveURL polls the page URL until it matches or the timeout expires. In CI everything upstream is slower: the browser starts cold, the dev server compiles on first request, service containers boot sequentially. The assertion's default timeout was tuned for a warm local environment, so it expires while the navigation is still legitimately in flight.
Edge cases / pitfalls
- SPA client-side routing changes the URL without a document navigation; waitForURL with the right waitUntil matches the app's behavior better
- a trailing slash or query-param difference between baseURL and the app's canonical URL fails the match; compare exactly what the browser shows
- raising the global expect timeout masks slow navigations suite-wide; prefer per-assertion timeouts with comments
- redirect chains (http to https, trailing-slash normalization) each cost round trips in CI; assert on the final URL, not an intermediate one
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_8bPWwqhCVtk-ajIvx4HwFA