How to Test Authenticated Pages with BackstopJS
Test login-protected pages with BackstopJS by loading a reusable session, waiting for the authenticated view, and comparing stable screenshots.
To test pages that require authentication with BackstopJS, give its browser a valid authenticated state before capture, wait until the intended application view is ready, and compare that screenshot with an approved reference. You can import cookies with cookiePath, prepare browser state in an onBeforeScript, or use Playwright’s storageState to load cookies and local storage. Choose the method that matches how your application stores its session.
BackstopJS captures a reference image and a new test image, then reports visual differences. Review a changed image before running backstop approve, which updates the reference baseline. Authentication is only one part of the setup: the session must still be valid, the application must finish rendering, and the same meaningful view must be captured on each run. [BackstopJS documentation]
1. Understand the capture and approval cycle
- Define a scenario for the page and viewport you want to check.
- Run
backstop referenceto create the approved baseline in a known-good state. - Run
backstop testto capture the current page and compare it with the baseline. - Inspect the report and decide whether each difference is an intended UI change or a regression.
- Only after review, run
backstop approveto replace the reference image with the new capture.
BackstopJS can run in a build or deploy workflow and can produce CI/JUnit reports. A layout test failure returns a nonzero exit status, so a CI job can stop when a visual comparison fails. Keep reference generation and approval under deliberate review; approving every CI result automatically removes the value of the comparison. [BackstopJS repository documentation]
2. Choose how to provide authenticated state
| Method | Use it when | State it covers |
|---|---|---|
cookiePath |
You already have a suitable JSON cookie file. | Cookies represented in that file. |
onBeforeScript |
You need scenario-specific setup or custom browser preparation. | Whatever state your script establishes for the configured engine. |
Playwright storageState |
Your authenticated session needs cookies and local storage. | Cookies and local storage from a Playwright state file. |
| Automated login flow | A static state file is insufficient and your app permits a repeatable test login. | The state produced by the scripted flow; exact implementation depends on the app and identity provider. |
These approaches are alternatives, not interchangeable settings. A cookie file cannot supply local storage values. Playwright storage state is a Playwright engine option; do not pass it to Puppeteer and expect it to work. BackstopJS documents Puppeteer as the default engine and Playwright as an option for Chromium, Firefox, or WebKit. Verify option behavior against the README for the BackstopJS version installed in your project. [BackstopJS README]
Option A: Import a cookie file
Use cookiePath when the server-side session is represented by cookies and the cookie file can be refreshed safely. BackstopJS’s default onBefore script imports the JSON file. The path is relative to the current working directory, so run the CLI from the directory expected by the configuration.
// backstop.json (relevant portion)
{
"scenarios": [
{
"label": "Account dashboard",
"url": "https://example.com/account",
"cookiePath": "./test-state/account-cookies.json",
"readySelector": "[data-testid='account-dashboard']",
"readyTimeout": 30000
}
]
}
The example assumes a cookie JSON file in a format accepted by the installed BackstopJS engine and a page selector that appears only after the dashboard is ready. The actual cookie values and accepted cookie shape are application- and version-dependent. Never commit a real session file or token to a public repository.
Option B: Set state in a custom before script
Use a custom onBeforeScript when a static import is not enough or each scenario needs its own preparation. BackstopJS invokes the setup before each scenario. The script receives the browser page and scenario; the exact browser APIs available depend on the configured engine. The following Puppeteer-style example loads a JSON array of cookies from a private file.
// backstop.json (relevant portion)
{
"paths": {
"engine_scripts": "./backstop_data/engine_scripts"
},
"scenarios": [
{
"label": "Account dashboard",
"url": "https://example.com/account",
"onBeforeScript": "load-account-session.js",
"readySelector": "[data-testid='account-dashboard']",
"readyTimeout": 30000
}
]
}
// backstop_data/engine_scripts/load-account-session.js
const fs = require('fs');
const path = require('path');
module.exports = async (page, scenario) => {
const sessionFile = path.resolve(process.cwd(), 'test-state/account-cookies.json');
const cookies = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
if (!Array.isArray(cookies) || cookies.length === 0) {
throw new Error(`No cookies found for ${scenario.label}`);
}
await page.setCookie(...cookies);
};
Place script files under the directory configured by paths.engine_scripts. This example uses Puppeteer’s page.setCookie API; adapt it to the browser engine and cookie representation your installed version uses. A scripted login is another possibility, but its selectors, MFA behavior, and identity-provider rules are app-specific and are not guaranteed by BackstopJS configuration alone. [BackstopJS hook documentation]
Option C: Load Playwright storage state
When the application stores authentication data in cookies and local storage, use BackstopJS’s Playwright engine and point engineOptions.storageState to a state JSON file created for the test account. The repository documents this as a way to set cookies and local storage before a capture.
// backstop.json (relevant portion)
{
"engine": "playwright",
"engineOptions": {
"browser": "chromium",
"storageState": "./test-state/playwright-state.json"
},
"scenarios": [
{
"label": "Account dashboard",
"url": "https://example.com/account",
"readySelector": "[data-testid='account-dashboard']",
"readyTimeout": 30000
}
]
}
BackstopJS documents Playwright browser choices including Chromium, Firefox, and WebKit. Confirm the exact engineOptions shape for your installed BackstopJS release. Generate the state with an authorized test account, keep it outside version control, and refresh it when it expires. A saved state does not bypass an identity provider’s MFA, expiry, or session policy. [BackstopJS README]
3. Make the authenticated page deterministic
A valid session does not prove that the screenshot shows the desired view. The browser may still show a loading shell, redirect, access-denied message, or partially rendered app. Wait on a condition tied to the view under test.
readySelector: wait until a meaningful element exists, such as the dashboard root or signed-in navigation.readyEvent: wait for the app to log a chosen readiness string when the application exposes one.delay: add a fixed pause for a known late-rendering state. A selector or explicit app event is usually more directly tied to completion than an arbitrary pause; this is an implementation recommendation based on the documented readiness options.readyTimeout: bound the wait so a missing state fails instead of hanging indefinitely.onReadyScript: perform additional work once the page is ready, for example choosing a tab that is part of the view being tested.
Use documented scenario properties such as url, optional referenceUrl, cookiePath, onBeforeScript, readySelector, readyEvent, readyTimeout, delay, and onReadyScript for their intended roles. BackstopJS’s custom onBefore handler also receives page, scenario, viewport, isReference, Engine, and config. [BackstopJS README]
// Scenario example with an explicit view and readiness condition
{
"label": "Billing settings, signed in",
"url": "https://example.com/settings/billing",
"cookiePath": "./test-state/billing-cookies.json",
"readySelector": "[data-testid='billing-settings']",
"readyTimeout": 30000,
"delay": 250,
"selectors": ["[data-testid='billing-settings']"]
}
Choose capture scope deliberately. If you specify a CSS selector, BackstopJS captures the first matching element by default. Enable selectorExpansion when the scenario should capture every matching element, and use expect when you need to assert how many elements are selected. An overly broad selector can add irrelevant page changes; a narrowly chosen target can miss a broken surrounding layout. [BackstopJS scenario documentation]
4. Keep login state secure and maintainable
- Use a dedicated test account with only the permissions needed by the scenario.
- Keep cookie files, storage-state JSON, passwords, and session tokens out of source control and logs.
- Provide state to CI through its protected secret or artifact mechanism, and restrict access to jobs that need it.
- Refresh expired state using a controlled process; do not assume a saved browser session is permanent.
- Use separate state files for different roles or permission levels so a test cannot silently capture the wrong account view.
- Make setup failures explicit. A missing session should fail at the readiness check or setup step rather than produce an approved-looking login screenshot.
BackstopJS documents the mechanisms for importing or setting browser state. Credential rotation, MFA policy, session expiration, and safe storage are responsibilities of the application and CI environment rather than guarantees of the screenshot tool.
5. Run the visual test in CI
- Pin the BackstopJS version and use the same engine and browser choice when generating references and test captures.
- Supply valid auth state to the job without checking it into the repository.
- Run the visual test as part of the build or before deployment.
- Publish the generated report or JUnit output where reviewers can inspect failures.
- Review image differences before approving a new baseline.
Rendering can vary between machines and environments. BackstopJS recommends Docker as one way to reduce such variation; it helps make the environment more consistent but does not eliminate every difference. Fonts, browser versions, viewport size, remote data, animation, timestamps, and account state can still affect pixels. [BackstopJS repository documentation]
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The capture shows a login page. | Cookies or storage state are missing, expired, for the wrong domain, or loaded after navigation. | Check the state file, its domain and expiry, and confirm the setup hook runs before the protected page is captured. For local-storage authentication, use the Playwright storage-state path where appropriate. |
| The page redirects to an access-denied or MFA screen. | The test account, identity-provider policy, or session does not permit this automated flow. | Use an authorized test account and an approved reusable state or test login flow. Follow your identity provider’s requirements; a screenshot configuration cannot remove those requirements. |
cookiePath cannot find the file. |
The path is resolved relative to the current working directory, which differs in local runs and CI. | Use a path relative to the CLI’s working directory or configure a project path and ensure CI creates the file before the run. |
| Cookie setup throws or has no effect. | The cookie JSON shape may not match the configured engine API, or cookie domain/path attributes do not apply to the target URL. | Validate the file format against the installed BackstopJS and engine version. Confirm the cookie is scoped to the target domain and use the engine-specific setup API. |
storageState is ignored or rejected. |
The scenario is using Puppeteer, or the option is placed in the wrong engine configuration. | Set the Playwright engine and place the state file in Playwright’s documented engineOptions. Do not mix Playwright-only configuration into a Puppeteer run. |
| Readiness times out although authentication worked. | The selector never appears, is different for this role, or the app uses a different render lifecycle. | Choose a selector for the actual authenticated view, verify it in the page, or use a documented ready event or suitable delay. Check for an application error in the capture. |
| Screenshots differ on every run. | Dynamic content, animation, remote data, fonts, viewport, browser, or session variation affects rendering. | Stabilize test data and browser environment, use a consistent viewport, wait for the intended state, and follow BackstopJS’s Docker guidance if environment drift is a factor. |
| Only part of a repeated component is captured. | Selector capture defaults to the first match. | Use selectorExpansion when all matches are intended and set expect to the expected count. |
| CI fails but local run passes. | Different browser/runtime, missing state file, environment variables, fonts, or network access. | Compare engine and browser versions, confirm CI provisions the same test state, and inspect the CI report and readiness failure before updating references. |
| Reference approval hides a real regression. | The baseline was updated without reviewing the visual diff. | Require a person to inspect intended changes before running backstop approve. |
7. Performance, reliability, and cost considerations
Each scenario requires browser navigation, state setup, readiness waiting, image capture, and comparison. Keep scenario count and captured regions focused on the coverage you need; full-page captures and slow authenticated routes naturally take longer than a stable element capture. Prefer event- or selector-based readiness over long blanket delays so a fast run can proceed promptly while a slow run still waits for the relevant state.
Reliability depends on repeatable state and rendering conditions. Sessions expire, protected pages may redirect, and remote content can change independently of your code. Use explicit readiness checks, controlled test data, consistent browser settings, and reviewed references. Docker can reduce environment variation, but it is not a guarantee of pixel-identical output across every dependency.
BackstopJS is an open-source project; compute and maintenance costs depend on the environment used to run the browser, CI duration, and the work required to maintain sessions and references. The supplied project documentation does not establish a per-screenshot service price or a universal runtime benchmark, so estimate from your own scenario set and CI environment. [BackstopJS repository]
8. Or skip the browser setup
For a one-off screenshot of an authenticated page, or when you do not need a BackstopJS visual-regression baseline, ScreenshotNeo provides a website screenshot API and MCP server. A typical public-page request looks like this; see the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. This API example does not itself establish a logged-in session; authenticated capture requires the appropriate supported request configuration and credentials. For repeatable visual regression against approved baselines, keep the BackstopJS workflow above.
Create a free ScreenshotNeo account for 1,000 screenshots a month, with no card required.
FAQ
Can BackstopJS use a saved session?
Yes. It can import cookies with cookiePath, run custom setup, or use Playwright storage state for cookies and local storage. Whether saved state remains valid depends on the application and identity provider.
Does a cookie file handle every authenticated app?
No. It works when the required authentication state is represented by compatible cookies. Apps that depend on local storage or additional setup need another approach.
Should I approve every failed visual test?
No. Review the reported difference first, then approve only when the new image is the intended baseline.
Can I use BackstopJS to test Firefox or WebKit?
The repository documents browser selection through its Playwright engine, including Chromium, Firefox, and WebKit. Check the documentation matching your installed version for exact configuration.
Does ScreenshotNeo replace authenticated visual regression testing?
A screenshot API can simplify an individual capture, while BackstopJS supplies the reference-and-diff approval workflow described here. Choose based on whether you need regression comparison and how your page’s authenticated state is provisioned.


