sodcalculator

10 Tech How-To

How to Visually Test a Remix App with Cypress

Guide 10-28279 · Updated · Step by step

Visual regression testing catches unintended UI changes before they reach production. This guide walks you through setting up Cypress with its Visual Regression Testing plugin in a Remix application, so you can automatically compare screenshots of your routes against a baseline and get a clear pass/fail signal on every run.

What You’ll Need

  • A Remix app (v1 or v2) with a running dev server or build script
  • Node.js 18 or later and npm/yarn/pnpm installed
  • Basic familiarity with Cypress E2E testing (if not, the Cypress docs are a good primer)
  • A terminal and a code editor

Step-by-Step

  1. Install Cypress and the visual testing plugin. In your project root, run: npm install -D cypress @cypress/visual-regression. This adds Cypress and the official visual regression plugin that captures and compares screenshots.
  2. Open Cypress for the first time. Run npx cypress open. This creates the cypress/ folder with default config files and example specs. Close Cypress after it finishes scaffolding.
  3. Configure the visual regression plugin. In cypress.config.ts (or .js), import the plugin and register it. Add the following inside the defineConfig object:
    import { defineConfig } from 'cypress';
    import { addVisualRegressionPlugin } from '@cypress/visual-regression';
    
    export default defineConfig({
      e2e: {
        setupNodeEvents(on, config) {
          addVisualRegressionPlugin(on);
          return config;
        },
      },
    });

    This tells Cypress to handle screenshot comparison in its Node process.

  4. Set up the visual regression commands. In cypress/support/e2e.ts (or e2e.js), add:
    import { addVisualRegressionCommands } from '@cypress/visual-regression';
    addVisualRegressionCommands();

    This exposes cy.visualSnapshot() in your specs.

  5. Point Cypress at your Remix app. In the Cypress config, set baseUrl to your app’s local URL, typically http://localhost:3000. Also set viewportWidth and viewportHeight to a consistent size, e.g., 1280×720, so screenshots are comparable across runs.
  6. Write your first visual spec. Create cypress/e2e/visual.cy.ts with a test that visits a route and takes a snapshot:
    describe('Visual regression', () => {
      it('home page matches baseline', () => {
        cy.visit('/');
        cy.visualSnapshot('home');
      });
    });

    The string argument is a name for the screenshot; use something descriptive like ‘home-desktop’ or ‘login-error-state’.

  7. Run Cypress to generate the baseline. In your terminal, run npx cypress run. This executes the spec in headless mode. On the first run, there is no baseline, so Cypress will save the current screenshots as the baseline and mark the test as passing (it may log a warning). You’ll see a cypress-visual-regression folder with baseline/ and actual/ subfolders.
  8. Review the baseline images. Open the generated PNG files in cypress-visual-regression/baseline/. Confirm they look correct – that’s what future runs will be compared against. If something is off (e.g., a font didn’t load), fix it and delete the baseline folder, then re-run to regenerate.
  9. Run the test again to verify it passes. Execute npx cypress run again. This time it should compare the actual screenshot to the baseline and pass if they match within the default tolerance (0.1% pixel difference).
  10. Make a change and see it fail. Edit a style in your Remix app, e.g., change a heading color in your CSS. Re-run the test. Cypress will output a diff image in cypress-visual-regression/diff/ and fail the test, showing you exactly what changed.
  11. Integrate into your workflow. Add npx cypress run to your CI pipeline (e.g., GitHub Actions) and run it on every pull request. When you intentionally change the UI, update the baseline by deleting the baseline/ folder and re-running, or use the CYPRESS_UPDATE_SNAPSHOTS=true environment variable to overwrite baselines automatically.

