PRACTICAL GUIDE / playwright test session expiry

Test Session Expiry and Token Refresh with Playwright

Test Playwright session expiry, inactivity logout, token refresh, and rejected sessions with controlled time, network evidence, and secure assertions.

By The Testing AcademyUpdated July 11, 20268 min read
All field guides
In this guide10 sections
  1. Map the Session State Machine
  2. Test Client Inactivity With the Page Clock
  3. Test Refresh Orchestration Deterministically
  4. Prove the Rejection Path
  5. Validate Real Server Expiry Separately
  6. Inspect Network Evidence Without Exposing Tokens
  7. Analyze Failure Patterns
  8. Balance Mocked and Integrated Coverage
  9. Operational Checklist
  10. Action Plan

What you will learn

  • Map the Session State Machine
  • Test Client Inactivity With the Page Clock
  • Test Refresh Orchestration Deterministically
  • Prove the Rejection Path

Session-expiry testing is really three tests: client inactivity behavior, server enforcement of an expired credential, and refresh orchestration between them. Combining all three into one long sleep produces slow, ambiguous failures. Separate the clocks and contracts, then prove each boundary with controlled state and observable network outcomes.

Playwright can advance time inside the page, intercept protected calls, and create authenticated state through APIs. It cannot make an external identity provider's clock move. A reliable strategy uses page time for browser timers, backend time controls or short-lived credentials for server enforcement, and focused network scenarios for refresh logic.

Map the Session State Machine

Write the expected transitions before automating: authenticated, access credential expired, refresh in progress, authenticated with renewed state, and unauthenticated after refresh rejection. Define what concurrent protected requests should do during refresh. Usually one refresh is allowed while other requests wait, and a failed refresh clears state and sends the user to a safe login route.

The Playwright clock guide shows how installed page time can drive inactivity monitoring. Its scope is browser APIs such as Date and timers. Keep that separate from JWT validation, database session expiry, or identity-provider time, all of which happen outside the page process.

Animated field map

Session Expiry and Refresh Test Flow

A known authenticated state crosses an expiry boundary, makes a protected call, and ends in either renewed access or explicit rejection.

  1. 01 / authenticated state

    Authenticated state

    Start with a known account, session lifetime, and protected resource.

  2. 02 / expiry boundary

    Advance expiry boundary

    Move browser or controlled server time according to the contract under test.

  3. 03 / protected request

    Protected request

    Trigger one observable operation that requires the current session.

  4. 04 / refresh or rejection

    Refresh or rejection

    Allow one renewal attempt or return a final unauthorized response.

  5. 05 / session assertion

    Session assertion

    Verify recovered identity, safe logout, retry count, and preserved user intent.

Test Client Inactivity With the Page Clock

Install the clock before navigating so application timers use the controlled implementation from startup. Check both sides of the threshold: the session remains active immediately before the boundary and logs out after crossing it. This catches off-by-one errors that a single after-timeout assertion misses.

TypeScript
import { expect, test } from "@playwright/test";

test("logs out after fifteen minutes without interaction", async ({ page }) => {
  await page.clock.install();
  await page.goto("/account");
  await expect(page.getByTestId("account-menu")).toBeVisible();

  await page.clock.fastForward(14 * 60_000 + 59_000);
  await expect(page.getByTestId("account-menu")).toBeVisible();

  await page.clock.fastForward(2_000);
  await expect(page.getByText("Your session ended due to inactivity")).toBeVisible();
  await expect(page).toHaveURL(/\/login\?reason=inactive$/);
});

Add a companion test showing that a qualifying interaction resets the timer. Use the interaction defined by product policy, not an arbitrary click. Mouse movement, API polling, and background WebSocket traffic should not automatically count as user activity unless the requirement says they do.

Test Refresh Orchestration Deterministically

Network routing is useful for the browser's refresh state machine. Return an unauthorized response for the first protected call, a successful refresh, then a successful retry. Track counts so an implementation that refreshes twice or loops cannot pass only because the final UI eventually appears.

TypeScript
import { expect, test } from "@playwright/test";

test("refreshes once and retries the protected request", async ({ page }) => {
  let accountCalls = 0;
  let refreshCalls = 0;

  await page.route("**/api/account", async (route) => {
    accountCalls += 1;
    if (accountCalls === 1) {
      await route.fulfill({ status: 401, json: { code: "TOKEN_EXPIRED" } });
      return;
    }
    await route.fulfill({ status: 200, json: { name: "Asha", plan: "Team" } });
  });

  await page.route("**/api/auth/refresh", async (route) => {
    refreshCalls += 1;
    await route.fulfill({ status: 204 });
  });

  await page.goto("/account");
  await expect(page.getByRole("heading", { name: "Asha" })).toBeVisible();
  expect(refreshCalls).toBe(1);
  expect(accountCalls).toBe(2);
});

This test proves client orchestration, not that a real refresh token is valid or rotated. Keep that claim narrow. A separate integration test should issue real credentials and validate cookie updates, token rotation, replay rejection, and server-side revocation.

Prove the Rejection Path

The failure path deserves equal attention. Make the protected endpoint return an expiry signal and make refresh return 401 or 403 according to the API contract. Assert that credentials are cleared, the user reaches login once, and the original unsafe operation is not replayed indefinitely.

Preserve intent carefully. A read-only route can carry a safe return path so the user resumes after signing in. A payment or destructive action should not automatically replay after reauthentication. The test should encode that distinction instead of merely checking a generic login heading.

