Skip to Content

FirestoreAdapter

Writes test results to Google Cloud Firestore.

Installation

import { FirestoreAdapter } from '@lytics/playwright-adapters/firestore';

This adapter requires @google-cloud/firestore as a peer dependency. It’s included when you install @lytics/playwright-adapters.

Overview

The FirestoreAdapter stores test results in Firestore, enabling:

  • Dashboards — Query results for visualization
  • Trend Analysis — Track test stability over time
  • Coverage Reports — Aggregate data across test runs
  • Real-time Updates — Firestore’s real-time capabilities

Configuration

interface FirestoreAdapterConfig { /** GCP project ID (required) */ projectId: string; /** Service account credentials (JSON string or object) */ credentials?: string | Record<string, unknown>; /** Collection names (required - configure your own schema) */ collections: { /** Collection for test run summaries */ testRuns: string; /** Collection for individual test executions */ testCases: string; /** Collection for latest test case status */ latestTestCases: string; }; /** Optional conditions to skip writes */ skipConditions?: { /** Skip writes for pull requests (default: false) */ skipPullRequests?: boolean; }; /** Retry configuration */ retry?: { /** Maximum number of retries (default: 3) */ maxRetries?: number; /** Initial delay in milliseconds (default: 1000) */ initialDelayMs?: number; /** Maximum delay in milliseconds (default: 10000) */ maxDelayMs?: number; }; }

Usage

Basic Usage

Create a reporter file:

// reporter.ts import { CoreReporter } from '@lytics/playwright-reporter'; import { FirestoreAdapter } from '@lytics/playwright-adapters/firestore'; class CustomReporter extends CoreReporter { constructor() { super({ adapters: [ new FirestoreAdapter({ projectId: 'my-gcp-project', collections: { testRuns: 'e2e_test_runs', testCases: 'e2e_test_cases', latestTestCases: 'e2e_latest_test_cases', }, }), ], }); } } export default CustomReporter;

Reference it in your config:

// playwright.config.ts export default { reporter: [['list'], ['./reporter.ts']], };

With Service Account

new FirestoreAdapter({ projectId: 'my-gcp-project', credentials: process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON, collections: { testRuns: 'e2e_test_runs', testCases: 'e2e_test_cases', latestTestCases: 'e2e_latest_test_cases', }, })

With Skip Conditions

