Skip to main content

Cypress

The happo-cypress module adds Happo.io cross-browser screenshots to your Cypress.io test suite.

Check out the happo-cypress-example-todomvc for a demo of how this module is used.

Pre-requisites

Before you start following the instructions here, you'll need a working Cypress test suite and a Happo account.

Installation

In your project, install the happo-cypress, happo-e2e, and the happo.io npm modules.

npm install --save-dev happo-cypress happo-e2e happo.io

Setup

Import the happo-cypress module in your cypress/support/commands.js file:

// At the top of cypress/support/commands.js
import 'happo-cypress';

Then, register the provided happoTask in your cypress.config.js file (or cypress/plugins/index.js if you are on Cypress v9 or earlier):

const { defineConfig } = require('cypress');
const happoTask = require('happo-cypress/task');

module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
happoTask.register(on);
return config;
},
},
});
// In cypress/plugins/index.js
const happoTask = require('happo-cypress/task');

module.exports = on => {
happoTask.register(on);
};

Add a .happo.js file with some minimal/required configuration:

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

module.exports = {
apiKey: <your api key>,
apiSecret: <your api secret>,
targets: {
chrome: new RemoteBrowserTarget('chrome', {
viewport: '1024x768',
}),
},
};

See https://github.com/happo/happo.io#targets for more configuration options.

NOTE: For security reasons, you'll most likely want to pass in apiKey and apiSecret via environment variables:

// .happo.js
module.exports = {
apiKey: process.env.HAPPO_API_KEY,
apiSecret: process.env.HAPPO_API_SECRET,
// ... more config
};

Usage with cypress run

To enable Happo in your test suites triggered via cypress run, you'll need to use the happo-e2e wrapper. Here's an example:

npx happo-e2e -- npx cypress run

If you're using yarn, you might have to specify the double dashes twice (the first one is consumed by yarn itself):

yarn happo-e2e -- -- yarn cypress run

If you're not using the happo-e2e wrapper with cypress run, Happo will be disabled for the whole test suite.

Usage with cypress open

When running Cypress tests locally using the cypress open command, Happo is disabled by default. The happo-e2e wrapper won't work with cypress open since it depends on the Cypress command to finish after a test run (which won't happen with cypress open). To enable Happo in these scenarios, set HAPPO_ENABLED=true as an environment variable and set the experimentalInteractiveRunEvents config flag. Here's an example with the environment variable inlined with the command:

HAPPO_ENABLED=true npx cypress open --config experimentalInteractiveRunEvents=true

When Happo is enabled with cypress open, Happo reports are created every time you trigger a test run. Check your console logs for a link to the reports.

Usage

To record Happo screenshots in your test suite, use happoScreenshot:

describe('Home page', function () {
it('loads properly', function () {
cy.visit('/');
cy.get('.header').happoScreenshot();
});
});

Happo focuses more on component screenshots as opposed to full-page screenshots. Because of that, you always need to select a child before you call happoScreenshot. If you still need a full-page screenshot you can use the <body> element:

cy.get('body').happoScreenshot();

Happo identifies screenshots by component and variant. By default, the component name and variant are inferred from the current test case. If you want more control, you can provide these in an options argument:

// Full control, pass in both component and variant:
cy.get('.header').happoScreenshot({ component: 'Header', variant: 'large' });

// Control the component name, but let variant be auto-assigned
cy.get('.footer').happoScreenshot({ component: 'Footer' });

// Control variant, but let component name be inferred
cy.get('.footer').happoScreenshot({ variant: 'dark' });

// No control, infer component and variant from current test
cy.get('.footer').happoScreenshot();

Allowing failures

By default, no Happo reports are produced when the Cypress run fails. In some cases, you might want to allow Happo to succeed even if the overall test run fails. The --allow-failures flag for the happo-e2e command can then be used:

npx happo-e2e --allow-failures -- npx cypress run

Selecting targets

If you want to avoid rendering an example in all browser targets (found in .happo.js), you can use the targets option. The example will then be rendered in the specified targets exclusively.

cy.get('.footer').happoScreenshot({
component: 'Footer',
targets: ['chrome-small'],
});

