AI Tools Compared

Use AI to generate Playwright authentication tests using the storageState feature to capture authenticated sessions once, then reuse them across tests. AI assistants generate code that records login state to JSON, loads it in subsequent tests, and eliminates repeated login overhead—reducing test execution time significantly while maintaining proper session management and eliminating inter-test dependencies.

Playwright’s storage state feature solves this problem by capturing authenticated session data once and reusing it across tests. When you combine this capability with AI-generated test code, you get fast, maintainable authentication tests that don’t require repeated login overhead.

Understanding Playwright Storage State

Playwright provides a mechanism to save and load authentication state through the storageState option. Instead of executing login steps in every test, you record the authenticated state once and reuse it. This approach reduces test execution time and eliminates dependencies between tests.

The storage state contains cookies, local storage, and session storage—the complete authentication context needed to restore a logged-in session. Playwright can export this state to a JSON file and load it when creating new browser contexts.

// Recording authentication state
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();

  // Perform login
  await page.goto('https://your-app.com/login');
  await page.fill('[data-testid="email"]', 'test@example.com');
  await page.fill('[data-testid="password"]', 'securepassword');
  await page.click('[data-testid="login-button"]');
  await page.waitForURL('https://your-app.com/dashboard');

  // Save authenticated state
  await context.storageState({ path: 'auth-state.json' });

  await browser.close();
})();

Once you have the auth-state.json file, you can launch new contexts with the authenticated state pre-loaded.

Prompting AI for Authentication Flow Tests

When working with AI coding assistants to generate authentication tests, specificity matters. A vague prompt produces generic code that won’t match your application’s actual flow. Provide the AI with clear context about your authentication mechanism, the page structure, and how you want to use stored state.

A well-structured prompt includes your login URL, the selectors for login form elements, the expected post-login URL, and instructions to use storage state. Here’s a template that works well:

Create a Playwright test file for authentication flows. Use stored state (auth-state.json)
for tests that require a logged-in user. Include these test cases:
1. Verify that a user with valid credentials can log in successfully
2. Verify that logged-in users can access protected routes
3. Verify that the logout action clears the session properly

Login URL: https://your-app.com/login
Protected URL: https://your-app.com/dashboard
Use these selectors: [data-testid="email"], [data-testid="password"], [data-testid="login-button"], [data-testid="logout-button"]

The AI will generate tests that incorporate the storage state option in the context creation, skipping the login steps for authenticated tests.

Generated Test Structure

AI-generated authentication tests with stored state typically follow a consistent pattern. The tests separate concerns between unauthenticated and authenticated scenarios, using the storage state only where appropriate.

const { test, expect } = require('@playwright/test');

test.describe('Authentication Flow', () => {
  // Test without authentication
  test('unauthenticated user redirected to login', async ({ page }) => {
    await page.goto('https://your-app.com/dashboard');
    await expect(page).toHaveURL(/.*login/);
  });

  // Test using stored authentication state
  test('authenticated user can access dashboard', async ({ page }) => {
    await page.goto('https://your-app.com/dashboard');
    await expect(page.locator('[data-testid="user-welcome"]')).toBeVisible();
  });

  // Test logout functionality
  test('user can logout successfully', async ({ page, context }) => {
    // Use storage state for authenticated context
    await context.addInitScript(() => {
      window.localStorage.setItem('authToken', 'test-token');
    });

    await page.goto('https://your-app.com/dashboard');
    await page.click('[data-testid="logout-button"]');
    await expect(page).toHaveURL(/.*login/);
  });
});

The key is ensuring the AI understands when to use storage state versus when to test unauthenticated behavior.

Managing Multiple User Types

Real applications often have different user roles with varying permissions. AI can help generate tests for multiple authenticated states, but you need to specify each role in your prompts. Request separate storage state files for each user type.

Generate tests for a multi-role application:
- Regular user: can view dashboard but cannot access settings
- Admin user: can view dashboard and access settings
Create separate storage state files: regular-user-state.json and admin-user-state.json

