Skip to main content

Writing Tests

Learn best practices for writing effective analytics tests with DLest.

Test Structure

DLest uses a Jest-like API that should feel familiar:

const { test, expect } = require('dlest');

test('descriptive test name', async ({ page, dataLayer }) => {
// Your test code here
});

Test Suites

Group related tests with test.describe:

test.describe('E-commerce Flow', () => {
test('product view tracking', async ({ page, dataLayer }) => {
await page.goto('http://localhost:3000/product/123');
await expect(dataLayer).toHaveEvent('view_item');
});

test('add to cart tracking', async ({ page, dataLayer }) => {
await page.goto('http://localhost:3000/product/123');
await page.click('#add-to-cart');
await expect(dataLayer).toHaveEvent('add_to_cart');
});

test('purchase tracking', async ({ page, dataLayer }) => {
await page.goto('http://localhost:3000/product/123');
await page.click('#add-to-cart');
await page.click('#checkout');

await expect(dataLayer).toHaveEvent('purchase', {
transaction_id: expect.any(String),
value: expect.any(Number)
});
});
});

Test Context

Every test receives a context object with fixtures:

Page - Browser Control

test('example', async ({ page }) => {
// Navigate
await page.goto('https://example.com');

// Interact with elements
await page.click('#button');
await page.fill('#email', 'test@example.com');

// Wait for elements
await page.waitForSelector('.product');

// Type text
await page.type('#search', 'laptop');

// Press keys
await page.press('#input', 'Enter');

// Take screenshot
await page.screenshot({ path: 'screenshot.png' });
});

Full Playwright Page API →

DataLayer - Analytics Events

test('example', async ({ page, dataLayer }) => {
await page.goto('http://localhost:3000');

// Check event exists
await expect(dataLayer).toHaveEvent('page_view');

// Check with specific data
await expect(dataLayer).toHaveEvent('purchase', {
value: 99.99,
currency: 'USD'
});

// Count events
await expect(dataLayer).toHaveEventCount('page_view', 1);

// Verify sequence
await expect(dataLayer).toHaveEventSequence([
'page_view',
'add_to_cart',
'purchase'
]);
});

All matchers →

Network - HTTP Requests

test('example', async ({ page, network }) => {
await page.goto('http://localhost:3000');

// Validate GA4 request
await expect(network).toHaveGA4Event('purchase');

// Get all GA4 events
const events = network.getGA4Events();

// Get events by name
const purchases = network.getGA4EventsByName('purchase');
});

Network validation guide →

Jest Matchers

DLest supports Jest's powerful expect helpers:

test('flexible matching', async ({ page, dataLayer }) => {
await page.goto('http://localhost:3000');

await expect(dataLayer).toHaveEvent('purchase', {
// Match any string
transaction_id: expect.any(String),

// Match any number
value: expect.any(Number),

// Exact value
currency: 'USD',

// String containing
description: expect.stringContaining('discount'),

// String matching regex
coupon_code: expect.stringMatching(/^SAVE[0-9]+$/),

// Array containing items
items: expect.arrayContaining([
expect.objectContaining({
item_id: 'prod-123',
quantity: 1
})
]),

// Partial object match
user: expect.objectContaining({
id: '12345'
})
});
});

Common Patterns

Testing Async Events

Some events fire after async operations:

test('async purchase event', async ({ page, dataLayer }) => {
await page.click('#checkout');

// Option 1: Wait for specific time
await page.waitForTimeout(2000);

// Option 2: Wait for UI element
await page.waitForSelector('.success-message');

// Option 3: Wait for network idle
await page.waitForLoadState('networkidle');

await expect(dataLayer).toHaveEvent('purchase');
});

Testing SPAs (Single Page Apps)

For client-side routing:

test('SPA navigation tracking', async ({ page, dataLayer }) => {
await page.goto('http://localhost:3000');

// First page view
await expect(dataLayer).toHaveEvent('page_view', {
page_path: '/'
});

// Navigate via client-side routing
await page.click('a[href="/products"]');

// Wait for route change
await page.waitForURL('**/products');

// Second page view
await expect(dataLayer).toHaveEventCount('page_view', 2);
await expect(dataLayer).toHaveEvent('page_view', {
page_path: '/products'
});
});

Testing Forms

test('form submission tracking', async ({ page, dataLayer }) => {
await page.goto('http://localhost:3000/contact');

// Fill form fields
await page.fill('#name', 'John Doe');
await page.fill('#email', 'john@example.com');
await page.selectOption('#category', 'sales');
await page.fill('#message', 'Hello world');

// Submit
await page.click('#submit');

// Validate tracking
await expect(dataLayer).toHaveEvent('form_submit', {
form_name: 'contact',
form_destination: 'sales'
});
});

