OAuth guides

Playwright OAuth testing: end-to-end login without flaking

Automate OAuth sign-in with Playwright using a mock IdP. Your app stays on localhost; consent runs on the issuer with stable selectors.

8 min read

Playwright proves what users experience: click "Sign in," complete consent, land on a dashboard. Playwright OAuth testing stays maintainable when the authorization server is a predictable mock (HTTPS issuer, dummy users you control) and you separate one-time login setup from specs that only need an authenticated session.

What to cover in E2E tests

  • Starting login from your app triggers a redirect to the expected authorize URL (issuer host, client_id, scope).
  • After consent on the mock provider, the browser returns to your localhost callback and shows authenticated UI.
  • Protected routes fail or redirect when the session is cleared.
  • Optional: claim-based UI when your app reads email or custom claims from the ID token.

You do not need Playwright to hit Google in CI. Use a mock OAuth provider with a stable consent screen and dummy users.

Common Playwright patterns

storageState (fastest for most suites)

Run a dedicated auth.setup.ts project that performs login once and saves storageState to disk. Other projects load that file in use.storageState.

// auth.setup.ts — run before dependent projects
import { test as setup } from '@playwright/test'

setup('authenticate', async ({ page }) => {
  await page.goto('http://127.0.0.1:3000/login')
  await page.getByRole('button', { name: 'Sign in' }).click()
  await page.getByRole('button', { name: 'Approve' }).click()
  await page.waitForURL('**/api/auth/callback**')
  await page.waitForURL('**/dashboard')
  await page.context().storageState({ path: '.auth/user.json' })
})

Full redirect flow every time

Slower but catches regressions in callback routes and cookie flags. Use for a small smoke project, not hundreds of specs.

Authorize and consent in the browser

Your app runs locally; the mock IdP serves authorize and consent over HTTPS.

  1. Click login in your app (localhost).
  2. Assert the URL host matches your configured issuer (e.g. dummyoauth.com) and path includes /oauth/authorize.
  3. Select a dummy user if offered, then approve.
  4. Wait for your loopback callback URL and an element only visible when logged in.

Keep PKCE and state enabled. See PKCE, state, and redirect URIs.

Speed and parallelism

  • One shared auth setup per worker shard, not per test file.
  • Run OAuth smoke tests in one project; use storageState elsewhere.
  • Reuse the same project and dummy users across the team for consistent selectors.

Reducing flakes

  • Use waitForURL on your callback path instead of fixed waitForTimeout.
  • Pin OAUTH_ISSUER and redirect URI in CI env; do not rely on stale tunnel URLs.
  • Ensure runners have HTTPS egress to the mock provider.
  • Isolate cookies per test with Playwright contexts when testing logout.

Pipeline setup: OAuth testing in CI.