In this example, the "Footer" snapshot will only be rendered in the target named 'chrome-small' found in .happo.js.

Dynamic targets

If you want to create a snapshot in a target that isn't defined in the .happo.js config, you can use an object with name, browser and viewport properties. Here's an example where a snapshot is taken in a dynamically generated target:

cy.get('.footer').happoScreenshot({
component: 'Footer',
targets: [{ name: 'firefox-small', browser: 'firefox', viewport: '400x800' }],
});

Here, "Footer" will only be rendered in a 400x800px Firefox window.

You can mix and match dynamic targets and target names as well:

cy.get('.footer').happoScreenshot({
component: 'Footer',
targets: [
'chrome-small',
{ name: 'firefox-small', browser: 'firefox', viewport: '400x800' },
],
});

"Footer" is now rendered in Chrome (target specified in .happo.js) and Firefox (dynamic target).

Hiding dynamic content

If you have dynamic content that changes often, you can ask Happo to hide it from the screenshot. This can help prevent unwanted diffs caused by e.g. timestamps, dates, randomized content.

cy.happoHideDynamicElements();

Confusingly enough, elements keep being visible while you run the Cypress test suite. Instead, a data-happo-hide attribute is added. This will inform Happo's workers to hide the elements before taking the screenshot. To cover up/replace elements instead of hiding them, use the replace option:

cy.happoHideDynamicElements({ replace: true });

Elements will then be replaced (actually covered) by black rectangles. Please note that in some cases this can affect layout. Simply hiding elements (the default) is slightly safer, since all we have to do is set visibility: hidden.

You can use data-happo-hide manually in your source code as well if you prefer to hide certain elements without using happoHideDynamicElements.

Selectors

By default happoHideDynamicElements will attempt to find <time> elements to hide. You can add your own selectors using the selectors option.

cy.happoHideDynamicElements({
selectors: ['.date'],
});

Using selectors will not affect the default. To remove the default selectors, you can set defaultSelectors to an empty array:

cy.happoHideDynamicElements({
defaultSelectors: [],
selectors: ['.date'],
});

Matchers

The default implementation of happoHideDynamicElements will attempt to find things like "2 days ago", "2:45 PM", etc. You can supply your own content matchers (regular expressions) if the default implementation isn't working for you.

cy.happoHideDynamicElements({
matchers: [/liked by [0-9]+ people/],
});

To minimize false negatives, only leaf nodes are hidden by matchers.

Using matchers will not affect the default. To remove the default matchers, you can set defaultMatchers to an empty array:

cy.happoHideDynamicElements({
defaultMatchers: [],
matchers: [/liked by [0-9]+ people/],
});

Continuous Integration

If you run the test suite in a CI environment, the happo-cypress and happo-e2e modules will do their best to auto-detect your environment and adapt its behavior accordingly:

  • On PR builds, compare the screenshots against the main branch
  • On main branch builds, simply create the Happo report

To get the results of the Happo jobs back to your PRs/commits, you need to install and configure the Happo GitHub app. Instructions are available in the Continuous Integration docs.

Happo auto-detects the following CI environments:

  • GitHub Actions
  • Circle CI
  • Travis CI
  • Azure DevOps

If you are using a different CI service, you'll have to set a few environment variables before invoking the test suite:

  • HAPPO_PREVIOUS_SHA the commit sha that the branch/PR is based on (usually a commit on main). Only set this for PR builds.
  • HAPPO_CURRENT_SHA the sha of the commit currently under test. Always set this.
  • HAPPO_BASE_BRANCH the default/base branch you use, e.g. origin/dev. Defaults to origin/main, so you only need to set this if you are using a different base branch.
  • HAPPO_CHANGE_URL a url to the PR/commit. Optional.

GitHub Actions example

Here's an example of how you can use Happo with Cypress in a GitHub Actions workflow. It makes use of the Cypress GitHub action.

name: Cypress with Happo workflow

on:
# Configure this workflow to trigger on pull requests and pushes to main
push:
branches:
- main
pull_request:
branches:
- main

jobs:
cypress:
name: Cypress with Happo
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Run cypress
uses: cypress-io/github-action@v2
with:
command-prefix: npx happo-e2e -- npx
env:
# Inject secrets to the build
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}

