Skip to content

Test Framework Integration

Vizzly works with any JavaScript test framework. You capture screenshots during your tests, and Vizzly handles the comparison and reporting.

How it Works

When you run vizzly run "npm test":

  1. Vizzly starts a local server
  2. Sets environment variables so your tests can find it
  3. Runs your test command
  4. Collects screenshots as tests call vizzlyScreenshot()
  5. Uploads everything for comparison

The key: your tests stay normal. You just add screenshot calls where you need them.

Environment Variables

The CLI sets these automatically. You don’t configure them yourself:

  • VIZZLY_SERVER_URL - Where to send screenshots (like http://localhost:47392)
  • VIZZLY_BUILD_ID - Unique ID for this test run
  • VIZZLY_ENABLED - Set to true when running under vizzly run
  • VIZZLY_TDD - Set to true in TDD mode

In TDD mode, the client auto-discovers the server by looking for .vizzly/server.json in your project directory and parent directories. You can run tests directly without vizzly run and screenshots still work.

Playwright

Playwright’s screenshot API maps directly to Vizzly.

Basic Example

import { test, expect } from '@playwright/test';
import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
test('homepage loads correctly', async ({ page, browserName }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
const screenshot = await page.screenshot();
await vizzlyScreenshot('homepage', screenshot, {
browser: browserName,
viewport: page.viewportSize(),
});
});

Properties like browser and viewport become part of the screenshot’s signature. Change the viewport size and Vizzly treats it as a different screenshot with its own baseline.

Helper Function

Create a wrapper to reduce repetition:

import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
async function capturePlaywright(page, browserName, name, properties = {}) {
const screenshot = await page.screenshot();
return vizzlyScreenshot(name, screenshot, {
browser: browserName,
viewport: page.viewportSize(),
...properties,
});
}
test('responsive homepage', async ({ page, browserName }) => {
await page.goto('/');
// Desktop
await page.setViewportSize({ width: 1920, height: 1080 });
await capturePlaywright(page, browserName, 'homepage-desktop', {
device: 'desktop'
});
// Mobile
await page.setViewportSize({ width: 375, height: 667 });
await capturePlaywright(page, browserName, 'homepage-mobile', {
device: 'mobile'
});
});

Running Tests

Terminal window
# Basic
vizzly run "npx playwright test"
# Custom build name
vizzly run "npx playwright test" --build-name "PR #123"
# Wait for results
vizzly run "npx playwright test" --wait

Cypress

Cypress works the same way but needs a custom command since it runs in the browser.

Setup

Add this to cypress/support/commands.js:

import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
import { join } from 'path';
Cypress.Commands.add('vizzlyScreenshot', (name, options = {}) => {
const testName = `${Cypress.spec.name}-${name}`;
cy.screenshot(testName, {
capture: 'viewport',
...options.screenshotOptions
});
const screenshotPath = join(
Cypress.config('screenshotsFolder'),
Cypress.spec.name,
`${testName}.png`
);
return cy.wrap(
vizzlyScreenshot(name, screenshotPath, {
browser: Cypress.browser.name,
viewport: {
width: Cypress.config('viewportWidth'),
height: Cypress.config('viewportHeight')
},
...options.properties
})
);
});

Pass a file path instead of a buffer. Vizzly reads the file for you.

Using the Command

describe('Homepage', () => {
it('displays correctly on desktop', () => {
cy.viewport(1920, 1080);
cy.visit('/');
cy.get('h1').should('be.visible');
cy.vizzlyScreenshot('homepage-desktop', {
properties: {
page: 'homepage',
device: 'desktop'
}
});
});
it('displays correctly on mobile', () => {
cy.viewport(375, 812);
cy.visit('/');
cy.get('h1').should('be.visible');
cy.vizzlyScreenshot('homepage-mobile', {
properties: {
page: 'homepage',
device: 'mobile'
}
});
});
});

Running Cypress

Terminal window
vizzly run "npx cypress run"
# Specific browser
vizzly run "npx cypress run --browser chrome"
# With environment
vizzly run "npx cypress run" --environment "staging"

Jest + Puppeteer

Combine Puppeteer’s automation with Jest’s test runner.

Basic Setup

import puppeteer from 'puppeteer';
import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
describe('Visual Tests', () => {
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch({ headless: 'new' });
});
beforeEach(async () => {
page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
});
afterEach(async () => {
await page.close();
});
afterAll(async () => {
await browser.close();
});
test('homepage renders', async () => {
await page.goto('http://localhost:3000');
await page.waitForSelector('h1');
const screenshot = await page.screenshot();
await vizzlyScreenshot('homepage', screenshot, {
browser: 'chrome',
viewport: page.viewport(),
});
});
});

Puppeteer Helper

