How to Capture Screenshots in Karma Tests Running PhantomJS 2
Configure a custom Karma PhantomJS launcher, call page.render from tests, save deterministic artifacts, and troubleshoot CI capture failures.

Direct answer: configure a custom Karma launcher based on PhantomJS, provide an options.onCallback handler that calls PhantomJS page.render(), then ask for the capture from the test page with window.top.callPhantom({ type: 'render', fname: 'path/to/file.png' }). A bare callPhantom('render') does nothing useful unless the launcher receives the callback and invokes page.render in PhantomJS’s script context.
This is a legacy maintenance recipe for Karma suites that still run on PhantomJS 2. PhantomJS is headless command-line software, and its project development is suspended. Keep this approach when an existing test suite depends on it; for new work, evaluate a maintained browser runner separately.
What happens during a capture
Karma test code runs inside the browser page. page.render belongs to the outer PhantomJS script, so the page cannot call it directly. window.top.callPhantom bridges those contexts:
- The test finishes arranging the DOM and calls
window.top.callPhantom. - PhantomJS passes the object to the launcher’s
onCallbackfunction. - The callback validates the message and calls
page.render(filename). - PhantomJS writes an image or PDF to the requested path.
The PhantomJS page.render API supports PNG, JPEG, GIF, and PDF output. The rendered viewport comes from page.viewportSize; clipRect can restrict the output to a region.
Install the Karma integration
The Karma integration is provided by the karma-phantomjs-launcher package. In an older project, install it alongside Karma and PhantomJS:

npm install --save-dev karma karma-phantomjs-launcher phantomjs-prebuilt
Use the versions already supported by your project when possible. PhantomJS 2 has an older JavaScript engine, so avoid relying on modern syntax in the launcher file or test bundle unless your build transpiles it.
Configure a custom PhantomJS launcher
Add a launcher to karma.conf.js. The callback receives the data sent by callPhantom. The page object is available in this PhantomJS launcher context, where render can be called.
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: [
'src/**/*.js',
'test/**/*.spec.js'
],
plugins: [
'karma-jasmine',
'karma-phantomjs-launcher'
],
customLaunchers: {
PhantomJSCustom: {
base: 'PhantomJS',
options: {
onCallback: function (data) {
if (!data || data.type !== 'render') return;
var filename = data.fname || '.tmp/screenshots/unnamed.png';
page.render(filename);
}
}
}
},
browsers: ['PhantomJSCustom'],
singleRun: true,
reporters: ['progress']
});
};
Create the output directory before Karma starts. PhantomJS does not create missing parent directories for you:
mkdir -p .tmp/screenshots
npm test -- --single-run
A workspace-relative directory makes CI artifact collection predictable. If tests can run in parallel, include a suite name, browser identifier, or build number in each filename.
Request a screenshot from a test
Expose a small helper in the test bundle. The guard makes the helper harmless when the same tests run in a normal browser without PhantomJS.
var renderId = 0;
function takeScreenshot(file) {
if (!window.top.callPhantom) return;
var options = {
type: 'render',
fname: file || '.tmp/screenshots/' + (renderId++) + '.png'
};
window.top.callPhantom(options);
}
describe('checkout', function () {
it('renders the completed state', function () {
// Arrange the page and wait for the state your test needs.
document.body.setAttribute('data-state', 'complete');
takeScreenshot('.tmp/screenshots/checkout-complete.png');
});
});
Pass an explicit filename when the image is an artifact that a person or another job will inspect. The counter fallback is useful for exploratory captures, but deterministic names are better for visual comparisons and CI retention.
Wait until the page is actually ready
A screenshot request is synchronous from the test’s point of view, but the page may still be changing. Capture only after the application has reached a stable state. In asynchronous tests, wait for the relevant callback, DOM marker, or network-driven render before calling the helper.
it('captures loaded results', function (done) {
loadResults(function () {
var ready = document.querySelector('[data-results-ready="true"]');
if (!ready) {
done.fail('Results never became ready');
return;
}
takeScreenshot('.tmp/screenshots/results.png');
done();
});
});
PhantomJS can render before fonts, images, or animations settle. Disable animations in a test stylesheet, wait for image complete state when needed, and use a stable fixture rather than a live endpoint. If a page continuously animates, capture after setting a deterministic class or frozen clock state.
Viewport size and clipping
The launcher pattern controls the callback, while PhantomJS page settings control what is visible. Set a viewport before loading the Karma page when you need a known layout:
page.viewportSize = {
width: 1280,
height: 900
};
Use clipRect for a focused region. The exact property setup depends on how your Karma launcher exposes the page in your PhantomJS version, so keep page configuration in the PhantomJS-side launcher code rather than attempting to set it from the browser test.
page.clipRect = {
top: 80,
left: 120,
width: 640,
height: 480
};
page.render('.tmp/screenshots/panel.png');
Use PNG for lossless debugging, JPEG when file size matters, GIF only for compatibility with an existing pipeline, and PDF when the artifact is intended as a printable page. Match the extension to the requested format.
Use a safer callback contract
Callback data comes from the page, so validate it before using it as a path. At minimum, check the message type and require a filename or generate one. You can also restrict captures to a known directory and reject path traversal characters in a shared test environment.
function safeFilename(value) {
if (typeof value !== 'string' || !value) {
return '.tmp/screenshots/unnamed.png';
}
// Keep this simple for a test artifact directory.
var name = value.replace(/[^a-zA-Z0-9._\/-]/g, '_');
if (name.indexOf('..') !== -1) {
return '.tmp/screenshots/unnamed.png';
}
return name;
}
// Inside options.onCallback:
if (data && data.type === 'render') {
page.render(safeFilename(data.fname));
}
Common errors and fixes
| Symptom | Cause | Fix |
|---|---|---|
callPhantom is undefined |
The test is running in a normal browser, or the PhantomJS bridge is unavailable. | Keep the guard in takeScreenshot; confirm the custom launcher is the browser selected by Karma. |
Calling window.top.callPhantom('render') does nothing |
No onCallback handler interprets the message, or the handler expects an object with type. |
Send { type: 'render', fname: '...' } and add the callback that calls page.render. |
| No file appears | The parent directory does not exist, or the process cannot write there. | Create .tmp/screenshots before Karma and check CI workspace permissions. |
| Only one image survives | Every test uses the same filename. | Use suite-specific names or a counter; include the build or shard identifier for parallel jobs. |
| The image is blank | Capture happened before the DOM or assets were ready. | Wait for a readiness marker, image completion, or application callback; disable transitions. |
| Unexpected dimensions | PhantomJS used its default viewport. | Set page.viewportSize in the PhantomJS-side configuration and keep layout fixtures deterministic. |
| Karma cannot start PhantomJS | The launcher or PhantomJS binary is missing, incompatible, or unavailable on the CI image. | Verify the dev dependencies, install the expected binary for the agent architecture, and run the same command locally and in CI. |
| Capture works locally but not in CI | Different working directory, missing output path, or artifact cleanup. | Use workspace-relative paths, create the directory in the job, and configure CI to collect it after tests. |
Debugging checklist
- Confirm
browserscontainsPhantomJSCustom, not onlyPhantomJS. - Log the callback object temporarily to verify that
typeandfnamearrive. - Use an absolute path once to distinguish a path problem from a callback problem.
- Capture a simple static page before debugging application rendering.
- Check the CI job’s current directory and write permissions.
- Preserve the screenshot directory as a build artifact even when the test command fails.
Performance, reliability, and maintenance
Rendering adds work to every test that requests an image. Capture at assertion boundaries or on failure instead of after every test. Keep screenshots out of the critical path when possible by running a focused artifact suite. Small, deterministic fixtures reduce render time and make diffs meaningful.

