Logging In Every Test Is Slow and Flaky
Short answer
Filling email and password in beforeEach burns CI minutes and flakes on MFA, rate limits, and captcha. Prefer API token Arrange or storageState from setup projects—UI login only when the login flow itself is under test. Pair with per-role, per-worker isolation from shared auth pollution.
Part of Common E2E testing gotchas.
Symptom
- Suite runtime dominated by login screens
- Random failures on "Sign in" button or OTP field
- CI hits Auth0/Firebase rate limits after parallel workers multiply logins
- Captcha or bot detection appears only in CI
- Tests pass headed, fail headless on SSO redirect timing
Root cause
Auth is treated as Act in every spec instead of Arrange:
beforeEachnavigates to/loginand types credentials- No
storageState; every worker repeats full OAuth dance - Shared staging user across workers triggers lockout
- Waiting for post-login redirect with
waitForTimeoutinstead of URL or probe
Login UI is slow, external, and order-sensitive—the opposite of what parallel E2E needs.
Fix: session in Arrange, not Act
1. API token / cookie seed (preferred)
const runId = `login-${test.info().workerIndex}-${Date.now()}`;
test('dashboard loads for viewer', async ({ page, request }) => {
const { cookieHeader } = await request.post('/api/test/seed-user', {
data: { runId, role: 'viewer' },
}).then(r => r.json());
await page.context().setExtraHTTPHeaders({ Cookie: cookieHeader });
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
Seed routes create the user and return session material in one round trip—see seed routes and probe Assert.
2. Playwright storageState from setup projects
When UI login is unavoidable (smoke) or you need real IdP cookies:
// playwright.config.ts
projects: [
{ name: 'setup-viewer', testMatch: /auth\.setup\.ts/ },
{
name: 'viewer',
dependencies: ['setup-viewer'],
use: { storageState: '.auth/viewer.json' },
},
],
// auth.setup.ts — run once per role, not per spec
import { test as setup } from '@playwright/test';
setup('authenticate as viewer', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.E2E_VIEWER_EMAIL!);
await page.getByLabel('Password').fill(process.env.E2E_VIEWER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: '.auth/viewer.json' });
});
Official patterns: Playwright authentication. Generate per-role files—never one admin storageState for the whole suite.
3. When UI login belongs in a spec
| Test this | Skip UI login |
|---|---|
| MFA enrollment flow | Dashboard CRUD |
| OAuth provider picker | RBAC on export button |
| "Forgot password" email | Cart checkout |
| Session expiry banner | Probe-gated API actions |
Extract login specs to a small smoke project; everything else uses Arrange.
4. Probe auth, do not trust nav alone
await expect.poll(async () => {
const res = await request.get('/api/test/probe-session');
return (await res.json()).role;
}).toBe('viewer');
UI may render admin chrome from cache while APIs return 403—probes are authoritative for RBAC.
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
Login in every beforeEach | Slow; rate limits; captcha | Seed route or storageState setup |
| One shared staging password in CI | Lockout under parallel workers | Unique user per runId |
waitForTimeout(5000) after Sign in | Race on slow SSO | waitForURL or probe session |
| Skip auth Arrange in "read-only" tests | Session refresh races | Fresh seed per spec anyway |
| UI login for API-only coverage | Wasted minutes | request fixture with bearer token |
TestChimp workflow
/testchimp init scaffolds POST /api/test/seed-user with role and runId so SmartTests stop replaying login forms. /testchimp test on PRs converts legacy beforeEach login blocks to seed Arrange when markdown scenarios list required roles—agents preserve probe Assert while removing redundant UI auth.
Related
- Shared auth state pollution
- Firebase auth E2E
- Auth0 testing
- Seed routes and probe Assert
- Playwright authentication
- Playwright test fixtures
Frequently asked questions
Should every Playwright test log in through the UI?
No—only when the login or SSO flow is what you are testing. For most specs, seed a session via API or load storageState from a setup project. UI login every time is slow and flaky under parallel CI.
storageState vs API seed—which is faster?
API seed is usually fastest—one POST returns cookies or headers without rendering login. storageState is fine when setup projects capture real IdP cookies for smoke specs; regenerate per role, not per spec.
Our tests hit Auth0 rate limits in CI—why?
Parallel workers each running full UI login multiply token requests. Use test tenants, programmatic user creation, seed routes, or setup-project storageState so workers do not share one staging account.
Can I use storageState globally in playwright.config?
Only if every project needs the same role—and parallel workers still race on refresh. Prefer per-role projects or per-runId seed routes; see shared auth pollution gotcha.
Login passes locally but fails headless in CI?
SSO redirects, captcha, and MFA often appear only in CI. Move auth to Arrange via seed or documented setup projects; reserve headed capture for rare smoke runs.
How do fixtures help avoid repeated login?
Custom fixtures can call seed-user once per test and set cookies before page.goto. Playwright default is fresh context per test—use that plus Arrange instead of chaining login across specs.
Does /testchimp init replace login beforeEach blocks?
It scaffolds seed-user and probe-session patterns so agents and SmartTests use API Arrange by default. /testchimp test on PRs refactors specs that still type credentials when scenarios only require a role.
When is UI login still required?
When testing MFA enrollment, OAuth provider selection, password reset email, or session expiry UX. Keep those in a dedicated smoke project; bulk functional specs should not pay the login tax.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.