Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Playwright›Working with authentication and sessions

Advanced Playwright Techniques

Working with authentication and sessions

Authentication in automation involves maintaining valid user states across multiple test scenarios to avoid redundant login actions. By persisting session storage, we significantly reduce execution time and prevent potential lockouts from excessive authentication attempts. This approach is essential for scaling complex end-to-end suites where shared state or user-specific privileges are required.

The Challenge of Stateless Testing

In a default browser environment, every test begins with a clean slate, meaning cookies, local storage, and session data are cleared. While this isolation is ideal for ensuring tests do not influence one another, it creates a massive performance bottleneck when every test file must manually navigate to a login page, input credentials, and wait for authentication middleware to process the request. Because authentication is often the most time-consuming part of a workflow, repeating it thousands of times per deployment cycle is highly inefficient. Furthermore, relying on programmatic login via the UI can make tests brittle if the login form design changes frequently. By understanding that browser state is simply a collection of local browser data, we can move away from UI-driven login and instead inject pre-authenticated state directly into the browser context to initiate sessions in milliseconds.

// This baseline approach demonstrates the problem: repeating login
// logic in every test slows down the suite significantly.
test('user can view dashboard', async ({ page }) => {
  await page.goto('/login');
  await page.fill('#username', 'test_user');
  await page.fill('#password', 'secure_password');
  await page.click('#login-button');
  // Wait for authentication middleware to redirect to dashboard
  await expect(page).toHaveURL('/dashboard');
});

Storing Authentication State

To optimize authentication, we leverage Playwright's ability to save the storage state of a browser context to a JSON file. This file acts as a snapshot of the authenticated session, capturing cookies and local storage items required by the server to identify the user. By performing the login action once and saving the state, we decouple the authentication process from the functional test execution. This works because the browser does not care how the session was established; it only verifies that the current context contains the required session identifier. Once this storage state is written to disk, it can be reused across any number of test files, effectively 'warming up' the browser context before the test logic even begins. This mechanism ensures that each test starts as a logged-in user, which is a major efficiency win for enterprise-scale automation suites.

// Perform login once and export the storage state to a file.
const { test, expect } = require('@playwright/test');

test('generate storage state', async ({ page }) => {
  await page.goto('/login');
  await page.fill('#username', 'admin');
  await page.fill('#password', 'secret');
  await page.click('#login-button');
  
  // Save the state to a file to be used by other tests.
  await page.context().storageState({ path: 'state.json' });
});

Global Setup for Shared Sessions

Instead of manually running a login test, we can use the 'global setup' configuration to automate the authentication process before any actual tests execute. By defining a global setup file, we ensure that a valid authentication state is always available. This configuration is declared in the main setup file, allowing the engine to orchestrate the lifecycle of the browser sessions. The logic behind this is that the global setup runs in its own context, creates the necessary credentials, writes the 'state.json' file, and then tears down, leaving the authenticated file ready for the test workers. This centralizes authentication logic, making it easier to manage credentials and update login flows without touching dozens of individual test files. It is the most robust method for handling common auth requirements where every test in the suite shares the same base authentication context.

// playwright.config.js snippet for automatic global setup.
module.exports = {
  globalSetup: require.resolve('./global-setup'),
  use: {
    // Inject the previously saved state into all test contexts.
    storageState: 'state.json',
  },
};
// Inside global-setup.js, perform the login and save the JSON file.

Handling Multi-User Scenarios

Often, complex applications require testing scenarios where multiple user roles (e.g., admin, editor, guest) interact simultaneously or within the same suite. Relying on a single global state is insufficient here. Instead, we generate separate storage files for each role. By defining these distinct states, we can selectively inject them into specific test groups. This modular approach allows for testing permission-based features effectively. We might, for example, have an 'admin-state.json' and an 'editor-state.json'. Because the browser context is isolated per test, we can assign a specific state to a test block, ensuring that we maintain absolute control over the identity of the user running that task. This logic is fundamental for testing access control lists and ensuring that unauthorized users correctly encounter restricted content blocks during execution.

// Applying a specific state to a test group for role-based access.
test.describe('Admin actions', () => {
  test.use({ storageState: 'admin-state.json' });

  test('delete resource', async ({ page }) => {
    await page.goto('/settings');
    await page.click('#delete-btn');
  });
});