Testing E-commerce Flows

test('complete purchase journey', async ({ page, dataLayer }) => {
// 1. View product
await page.goto('http://localhost:3000/products.html');
await expect(dataLayer).toHaveEvent('view_item', {
value: 99.99,
currency: 'USD'
});

// 2. Add to cart
await page.click('#add-to-cart');
await expect(dataLayer).toHaveEvent('add_to_cart', {
value: 99.99,
items: expect.arrayContaining([
expect.objectContaining({
item_id: 'prod-123',
quantity: 1
})
])
});

// 3. Complete purchase
await page.click('#checkout');
await page.waitForTimeout(1000);

await expect(dataLayer).toHaveEvent('purchase', {
transaction_id: expect.any(String),
value: 99.99,
currency: 'USD'
});

// 4. Verify complete sequence
await expect(dataLayer).toHaveEventSequence([
'view_item',
'add_to_cart',
'purchase'
]);
});

Testing Error Scenarios

test('track 404 page', async ({ page, dataLayer }) => {
await page.goto('http://localhost:3000/nonexistent-page');

await expect(dataLayer).toHaveEvent('page_view', {
page_path: '/nonexistent-page',
error_type: '404'
});
});

test('track form validation error', async ({ page, dataLayer }) => {
await page.goto('http://localhost:3000/contact');
await page.click('#submit'); // Submit empty form

await expect(dataLayer).toHaveEvent('form_error', {
form_name: 'contact',
error_message: expect.stringContaining('required')
});
});

Best Practices

1. Use Descriptive Test Names

// ❌ Bad
test('test 1', async ({ page, dataLayer }) => {});

// ✅ Good
test('purchase event fires with correct transaction_id and value', async ({ page, dataLayer }) => {});

2. Test One Thing at a Time

// ❌ Bad - testing multiple things
test('all tracking works', async ({ page, dataLayer }) => {
// Tests 10 different events
});

// ✅ Good - focused tests
test('page view event fires on homepage', async ({ page, dataLayer }) => {
// ...
});

test('add to cart event fires with correct product data', async ({ page, dataLayer }) => {
// ...
});

3. Keep Tests Independent

// ❌ Bad - tests depend on each other
let productId;

test('create product', async () => {
productId = '123'; // Modifies shared state
});

test('view product', async ({ page }) => {
await page.goto(`/product/${productId}`); // Depends on previous test
});

// ✅ Good - tests are independent
test('view product tracking', async ({ page, dataLayer }) => {
const productId = '123'; // Defined locally
await page.goto(`/product/${productId}`);
await expect(dataLayer).toHaveEvent('view_item');
});

4. Use Setup Helpers

// Create helper functions for common setups
async function addProductToCart(page, productId) {
await page.goto(`http://localhost:3000/product/${productId}`);
await page.click('#add-to-cart');
}

test('checkout from cart', async ({ page, dataLayer }) => {
await addProductToCart(page, '123');

await page.click('#checkout');
await expect(dataLayer).toHaveEvent('begin_checkout');
});

test('purchase from cart', async ({ page, dataLayer }) => {
await addProductToCart(page, '123');

await page.click('#checkout');
await expect(dataLayer).toHaveEvent('purchase');
});

5. Handle Timing Issues

test('event fires after animation', async ({ page, dataLayer }) => {
await page.click('#animated-button');

// Wait for animation to complete
await page.waitForTimeout(500);

await expect(dataLayer).toHaveEvent('button_click');
});

test('event fires after API call', async ({ page, dataLayer }) => {
await page.click('#submit');

// Wait for network to be idle
await page.waitForLoadState('networkidle');

await expect(dataLayer).toHaveEvent('form_submit');
});

Debugging Tests

Verbose Output

npx dlest --verbose

Shows:

  • All captured dataLayer events
  • Full event data
  • Expected vs actual comparison

Visual Debugging

npx dlest --no-headless

Opens a visible browser window so you can see what's happening.

Screenshots

test('debug with screenshot', async ({ page, dataLayer }) => {
await page.goto('http://localhost:3000');

// Take screenshot at specific point
await page.screenshot({ path: 'debug.png' });

await expect(dataLayer).toHaveEvent('page_view');
});

Console Logs

test('debug with console', async ({ page, dataLayer }) => {
// Listen to browser console
page.on('console', msg => console.log('Browser:', msg.text()));

await page.goto('http://localhost:3000');
await expect(dataLayer).toHaveEvent('page_view');
});

Next Steps