async function capturePuppeteer(page, name, properties = {}) {
const screenshot = await page.screenshot();
return vizzlyScreenshot(name, screenshot, {
browser: 'chrome',
viewport: page.viewport(),
...properties,
});
}
test('responsive design', async () => {
await page.goto('http://localhost:3000');
// Desktop
await page.setViewport({ width: 1920, height: 1080 });
await capturePuppeteer(page, 'homepage-desktop', {
device: 'desktop'
});
// Mobile
await page.setViewport({ width: 375, height: 812 });
await capturePuppeteer(page, 'homepage-mobile', {
device: 'mobile'
});
});

Running Jest

Terminal window
vizzly run "npm test"
# Specific file
vizzly run "npx jest visual.test.js"
# Custom config
vizzly run "npx jest --config jest.visual.config.js"

Selenium WebDriver

Cross-browser testing with WebDriver.

import { Builder, By } from 'selenium-webdriver';
import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
describe('Cross-Browser Tests', () => {
let driver;
beforeEach(async () => {
driver = await new Builder()
.forBrowser('chrome')
.build();
await driver.manage().window().setRect({
width: 1920,
height: 1080
});
});
afterEach(async () => {
await driver.quit();
});
test('homepage in Chrome', async () => {
await driver.get('http://localhost:3000');
const screenshot = await driver.takeScreenshot();
await vizzlyScreenshot('homepage-chrome', screenshot, {
browser: 'chrome',
viewport: '1920x1080',
});
});
test('specific element', async () => {
await driver.get('http://localhost:3000');
const element = await driver.findElement(By.css('[data-testid="hero"]'));
const screenshot = await element.takeScreenshot();
await vizzlyScreenshot('hero-component', screenshot, {
browser: 'chrome',
component: 'hero',
});
});
});

WebDriver’s takeScreenshot() returns a base64 string. Pass it directly - Vizzly handles both buffers and base64.

WebdriverIO

Modern WebDriver with simpler syntax.

import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
describe('Visual Tests', () => {
it('captures homepage', async () => {
await browser.url('/');
const screenshot = await browser.takeScreenshot();
await vizzlyScreenshot('homepage', screenshot, {
browser: browser.capabilities.browserName,
viewport: '1920x1080',
});
});
it('captures component', async () => {
await browser.url('/components');
const element = await $('[data-testid="hero"]');
const screenshot = await element.takeScreenshot();
await vizzlyScreenshot('hero-component', screenshot, {
component: 'hero',
browser: browser.capabilities.browserName,
});
});
});

Best Practices

Naming Screenshots

Use consistent patterns that reflect what you’re testing:

// Pattern: page-component-state
await vizzlyScreenshot('checkout-form-empty', screenshot, {
page: 'checkout',
component: 'form',
state: 'empty',
});
await vizzlyScreenshot('checkout-form-filled', screenshot, {
page: 'checkout',
component: 'form',
state: 'filled',
});

Good names make diffs easier to understand.

Properties as Signatures

Properties become part of the screenshot’s identity. Same name but different viewport? Different screenshot.

// These are two separate screenshots
await vizzlyScreenshot('homepage', screenshot, {
viewport: { width: 1920, height: 1080 }
});
await vizzlyScreenshot('homepage', screenshot, {
viewport: { width: 375, height: 667 }
});

Keep properties consistent across test runs. If viewport changes between runs, Vizzly treats them as new screenshots.

Setting Thresholds

Use the threshold option to allow minor pixel differences:

await vizzlyScreenshot('dashboard', screenshot, {
threshold: 2.0, // CIEDE2000 Delta E units
browser: 'chrome',
});

Threshold uses CIEDE2000 color difference. Values around 1.0-3.0 work for most cases. Higher values allow more variation before flagging a diff.

Error Handling

Screenshots shouldn’t break your tests. The client disables itself after the first failure to prevent spam:

async function safeScreenshot(name, screenshot, properties = {}) {
try {
await vizzlyScreenshot(name, screenshot, properties);
} catch (error) {
console.warn(`Screenshot '${name}' failed:`, error.message);
// Test continues
}
}

In TDD mode, visual diffs log warnings but don’t throw errors. This lets you capture all screenshots before reviewing changes.

Creating Framework Helpers

Wrap vizzlyScreenshot with framework-specific defaults:

helpers/vizzly.js
import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
// Base wrapper with defaults
export async function screenshot(name, imageBuffer, properties = {}) {
return vizzlyScreenshot(name, imageBuffer, {
threshold: 2.0,
environment: process.env.NODE_ENV || 'test',
...properties,
});
}
// Playwright-specific
export async function playwrightScreenshot(page, browserName, name, properties = {}) {
const imageBuffer = await page.screenshot();
return screenshot(name, imageBuffer, {
browser: browserName,
viewport: page.viewportSize(),
...properties,
});
}
// Puppeteer-specific
export async function puppeteerScreenshot(page, name, properties = {}) {
const imageBuffer = await page.screenshot();
return screenshot(name, imageBuffer, {
browser: 'chrome',
viewport: page.viewport(),
...properties,
});
}

Import from your helpers instead of using the client directly. Keeps things consistent.

Next Steps