Skip to main content
Ask AI

Unified Testing (Browser + API)

Unified testing combines browser-based E2E testing with API testing in a single test scenario. TestRelic captures both navigation timelines and API call details together, providing a complete view of your application's behavior.

What does unified testing track?

Unified tests use both the page and request fixtures to enable tracking of:

  • Browser Navigation — Page loads, clicks, form submissions
  • API Calls — HTTP requests made within tests
  • Hybrid Flows — API setup followed by UI verification
  • Complete Timeline — Single report with both navigation and API events
Best of Both Worlds

Unified testing is ideal for scenarios where you need to verify that API data is correctly displayed in the UI, or when you need to set up data via API before testing UI interactions.

How do I set up unified testing?

How do I set up unified testing using an AI assistant?

Copy one of the prompts below and paste it into your AI coding assistant.


Full setup (one prompt, does everything)

AI Prompt — Unified browser + API full setup
Set up @testrelic/playwright-analytics for unified browser + API testing in this project:

1. Run: npm install @testrelic/playwright-analytics
2. Install the browser: npx playwright install chromium
3. Open playwright.config.ts and add the TestRelic reporter to the reporter array:
['@testrelic/playwright-analytics', {
outputPath: './test-results/analytics-timeline.json',
includeStackTrace: true,
includeCodeSnippets: true,
includeNetworkStats: true,
}]
4. In test files that use both page and request, import from
'@testrelic/playwright-analytics/fixture' instead of '@playwright/test':

import { test, expect } from '@testrelic/playwright-analytics/fixture';

Then use both { page, request } in the same test function.

Keep the existing 'list' reporter if it is already there. Do not change any test logic.

Step-by-step prompts (one at a time)

Step 1 — Install the packages:

AI Prompt — Step 1: Install
Run the following in my project terminal:
npm install @testrelic/playwright-analytics
npx playwright install chromium

Step 2 — Configure the reporter:

AI Prompt — Step 2: Configure reporter
Open playwright.config.ts and add @testrelic/playwright-analytics to the reporter array.
Use this configuration:

['@testrelic/playwright-analytics', {
outputPath: './test-results/analytics-timeline.json',
includeStackTrace: true,
includeCodeSnippets: true,
includeNetworkStats: true,
}]

Keep any existing reporters (like 'list'). Show me the updated file.

Step 3 — Write a unified test:

AI Prompt — Step 3: Write unified test
Create a unified test that uses both page and request in the same test:

import { test, expect } from '@testrelic/playwright-analytics/fixture';

test('API data matches UI', async ({ page, request }) => {
const apiResponse = await request.get('https://jsonplaceholder.typicode.com/users/1');
const user = await apiResponse.json();

await page.goto('https://example.com/users/1');
await expect(page.locator('.user-name')).toHaveText(user.name);
});

Step 4 — Run the tests:

AI Prompt — Step 4: Run tests
Run my Playwright tests with:
npx playwright test

Then tell me where the analytics report was written.

How do I write unified tests?

What does a basic unified test look like?

tests/unified/basic.spec.ts
import { test, expect } from '@testrelic/playwright-analytics/fixture';

test('API data matches UI', { tag: ['@unified', '@e2e', '@api'] }, async ({ page, request }) => {
// Step 1: Fetch data from API
const apiResponse = await request.get('https://api.example.com/user/1');
expect(apiResponse.status()).toBe(200);
const userData = await apiResponse.json();

// Step 2: Navigate to UI
await page.goto('https://example.com/profile/1');

// Step 3: Verify UI matches API data
await expect(page.locator('.user-name')).toHaveText(userData.name);
await expect(page.locator('.user-email')).toHaveText(userData.email);
});
Fixture Usage

Unlike API-only tests that require testRelicApiFixture, unified tests use the standard TestRelic fixture which provides both page and request with full tracking capabilities.

What are the common unified testing patterns?

How do I use API to set up data, then verify in the UI?

tests/unified/api-setup-ui-verify.spec.ts
import { test, expect } from '@testrelic/playwright-analytics/fixture';

test('create post via API and verify in UI', { tag: ['@unified'] }, async ({ page, request }) => {
// Step 1: Create post via API
const newPost = {
title: 'Test Post from API',
body: 'This post was created via API and verified in UI',
userId: 1,
};

const createResponse = await request.post('https://jsonplaceholder.typicode.com/posts', {
data: newPost,
});

expect(createResponse.status()).toBe(201);
const createdPost = await createResponse.json();

// Step 2: Navigate to posts page
await page.goto('https://jsonplaceholder.typicode.com/posts');

// Step 3: Verify post appears (in a real scenario)
// Note: JSONPlaceholder doesn't persist data, this is for demonstration
console.log('Created post ID:', createdPost.id);
});

How do I perform a UI action and then verify it via API?

tests/unified/ui-interaction-api-verify.spec.ts
import { test, expect } from '@testrelic/playwright-analytics/fixture';

