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
Option 1: Application Default Credentials (Recommended)
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 loginOption 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 maxDelayMsOnly 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:
Pattern 1: Environment-Based Toggle (Recommended)
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 testThe @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 testGitLab CI
variables:
GOOGLE_APPLICATION_CREDENTIALS_JSON: $GCP_SA_KEY
TRIGGER_TYPE: $CI_PIPELINE_SOURCE
test:
script:
- npx playwright testQuerying 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:
- Verify service account has Firestore permissions
- Check
GOOGLE_APPLICATION_CREDENTIALSpath is correct - 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:
skipPullRequestsis true andTRIGGER_TYPEis'pull_request'- Tests missing required annotations
- 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