new FirestoreAdapter({ projectId: 'my-gcp-project', collections: { testRuns: 'e2e_test_runs', testCases: 'e2e_test_cases', latestTestCases: 'e2e_latest_test_cases', }, skipConditions: { skipPullRequests: true, // Don't write PR results to Firestore }, })

With Custom Retry

new FirestoreAdapter({ projectId: 'my-gcp-project', collections: { testRuns: 'e2e_test_runs', testCases: 'e2e_test_cases', latestTestCases: 'e2e_latest_test_cases', }, retry: { maxRetries: 5, initialDelayMs: 500, maxDelayMs: 30000, }, })

Authentication

When credentials is not provided, the adapter uses Application Default Credentials (ADC):

# Option A: Set environment variable to service account key file export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json # Option B: Use gcloud CLI (for local development) gcloud auth application-default login

Option 2: Service Account JSON

Provide credentials directly as JSON string or object:

// From environment variable (JSON string) new FirestoreAdapter({ projectId: 'my-gcp-project', credentials: process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON, collections: { /* ... */ }, }) // As object new FirestoreAdapter({ projectId: 'my-gcp-project', credentials: { type: 'service_account', project_id: 'my-gcp-project', private_key_id: '...', private_key: '-----BEGIN PRIVATE KEY-----\n...', client_email: '...', // ... rest of service account JSON }, collections: { /* ... */ }, })

Collections Schema

The adapter writes to three collections (you define the names):

testRuns Collection

Stores test run summaries:

{ runId: "github-run-12345", timestamp: Timestamp, overallStatus: "passed" | "failed" | ..., totalTests: 50, totalExecutions: 52, passed: 48, failed: 2, skipped: 0, durationMs: 120000, passRate: 0.96, averageTestDuration: 2400, slowestTestDuration: 15000, flakyTests: 2, environment: { branch: "main", commit: "abc123", // ... custom environment data } }

testCases Collection

Stores individual test executions:

{ testCaseId: "ACCOUNT_SECURITY_VIEW-TOKENS_VALID", journeyId: "ACCOUNT_SECURITY_VIEW-TOKENS", runId: "github-run-12345", title: "user can view access tokens", status: "passed", projectName: "chromium", durationMs: 2341, timestamp: Timestamp, buildId: "github-run-12345", annotations: { testSuiteName: "ACCOUNT_SECURITY", journeyId: "ACCOUNT_SECURITY_VIEW-TOKENS", testCaseId: "ACCOUNT_SECURITY_VIEW-TOKENS_VALID" }, // error field present if status is "failed" }

latestTestCases Collection

Stores the latest status for each test case (updated on each run):

{ // Document ID = testCaseId testCaseId: "ACCOUNT_SECURITY_VIEW-TOKENS_VALID", journeyId: "ACCOUNT_SECURITY_VIEW-TOKENS", lastStatus: "passed", lastRunId: "github-run-12345", lastTimestamp: Timestamp, lastDurationMs: 2341, }

This collection is useful for:

  • Quick status dashboards
  • Identifying consistently failing tests
  • Coverage reports

Features

Automatic Retry

The adapter automatically retries failed operations with exponential backoff:

Attempt 1: immediate Attempt 2: wait 1000ms Attempt 3: wait 2000ms Attempt 4: wait 4000ms ...up to maxDelayMs

Only transient errors are retried:

  • Network issues
  • Rate limits
  • Temporary unavailability

Non-retryable errors fail immediately:

  • Authentication failures
  • Invalid data
  • Permission denied

Skip Pull Requests

Avoid cluttering your database with PR test runs:

new FirestoreAdapter({ projectId: 'my-gcp-project', collections: { /* ... */ }, skipConditions: { skipPullRequests: true, }, })

When TRIGGER_TYPE environment variable is 'pull_request', no data is written.

Non-Blocking Errors

Write errors are logged but don’t throw, allowing other adapters to continue.

Local Development

By default, you may want to disable Firestore writes for local runs to avoid polluting production data. Here are several patterns for handling this:

Create a custom reporter that conditionally enables Firestore:

// reporter.ts import { CoreReporter } from '@lytics/playwright-reporter'; import { FirestoreAdapter } from '@lytics/playwright-adapters/firestore'; import { FilesystemAdapter } from '@lytics/playwright-adapters/filesystem'; import type { ResultAdapter } from '@lytics/playwright-reporter'; import { readFileSync } from 'fs'; const { GCP_PROJECT_ID, GOOGLE_APPLICATION_CREDENTIALS, TEST_ENV } = process.env; const isProduction = TEST_ENV === 'production'; // Build adapters list const adapters: ResultAdapter[] = [ // Always write to filesystem for local debugging new FilesystemAdapter({ outputDir: './test-results' }), ]; // Only add Firestore in production with valid credentials if (isProduction && GCP_PROJECT_ID && GOOGLE_APPLICATION_CREDENTIALS) { try { // Handle both file path and JSON string const credentials = GOOGLE_APPLICATION_CREDENTIALS.startsWith('{') ? GOOGLE_APPLICATION_CREDENTIALS : readFileSync(GOOGLE_APPLICATION_CREDENTIALS, 'utf8'); adapters.push(new FirestoreAdapter({ projectId: GCP_PROJECT_ID, credentials, collections: { testRuns: 'test_runs', testCases: 'test_cases', latestTestCases: 'latest_test_cases', }, skipConditions: { skipPullRequests: true, }, })); console.log('✅ Firestore adapter configured'); } catch (error) { console.warn('⚠️ Failed to configure Firestore:', error); } } else { console.log('ℹ️ Firestore disabled (not production or missing credentials)'); } export default class CustomReporter extends CoreReporter { constructor() { super({ adapters }); } }

Then in playwright.config.ts:

export default defineConfig({ reporter: [ ['list'], ['./reporter.ts'], ], });

Local run: npx playwright test → writes to filesystem only

Production run: TEST_ENV=production npx playwright test → writes to both

Pattern 2: Separate Dev Collections

Write local runs to separate collections so they don’t affect production data:

const env = process.env.TEST_ENV || 'dev'; new FirestoreAdapter({ projectId: 'my-gcp-project', credentials: process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON, collections: { testRuns: `${env}_test_runs`, // dev_test_runs or production_test_runs testCases: `${env}_test_cases`, latestTestCases: `${env}_latest_test_cases`, }, })

Local run: npx playwright test → writes to dev_* collections

Production run: TEST_ENV=production npx playwright test → writes to production_* collections

Pattern 3: Firestore Emulator

For testing Firestore integration without a real GCP project:

# Install and start the emulator gcloud components install cloud-firestore-emulator gcloud emulators firestore start --host-port=localhost:8080
# In another terminal, set the emulator host export FIRESTORE_EMULATOR_HOST="localhost:8080" npx playwright test

The @google-cloud/firestore client automatically uses the emulator when FIRESTORE_EMULATOR_HOST is set.

Pattern 4: Explicit Enable Flag

Add an explicit flag to enable Firestore:

const shouldWriteToFirestore = process.env.FIRESTORE_ENABLED === 'true' && process.env.GCP_PROJECT_ID && process.env.GOOGLE_APPLICATION_CREDENTIALS; if (shouldWriteToFirestore) { adapters.push(new FirestoreAdapter({ /* ... */ })); }

Local run with Firestore: FIRESTORE_ENABLED=true npx playwright test

Recommendation: Use Pattern 1 (environment-based toggle) for most teams. It’s explicit, safe by default, and matches how CI/CD pipelines typically work.

CI/CD Integration

GitHub Actions

env: GOOGLE_APPLICATION_CREDENTIALS_JSON: ${{ secrets.GCP_SA_KEY }} TRIGGER_TYPE: ${{ github.event_name }} steps: - name: Run tests run: npx playwright test

GitLab CI

variables: GOOGLE_APPLICATION_CREDENTIALS_JSON: $GCP_SA_KEY TRIGGER_TYPE: $CI_PIPELINE_SOURCE test: script: - npx playwright test

Querying Data

Get Latest Test Status

const snapshot = await db.collection('e2e_latest_test_cases') .where('lastStatus', '==', 'failed') .get(); snapshot.docs.forEach(doc => { console.log(`Failing: ${doc.id}`); });

Get Test History

const snapshot = await db.collection('e2e_test_cases') .where('testCaseId', '==', 'ACCOUNT_SECURITY_VIEW-TOKENS_VALID') .orderBy('timestamp', 'desc') .limit(10) .get();

Get Run Summary

const snapshot = await db.collection('e2e_test_runs') .orderBy('timestamp', 'desc') .limit(1) .get(); const latestRun = snapshot.docs[0].data(); console.log(`Pass rate: ${latestRun.passRate * 100}%`);

Troubleshooting

Authentication errors

Cause: Invalid or missing credentials.

Solutions:

  1. Verify service account has Firestore permissions
  2. Check GOOGLE_APPLICATION_CREDENTIALS path is correct
  3. Verify JSON credentials are valid

Permission denied

Cause: Service account lacks Firestore permissions.

Solution: Grant the service account these roles:

  • roles/datastore.user (read/write)
  • Or roles/datastore.owner (full access)

Rate limiting

Cause: Too many writes in short period.

Solution: The adapter handles this with automatic retry. For very high volume, consider batching or increasing retry limits.

Data not appearing

Possible causes:

  1. skipPullRequests is true and TRIGGER_TYPE is 'pull_request'
  2. Tests missing required annotations
  3. Write errors (check console logs)

Security: Store service account credentials as secrets in your CI/CD system. Never commit them to your repository.

Best Practices

1. Use Separate Collections per Environment

const env = process.env.TEST_ENV || 'dev'; new FirestoreAdapter({ projectId: 'my-gcp-project', collections: { testRuns: `${env}_test_runs`, testCases: `${env}_test_cases`, latestTestCases: `${env}_latest_test_cases`, }, })

2. Set Up Firestore Indexes

For efficient queries, create composite indexes:

Collection: e2e_test_cases Fields: testCaseId (Ascending), timestamp (Descending) Collection: e2e_test_runs Fields: environment.branch (Ascending), timestamp (Descending)

3. Configure TTL for Old Data

Use Firestore TTL policies to automatically delete old test results:

// In Firestore console or via API, set TTL on timestamp field // to automatically delete documents older than 90 days
Last updated on