Skip to main content
Version: Current

Migrating from Legacy Happo Packages

This guide will help you migrate from the legacy Happo packages (happo.io, happo-cypress, happo-playwright, etc.) to the new unified happo package.

It takes you straight to the current major version, happo v7, which is what npm install happo installs. The changes v7 made to v6 are folded in, so you can skip v6. Upgrading to happo v7 explains each of them in more detail, and its Changes that don't fail list is a good final check: those changes take effect without an error.

Overview​

The new happo package is a modern, TypeScript-first rewrite that consolidates functionality from multiple legacy packages into a single, unified package. This migration brings several benefits:

  • Unified package: One package instead of multiple (happo.io, happo-cypress, happo-playwright, happo-plugin-storybook, happo-e2e, and happo-static)
  • TypeScript support: Built-in TypeScript support with full type definitions
  • Modern ES modules: Uses ES modules
  • Improved configuration: More flexible configuration file formats
  • Simplified target configuration: Plain object syntax instead of class-based targets
  • One CLI for every CI: A single happo command replaces the happo-ci-* scripts and detects your CI environment automatically
  • Simpler authentication: HAPPO_API_KEY and HAPPO_API_SECRET are read from the environment automatically, and you can sign in through the browser when running locally

Breaking Changes​

Node.js Version Requirement​

The new happo package requires Node.js 22.18.0 or later (specifically ^22.18.0 || >=24.0.0; Node 23 is not supported). These versions can load TypeScript configuration files like happo.config.ts natively. Ensure your local environment and CI meet this requirement before migrating.

Storybook Version Requirement​

The Storybook integration requires Storybook 9 or later. If you used happo-plugin-storybook with an older Storybook, upgrade Storybook first.

Package Installation​

Remove all legacy Happo packages and install the new unified happo package:

npm uninstall happo.io \
happo-cypress \
happo-playwright \
happo-e2e \
happo-plugin-storybook \
happo-static
npm install --save-dev happo

The new happo package replaces all of these legacy packages. All functionality is now included in the single unified package.

Configuration File Format​

The legacy .happo.js file is no longer read. The new package looks for a happo.config.{js,mjs,cjs,ts,mts,cts} file, and encourages TypeScript/ES modules:

Before (legacy):

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

module.exports = {
apiKey: process.env.HAPPO_API_KEY,
apiSecret: process.env.HAPPO_API_SECRET,
targets: {
'chrome-desktop': new RemoteBrowserTarget('chrome', {
viewport: '1024x768',
}),
},
};

After (new):

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
project: 'default',

integration: {
type: 'storybook',
configDir: '.storybook',
},

targets: {
'chrome-desktop': {
type: 'chrome',
viewport: '1024x768',
},
},
});

The HAPPO_API_KEY and HAPPO_API_SECRET environment variables are picked up automatically, so you no longer need to reference them in the config file. You can still set apiKey and apiSecret explicitly if you store the credentials under different names. See Authentication for more details.

Watch out for defaults: If you leave out integration, it defaults to { type: 'storybook' }. If you leave out targets, a single chrome target with a 1024x768 viewport is used. Cypress and Playwright users must set integration.type explicitly.

Target Configuration​

The new package uses plain objects instead of the RemoteBrowserTarget class when configuring targets.

Before (legacy):

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

module.exports = {
// ... rest of config

targets: {
'firefox-desktop': new RemoteBrowserTarget('firefox', {
viewport: '1024x768',
chunks: 2,
freezeAnimations: 'last-frame',
}),
'chrome-mobile': new RemoteBrowserTarget('chrome', {
viewport: '375x667',
}),
'ios-safari': new RemoteBrowserTarget('ios-safari', {
viewport: '375x667',
}),
},
};

After (new):

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
// ... rest of config

targets: {
'firefox-desktop': {
type: 'firefox',
viewport: '1024x768',
freezeAnimations: 'last-frame',
},
'chrome-mobile': {
type: 'chrome',
viewport: '375x667',
},
'ios-safari': {
type: 'ios-safari',
},
},
});

Key changes:

  • Remove new RemoteBrowserTarget() wrapper
  • Use type instead of the first constructor argument
  • Target options keep their names (viewport, freezeAnimations, etc.), but some defaults have changed. See Changed Target Defaults.
  • Remove chunks: Happo decides how many chunks to use from the size of the run, and the option is an error.
  • Mobile Safari targets (ios-safari, ipad-safari) don't require a viewport (they use a fixed size)

