Authentication Testing Guide
How to Test Firebase Auth with Playwright: Complete 2026 Guide
A scenario-by-scenario walkthrough of testing Firebase Authentication with Playwright. Emulator suite setup, email/password, Google sign-in, phone auth, anonymous auth, reCAPTCHA Enterprise bypass, token management, and the onAuthStateChanged patterns that break real test suites.
“Firebase powers over four billion app installs worldwide, and Firebase Authentication is its most widely adopted product, handling identity for millions of apps across every platform.”
Google I/O 2024
Firebase Auth Email/Password Sign-In Flow
1. Why Testing Firebase Auth Is Harder Than It Looks
Firebase Authentication looks deceptively simple from the API surface. Call signInWithEmailAndPassword(), get a user object, done. But underneath that thin SDK layer sits a system with six structural challenges that make end-to-end testing genuinely difficult. Understanding these challenges before you write your first test will save you hours of debugging.
First, reCAPTCHA Enterprise. Firebase Auth uses reCAPTCHA to protect sign-up and phone auth flows. In production, the reCAPTCHA widget renders an invisible challenge or a full checkbox that your Playwright test cannot solve. The emulator suite disables reCAPTCHA entirely, but if you accidentally point your tests at a real Firebase project, every phone auth and sign-up test will hang waiting for a challenge that never resolves.
Second, the onAuthStateChanged listener is asynchronous and fires at unpredictable times. Your app probably gates its UI on this listener. A test that checks for authenticated content before the listener fires will flake. A test that navigates away before the listener completes will leave stale state.
Third, Firebase supports multiple auth providers (email/password, Google, Facebook, Apple, phone, anonymous) and each has a completely different sign-in mechanism. Google sign-in opens a popup window. Phone auth sends an SMS and waits for a verification code. Anonymous auth creates an ephemeral account with no credentials at all. Your test suite needs provider specific strategies for each.
Fourth, token management. Firebase Auth issues short-lived ID tokens (one hour) and long-lived refresh tokens. Tests that run for more than an hour, or tests that save authentication state between runs, will encounter expired tokens and silent re-authentication attempts. Fifth, the Firebase Auth emulator has a REST API that differs from the production API in subtle ways, particularly around error codes and rate limiting. Sixth, account linking (merging an anonymous account with an email/password account, or linking Google credentials to an existing email account) introduces race conditions that only manifest under test automation speeds.
Firebase Auth Provider Decision Tree
User Action
Clicks sign-in
Provider Check
Email? Google? Phone?
reCAPTCHA
Enterprise verification
Firebase Auth
Verify credentials
Token Issued
idToken + refreshToken
onAuthStateChanged
App receives user
Firebase Auth Emulator Bypass Flow
Start Emulator
firebase emulators:start
Connect SDK
connectAuthEmulator()
No reCAPTCHA
Challenges disabled
REST API
localhost:9099
Instant Delivery
No real SMS or email
Clean State
Clear between runs
2. Setting Up the Firebase Auth Emulator
The Firebase Auth emulator is the foundation of every reliable Firebase Auth test suite. It runs locally, requires no network access, disables reCAPTCHA, provides a REST API for creating and managing test users, and can be cleared between test runs. Without the emulator, you are testing against production infrastructure that will rate limit you, require real phone numbers, and present reCAPTCHA challenges.
Firebase Auth Emulator Setup Checklist
- Install the Firebase CLI globally (npm install -g firebase-tools)
- Run firebase init emulators and enable the Auth emulator
- Set the Auth emulator port in firebase.json (default: 9099)
- Add connectAuthEmulator() to your app initialization code
- Configure environment variables to distinguish test vs production
- Add the emulator UI port (default: 4000) for visual debugging
- Create a global setup script that starts the emulator before tests
- Create a global teardown script that clears emulator state after tests
firebase.json Configuration
Connecting Your App to the Emulator
Your app must call connectAuthEmulator() before any other auth operation. This redirects all Firebase Auth SDK calls to your local emulator instead of production. Gate this behind an environment variable so it only activates during testing.
Environment Variables
Starting the Emulator in CI
Playwright Configuration
Emulator REST API for Test User Management
The Firebase Auth emulator exposes a REST API for managing users programmatically. Use it in your global setup and teardown to create fresh test users and clear all state between runs. This is the most reliable way to ensure test isolation.
3. Scenario: Email/Password Sign-In Happy Path
The email/password sign-in is your smoke test. If this breaks, nothing else matters. The flow is: your app renders a sign-in form, the user enters credentials, the Firebase SDK callssignInWithEmailAndPassword(), Firebase returns an ID token and refresh token, theonAuthStateChanged listener fires with the authenticated user, and your app renders the authenticated UI. The critical testing challenge is waiting for the onAuthStateChanged callback to complete before asserting the UI state.
Email/Password Sign-In Happy Path
StraightforwardGoal
Starting from the sign-in page, complete a full email/password authentication flow against the Firebase Auth emulator, land on the authenticated dashboard, and confirm the session is valid.
Preconditions
- Firebase Auth emulator running on port 9099
- Test user created via the emulator REST API
- App connected to the emulator via
connectAuthEmulator()
Playwright Implementation
Email/Password Sign-In: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import { createTestUser, clearAllUsers } from '../helpers/firebase-emulator';
test.beforeEach(async () => {
await clearAllUsers();
await createTestUser('e2e-test@example.com', 'SuperSecret123!');
});
test('email/password sign-in: happy path', async ({ page }) => {
await page.goto('/signin');
await page.getByLabel('Email').fill('e2e-test@example.com');
await page.getByLabel('Password').fill('SuperSecret123!');
await page.getByRole('button', { name: /sign in/i }).click();
await page.waitForURL('/dashboard', { timeout: 10_000 });
await expect(page.getByRole('heading', { name: /dashboard/i }))
.toBeVisible();
await expect(page.getByText('e2e-test@example.com'))
.toBeVisible();
const idToken = await page.evaluate(() =>
Object.keys(localStorage).find(k =>
k.startsWith('firebase:authUser:')
)
);
expect(idToken).toBeTruthy();
});4. Scenario: Email/Password Sign-Up with Verification
Sign-up is more complex than sign-in because it involves email verification. In production, Firebase sends a verification email with a link the user must click. In the emulator, Firebase does not send real emails, but it does generate verification codes that you can retrieve through the emulator REST API. This lets you test the full verification flow without an actual email inbox.
The emulator also skips reCAPTCHA verification during sign-up, which is critical. In production, createUserWithEmailAndPassword() triggers a reCAPTCHA Enterprise check that would block your Playwright test completely. The emulator makes this invisible.
Email/Password Sign-Up with Verification
ModeratePlaywright Implementation
What to Assert Beyond the UI
- The emulator REST API shows the user with
emailVerified: true - The
oobCodesendpoint no longer contains a pending verification for this email - The ID token claims include
email_verified: true - A second sign-up attempt with the same email returns a “user already exists” error
5. Scenario: Google Sign-In via Popup
Google sign-in is Firebase Auth's most popular social provider. In production, signInWithPopup(auth, googleProvider)opens a popup window to Google's OAuth consent screen. Testing this popup flow with Playwright is possible but fragile, because Google may present CAPTCHAs, enforce device trust, or require two-factor authentication. The reliable strategy uses the Firebase Auth emulator, which lets you create users with Google provider data through its REST API, bypassing the popup entirely.
There are two practical approaches. The first approach tests that the popup mechanism works correctly without actually completing the Google OAuth flow. The second approach uses the emulator REST API to simulate a user who already signed in with Google, so you can test the downstream behavior of your app without touching Google's servers.
Google Sign-In via Popup
ComplexPlaywright Implementation: Popup Interception
REST API Approach for CI
For CI environments where popup windows are unreliable, use the emulator REST API to create a user with Google provider data already attached. Then sign in with email/password (which the emulator allows for any user) to test the downstream experience of a Google-authenticated user.
6. Scenario: Phone Number Authentication
Phone authentication is the hardest Firebase Auth flow to test because it involves three moving parts in production: a reCAPTCHA verification, an SMS message sent to a real phone number, and a time-limited verification code the user must enter. The emulator eliminates all three obstacles. It skips reCAPTCHA, does not send real SMS messages, and generates verification codes you can retrieve via the REST API.
The Firebase Auth emulator also supports a set of “magic” phone numbers for testing. You can configure specific phone numbers to always receive a specific verification code, which makes your tests deterministic. Without the emulator, phone auth is practically untestable in CI.
Phone Number Authentication
ComplexPlaywright Implementation
Phone Auth: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import { clearAllUsers } from '../helpers/firebase-emulator';
const EMULATOR = '127.0.0.1:9099';
const PROJECT = 'demo-test-project';
test('phone auth: send code and verify', async ({ page }) => {
await page.goto('/signin');
await page.getByRole('button', { name: /sign in with phone/i }).click();
await page.getByLabel('Phone number').fill('+15555550100');
await page.getByRole('button', { name: /send code/i }).click();
await expect(page.getByLabel(/verification code/i)).toBeVisible();
const verRes = await fetch(
`http://${EMULATOR}/emulator/v1/projects/${PROJECT}/verificationCodes`
);
const verData = await verRes.json();
const codeEntry = verData.verificationCodes.find(
(c: { phoneNumber: string }) => c.phoneNumber === '+15555550100'
);
await page.getByLabel(/verification code/i).fill(codeEntry.code);
await page.getByRole('button', { name: /verify/i }).click();
await page.waitForURL('/dashboard', { timeout: 10_000 });
await expect(page.getByText('+15555550100')).toBeVisible();
});7. Scenario: Anonymous Auth and Account Upgrade
Anonymous authentication is a pattern where Firebase creates a temporary user account with no credentials. This is commonly used for shopping carts, content previews, or onboarding flows where you want to track user activity before requiring sign-up. The testing challenge is the account upgrade flow: converting an anonymous user to a permanent account by linking email/password or Google credentials, while preserving the anonymous user's data.
The critical edge case is a failed upgrade. If the email the anonymous user tries to link is already taken by another account, Firebase throws an auth/credential-already-in-use error. Your app must handle this gracefully, typically by offering to merge accounts or sign in with the existing account instead.
Anonymous Auth and Account Upgrade
ModeratePlaywright Implementation
8. Scenario: Token Persistence and Session Management
Firebase Auth stores tokens in the browser's IndexedDB (or localStorage as a fallback) by default. This means a user who signs in once stays signed in across page reloads and even browser restarts. For testing, this creates both an opportunity and a challenge. The opportunity is that you can save authentication state once and reuse it across many tests, dramatically speeding up your suite. The challenge is that stale tokens from a previous test run can contaminate the next one.
Playwright's storageStatefeature pairs perfectly with Firebase's token persistence. After authenticating once in a setup step, save the browser's storage state to a JSON file. Subsequent tests load that file to start with a pre-authenticated session, skipping the sign-in flow entirely.
Token Persistence and Session Reuse
ModeratePlaywright Implementation
Now any test in the app-tests project starts with the saved session and skips the sign-in flow. This reduces per-test execution time from 3 to 5 seconds (for the sign-in flow) to near zero. It also avoids hitting the emulator with redundant authentication requests.
Session Reuse: Playwright vs Assrt
// test/global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
import { createTestUser, clearAllUsers } from './helpers/firebase-emulator';
export default async function globalSetup(config: FullConfig) {
await clearAllUsers();
await createTestUser('e2e-test@example.com', 'SuperSecret123!');
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(`${config.projects[0].use.baseURL}/signin`);
await page.getByLabel('Email').fill('e2e-test@example.com');
await page.getByLabel('Password').fill('SuperSecret123!');
await page.getByRole('button', { name: /sign in/i }).click();
await page.waitForURL('/dashboard', { timeout: 15_000 });
await context.storageState({ path: './test/.auth/session.json' });
await browser.close();
}9. Common Pitfalls That Break Firebase Auth Test Suites
These pitfalls are sourced from real Firebase Auth testing failures reported in GitHub issues, Stack Overflow threads, and the Firebase community forums. Each one has caused hours of debugging for teams running Playwright tests against Firebase Auth.
Firebase Auth Testing Anti-Patterns
- Testing against production Firebase instead of the emulator (reCAPTCHA blocks all automated sign-ups)
- Forgetting to call connectAuthEmulator() before any auth operation (SDK silently uses production)
- Not clearing emulator state between test runs (user collisions cause auth/email-already-in-use errors)
- Asserting UI state before onAuthStateChanged fires (race condition, test passes locally but flakes in CI)
- Using page.waitForTimeout() instead of waiting for a real signal like URL change or element visibility
- Hardcoding the emulator port without checking firebase.json (port conflicts when running multiple projects)
- Not handling the Firebase Auth persistence layer (IndexedDB/localStorage) when clearing state between tests
- Testing phone auth without the emulator (requires real phone number and SMS delivery)
The onAuthStateChanged Race Condition
This is the single most common source of flaky Firebase Auth tests. The onAuthStateChanged listener fires asynchronously after the Firebase SDK initializes and checks for a persisted session. If your app gates its router or UI rendering on this listener, and your test checks for authenticated content before the listener fires, the test will intermittently fail. The fix is to wait for a concrete UI signal (like a URL change or the appearance of a user-specific element) rather than using arbitrary timeouts.
A related issue: on cold starts, the Firebase SDK re-validates the persisted token against the Auth service (or emulator). This network round-trip adds 100 to 500 milliseconds of latency before onAuthStateChangedfires. In CI environments with slow network or DNS resolution, this can extend to several seconds. Always use Playwright'swaitForURL ortoBeVisible with a generous timeout rather than assuming instant state changes.
Emulator State Leakage
The Firebase Auth emulator persists users in memory across requests but not across restarts. If your CI pipeline starts the emulator once for the entire suite, users created in one test file will still exist when the next test file runs. This causes auth/email-already-in-useerrors that only appear when tests run in a specific order. The fix is to call the emulator'sDELETE /emulator/v1/projects/{projectId}/accounts endpoint in test.beforeEach or in a Playwright global setup script.
Production reCAPTCHA Blocking
If your environment variable configuration is wrong and your tests accidentally connect to a real Firebase project instead of the emulator, every operation that triggers reCAPTCHA (sign-up, phone auth, password reset) will hang indefinitely. The test will time out after 30 seconds with no useful error message. Add a guard at the top of your test suite that verifies the emulator is reachable before running any tests.
10. Writing These Scenarios in Plain English with Assrt
Every scenario above involves 25 to 60 lines of Playwright TypeScript, plus helper functions for emulator management. Multiply that by the eight scenarios you need for complete coverage and you have a substantial test suite that breaks the moment Firebase updates its SDK, your app renames a button, or the emulator changes its REST API shape. Assrt lets you describe each scenario in plain English, generates the Playwright code, and automatically updates selectors when the underlying UI changes.
The phone authentication scenario from Section 6 demonstrates this clearly. In raw Playwright, you need to know the emulator REST endpoint for verification codes, parse the JSON response, find the matching phone number, extract the code, and fill it into the correct input. In Assrt, you describe the intent and the framework handles the emulator integration.
Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When Firebase updates its SDK or your app changes its sign-in form layout, Assrt detects the failure, analyzes the new DOM, and opens a pull request with updated selectors. Your scenario files remain unchanged.
Start with the email/password happy path. Once it is green in your CI, add phone auth, then anonymous upgrade, then Google sign-in, then the session persistence pattern. In a single afternoon you can have complete Firebase Auth coverage that most production applications never achieve by hand.
Related Guides
How to Test Auth0 Universal Login
A practical, scenario-by-scenario guide to testing Auth0 Universal Login with Playwright....
How to Test Azure AD Login
A practical, scenario-by-scenario guide to testing Azure AD (Entra ID) login with...
How to Test Clerk Sign-In
A practical, scenario-by-scenario guide to testing Clerk authentication with Playwright....
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.