Playwright tests pass on wrong page — weak URL checks

Playwright

Weak URL assertions allow tests to pass on the wrong page, producing false positives that hide navigation bugs. expect(page.url()).toContain('/settings') will pass on /account/settings, /billing/settings, /admin/settings, and any other route that contains the substring — including paths you never intended. Tests that only loosely verify the current URL provide false confidence about navigation behavior.

Common mistake

test('navigates to security settings', async ({ page }) => {
  await page.goto('https://app.example.com');

  await page.getByRole('link', { name: 'Security' }).click();

  // Too loose — passes on /settings/password, /settings/security, /settings/security?tab=2fa
  expect(page.url()).toContain('/settings');
});

Also weak: using .toContain() for URLs where query parameters or hash fragments are significant:

// Passes even if tab=general instead of tab=security
expect(page.url()).toContain('/account/settings');

The fix

Use expect(page).toHaveURL() with a regex that anchors the meaningful parts of the URL:

import { test, expect } from '@playwright/test';

test('navigates to security settings', async ({ page }) => {
  await page.goto('https://app.example.com/account');

  await page.getByRole('link', { name: 'Security' }).click();

  // Anchored regex — requires exact path segment
  await expect(page).toHaveURL(/\/account\/settings\/security$/);
});

test('opens billing with correct tab', async ({ page }) => {
  await page.goto('https://app.example.com/settings');
  await page.getByRole('tab', { name: 'Billing' }).click();

  // Assert both path and query parameter
  await expect(page).toHaveURL(/\/settings\?tab=billing/);
});

test('redirects to dashboard after login', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('secret');
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Exact path match — no substring ambiguity
  await expect(page).toHaveURL('https://app.example.com/dashboard');
});

For cases where the path is known but the origin varies by environment:

// Use waitForURL with a relative pattern when baseURL is configured
await expect(page).toHaveURL(/\/dashboard$/);

Why it works

expect(page).toHaveURL() retries with Playwright's assertion loop — it waits for the URL to match the pattern rather than checking the current URL snapshot at call time. This handles asynchronous redirects gracefully. Using a regex with start (^) and end ($) anchors, or embedding the exact expected path segment, prevents partial substring matches from hiding navigation to the wrong route. The auto-retry behavior is also useful after click actions that trigger a navigation: the assertion waits for the navigation to complete rather than failing on the URL before the redirect fires.

Tips

  • expect(page).toHaveURL() accepts a string for exact match, a regex for pattern matching, and a URL object for structured URL matching — use the form that matches the precision the test needs.
  • For navigation tests, assert both the URL and a key heading or element that confirms the correct page rendered — URL alone can pass if the router lands on a route but the page component crashes.
  • Use page.waitForURL('**/expected-path') when you need to wait for navigation without making an assertion — useful in setup code before the actual test assertions begin.
  • Loose URL checks in smoke tests are particularly dangerous because they're intended to catch regressions, but they'll pass through broken routing silently.