ScreenshotNeo

BlogHow-to

How to Add and Reuse Scenarios in BackstopJS

Add BackstopJS scenarios, share defaults safely, generate routes from data, and run focused visual regression checks without duplicating configuration.

By the ScreenshotNeo team4 October 20268 min read

Define each visual regression case as an object in BackstopJS’s top-level scenarios array. Give every scenario a descriptive label and a url. Put settings shared by multiple scenarios in scenarioDefaults; a value set directly on a scenario takes precedence. Use JSON for a static list, or a JavaScript config to generate scenarios from route data. Run all cases with backstop test, or focus on labels with --filter.

This guide shows how to add cases, avoid duplicated settings, filter local runs, and handle reference images deliberately. It also includes a one-call screenshot alternative for captures where setting up a browser is unnecessary.

1. Add a scenario to the configuration

Start with BackstopJS’s generated configuration or edit the project’s existing one. By default, BackstopJS looks for backstop.json in the project root. Each test case belongs in the top-level scenarios array.

{
  "id": "shop",
  "viewports": [
    { "label": "desktop", "width": 1280, "height": 800 }
  ],
  "scenarios": [
    {
      "label": "Product listing",
      "url": "https://example.test/products"
    },
    {
      "label": "Product detail",
      "url": "https://example.test/products/example"
    }
  ]
}

The label identifies the case in reports and is used in screenshot naming. It is also the value searched by the CLI’s label filter. Keep labels distinct and specific enough that a filter selects only the intended cases. The url is the page BackstopJS captures for the test. The README identifies label and url as required; other scenario properties are optional and should be added when a page needs particular setup or capture behavior.

If the project has not been initialized, the documented backstop init command scaffolds a starting configuration. Check the README and schema for the BackstopJS version installed in your project before relying on version-specific options.

2. Share common settings with scenarioDefaults

When several cases need the same scenario settings, define them once under scenarioDefaults. Each scenario can then contain only its label, URL, and any exceptions. Scenario-level values override the defaults.

{
  "id": "shop",
  "scenarioDefaults": {
    "readySelector": "main",
    "delay": 500
  },
  "scenarios": [
    {
      "label": "Product listing",
      "url": "https://example.test/products"
    },
    {
      "label": "Product detail",
      "url": "https://example.test/products/example",
      "delay": 1200
    }
  ]
}

In this example, both pages wait for main. The detail page uses its own delay, replacing the shared 500 millisecond value. Defaults are useful for settings that genuinely apply across cases, such as common selectors or wait behavior. Keep page-specific exceptions on the individual scenario so the shared configuration stays understandable.

Watch out for explicit empty arrays

An explicitly supplied empty array is still a scenario-level value. For example, "selectors": [] on a scenario overrides selectors supplied by scenarioDefaults; it does not mean “inherit the default.” Remove the property when you want the default applied. Use an empty array only when intentionally clearing that setting, and confirm the behavior against your installed version.

3. Generate scenarios from route data in JavaScript

For a large route list or repeated page patterns, use a JavaScript config and construct ordinary scenario objects from data. This avoids copying the same fields into every entry while keeping the route inventory easy to review.

const routes = [
  { label: 'Product listing', path: '/products' },
  { label: 'Product detail', path: '/products/example' }
];

const scenarios = routes.map(({ label, path }) => ({
  label,
  url: `https://example.test${path}`,
  readySelector: 'main'
}));

module.exports = {
  id: 'shop',
  viewports: [{ label: 'desktop', width: 1280, height: 800 }],
  scenarioDefaults: {
    delay: 500
  },
  scenarios
};

Save this as a JavaScript configuration file and pass its path with --config, as supported by the project documentation. For example, if the file is named backstop.config.js, the command shape is:

backstop test --config=backstop.config.js

The route-building function is regular JavaScript; the resulting value must have the BackstopJS configuration shape. Validate labels and paths when route data comes from another file or is generated dynamically. A missing path can create an unintended URL, while duplicate labels make filtering and report review harder. The exact helper layout is a project choice, not a required BackstopJS folder structure.

Using the Node API

The Node API can accept a configuration object directly. This is useful when another Node application or task runner already constructs the route list:

const backstop = require('backstopjs');

const config = {
  id: 'shop',
  viewports: [{ label: 'desktop', width: 1280, height: 800 }],
  scenarios: [
    { label: 'Product listing', url: 'https://example.test/products' }
  ]
};

backstop('test', { config });

Use the API entry point and invocation pattern documented for the BackstopJS version installed in the project. The example illustrates the documented ability to pass a config object; it is not a claim about a particular project’s runtime or installed version.

4. Run all scenarios or just a subset