Dynamic targets in Cypress/Playwright screenshots​

The dynamic target objects you can pass to a happoScreenshot call's targets option use the same renamed field — browser becomes type.

Before (legacy):

await happoScreenshot(heroImage, {
component: 'Footer',
variant: 'Default',
targets: [{ name: 'firefox-small', browser: 'firefox', viewport: '400x800' }],
});

After (new):

await happoScreenshot(heroImage, {
component: 'Footer',
variant: 'Default',
targets: [{ name: 'firefox-small', type: 'firefox', viewport: '400x800' }],
});

Changed Target Defaults​

Several target options have new defaults. Expect some diffs on your first run after migrating if you relied on the old defaults.

OptionLegacy behaviorNew default
freezeAnimations'first-frame''last-frame'
prefersReducedMotionfalsetrue
allowPointerEventsfalsetrue
allowedHostnamesEvery request allowed[]: every external request blocked
includeOutlinesAndShadowsCropped to the elements' boxestrue
detectHorizontalOverflowNot checkedtrue
  • freezeAnimations: Animations are now frozen on their last frame instead of their first frame.
  • prefersReducedMotion: The browser now prefers reduced motion, so @media (prefers-reduced-motion: reduce) rules apply.
  • allowPointerEvents: Happo no longer injects * { pointer-events: none !important; }. This makes mouse interactions work in tests, but hover styles triggered by the system mouse pointer can now show up in screenshots.
  • allowedHostnames: Fonts, images, scripts and styles loaded from other hosts are blocked, so they go missing from screenshots unless the target lists their hostnames. With the pages integration, the pages themselves fail to load unless their own hostnames are listed. This is the change most likely to surprise you. Every run logs the hostnames its pages reached for, so run once, find the External requests: line in the run's logs on happo.io, and list the hostnames your snapshots need. There is no setting that allows every hostname. See allowedHostnames defaults to [].
  • includeOutlinesAndShadows: Screenshots make room for outlines and box shadows that reach past the content, so examples with a focus ring or a shadow at their edge get bigger screenshots.
  • detectHorizontalOverflow: Happo checks whether each page scrolls sideways. This brings no diffs by itself.

To get the legacy behavior back, set the old values explicitly. allowedHostnames has no setting that allows everything, so list the hostnames your snapshots load from:

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
// ... rest of config
targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
freezeAnimations: 'first-frame',
prefersReducedMotion: false,
allowPointerEvents: false,
allowedHostnames: ['fonts.gstatic.com', '*.mycdn.example.com'],
includeOutlinesAndShadows: false,
detectHorizontalOverflow: false,
},
},
});

allowedHostnames has no effect on ios-safari and ipad-safari targets.

Other Changed Defaults​

These defaults apply to the whole config rather than to a target:

  • Status checks: Without group, every project for a commit combines into one Happo job, which posts a single Happo status check. If branch protection requires a check named Happo (<project>), pull requests wait for a check that never arrives. Require Happo instead, or set group: false to give a project a job and a check of its own. See group defaults to true.
  • Blocked approval: A comparison can't be approved while the report has examples that failed to render, while it introduces new accessibility violations, or while a page starts to scroll sideways. Render errors already in your reports block approval too, so fix them first. Set a blockApproval condition to false to allow approval despite it.
  • Hover and focus states: For Cypress and Playwright, integration.autoApplyPseudoStateAttributes is on, so a snapshot shows the hover, focus or active state an element is in when it is taken.
  • Chunking: Pages, Cypress and Playwright runs are split into chunks automatically: pages runs on the number of pages, Cypress and Playwright runs on the number of snapshots.
  • Compare settings: Without deepCompare in the config, comparisons use a default threshold, and any compare settings in your project on happo.io stop applying.

compareThreshold​

The deprecated top-level compareThreshold option has been replaced by a deepCompare object. compareThreshold is required inside it, and the other settings are optional.

Before (legacy):

.happo.js
module.exports = {
compareThreshold: 0.005,

// ... rest of config
};

After (new):

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
deepCompare: {
compareThreshold: 0.005,
},

// ... rest of config
});

Without deepCompare, happo compares with { compareThreshold: 0.002, diffAlgorithm: 'color-delta' }, in place of any deep compare settings in your project on happo.io. Set deepCompare: false to keep using the project's settings instead, which are deprecated. See Compare with a threshold.