test('form submission triggers correct API call', { tag: ['@unified'] }, async ({ page, request }) => {
// Navigate to form page
await page.goto('https://example.com/create-post');

// Fill form
await page.fill('input[name="title"]', 'Test Post');
await page.fill('textarea[name="body"]', 'Test content');
await page.click('button[type="submit"]');

// Wait for navigation
await page.waitForURL('**/posts/*');

// Verify via API
const url = page.url();
const postId = url.split('/').pop();

const apiResponse = await request.get(`https://api.example.com/posts/${postId}`);
expect(apiResponse.status()).toBe(200);

const post = await apiResponse.json();
expect(post.title).toBe('Test Post');
expect(post.body).toBe('Test content');
});

How do I cross-validate data between API and UI?

tests/unified/cross-validation.spec.ts
import { test, expect } from '@testrelic/playwright-analytics/fixture';

test('user data consistency across UI and API', { tag: ['@unified'] }, async ({ page, request }) => {
const userId = 1;

// Step 1: Fetch user data from API
const apiResponse = await request.get(`https://api.example.com/users/${userId}`);
expect(apiResponse.status()).toBe(200);
const apiUserData = await apiResponse.json();

// Step 2: Navigate to user profile in UI
await page.goto(`https://example.com/users/${userId}`);
await page.waitForLoadState('networkidle');

// Step 3: Extract user data from UI
const uiName = await page.locator('.user-name').textContent();
const uiEmail = await page.locator('.user-email').textContent();
const uiPhone = await page.locator('.user-phone').textContent();

// Step 4: Cross-validate
expect(uiName).toBe(apiUserData.name);
expect(uiEmail).toBe(apiUserData.email);
expect(uiPhone).toBe(apiUserData.phone);
});

How do I combine API and UI operations sequentially?

tests/unified/sequential-operations.spec.ts
import { test, expect } from '@testrelic/playwright-analytics/fixture';

test('complete workflow: API creation → UI search → API update', { tag: ['@unified'] }, async ({ page, request }) => {
// Step 1: Create item via API
const newItem = {
name: 'Test Product',
price: 99.99,
category: 'Electronics',
};

const createResponse = await request.post('https://api.example.com/products', {
data: newItem,
});

expect(createResponse.status()).toBe(201);
const product = await createResponse.json();

// Step 2: Search for item in UI
await page.goto('https://example.com/products');
await page.fill('input[name="search"]', newItem.name);
await page.click('button[type="submit"]');

// Step 3: Verify item appears in search results
const productCard = page.locator('.product-card', { hasText: newItem.name });
await expect(productCard).toBeVisible();

// Step 4: Update item via API
const updateResponse = await request.put(`https://api.example.com/products/${product.id}`, {
data: {
...product,
price: 89.99,
},
});

expect(updateResponse.status()).toBe(200);

// Step 5: Refresh UI and verify update
await page.reload();
await expect(page.locator('.product-card .price')).toContainText('89.99');

// Step 6: Clean up via API
await request.delete(`https://api.example.com/products/${product.id}`);
});

What do real-world unified examples look like?

How do I combine Wikipedia with JSONPlaceholder API?

examples/unified-testing/tests/wikipedia-api.spec.ts
import { test, expect } from '@testrelic/playwright-analytics/fixture';

test.describe('Wikipedia + API Integration', () => {
test('search Wikipedia and validate data structure', { tag: ['@unified'] }, async ({ page, request }) => {
// Step 1: Fetch random post from API to use as search term
const apiResponse = await request.get('https://jsonplaceholder.typicode.com/posts/1');
expect(apiResponse.status()).toBe(200);
const post = await apiResponse.json();

// Step 2: Extract a keyword from post title
const searchTerm = 'JavaScript'; // In real scenario, extract from post.title

// Step 3: Search Wikipedia
await page.goto('https://en.wikipedia.org');
await page.fill('#searchInput', searchTerm);
await page.click('button[type="submit"]');

// Step 4: Verify article loaded
await expect(page).toHaveURL(/wiki/);
await expect(page.locator('#firstHeading')).toBeVisible();

// Step 5: Validate API response structure
expect(post).toHaveProperty('id');
expect(post).toHaveProperty('title');
expect(post).toHaveProperty('body');
});
});

How do I test an e-commerce flow with API validation?

examples/unified-testing/tests/ecommerce-api.spec.ts
import { test, expect } from '@testrelic/playwright-analytics/fixture';

test.describe('E-commerce + API', () => {
test('product search matches API data', { tag: ['@unified'] }, async ({ page, request }) => {
// Step 1: Get product data from API
const apiResponse = await request.get('https://api.example.com/products/laptop');
expect(apiResponse.status()).toBe(200);
const apiProducts = await apiResponse.json();

// Step 2: Search for products in UI
await page.goto('https://example.com');
await page.fill('input[name="search"]', 'laptop');
await page.click('button[type="submit"]');

// Step 3: Wait for results
await page.waitForSelector('.product-item');

// Step 4: Compare counts
const uiProductCount = await page.locator('.product-item').count();
expect(uiProductCount).toBe(apiProducts.length);

// Step 5: Validate first product details
const firstProductPrice = await page.locator('.product-item:first-child .price').textContent();
expect(firstProductPrice).toContain(apiProducts[0].price.toString());
});
});