Use backstop test to capture test images and compare them with the current references. To work on one route, use --filter with a regular expression that matches scenario labels:

# Run the configured suite
backstop test

# Run labels containing "Product detail"
backstop test --filter="Product detail"

# Run labels beginning with "Product"
backstop test --filter="^Product"

Quote the filter so the shell passes the regular expression intact. A broad expression can match more scenarios than intended; inspect the labels and report output when narrowing a run. Filtering is useful for fast iteration, but run the full configured suite in the project’s normal validation workflow so changes affecting shared components are checked across routes.

5. Understand reference, test, and approve

BackstopJS separates creating baselines, comparing captures, and accepting changes:

  1. backstop reference captures the reference images. It normally uses each scenario’s url; if referenceUrl is configured, that URL is used for the reference capture.
  2. backstop test captures the test images and compares them with the references.
  3. backstop approve promotes the latest test images to the reference collection.

reference creates baselines and does not perform the comparison. Review the test differences before approving: a changed image may represent an intended UI update, a real regression, or unstable page content. Treat baseline approval as an explicit review decision rather than an automatic side effect of adding a scenario.

# Create or refresh reference captures
backstop reference

# Capture and compare against references
backstop test

# After reviewing the differences, accept the latest test images
backstop approve

For CI, the official project documentation describes integrating BackstopJS into a build process. A Node task runner can also invoke the Node API. Keep the comparison step in the normal validation process and make baseline changes reviewable.

6. Troubleshooting scenarios

Symptom Likely cause Fix
A scenario does not appear in the run It is missing from the top-level scenarios array, or a filter does not match its label. Check the config file being loaded, verify the scenario is in the array, then run without a filter or use a label-matching regular expression.
A route is captured at the wrong address The URL was assembled incorrectly from route data or the wrong config was loaded. Inspect the generated scenario object and run with the intended --config path.
A shared setting seems ignored The scenario defines the same property and overrides the default; an empty array can also override a default array. Remove the scenario-level property to inherit, or set the intended scenario-specific value explicitly.
Reference and test images differ unexpectedly The page state, content, or capture URL differs, or the reference needs deliberate updating. Check the scenario URL, any referenceUrl, and the page’s readiness behavior. Review the diff before using approve.
Several cases run when one was intended The filter is a regular expression and matches multiple labels. Use a narrower expression, such as an anchored label pattern, and quote it for the shell.
JavaScript configuration does not load The config path or export shape is wrong, or the command is using the default JSON path. Pass the JavaScript file through --config, verify it exports a configuration object, and check the installed version’s documentation.

7. Performance, reliability, and maintenance

  • Keep route data compact. Generate repeated cases from a small list of labels and paths instead of maintaining copied objects that can drift apart.
  • Use focused runs while editing. A label filter reduces the cases captured during local iteration. Follow it with the project’s normal full-suite run when validating shared changes.
  • Wait for a meaningful page state. Shared readiness settings are useful only if they apply to every page. Put exceptions on scenarios that need different readiness behavior and confirm options against the installed version.
  • Control baseline changes. Reference captures, comparisons, and approval have different purposes. Review diffs before promoting new images so a regression is not mistaken for an intended update.
  • Check configuration at the source. When a run behaves unexpectedly, inspect the resolved JavaScript data and the config path before changing baselines.

BackstopJS itself does not establish a fixed cost per scenario in the cited configuration documentation. Runtime and infrastructure cost depend on the project’s execution environment and suite. Keep suites scoped to the pages that provide useful coverage, while retaining full validation where shared UI changes can affect multiple routes.

Or skip the browser setup

If you need a screenshot artifact rather than a local visual-regression baseline, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. The API accepts the parameter names used by other screenshot APIs, which can make switching straightforward. See the ScreenshotNeo API documentation for the available options.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.test/products \
  -o shot.webp

Cookie banners are accepted like a visitor and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before the shot; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response says which outcome occurred in X-Page-Verdict and X-Billed headers. An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Get 1,000 screenshots a month free with no card.

FAQ

Can a scenario use a different URL for its reference image?

Yes. Configure referenceUrl when the baseline should come from a different URL than the scenario’s test URL.

Should I put every scenario property in scenarioDefaults?

No. Use defaults only for values shared by the cases. Keep route-specific behavior on the relevant scenario so exceptions are visible.

Does adding a scenario automatically update its baseline?

No. Create references with backstop reference, compare with backstop test, and approve reviewed changes with backstop approve.

When is JavaScript config worth using?

Use it when route data is generated, configuration needs comments or functions, or another Node task already builds the scenario list. A static JSON file is simpler for a small, fixed suite.