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' });
});