Common Mistakes

  • Forgetting to set a fixed viewport. If Cypress runs at different screen sizes, screenshots will differ and tests will fail randomly. Always set viewport dimensions in the config or per test.
  • Ignoring dynamic content. If your page shows a date, a random user ID, or an animated element, screenshots will differ between runs. Mock network requests or freeze time using cy.clock() to make the page deterministic.
  • Not waiting for fonts or images. If a webfont loads after the screenshot is taken, the text will look different. Use cy.waitForFonts() (if you have a plugin) or simply wait for document.fonts.ready before taking the snapshot.
  • Running tests in parallel without a shared baseline. In CI, if each parallel job has its own baseline, results will be inconsistent. Ensure the baseline folder is committed to your repo and not gitignored.
  • Using the same snapshot name for different pages. This overwrites the baseline. Use unique names per route and state.

Tips

  • Start small. Test only critical routes first (home, login, a form, a checkout). Visual tests are slower than unit tests, so keep the suite focused.
  • Use environment-specific baselines. If you run tests on different operating systems, font rendering may vary. Consider separate baseline folders per OS or use a Docker container for CI to keep it consistent.
  • Set a reasonable tolerance. The default 0.1% is strict. If you have anti-aliasing differences, increase it to 0.5% or 1% in the plugin config, but not so high that real regressions slip through.
  • Name snapshots by state. For example, login-empty, login-error, login-success. This makes it clear what each baseline represents.
  • Combine with functional tests. Run a few interaction tests to ensure the UI works, then a visual snapshot to ensure it looks right. Cypress supports both in the same spec.

Troubleshooting

  • Test fails on first run with no diff. This is normal – the first run creates the baseline. Re-run to confirm it passes.
  • Baseline folder is not created. Check that the plugin is properly registered in setupNodeEvents and that you’ve added the commands in the support file. Also ensure you’re running the spec via cypress run, not just the interactive runner (the interactive runner works too, but the folder is created after the test completes).
  • Diff shows a one-pixel shift across the whole page. This often comes from a scrollbar appearing/disappearing or a subpixel rendering difference. Set a fixed viewport and consider using cy.scrollTo(0, 0) before the snapshot.
  • Fonts render differently on CI than locally. Install the same fonts in your CI environment or use a headless browser that bundles fonts (Cypress’s Electron does). If using Chrome, ensure the system has the fonts installed.
  • Tests pass locally but fail in CI. Compare the actual screenshots from CI with your local ones. Common culprits: different viewport, missing fonts, or animations that run at different speeds. Freeze time and mock network responses to make the page deterministic.
  • Snapshot name collisions. If you accidentally use the same name twice in one spec, the second snapshot overwrites the first. Use unique names or include the route path.

Frequently Asked Questions

Do I need to commit the baseline images to version control?

Yes. The baseline folder should be committed so that every developer and CI job compares against the same reference. If you don’t commit it, each environment will create its own baseline and tests will be inconsistent.

Can I use Cypress visual testing with Remix’s server-side rendering?

Absolutely. Cypress runs against the fully rendered page in the browser, so SSR content is captured just like client-side content. Just make sure your dev server is running and reachable at the baseUrl.

How do I update baselines when I intentionally change the UI?

Run Cypress with the environment variable CYPRESS_UPDATE_SNAPSHOTS=true (e.g., CYPRESS_UPDATE_SNAPSHOTS=true npx cypress run). This overwrites the baseline with the current screenshot. Review the diff first to confirm the change is intentional.

What if my page has a video or animated element?

Visual tests will fail on animations. Use cy.clock() to freeze time or mock the animation by setting CSS animations to none in a test-specific stylesheet. For videos, mock the video source or pause it before the snapshot.

Can I test multiple viewports (mobile, tablet, desktop)?

Yes. Use cy.viewport(width, height) before each snapshot and give each a distinct name, like home-mobile and home-desktop. This creates separate baselines for each size.

Is visual testing slower than regular E2E testing?

Slightly, because each snapshot captures a full screenshot and compares pixels. For a small suite (10-20 snapshots), the difference is negligible. For large suites, consider running visual tests only on critical paths or on a nightly schedule.

Related

General steps for current mainstream versions; menus can differ by device and version. See the disclaimer or report an error.

More in this bed

The whole Tech How-To bed →