Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

If PhantomCSS saves ten screenshots but every file shows the first page, the loop is usually running synchronously inside one CasperJS callback while navigation and rendering are asynchronous. Queue one CasperJS step per iteration, change the page inside that step, wait for a page-specific ready condition, and then capture with a unique name. A fixed delay can mask the race, but a condition-based wait is safer.

Why every iteration captures the first page

PhantomCSS is a CasperJS module that takes screenshots and compares them with baseline images through Resemble.js. CasperJS executes its then callbacks in order, but JavaScript code inside one callback still runs immediately. A for loop can therefore issue ten page-change requests and ten capture calls before the browser has completed even the first transition.

The later captures then observe the same DOM state, or a partially updated state, instead of pages 2 through 10. This is an orchestration problem, not normally a PhantomCSS filename or comparison problem.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The reliable pattern: queue, change, wait, capture

Create a separate CasperJS step for each target page. In that step, trigger the application’s page change, wait until the application exposes proof that the requested page is ready, and only then call phantomcss.screenshot.

#1 Best Overall
Sale
var firstPage = 1;
var lastPage = 10;

for (var pageNo = firstPage; pageNo <= lastPage; pageNo++) {
    (function (targetPage) {
        casper.then(function () {
            this.evaluate(function (page) {
                moveNext(page); // application-specific page change
            }, targetPage);

            this.waitFor(function () {
                return this.evaluate(function (page) {
                    var indicator = document.querySelector('#page-number');
                    return indicator &&
                           indicator.textContent.trim() === String(page);
                }, targetPage);
            }, function () {
                phantomcss.screenshot('html', 'page-' + targetPage);
            }, function () {
                this.die('Timed out waiting for page ' + targetPage);
            }, 10000);
        });
    }(pageNo));
}

casper.run();

moveNext and #page-number are placeholders. Replace them with the function and readiness marker used by your application. The immediately invoked function expression preserves targetPage for older JavaScript runtimes; without it, callbacks may all refer to the final loop value.

Use the application’s strongest readiness signal

  • Page indicator: wait for a pager element to contain the requested number.
  • Unique text: wait for a heading or record label that only exists on that page.
  • Target element: wait for the selector that appears after the transition.
  • Resource completion: wait for the request or asset that supplies the new content.

CasperJS wait methods are not generally chainable; place a wait inside a casper.then step when you need it to participate in the ordered queue. Always provide a timeout callback so a missing page fails loudly instead of producing a plausible but wrong image.

Adapt the loop to common pagination models

Clicking a Next button

for (var pageNo = 1; pageNo <= 10; pageNo++) {
    (function (targetPage) {
        casper.then(function () {
            if (targetPage > 1) {
                this.click('#next');
            }
            this.waitForSelector('#results[data-page="' + targetPage + '"]',
                function () {
                    phantomcss.screenshot('#results', 'results-page-' + targetPage);
                },
                function () {
                    this.die('Results page ' + targetPage + ' did not load');
                }, 10000);
        });
    }(pageNo));
}

Use an attribute, text node, or other marker that changes on every click. Waiting only for #results is insufficient if that element exists on page 1 and is merely repopulated.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Changing a URL or route

for (var pageNo = 1; pageNo <= 10; pageNo++) {
    (function (targetPage) {
        casper.thenOpen('https://example.test/list?page=' + targetPage);
        casper.then(function () {
            this.waitForSelector('#page-number', function () {
                var actual = this.fetchText('#page-number').trim();
                if (actual !== String(targetPage)) {
                    this.die('Expected page ' + targetPage + ', got ' + actual);
                }
                phantomcss.screenshot('html', 'page-' + targetPage);
            }, function () {
                this.die('Timed out on page ' + targetPage);
            }, 10000);
        });
    }(pageNo));
}

For route navigation, the navigation step itself is asynchronous, so the capture still belongs in a later queued step after the readiness check.

Fixed delays versus condition-based waits

Approach When it helps Risk
Fixed delay A simple page with a known, stable render time The delay may be too short on a slow run or waste time on a fast run; an eight-second value from one historical report is not a universal setting.
Condition-based wait Pages exposing a page number, selector, text, or resource signal Requires a marker that truly changes for each state; a weak marker can pass before rendering is complete.

If no reliable marker exists, a short delay can be a fallback, but combine it with an assertion on the resulting state whenever possible. Do not copy a delay from another site or machine without measuring your own application.

Make each screenshot auditable

Pass an explicit name such as page-1, page-2, and page-10. Default generated names such as screenshot_0.png make it harder to match an image to an iteration and to select the correct baseline. Include other stable identifiers when useful, for example orders-page-3-dark.