CLI Commands​

The CLI is now provided by the happo package instead of happo.io, and the default command runs Happo:

Before (legacy):

npx happo run

After (new):

npx happo

Removed subcommands​

The new CLI has three commands: the default command, finalize, and flake. These legacy subcommands have been removed:

  • happo run [sha] -- use happo instead. To set the SHA, use --afterSha.
  • happo has-report <sha> -- this is no longer needed. The CLI looks up baseline reports on its own.
  • happo compare <sha1> <sha2> -- comparisons are created automatically as part of happo. Use --beforeSha and --afterSha to control which commits are compared. The --dry-run flag is gone as well. Try out threshold values with the deepCompare option instead.
  • happo dev -- this is no longer supported.

See the CLI documentation for all commands and options.

Removed happo-ci* scripts​

The following scripts have all been replaced by the main happo CLI command:

  • happo-ci
  • happo-ci-github-actions
  • happo-ci-travis
  • happo-ci-circleci
  • happo-ci-azure-pipelines

The main happo CLI command now detects these CI environments automatically. For example, in a GitHub Actions workflow:

Before (legacy):

.github/workflows/happo.yml
- run: npx --package=happo.io happo-ci-github-actions
env:
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}

After (new):

.github/workflows/happo.yml
- run: npx happo
env:
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}

See Continuous Integration for complete examples for each CI provider.

Base branch defaults to origin/main​

When looking for a baseline, the CLI compares against the merge base with origin/main. If your default branch has another name, such as master or dev, pass it with --baseBranch:

npx happo --baseBranch origin/master

Baselines must come from your default branch​

Legacy happo-ci scripts had a sync mode (HAPPO_IS_ASYNC=false) that checked out the base commit, installed dependencies (with INSTALL_CMD), and built a baseline report on the fly. The new CLI never checks out other commits. Instead, it compares against an existing report for the merge base commit, or for one of the commits before it (see --fallbackShas and --fallbackShasCount).

This means you need to run Happo on pushes to your default branch, not only on pull requests. Otherwise pull requests will have no baseline to compare against.

Statuses are posted by happo.io​

Without the Happo GitHub App, for example on GitHub Enterprise, the legacy scripts could post the Happo status as a pull request comment, given a GitHub token (HAPPO_GITHUB_TOKEN or HAPPO_GITHUB_USER_CREDENTIALS). The happo CLI doesn't take a GitHub token: happo.io posts the statuses itself, for github.com and for GitHub Enterprise. If you relied on a token, connect your repository before you migrate, so pull requests keep getting statuses. See Posting build statuses, or GitHub Enterprise.

Removed --allow-failures flag​

The --allow-failures flag from the happo-e2e command was removed. To allow failures, set integration.allowFailures: true in your configuration file instead.

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
integration: {
type: 'cypress',
allowFailures: true,
},

// ... rest of config
});

Removed --port flag​

The happo-e2e --port flag was removed. The happo wrapper now picks a free port automatically, so you can drop the flag.

Integration Types​

The new package uses an integration field in the configuration to specify the type of integration.

Custom Bundle Integration​

Before (legacy):

.happo.js
module.exports = {
generateStaticPackage: () => ({ path: './static' }),

// ... rest of config
};

After (new):

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
integration: {
type: 'custom',
build: async () => ({
rootDir: './tmp/happo-custom',
entryPoint: 'bundle.js',
estimatedSnapsCount: 42,
}),
},

// ... rest of config
});

The returned object from build has to include a rootDir (path to the folder where files have been built, e.g. /foo/bar/build) and entryPoint (local file name of the built JavaScript bundle, e.g. bundle.js). It also has to include an estimatedSnapsCount: about how many snapshots the package renders. Happo picks the number of workers to split the work across from it. The count doesn't have to be exact, but leaving it out is an error.

Note: If you're using the happo-static library, replace any happo-static imports with imports for happo/custom.

Storybook Integration​

Before (legacy):

.happo.js
const happoPluginStorybook = require('happo-plugin-storybook');

module.exports = {
plugins: [
happoPluginStorybook({
configDir: '.storybook',
}),
],

// ... rest of config
};

After (new):

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
integration: {
type: 'storybook',
configDir: '.storybook',
},

// ... rest of config
});

The configDir, staticDir, outputDir, and usePrebuiltPackage options you passed to happoPluginStorybook move to the integration object unchanged.