Security and State Lifecycle Management

Managing the lifecycle of stored authentication states requires caution to prevent the usage of expired or stale credentials. Since 'state.json' files contain sensitive session data, they should never be committed to version control systems. We use environment variables to populate credentials during the build process, ensuring that the local state generation remains dynamic. Furthermore, we implement logic to invalidate or refresh the storage state if the server-side session expires. By checking the validity of the session within the global setup, we can force a re-login if the stored state is older than the server's session timeout. This proactive management prevents 'flaky' tests that fail simply because a session token became invalid during a long test run, thereby maintaining the stability of the entire automated pipeline against real-world server limitations.

// Refreshing state if the session is detected as expired.
const fs = require('fs');

async function ensureFreshState(page) {
  const stateExists = fs.existsSync('state.json');
  if (!stateExists) {
    // Logic to trigger login flow if state file is missing.
    await performLogin(page);
  }
}

Key points

  • Authentication is the most time-consuming phase of end-to-end testing.
  • Persisting session cookies and storage state saves significant execution time.
  • The storageState method captures the browser context for later reuse.
  • Global setup allows for centralized authentication management before tests start.
  • Role-based testing requires generating and maintaining distinct session files.
  • Sensitive storage files should always be excluded from source control systems.
  • Session expiration must be managed to avoid false negatives in long suites.
  • Using pre-authenticated sessions isolates tests from UI changes on login forms.

Common mistakes

  • Mistake: Manually logging in for every test. Why it's wrong: This is slow, increases server load, and makes tests brittle. Fix: Use storage state to save the authentication session and reuse it.
  • Mistake: Relying on localStorage but forgetting sessionStorage. Why it's wrong: Modern applications often distribute authentication tokens across different storage mechanisms. Fix: Use browserContext.storageState() which captures both automatically.
  • Mistake: Over-relying on cookies for session persistence. Why it's wrong: Some apps use IndexedDB or specific headers that standard cookie sharing might miss. Fix: Ensure the entire browser context state is saved rather than just cookies.
  • Mistake: Hardcoding credentials in test files. Why it's wrong: This exposes sensitive data to version control and makes environment switching difficult. Fix: Use environment variables or global setup to handle credentials securely.
  • Mistake: Not handling session expiration during parallel execution. Why it's wrong: Reusing a stale storage state file leads to non-deterministic failures. Fix: Implement a robust setup script that refreshes authentication if the session is expired.

Interview questions

What is the simplest way to handle authentication in Playwright if you need to log in for every test?

The simplest approach is to perform the login steps programmatically within a 'beforeEach' hook using Playwright's page objects. You navigate to the login URL, fill in the credentials, and click submit. This ensures every test starts from a clean, authenticated state. The code would look like: 'await page.goto('/login'); await page.fill('#user', 'test'); await page.fill('#pass', 'secret'); await page.click('#submit');'. While reliable, this approach is slow because it repeats the login process for every single test case.

How does Playwright's 'storageState' feature improve testing performance during authentication?

Playwright's 'storageState' allows you to save the authenticated browser cookies and local storage to a file after a successful login. Instead of logging in repeatedly, subsequent tests can load this state file into the browser context. You use it in your configuration file or setup script like this: 'context = await browser.newContext({ storageState: 'state.json' });'. This dramatically improves test speed because the browser starts already authenticated, bypassing the entire login flow for every individual test case execution.

Can you explain the difference between a global setup approach and using a separate test file for authentication?

A global setup runs once before all tests, while a separate test file approach treats authentication as a prerequisite test. Global setup is defined in the Playwright config, executing a specific script to generate a storage state that all test workers consume. A separate test file allows for more granular control and dependency management, but it can be harder to orchestrate across parallel workers. Global setup is generally preferred for large suites because it centralizes authentication, keeping the main test files clean and focused solely on functional validation.

How would you handle a scenario where authentication requires a dynamic multi-factor authentication (MFA) code?

Handling MFA in Playwright usually requires using a library to generate time-based one-time passwords (TOTP) inside your setup script. You store a secret key in your environment variables, generate the token using the current timestamp, and input it into the MFA field. The code looks like: 'const token = totp.generate(process.env.MFA_SECRET); await page.fill('#mfa-code', token);'. This allows automated tests to bypass manual MFA challenges by programmatically simulating the correct security code entry during the authentication phase.