Circle CI example

This example triggers a Cypress with Happo run in a Circle CI environment. It uses the Cypress Circle CI Orb. Separately, you'll need to configure the Circle CI project to inject the Happo API key and secret through environment variables.

version: 2.1
orbs:
node: circleci/node@4.1.0
cypress: cypress-io/cypress@1.27.0

workflows:
cypress:
jobs:
- cypress/run:
name: Run Cypress with Happo
command-prefix: 'npx happo-e2e -- npx'

Travis CI example

This is a simplified example of using Cypress with Happo in Travis CI. For a more in-depth explanation of how to set up Cypress with Travis, see https://docs.cypress.io/guides/guides/continuous-integration.html#Travis

language: node_js
node_js:
- 12
install:
- npm ci
script:
- $(npm bin)/happo-e2e -- $(npm bin)/cypress run

Azure DevOps

Here's an example of how you can run Cypress with Happo in an Azure DevOps environment. This example assumes you have a Pull request trigger set up in your repository.

trigger:
- main

pool:
vmImage: ubuntu-latest

steps:
- task: NodeTool@0
inputs:
versionSpec: '16.x'
displayName: 'Install Node.js'
- script: |
npm ci
npx happo-e2e -- npx cypress run
displayName: 'Install and Run Happo'
env:
HAPPO_API_KEY: $(happoApiKey)
HAPPO_API_SECRET: $(happoApiSecret)
HAPPO_COMMAND: happo

Parallel builds

If you're running your Cypress test suite across multiple machines, you'll need to do two things:

  • Set a HAPPO_NONCE environment variable, to tie individual runs together. Any string unique to the build will do.
  • After the whole test suite is done, call npx happo-e2e finalize. Make sure that the same HAPPO_NONCE environment variable is set as for the individual builds.

In some CI tools, you can use a built-in environment variable as HAPPO_NONCE. In CircleCI for instance, you can use HAPPO_NONCE=${CIRCLE_WORKFLOW_ID}. You can also use a timestamp or a randomly generated string. The important thing is that it's unique to the current CI run.

Here's an example configuration for a parallel build running in Circle CI (adapted from the config used for the happo-cypress test suite):

version: 2.1
orbs:
node: circleci/node@4.1.0
cypress: cypress-io/cypress@1.27.0
workflows:
cypress:
jobs:
- cypress/install

- cypress/run:
name: cypress-parallel
requires:
- cypress/install
start: npm run start-dev-server
parallel: true
parallelism: 4
command-prefix:
'HAPPO_NONCE=${CIRCLE_WORKFLOW_ID} npx happo-e2e -- npx'
post-steps:
- run: 'HAPPO_NONCE=${CIRCLE_WORKFLOW_ID} npx happo-e2e finalize'

Notice how the same HAPPO_NONCE is used in both the Cypress run itself and the finalize call.

Email notifications

If you set a HAPPO_NOTIFY environment variable as part of your Cypress run, an email will be sent out when the Happo comparison report is ready.

Usage instructions for HAPPO_NOTIFY is available in the Continuous Integration docs.

Advanced usage

Setting a port for happo-e2e

When you're running the happo-e2e -- wrapper, a web server is used internally. By default, this server is listening on port 5339. If you need to change that, pass a --port argument, like so:

npx happo-e2e --port 5432 -- npx cypress run

Passing along options

Any options you pass to happoScreenshot or happoHideDynamicElements that we don't recognize we will pass along to the underlying Cypress tasks. This means you can for instance suppress logs by usinglog: false:

cy.get('.header').happoScreenshot({
component: 'Header',
variant: 'large',
log: false,
});

Transforming the DOM

If you need to transform the DOM in some way before a snapshot is taken, you can use the transformDOM option. Here's an example where <iframe>s are replaced with a div placeholder.

cy.get('.main').happoScreenshot({
component: 'Main',
transformDOM: {
selector: 'iframe',
transform: (element, doc) => {
// element here is an iframe
const div = doc.createElement('div');
div.innerHTML = '[iframe placeholder]';
return div; // return the element you want to replace the iframe with
},
},
});