Storybook preset​

Before (legacy):

.storybook/main.js
export default {
addons: ['happo-plugin-storybook/preset'],

// ... rest of storybook config
};

After (new):

.storybook/main.js
export default {
addons: ['happo/storybook/preset'],

// ... rest of storybook config
};
Storybook register and decorator​

Before (legacy):

.storybook/preview.js
import 'happo-plugin-storybook/register';
import happoDecorator from 'happo-plugin-storybook/decorator';

After (new):

.storybook/preview.js
import happoDecorator from 'happo/storybook/decorator';

Since happo v6.19.1, the happo CLI adds its client runtime to the Storybook package it builds, so you can delete the register import. Keep it, as happo/storybook/register, only if you use one of its helpers, such as setThemeSwitcher or forceHappoScreenshot:

.storybook/preview.js
import { setThemeSwitcher } from 'happo/storybook/register';

The decorator is optional too. Since v6.19.1 it works with every Storybook renderer, not only @storybook/react.

Cypress Integration​

The configuration changes for Cypress integration:

Before (legacy):

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

module.exports = {
targets: {
chrome: new RemoteBrowserTarget('chrome', {
viewport: '1024x768',
}),
},

// ... rest of config
};

After (new):

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
integration: {
type: 'cypress',
},

targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
},
},

// ... rest of config
});

In your Cypress test files, update your imports to use the new package:

Before (legacy):

cypress/support/commands.js
import 'happo-cypress';

After (new):

cypress/support/commands.js
import 'happo/cypress';

And in your cypress.config.js, update the task import. The happo package is published as an ES module, so use import:

Before (legacy):

cypress.config.js
const happoTask = require('happo-cypress/task');

After (new):

cypress.config.js
import happoTask from 'happo/cypress/task';

The happoTask.register(on) call inside setupNodeEvents stays the same.

The happo-e2e wrapper command is replaced by the main happo command. Replace npx happo-e2e -- npx cypress run with npx happo -- cypress run.

The HAPPO_ENABLED environment variable for cypress open has been removed. Happo only runs when Cypress is wrapped by the happo command, and is disabled with cypress open. See Usage with cypress open.

Removed cy.happoHideDynamicElements​

The cy.happoHideDynamicElements function has been removed in the unified happo package.

To achieve the same functionality, transition to using the data-happo-hide attribute on elements you want to hide from screenshots. This can be done directly in the code that renders the DOM nodes, or dynamically in your Cypress tests. For example, you might add:

cy.get('#some-dynamic-element').invoke('attr', 'data-happo-hide', 'true');

For more details, see Hiding Content.

Playwright Integration​

The configuration changes for Playwright integration:

Before (legacy):

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

module.exports = {
targets: {
chrome: new RemoteBrowserTarget('chrome', {
viewport: '1024x768',
}),
},
};

After (new):

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
integration: {
type: 'playwright',
},

targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
},
},
});

In your Playwright test files, update your imports to use the new package:

Before (legacy):

tests/test.spec.js
import { test } from 'happo-playwright';

After (new):

tests/test.spec.js
import { test } from 'happo/playwright';

The happo-e2e wrapper command is replaced by the main happo command. Replace npx happo-e2e -- npx playwright test with npx happo -- playwright test.

Parallel Cypress and Playwright builds​

If you split your Cypress or Playwright suite across multiple machines, the HAPPO_NONCE environment variable and the happo-e2e finalize command are replaced by the --nonce flag and the happo finalize command. Pass the same nonce to every parallel run and to the finalize step.

Before (legacy):

# On each parallel machine
HAPPO_NONCE=${CIRCLE_WORKFLOW_ID} npx happo-e2e -- npx cypress run

# After all parallel runs are done
HAPPO_NONCE=${CIRCLE_WORKFLOW_ID} npx happo-e2e finalize

After (new):

# On each parallel machine
npx happo --nonce ${CIRCLE_WORKFLOW_ID} -- cypress run

# After all parallel runs are done
npx happo finalize --nonce ${CIRCLE_WORKFLOW_ID}

See Finalize command for more details.

Pages Integration​

The top-level pages option has moved into a pages integration:

Before (legacy):

.happo.js
module.exports = {
pages: [{ url: 'https://example.com/', title: 'Home' }],

// ... rest of config
};

After (new):

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
integration: {
type: 'pages',
pages: [{ url: 'https://example.com/', title: 'Home' }],
},