The AI will configure Playwright to load the appropriate state file based on the test’s user role.

Best Practices for AI-Generated Authentication Tests

Review the generated code carefully. AI can make mistakes with selectors, timing, or assertions. Verify that the test actually checks what you expect.

Use test fixtures to manage authentication state cleanly. This approach makes it easy to switch between authenticated and unauthenticated states without duplicating code.

// playwright.config.js
module.exports = {
  use: {
    storageState: ({ params }) => {
      if (params.auth === 'admin') return 'admin-state.json';
      if (params.auth === 'user') return 'user-state.json';
      return undefined;
    },
  },
};

Keep your storage state files up to date. If your authentication mechanism changes, regenerate the state files and verify that existing tests still pass.

Automating State Generation

You can also use AI to generate scripts that create storage state files automatically. This is useful in CI/CD pipelines where you need fresh authentication state before running tests.

Write a Node.js script using Playwright that:
1. Logs in with admin credentials
2. Handles any 2FA steps if present
3. Saves the authenticated state to admin-state.json
4. Logs out and repeats for regular user credentials

This automation ensures your test state remains current without manual intervention.

Advanced Storage State Management

For complex applications with multiple environments and user roles:

// playwright.config.js - Advanced configuration
module.exports = {
  use: {
    baseURL: process.env.BASE_URL || 'https://localhost:3000',
    storageState: ({ browserName }) => {
      if (browserName === 'chromium') {
        return 'auth-state/chrome-state.json';
      }
      return 'auth-state/firefox-state.json';
    },
    contextOptions: {
      permissions: ['notifications'],
    },
  },
  projects: [
    {
      name: 'auth-setup',
      testMatch: '**/auth.setup.ts',
    },
    {
      name: 'authenticated-tests',
      dependencies: ['auth-setup'],
      use: { storageState: 'auth-state/admin-state.json' },
    },
    {
      name: 'user-tests',
      dependencies: ['auth-setup'],
      use: { storageState: 'auth-state/user-state.json' },
    },
  ],
};

Authentication Setup Script

AI can generate setup scripts that create multiple authentication states:

// tests/auth.setup.ts
import { test as setup } from '@playwright/test';

setup.describe.configure({ mode: 'parallel' });

setup('authenticate as admin', async ({ page, context }) => {
  // Admin login
  await page.goto('https://your-app.com/login');
  await page.fill('[data-testid="email"]', 'admin@example.com');
  await page.fill('[data-testid="password"]', 'admin-password');
  await page.click('[data-testid="login-button"]');

  // Wait for dashboard and verify admin features
  await page.waitForURL('**/dashboard');
  await page.waitForSelector('[data-testid="admin-panel"]');

  // Save authenticated state
  await context.storageState({ path: 'auth-state/admin-state.json' });
});

setup('authenticate as regular user', async ({ page, context }) => {
  // Regular user login
  await page.goto('https://your-app.com/login');
  await page.fill('[data-testid="email"]', 'user@example.com');
  await page.fill('[data-testid="password"]', 'user-password');
  await page.click('[data-testid="login-button"]');

  // Wait for dashboard (without admin features)
  await page.waitForURL('**/dashboard');

  // Save authenticated state
  await context.storageState({ path: 'auth-state/user-state.json' });
});

setup('authenticate as guest', async ({ page, context }) => {
  // Guest login (temporary access)
  await page.goto('https://your-app.com/login');
  await page.click('[data-testid="guest-login"]');

  // Handle any guest-specific flows
  await page.waitForURL('**/guest-dashboard');

  // Save authenticated state
  await context.storageState({ path: 'auth-state/guest-state.json' });
});

Handling OAuth and Social Login

For applications using OAuth providers, AI can generate appropriate test patterns:

test('OAuth authentication workflow', async ({ browser }) => {
  // Create a new context without existing storage state
  const context = await browser.newContext();
  const page = await context.newPage();

  // Intercept OAuth redirect to mock provider
  await page.goto('https://your-app.com/login');
  await page.click('[data-testid="github-login"]');

  // Handle OAuth popup
  const [popup] = await Promise.all([
    page.waitForEvent('popup'),
    page.click('[data-testid="github-login"]'),
  ]);

  // Mock GitHub login (would use real credentials in integration tests)
  await popup.goto('https://github.com/login?client_id=...');
  await popup.fill('[name="login"]', 'github-user');
  await popup.fill('[name="password"]', process.env.GITHUB_TEST_PASSWORD);
  await popup.click('[name="commit"]');

  // Return to main app
  await page.waitForURL('**/dashboard');

  // Verify OAuth token is stored
  const cookies = await context.cookies();
  const hasAuthToken = cookies.some(c => c.name === 'auth_token');
  expect(hasAuthToken).toBe(true);

  // Save state for future tests
  await context.storageState({ path: 'auth-state/oauth-state.json' });
  await context.close();
});

Testing Session Expiration and Renewal

AI assists in generating tests for auth state lifecycle:

test('session expiration handling', async ({ page, context }) => {
  // Use saved auth state
  const savedState = require('../auth-state/user-state.json');

  // Manually expire tokens by modifying storage
  await page.goto('https://your-app.com');
  await page.addInitScript((stateJSON) => {
    const state = JSON.parse(stateJSON);
    // Expire all tokens
    state.cookies.forEach(c => {
      if (c.name.includes('auth') || c.name.includes('session')) {
        c.expires = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago
      }
    });
    // Apply to storage
    Object.entries(state.origins[0].localStorage || {}).forEach(([key, value]) => {
      localStorage.setItem(key, value);
    });
  }, JSON.stringify(savedState));

  // Navigate to protected route
  await page.goto('https://your-app.com/protected');

  // Should redirect to login
  await expect(page).toHaveURL(/.*login/);
});

test('token refresh flow', async ({ page }) => {
  await page.goto('https://your-app.com/api-test');

  // Intercept API calls to capture refresh token logic
  const responses = [];
  page.on('response', response => {
    if (response.url().includes('/api/auth/refresh')) {
      responses.push(response);
    }
  });

  // Make request that should trigger refresh
  const response = await page.request.get(
    'https://your-app.com/api/protected',
    { headers: { 'Authorization': 'Bearer expired_token' }}
  );

  // Verify refresh was called
  expect(responses.length).toBeGreaterThan(0);
  expect(response.status()).toBe(200);
});

Parallel Test Execution with Different Auth States

test.describe('Parallel tests with different auth', () => {
  test('admin operations', async ({ page, storageState }) => {
    // Uses admin-state.json from config
    await page.goto('https://your-app.com/settings');
    await expect(page.locator('[data-testid="user-management"]')).toBeVisible();
  });

  test('regular user operations', async ({ page, storageState }) => {
    // Uses user-state.json from config
    await page.goto('https://your-app.com/dashboard');
    await expect(page.locator('[data-testid="user-management"]')).not.toBeVisible();
  });

  test('guest operations', async ({ page, storageState }) => {
    // Uses guest-state.json from config
    await page.goto('https://your-app.com');
    await expect(page.locator('[data-testid="upgrade-prompt"]')).toBeVisible();
  });
});

Debugging Storage State Issues

When storage state tests fail, use AI-assisted debugging:

test('debug storage state', async ({ page, context }) => {
  // Log all cookies and storage
  const cookies = await context.cookies();
  const storageData = await page.evaluate(() => ({
    localStorage: { ...localStorage },
    sessionStorage: { ...sessionStorage },
  }));

  console.log('Cookies:', JSON.stringify(cookies, null, 2));
  console.log('Storage:', JSON.stringify(storageData, null, 2));

  // Verify expected auth markers exist
  expect(cookies.some(c => c.name === 'session_id')).toBe(true);
  expect(storageData.localStorage['user_id']).toBeDefined();
});

Built by theluckystrike — More at zovo.one