Compare using 'storageState' versus 'API-based authentication' for populating session data.

StorageState involves interacting with the UI to generate a file, whereas API-based authentication performs a POST request to your backend's login endpoint to receive an authentication token. API-based authentication is significantly faster and more stable because it avoids UI flakiness. For example: 'const response = await request.post('/api/login', { data: { ... } });'. You then inject the resulting cookies or tokens directly into the browser context. While API auth is superior for speed, storageState is better if you need to capture complex browser-side state that the API doesn't expose.

How do you manage session persistence if your application uses custom HTTP-only cookies that are difficult to capture via standard methods?

If cookies are strictly HTTP-only and managed by the backend, I leverage Playwright’s 'request' context to perform an authenticated handshake. I log in using the API, extract the required cookies from the response object, and then use 'context.addCookies()' to manually inject them into the browser context. This approach is highly resilient because it treats authentication as a server-side transaction rather than relying on brittle UI interactions. By explicitly controlling the cookie injection, I ensure that even strictly guarded session headers are correctly passed to the browser for all subsequent navigation events during the test execution.

All Playwright interview questions →

Check yourself

1. When using storageState to persist authentication, what is the primary benefit over performing a full login sequence?

  • A.It prevents the browser from loading images, speeding up the test.
  • B.It allows tests to start directly at a logged-in state without re-executing login steps.
  • C.It automatically validates the security of the login form against SQL injection.
  • D.It forces the application to use a development-only bypass mode.
Show answer

B. It allows tests to start directly at a logged-in state without re-executing login steps.
Storage state snapshots the browser's cookies and local/session storage, letting subsequent tests start already authenticated. Option 0 is incorrect as it's not a performance feature of storage; option 2 is not the purpose of Playwright; option 3 is incorrect as this is a functional testing tool, not a security auditor.

2. Why should you use 'globalSetup' for authentication instead of putting login steps in 'beforeEach' for every test?

  • A.It increases the likelihood of test failures to ensure the system is robust.
  • B.It allows you to test the login UI separately from the functional features.
  • C.It saves time by performing the login action only once per test run or worker.
  • D.It ensures that cookies are cleared after every single test file is completed.
Show answer

C. It saves time by performing the login action only once per test run or worker.
Global setup executes once, and its artifacts can be reused by multiple tests, drastically improving efficiency. Option 0 is illogical; option 1 is a side effect but not the purpose; option 3 is false as you usually want to keep cookies for the duration of the test suite.

3. If your application uses different storage keys for auth tokens, which Playwright feature ensures a complete capture of the session?

  • A.browserContext.storageState()
  • B.page.waitForLoadState()
  • C.request.post()
  • D.browser.newContext({ ignoreHTTPSErrors: true })
Show answer

A. browserContext.storageState()
storageState captures all state within the context including cookies, local storage, and session storage. Option 1 is for page loading; option 2 is for API calls; option 3 relates to certificate handling.

4. When writing a test that relies on a pre-saved storage state, how does Playwright know which file to use?

  • A.The file is automatically detected by searching for cookies.json in the project root.
  • B.You define the 'storageState' property in the Playwright configuration file.
  • C.You must manually import the JSON file in every single 'test' block.
  • D.Playwright uses the last modified file in the 'test-results' folder.
Show answer

B. You define the 'storageState' property in the Playwright configuration file.
The Playwright config allows global setting of storageState for all tests. Option 0 is false; option 2 is inefficient and not standard practice; option 3 is irrelevant to configuration management.

5. What is the consequence of using a stale storage state file in a CI environment?

  • A.The test will automatically fail with a 401 Unauthorized error from the application.
  • B.The browser will automatically refresh the login page without any code intervention.
  • C.The test will proceed to the dashboard without checking for valid credentials.
  • D.Playwright will throw a syntax error in the configuration file.
Show answer

A. The test will automatically fail with a 401 Unauthorized error from the application.
A stale file means expired tokens; the application will reject requests, causing a 401. Option 1 is incorrect as Playwright doesn't know how to handle login redirects unless programmed; option 2 is false; option 3 is false as it's a runtime issue, not a syntax one.

Take the full Playwright quiz →

← PreviousVisual regression testing with PlaywrightNext →Parallel test execution and sharding

Playwright

37 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app