targets: {
chrome: {
type: 'chrome',
viewport: '1024x768',
allowedHostnames: ['example.com'],
},
},

// ... rest of config
});

List your pages' hostnames in each target's allowedHostnames, along with the hosts they load assets from. Otherwise the pages fail to load (see Changed Target Defaults). See integration.pages for the full list of page options.

Happo Examples Integration​

The Happo examples integration has been removed in favor of the custom integration type (originally in the happo-static package).

This puts the bundling and rendering of the examples to the DOM completely in your control. If you are using the legacy Happo examples integration, you will need to convert these to a Happo custom type. Follow the Happo Custom integration documentation.

Removed Configuration Options​

These options were only used by the Happo Examples integration, and have been removed:

  • type
  • include
  • exclude
  • stylesheets
  • publicFolders
  • setupScript
  • cleanupScript
  • customizeWebpackConfig
  • prerender
  • renderWrapperModule
  • rootElementSelector
  • tmpdir
  • jsdomOptions
  • asyncTimeout

These options have been removed too:

  • chunks (target option) -- remove it. Happo decides how many chunks to use from the size of the run.
  • useFullPageFallbackForTallScreenshots (target option) -- remove it.
  • githubApiUrl -- remove it. happo.io posts statuses itself (see Statuses are posted by happo.io).

These options have moved:

A removed or unknown option is an error, not a warning, and every problem in the config is reported together. The errors for chunks, useFullPageFallbackForTallScreenshots and githubApiUrl say what to do instead. The other options above are reported as unknown, and a misspelled option gets a suggestion of the closest known one:

Unknown option `targets.chrome.viewPort` in config file happo.config.ts. Did you mean `viewport`?

Wait Timeouts Now Fail by Default​

Starting in happo v6.12.0, a waitForContent, waitForSelector, or waitFor that times out before the expected content, selector, or condition appears will fail the snap. In the legacy packages, a timeout only emitted a warning in the worker logs and the screenshot was taken anyway.

If a run starts failing after migrating because of this, update the offending waitForContent / waitForSelector / waitFor so it matches what actually renders — this is almost always the right fix, since it also removes the multi-second wait that was running on every snap. As a temporary escape hatch, set failOnWaitForTimeout: false in your happo.config.{js,ts,...} to restore the legacy warn-and-screenshot behavior while you investigate.

Environment Variables​

The following environment variables have been removed and are no longer necessary:

  • BASE_BRANCH and HAPPO_BASE_BRANCH -- use --baseBranch instead
  • CHANGE_URL and HAPPO_CHANGE_URL -- use --link instead
  • CURRENT_SHA -- use --afterSha instead
  • HAPPO_BEFORE_SHA_TAG_MATCHER -- use --beforeShaTagMatcher instead
  • HAPPO_BUILD_STORYBOOK_COMMAND -- this is no longer used
  • HAPPO_DELETE_OLD_COMMENTS -- this is now the behavior and the env var is no longer used
  • HAPPO_DOWNLOAD_ALL -- use integration.downloadAllAssets in your config file instead
  • HAPPO_ENABLED -- this is no longer used. Happo runs when Cypress is wrapped by the happo command.
  • HAPPO_FALLBACK_SHAS_COUNT -- use --fallbackShasCount instead
  • HAPPO_FALLBACK_SHAS -- use --fallbackShas instead
  • HAPPO_GITHUB_BASE and GITHUB_BASE -- this is no longer used (see Statuses are posted by happo.io)
  • HAPPO_GITHUB_TOKEN -- this is no longer used (see Statuses are posted by happo.io)
  • HAPPO_GITHUB_USER_CREDENTIALS -- this is no longer used (see Statuses are posted by happo.io)
  • HAPPO_IS_ASYNC -- async mode is now the only mode (see Baselines must come from your default branch)
  • HAPPO_MESSAGE -- use --message instead
  • HAPPO_NONCE -- use --nonce instead
  • HAPPO_NOTIFY -- use --notify instead
  • HAPPO_SIGNED_URL -- we use signed upload URLs by default now
  • INSTALL_CMD -- this is no longer used, since sync mode has been removed
  • PREVIOUS_SHA -- use --beforeSha instead
  • VERBOSE -- this is no longer used

These environment variables are still supported:

  • HAPPO_API_KEY and HAPPO_API_SECRET -- used when apiKey and apiSecret are not set in the config file
  • HAPPO_CONFIG_FILE -- path to your config file (same as --config)