How do I test a complete authentication flow?

tests/unified/authentication.spec.ts
import { test, expect } from '@testrelic/playwright-analytics/fixture';

test.describe('Authentication Flow', () => {
test('login via API and verify UI session', { tag: ['@unified', '@auth'] }, async ({ page, request }) => {
// Step 1: Login via API
const loginResponse = await request.post('https://api.example.com/auth/login', {
data: {
email: 'test@example.com',
password: 'password123',
},
});

expect(loginResponse.status()).toBe(200);
const authData = await loginResponse.json();
const token = authData.token;

// Step 2: Set authentication in browser context
await page.goto('https://example.com');
await page.evaluate((token) => {
localStorage.setItem('authToken', token);
}, token);

// Step 3: Navigate to protected page
await page.goto('https://example.com/dashboard');

// Step 4: Verify authenticated state
await expect(page.locator('.user-menu')).toBeVisible();
await expect(page.locator('.logout-button')).toBeVisible();

// Step 5: Verify API access with token
const profileResponse = await request.get('https://api.example.com/profile', {
headers: {
'Authorization': `Bearer ${token}`,
},
});

expect(profileResponse.status()).toBe(200);
});
});

How do I measure performance in unified tests?

tests/unified/performance.spec.ts
import { test, expect } from '@testrelic/playwright-analytics/fixture';

test('measure API and page load performance', { tag: ['@unified', '@performance'] }, async ({ page, request }) => {
// Measure API response time
const apiStart = Date.now();
const apiResponse = await request.get('https://api.example.com/products');
const apiDuration = Date.now() - apiStart;

expect(apiResponse.status()).toBe(200);
console.log(`API response time: ${apiDuration}ms`);

// Measure page load time
const pageStart = Date.now();
await page.goto('https://example.com/products');
await page.waitForLoadState('networkidle');
const pageDuration = Date.now() - pageStart;

console.log(`Page load time: ${pageDuration}ms`);

// Assert performance thresholds
expect(apiDuration).toBeLessThan(2000); // API should respond in < 2s
expect(pageDuration).toBeLessThan(5000); // Page should load in < 5s
});

What are the unified testing configuration options?

Use the standard TestRelic reporter options. See the full configuration reference for all available options.

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
reporter: [
['list'],
['@testrelic/playwright-analytics', {
outputPath: './test-results/analytics-timeline.json',
includeStackTrace: true,
includeCodeSnippets: true,
includeNetworkStats: true,
redactPatterns: [
/api_key=[^&\s]+/gi,
/password=[^&\s]+/gi,
],
}],
],
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
});

How do I run unified tests?

How do I run all unified tests?

npx playwright test --grep @unified

How do I run the unified example project?

cd examples/unified-testing
npm install
npx playwright install chromium
npx playwright test

How do I run with specific tags?

npx playwright test --grep "@unified.*@auth"

How do I read the unified report?

Unified reports contain both E2E and API data:

cat test-results/unified-analytics.json | jq .

Report includes:

  • Navigation timeline with page loads
  • API call details with request/response
  • Combined timing information
  • Cross-referenced events
  • Complete test flow visualization
Report Visualization

See the Unified Report Documentation for detailed information on analyzing combined E2E and API data.

What are the best practices for unified tests?

Should I use API or UI for test setup?

Use API for setup — it is faster than driving the browser:

// Good: Fast API setup, thorough UI verification
const user = await createUserViaApi(request);
await verifyUserInUI(page, user);

// Avoid: Slow UI-only setup
await createUserViaUI(page);

How do I validate data consistency between API and UI?

// Always cross-validate API and UI data
const apiData = await fetchFromApi(request);
const uiData = await extractFromUI(page);
expect(uiData).toEqual(apiData);

How do I set up authentication properly in unified tests?

// Set up auth via API, then use in UI
const token = await loginViaApi(request);
await page.evaluate((token) => {
localStorage.setItem('authToken', token);
}, token);

How do I ensure test data is always cleaned up?

// Always clean up via API
const testData = await createTestData(request);
try {
await testUIWithData(page, testData);
} finally {
await deleteTestData(request, testData.id);
}

How do I organize unified tests with tags?

test('hybrid test', { tag: ['@unified', '@e2e', '@api', '@critical'] }, async ({ page, request }) => {
// Test code
});

How do I troubleshoot unified test issues?

Why are API calls from the browser failing with CORS errors?

Use the request fixture instead of page.fetch() — the request fixture bypasses CORS:

// Use request fixture instead of page.fetch()
const response = await request.get('https://api.example.com/data');
// request fixture bypasses CORS

How do I maintain authentication state across page navigations?

Set cookies in the browser context:

// Set cookies in context
await page.context().addCookies([{
name: 'auth_token',
value: token,
domain: 'example.com',
path: '/',
}]);

Why does my UI not reflect the API changes immediately?

Add waits after async operations before checking the UI:

// Wait for API call to complete before UI check
await request.post('https://api.example.com/action');
await page.waitForTimeout(1000); // Give server time to process
await page.reload();

Where do I go next?

Additional Resources

Was this doc helpful?
Last updated on by Srivishnu Ayyagari