</>

Technology

Playwright

Difficulty

Advanced

Interview Question

How would you handle a login flow that requires OTP or 2FA verification in Playwright?

Answer

Scenario: Testing Login with OTP / 2FA

Approach 1 — Mock the OTP API (Most Common for E2E)

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

test('login with OTP via API mock', async ({ page }) => {
  // Intercept the OTP generation endpoint and return a fixed code
  await page.route('/api/auth/send-otp', route =>
    route.fulfill({
      status: 200,
      json: { message: 'OTP sent', expiresIn: 300 },
    })
  );

  // Intercept OTP verification
  await page.route('/api/auth/verify-otp', async route => {
    const body = JSON.parse(route.request().postData()!);
    if (body.otp === '123456') {
      await route.fulfill({ status: 200, json: { token: 'test-jwt-token' } });
    } else {
      await route.fulfill({ status: 400, json: { error: 'Invalid OTP' } });
    }
  });

  // Perform login
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@test.com');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Sign in' }).click();

  // OTP screen appears
  await expect(page.getByText('Enter the 6-digit code')).toBeVisible();

  // Enter the mocked OTP
  await page.getByLabel('OTP Code').fill('123456');
  await page.getByRole('button', { name: 'Verify' }).click();

  await expect(page).toHaveURL('/dashboard');
});

Approach 2 — Read OTP from Test Database (Integration Test)

TypeScript
test('login with real OTP from database', async ({ page, request }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@test.com');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Wait for OTP to be generated in database
  await page.waitForURL('**/verify-otp');

  // Fetch the OTP from test API (only available in test/staging env)
  const otpResponse = await request.get('/test-utils/latest-otp?email=user@test.com');
  const { otp } = await otpResponse.json();

  await page.getByLabel('OTP Code').fill(otp);
  await page.getByRole('button', { name: 'Verify' }).click();

  await expect(page).toHaveURL('/dashboard');
});

Approach 3 — TOTP (Time-Based OTP like Google Authenticator)

Bash
npm install otpauth
TypeScript
import { test, expect } from '@playwright/test';
import * as OTPAuth from 'otpauth';

test('login with TOTP', async ({ page }) => {
  // Create TOTP generator matching the test account's secret
  const totp = new OTPAuth.TOTP({
    secret: process.env.TEST_TOTP_SECRET!, // stored in CI secrets
    algorithm: 'SHA1',
    digits: 6,
    period: 30,
  });

  await page.goto('/login');
  await page.getByLabel('Email').fill('user@test.com');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await page.waitForURL('**/2fa');

  // Generate the current TOTP code
  const code = totp.generate();
  await page.getByLabel('Authenticator Code').fill(code);
  await page.getByRole('button', { name: 'Verify' }).click();

  await expect(page).toHaveURL('/dashboard');
});

Approach 4 — Bypass 2FA Entirely (Preferred for Unit/Regression Tests)

TypeScript
// Use storageState with a pre-authenticated session that has 2FA already satisfied
setup('create 2fa session', async ({ page }) => {
  // Programmatically create a session token that has 2FA verified
  // (via backend API endpoint only available in test environment)
  const res = await fetch('/test-utils/create-session', {
    method: 'POST',
    body: JSON.stringify({ email: 'user@test.com', skip2fa: true }),
  });
  const { sessionCookie } = await res.json();

  const context = page.context();
  await context.addCookies([{ name: 'session', value: sessionCookie, domain: 'localhost', path: '/' }]);
  await page.context().storageState({ path: '.auth/user-2fa.json' });
});

Follow AutomateQA

Related Topics