Migration Steps​

  1. Update Node.js: Ensure you're running Node.js 22.18.0 or later (not Node 23), both locally and in CI
  2. Update Storybook: If you use the Storybook integration, upgrade to Storybook 9 or later
  3. Remove legacy packages: Uninstall all legacy Happo packages (happo.io, happo-cypress, happo-playwright, happo-e2e, happo-plugin-storybook, happo-static)
  4. Install new package: Install the unified happo package
  5. Update configuration file: Convert your .happo.js to a happo.config.{js,ts,...} file:
    • Remove RemoteBrowserTarget imports/usage
    • Convert targets to plain objects with type
    • List the hostnames your snapshots load from in each target's allowedHostnames
    • Decide whether to keep or override the other changed target defaults
    • Remove chunks from targets
    • Move compareThreshold into deepCompare
    • Remove options that no longer exist
    • Optionally convert to TypeScript (happo.config.ts)
  6. Update integration configs: Add an integration field. This is required for Cypress, Playwright, custom, and pages integrations.
  7. Update imports: Update any imports from legacy packages (e.g., happo-cypress → happo/cypress, happo-playwright → happo/playwright, happo-plugin-storybook/* → happo/storybook/*)
  8. Update CLI commands: Replace happo run, happo-ci* and happo-e2e commands with happo. Replace happo-e2e finalize with happo finalize.
  9. Update environment variables: Replace removed env vars with the corresponding CLI flags or config options
  10. Connect your repository: Make sure happo.io posts your statuses (see Statuses are posted by happo.io), and that branch protection requires the Happo check (see Other Changed Defaults)
  11. Check your CI triggers: Make sure Happo runs on pushes to your default branch, so pull requests have a baseline to compare against
  12. Test your setup: Run your Happo tests to ensure everything works. Expect some diffs on the first run from the changed target defaults.

Troubleshooting​

"Cannot find module 'happo.io'" or "Cannot find module 'happo-cypress'"​

This error occurs if you haven't updated your dependencies. Make sure to:

  1. Remove all legacy Happo packages from package.json (happo.io, happo-cypress, happo-playwright, happo-e2e, happo-plugin-storybook, happo-static)
  2. Add happo to package.json
  3. Run npm install (or pnpm install / yarn install)
  4. Update any imports in your code to use the new package exports (e.g., happo-cypress → happo/cypress)

"RemoteBrowserTarget is not defined"​

This error occurs if you're still using the old target syntax. Convert all new RemoteBrowserTarget(...) to plain objects with type.

"using an extension that is not supported by this version of Node.js"​

Your Node.js version is too old to load a TypeScript config file. Upgrade to Node.js 22.18.0 or later. You can check your version with:

node --version

Configuration file not found​

The new package no longer reads .happo.js. It looks for configuration files named happo.config.{ext} in this order:

  1. .js
  2. .mjs
  3. .cjs
  4. .ts
  5. .mts
  6. .cts

Make sure your configuration file matches one of these names. If your config file lives somewhere else or has a different name, point to it with the --config flag or the HAPPO_CONFIG_FILE environment variable:

npx happo --config path/to/happo.config.ts

"Unknown option" or "has been removed"​

Your config still has an option the new package doesn't take, often one carried over from .happo.js. Each error names the option: see Removed Configuration Options for where it went.

Fonts, images or styles missing from screenshots​

Targets block external requests unless their hostnames are listed in allowedHostnames. The run's logs on happo.io name the hostnames that were blocked. See Changed Target Defaults and Missing fonts, images, or other external assets.

Pull requests wait for a Happo check that never arrives​

Branch protection probably requires a check name the new package doesn't post. See the status checks in Other Changed Defaults, and make sure your repository is connected (see Statuses are posted by happo.io).

Pull requests have no baseline to compare against​

The new CLI doesn't build baselines on the fly. Make sure Happo runs on every push to your default branch, and that --baseBranch points to that branch if it isn't origin/main. See Baselines must come from your default branch.

New diffs after migrating​

If screenshots changed even though your UI didn't, check the changed target defaults. Animations, reduced-motion styles, hover states, blocked external assets and outlines or shadows at the edge of an example are the most common causes.

New Features Worth Exploring​

Once you have migrated, these features are available in the new package:

Need Help?​

If you encounter issues during migration, please reach out to support@happo.io