Happo: Cross-Browser Visual Testing for UI Components

Happo: Cross-Browser Visual Testing for UI Components

Happo solves a specific problem: visual regression testing across multiple browsers simultaneously. While Chromium-only tools catch 80% of visual regressions, cross-browser differences — font rendering, flexbox quirks, Safari's date inputs — require testing against real browser engines. Happo runs your components in Chrome, Firefox, Safari, and Edge and compares screenshots across all of them.

What Makes Happo Different

Most visual regression tools (Percy, BackstopJS, Lost Pixel) use a single browser engine — typically Chromium. Happo captures screenshots in multiple real browsers and compares:

  1. Previous version vs current version — catch regressions in your primary browser
  2. Cross-browser — verify consistency between Chrome and Safari, for example

The second comparison is especially valuable for design systems and component libraries where you need to guarantee visual consistency across browsers.

Architecture

Happo has two parts:

  • happo-e2e — CLI tool that captures screenshots
  • happo.io — hosted service that stores screenshots and runs comparisons

You run screenshots locally or in CI, upload to happo.io, and the service handles storage, diffing, and review UI.

Installation

npm install --save-dev happo.js

You need a Happo account for the API key. For open-source projects, Happo offers free plans.

Create .happo.js:

const { RemoteBrowserTarget } = require('happo.js');

module.exports = {
  apiKey: process.env.HAPPO_API_KEY,
  apiSecret: process.env.HAPPO_API_SECRET,
  
  targets: {
    'chrome-desktop': new RemoteBrowserTarget('chrome', {
      viewport: '1024x768',
    }),
    'firefox-desktop': new RemoteBrowserTarget('firefox', {
      viewport: '1024x768',
    }),
    'safari-desktop': new RemoteBrowserTarget('safari', {
      viewport: '1024x768',
    }),
    'chrome-mobile': new RemoteBrowserTarget('chrome', {
      viewport: '375x812',
    }),
  },
  
  project: 'my-component-library',
};

Writing Happo Examples

Happo uses "examples" — components rendered in isolation. For React:

// src/components/Button/Button-happo.jsx
export const primary = () => (
  <Button variant="primary">Click me</Button>
);

export const secondary = () => (
  <Button variant="secondary">Click me</Button>
);

export const disabled = () => (
  <Button variant="primary" disabled>
    Disabled
  </Button>
);

export const loading = () => (
  <Button variant="primary" loading>
    Loading...
  </Button>
);

export const allSizes = () => (
  <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
    <Button size="sm">Small</Button>
    <Button size="md">Medium</Button>
    <Button size="lg">Large</Button>
  </div>
);

Happo discovers files matching *-happo.{js,jsx,tsx} by default. Configure in .happo.js:

module.exports = {
  // ...
  include: '**/*-happo.{js,jsx,tsx}',
  // or
  include: 'src/**/*.happo.{js,jsx,tsx}',
};

Storybook Integration

For projects already using Storybook, use happo-plugin-storybook to reuse stories as Happo examples:

npm install --save-dev happo-plugin-storybook
// .happo.js
const { RemoteBrowserTarget } = require('happo.js');
const happoPluginStorybook = require('happo-plugin-storybook');

module.exports = {
  apiKey:    process.env.HAPPO_API_KEY,
  apiSecret: process.env.HAPPO_API_SECRET,
  
  targets: {
    chrome:  new RemoteBrowserTarget('chrome',  { viewport: '1024x768' }),
    firefox: new RemoteBrowserTarget('firefox', { viewport: '1024x768' }),
    safari:  new RemoteBrowserTarget('safari',  { viewport: '1024x768' }),
  },
  
  plugins: [
    happoPluginStorybook({
      outputDir: 'storybook-static',
    }),
  ],
};

Build your Storybook and run Happo:

npm run build-storybook
npx happo run

Every story becomes a Happo screenshot, tested across all configured browser targets.

Running Tests

# Take screenshots and upload to happo.io
npx happo run

<span class="hljs-comment"># Compare two commits
npx happo compare <sha1> <sha2>

<span class="hljs-comment"># Check comparison status (exits 0 if no differences)
npx happo check --<span class="hljs-built_in">link <happo-comparison-url>

CI Integration

The typical workflow compares the PR branch against the base branch:

name: Visual Regression

on: [pull_request]

jobs:
  happo:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: npm
      
      - run: npm ci
      
      - name: Run Happo on current commit
        env:
          HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
          HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}
        run: npx happo run
      
      - name: Run Happo on base commit
        env:
          HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
          HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}
        run: |
          git checkout ${{ github.base_ref }}
          npx happo run
      
      - name: Compare
        env:
          HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
          HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}
        run: |
          npx happo compare ${{ github.sha }} ${{ github.event.pull_request.base.sha }}

Happo integrates directly with GitHub's status checks — a green checkmark when no differences, a link to the review UI when changes are detected.

Using happo-ci

For a simpler setup, use happo-ci which handles the base/current comparison automatically:

npm install --save-dev happo-ci
- name: Run Happo CI
  env:
    HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
    HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}
    PREVIOUS_SHA: ${{ github.event.pull_request.base.sha }}
    CURRENT_SHA: ${{ github.sha }}
    CHANGE_URL: ${{ github.event.pull_request.html_url }}
  run: npx happo-ci

Handling Dynamic Content

Dynamic content (timestamps, random IDs, user-specific data) causes false positives. Use Happo's hide utility:

import { hide } from 'happo.js';

export const userCard = () => (
  <UserCard
    name="Test User"
    joinDate={hide(<span>January 1, 2024</span>)}
    avatar={hide(<Avatar src="/avatar.jpg" />)}
  />
);

The hide wrapper replaces the element with an empty box of the same dimensions in screenshots, preserving layout while removing dynamic content.

Cross-Browser Comparison

After running, Happo's comparison UI shows:

  1. Diff between your branch and base — per browser
  2. Cross-browser diffs — Chrome vs Firefox, Chrome vs Safari

Cross-browser diffs help you spot platform-specific rendering issues before users report them. Common findings:

  • Font rendering differences between Chrome and Safari
  • Border-radius rendering on older Firefox versions
  • Input styling differences between platforms
  • Flexbox gap support differences

Reviewing and Approving Changes

When Happo finds differences:

  1. Click the comparison link in the PR status check
  2. Review each changed component across browsers
  3. Accept intentional changes (updates the baseline)
  4. Reject unintentional regressions (blocks merge)

Happo tracks acceptance per-component, per-browser — you can accept a change in Chrome while rejecting it in Firefox if only one browser has the right rendering.

Happo vs Percy vs Chromatic

Feature Happo Percy Chromatic
Multi-browser Yes (Chrome/Firefox/Safari/Edge) Chrome + Firefox Chrome only
Storybook native Plugin Native Native
Cross-browser diffs Yes Limited No
Self-hosted option No No No
Open source plan Yes No Yes

Happo's multi-browser support justifies it for projects where Safari or Firefox compatibility matters — design systems, consumer-facing applications, or accessibility-focused products.

Summary

Happo fills the cross-browser gap in visual regression testing:

  • Simultaneous screenshots across Chrome, Firefox, Safari, and Edge
  • Cross-browser diff detection beyond regression tracking
  • Storybook integration through happo-plugin-storybook
  • CI-native with GitHub status checks
  • Review UI with per-component, per-browser approval workflow

For teams shipping components that need to look consistent across browsers, Happo catches the class of bugs that Chromium-only tools miss.

Read more