During diagnosis, log the requested page and the observed marker immediately before capture:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
this.echo('Expected ' + targetPage + ', observed ' +
          this.fetchText('#page-number').trim());

If the log is correct but files still look identical, inspect the capture selector, CSS state, and baseline comparison. If the log is wrong, fix navigation or the wait condition first.

Keep visual regression runs deterministic

  • Use static fixtures or fake data when records, ads, timestamps, or rotating recommendations can change between runs.
  • Freeze the viewport, device scale, locale, timezone, and authentication state used by the baseline.
  • Hide animation or wait until transitions finish before capturing.
  • Ensure lazy-loaded images and fonts have completed before the readiness marker is considered valid.
  • Run one page manually and verify the marker changes before scaling to ten or more iterations.

PhantomCSS comparisons are most useful when the input UI is predictable. Mutable content can create visual diffs even after the loop is correctly synchronized.

Diagnostic checklist for identical files

  1. Confirm the loop schedules steps. The for loop should add callbacks; it should not perform all navigation and captures directly inside one callback.
  2. Confirm the closure value. Log targetPage inside the callback. Every step must print a different value.
  3. Confirm the page-change operation. Check that the click, route assignment, or application function actually changes the page.
  4. Confirm the readiness marker. It must update for every target page, not merely exist from page 1.
  5. Confirm timeout handling. A timeout should stop the run with the page number, not continue to capture stale content.
  6. Confirm the screenshot name. Names must include the iteration or another unique state identifier.
  7. Confirm the capture target. Capturing html and capturing a component can expose different state or styling.
  8. Confirm deterministic data. Remove timestamps, random ordering, and changing network content from regression fixtures.

Common failures and precise fixes

All callbacks use the last page number

Cause: the loop variable is closed over by callbacks in an older runtime. Fix: wrap each iteration in a function, as shown above, or otherwise create a per-iteration value.

The wait passes immediately

Cause: the selector or text already exists on the previous page. Fix: wait for a value containing the requested page, a changed data attribute, or a unique element; clear or invalidate stale content before navigation if necessary.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Intermittent timeouts

Cause: the timeout is shorter than occasional network or rendering time, or the readiness signal depends on a failed request. Fix: inspect network and console behavior, choose a signal tied to successful rendering, and set a timeout appropriate for the application. Do not simply increase the timeout indefinitely.

Correct pages, unstable diffs

Cause: mutable data, animations, delayed images, or environment differences. Fix: use fixtures, disable motion, wait for assets, and keep runtime settings constant.

The code works locally but not in CI

Cause: different PhantomJS/CasperJS versions, fonts, viewport dimensions, credentials, or network speed. Fix: pin the runtime where possible, record environment settings, and make waits state-based rather than tuned to a local delay. PhantomCSS and CasperJS are historical tools; verify that their versions run on your current environment before committing to a new test suite.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server if you need rendered pages without maintaining a CasperJS/PhantomJS loop. A separate request can capture each URL or route after your application exposes it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

One GET request returns PNG, JPEG, WebP, or PDF. The API accepts options for full-page captures, lazy images, CSS selectors, device and viewport settings, custom JavaScript and CSS, waits for selectors, delays or network idle, cookies and headers, blocking resources, and caching. For an asynchronous batch, its bulk capture call accepts up to 100 URLs; signed webhooks can report completion.

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

See the ScreenshotNeo documentation for the complete parameter list and response headers. The same call in Python is:

import requests
r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

And in Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers identify the page verdict and whether the request was billed.
  • An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
  • The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan.

Sign up for ScreenshotNeo to try the free 1,000-shot plan without a card.

FAQ

Does PhantomCSS itself make the loop asynchronous?

No. PhantomCSS captures when its call runs; CasperJS must schedule the surrounding navigation, wait, and capture operations in its ordered step queue.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I capture an element instead of the whole page?

Yes. Pass the element selector to phantomcss.screenshot, but still wait for that element’s content and styling to represent the requested page.

How do I know whether the bug is navigation or capture?

Log the page marker immediately before each capture and open the generated files using their unique names. A wrong marker indicates navigation or waiting; a correct marker with identical crops points to the capture target or page styling.

Frequently Asked Questions

Does PhantomCSS itself make the loop asynchronous?

No. CasperJS must schedule navigation, waiting, and capture in ordered steps.

Can I capture an element instead of the whole page?

Yes; pass its selector, while waiting until its content and styling are ready.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do I separate navigation errors from capture errors?

Log the page marker before capture and inspect uniquely named files; the marker identifies which stage is wrong.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.