For concurrent requests, delay the mocked refresh response until several protected calls have received the expiry signal. Then assert one refresh call and one retry per safe request. This exposes refresh stampedes that appear only when dashboards load multiple resources at once.

Validate Real Server Expiry Separately

Use a protected test endpoint or identity-provider configuration that can issue an access session with a known short lifetime. Another sound pattern is dependency-injected server time that a test-support API can advance. Both controls must be authenticated, environment-gated, audited, and unavailable in production.

Create the session through an APIRequestContext, export its storage state, and open a browser context from that state. Trigger a protected request before expiry, advance the controlled server clock, then repeat the same request. The server should reject it even if the UI still believes the user is signed in. This catches systems that only hide expired sessions in the browser while accepting the credential at the API.

Do not use a fabricated JWT signed with a copied production secret. Test keys and issuers should be dedicated to the test environment. Signature verification, issuer, audience, expiry, and revocation behavior belong in service-level security tests as well as a small browser integration path.

Inspect Network Evidence Without Exposing Tokens

Useful evidence includes endpoint, method, status, response code, refresh count, and the order of requests. Secret evidence includes cookie values, authorization headers, raw refresh tokens, and full storage-state files. Attach the former and redact the latter.

The Playwright API testing guide explains how API request contexts can prepare state and share it with browser contexts. Use that bridge to create controlled sessions quickly, but remember that the saved artifact is credential material. Restrict trace and report retention for security suites, especially when a failure captures request headers or response bodies.

Analyze Failure Patterns

If the inactivity test never logs out after fast-forwarding, the clock may have been installed after application timers were created. The app may also use a Web Worker, server heartbeat, or another clock source outside the controlled page. Identify the actual enforcement layer before adding more time.

If refresh succeeds but the retry remains unauthorized, inspect cookie domain, path, secure attributes, and whether the application updates in-memory authorization state. If many refresh calls appear, look for a missing single-flight lock. If the user is redirected before refresh finishes, the unauthorized interceptor may race the refresh handler.

An expired storageState file is a poor diagnostic tool by itself. It does not reveal the precise expiry instant, whether refresh was possible, or why the server rejected the session. Replace accidental staleness with a deliberately issued lifetime and explicit transition assertions.

Balance Mocked and Integrated Coverage

Mocked responses make edge cases fast and deterministic. They are ideal for retry counts, navigation, messaging, and concurrent refresh behavior. They cannot prove cookie semantics, token signatures, identity-provider policy, or backend time enforcement. Integrated tests prove those contracts but require controlled accounts, clocks, and secure artifact handling.

Use a layered set: several deterministic browser tests for the state machine, service-level tests for token validation and rotation, and one or two end-to-end tests with real test-environment sessions. The overlap is intentional because each layer catches a different class of failure.

Operational Checklist

  • Draw the authenticated, expired, refreshing, renewed, and rejected states.
  • Distinguish browser time from API and identity-provider time.
  • Install the Playwright clock before application timers start.
  • Assert immediately before and after the inactivity boundary.
  • Count refresh calls and protected-request retries.
  • Test refresh success, rejection, and concurrent-request behavior.
  • Prevent unsafe operations from replaying after reauthentication.
  • Use dedicated test issuers, accounts, and backend time controls.
  • Redact cookies and tokens from traces, logs, and attachments.
  • Confirm the server rejects expired credentials independently of the UI.

Action Plan

Start with the state diagram and one browser inactivity test that crosses a precise boundary. Add deterministic refresh success and rejection scenarios with request counters. Next, create a secured test-environment mechanism for short-lived sessions or controlled server time, and use it for one real enforcement path. Review artifacts for credential leakage before enabling the suite in CI. This sequence produces fast diagnosis while preserving a genuine security check at the server boundary.

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 11, 2026 / Reviewed July 11, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Playwright documentation

    Microsoft

    Canonical API, locator, fixture, browser, and test-runner behavior.

  2. 02
    Playwright best practices

    Microsoft

    Official guidance for resilient tests, isolation, and user-facing locators.

  3. 03
    Web Security Testing Guide

    OWASP Foundation

    Primary web application security testing scenarios and methodology.

FAQ / QUICK ANSWERS

Questions testers ask

Can Playwright fast-forward time to test an inactivity timeout?

Yes. Install the page clock before application code starts, then fast-forward browser time to exercise client-side timers without waiting in real time. This does not advance an identity provider or backend server clock.

Does an expired Playwright storageState file prove session expiry behavior?

No. A stale file may simply redirect to login and provides little control over the exact boundary. Prefer sessions with known lifetimes or controlled backend time so the test can assert behavior just before and after expiry.

What should a token refresh test verify?

Verify that the protected request fails or reaches the refresh boundary, refresh occurs once, the original operation is retried safely, and the UI recovers. Also test refresh rejection and confirm the user is logged out without a retry loop.

Should token values be logged in Playwright traces?

No. Tokens, session cookies, and refresh responses are credentials. Restrict artifact access, sanitize diagnostic attachments, and assert status or claim shape without printing secret values.

How do I test server token expiry without waiting for a long TTL?

Use a protected test environment that can issue a deliberately short-lived session or advance an injected backend clock. Keep that control unavailable in production, and test cryptographic expiry at the service boundary in addition to browser behavior.