For reliability, avoid live third-party resources, random data, current-time labels, and animated content. Pin the PhantomJS and launcher versions used by CI. Treat the output directory as disposable build data, and clean it at the start of a run so stale files cannot be mistaken for current artifacts.
PhantomJS’s project homepage states that development is suspended, and the project documentation describes PhantomJS as a headless runtime rather than a test framework. That status matters when choosing whether to invest in new tooling around this recipe. The configuration remains useful for maintaining an existing Karma/PhantomJS 2 suite, but browser-engine currency and future dependency support should be part of any migration plan.
Or skip the browser setup
If your goal is a URL screenshot rather than an assertion inside a legacy Karma browser, ScreenshotNeo provides a single GET request that returns PNG, JPEG, WebP, or PDF. It removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status.
See the ScreenshotNeo API documentation for the complete option list. The service supports full-page capture with lazy images, CSS-selector element capture, dark mode, device presets, custom viewports, retina scale, PDF paper and margin settings, custom CSS and JavaScript, clicks, selector or network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL-based caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
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)
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()));
ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots each month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account and start with the included monthly shots.
FAQ
Can I call page.render directly from a Jasmine test?
No. Jasmine runs in the page context. Send a callback through window.top.callPhantom and let the PhantomJS launcher call page.render.
Does the filename have to end in .png?
No. PhantomJS supports PNG, JPEG, GIF, and PDF. Use an extension that matches the format your artifact consumers expect.
Where should CI store the files?
Use a workspace-relative directory such as .tmp/screenshots/, create it before the run, and configure the CI system to upload it after tests finish.
Why is this considered legacy?
PhantomJS development is suspended. The recipe is appropriate for maintaining an existing PhantomJS 2 suite; new projects should assess a maintained browser engine and runner.