The options passed to transformDOM are:

  • selector: an argument passed to querySelectorAll that describes what elements to transform.
  • transform: a function that is called for each element matching the selector (there can be more than one). Make sure you return a replacement element.

Download all assets

By default, happo-e2e will download assets found locally and include in an assets package sent to happo.io. Any external URL will be left as-is, which means they are expected to be publicly accessible by Happo workers. To include external assets in the assets package as well, set a HAPPO_DOWNLOAD_ALL environment variable.

HAPPO_DOWNLOAD_ALL=true npx happo-e2e -- npx cypress run

With this environment variable set, all assets are assumed to be private (i.e. not publicly accessible).

Locally produced images

The way happo-cypress takes screenshots is to take a DOM snapshot of the current page/element and send that snapshot to Happo's workers for screenshots (in different browsers). In some cases, DOM snapshotting doesn't always work. One such case is if you are using web components/custom elements. The snapshots will then not contain all the state needed to "reproduce" the elements on Happo's workers. In this situation, and similar ones, you can use localSnapshots: true. When this option is enabled, screenshots are produced directly on the machine running the Cypress test run.

Here's how to enable local snapshots:

In cypress/support/commands.js, enable the localSnapshots option:

import { configure } from 'happo-cypress';

configure({
localSnapshots: true,
});

With this configuration, the happoScreenshot() method will take a local screenshot (using cy.screenshot()) and upload images to Happo. The rest of the flow is the same as a normal happo-cypress run, meaning you get a link to a report to review, etc.

The downside of this approach is that you can only get screenshots in the browser that Cypress currently runs. If you want cross-browser, you'll have to make sure to run Cypress in the browsers you are interested in.

There might also be consistency issues with the screenshots. Happo's browser workers do a lot of work to ensure that screenshots are consistently produced, and leaving that work over to Cypress could potentially lead to more spurious diffs.

When you use localSnapshots: true, we ignore the target configuration in .happo.js. Instead, a dynamically resolved target is used based on the browser and the viewport size used during the test.

Skipping snapshots

If you run a subset of the tests in your test suite, Happo will show "Deleted examples" in the report. To prevent this from happening, you can pass a --skippedExamples option to the happo-e2e finalize call.

The --skippedExamples option needs to be an array coded as a JSON string. Each item in the array needs a component, variant, and target, all strings. Here's an example call:

happo-e2e finalize --skippedExamples '[{"component":"Button","variant":"default","target":"chrome-small"}]'

Remember to skip examples in all targets you have defined in .happo.js. If you for instance have targets named "chrome-small" & "firefox-large" in .happo.js, you should add two items per snapshot you are skipping. E.g.

happo-e2e finalize --skippedExamples '[{"component":"Button","variant":"default","target":"chrome-small"}, {"component":"Button","variant":"default","target":"firefox-large"}]'

Finding the skipped snapshots can be a little tricky, but a little bit of code introspection could help. Here's an example of a script that can serve as a base: https://github.com/happo/happo-e2e/issues/21#issuecomment-1825776491

Troubleshooting

I need support!

We're here to help — send an email to support@happo.io and we'll assist you.

Happo isn't producing any screenshots

The happo-cypress module will disable itself if it can't detect any api tokens (apiKey and apiSecret in config). Check to make sure your .happo.js config is properly set up. There might also be more information in the console logs from Cypress. Look for lines starting with [HAPPO].

Where are my screenshots?

During test suite execution, Happo will only record information. All screenshots are taken asynchronously outside of the test run. This means that your test suite will finish sooner than the Happo job is done. To follow along the progress, look for a url logged by Happo:

[HAPPO] https://happo.io/a/284/async-reports/34

Styles are missing from my screenshots

Styles and assets are collected automatically during your test suite. If you notice styles/images/fonts etc are missing, one of a few things might have happened:

  • CSS selectors depend on context that is missing to Happo. If you e.g. have something like #start-page .header { color: red } and screenshoot .header, the red color will be missing. This is because Happo only sees the .header element, never the surrounding page.
  • There could be a bug in how happo-cypress collects styles and assets. Reach out to support@happo.io and we'll triage.