Puppeteer Screenshot Testing in GitHub Actions: Setup for Indian Developers
Set up repeatable Puppeteer screenshots in GitHub Actions with Node.js, browser caching, Linux fonts, and saved artifacts. The workflow is the same for developers in India.
To run Puppeteer screenshot tests in GitHub Actions, add Puppeteer to your Node.js project, commit the lockfile, install dependencies and the browser in the workflow, capture a page with page.screenshot(), and upload the output as an artifact. The hosted workflow runs on GitHub’s selected runner, so being in India does not require a different Puppeteer configuration.
The example below assumes an npm project with a package-lock.json. Puppeteer’s install process normally downloads a compatible Chrome for Testing browser; if a package manager skips install scripts, that browser may be missing. See the Puppeteer installation guide.
1. Add a screenshot script
Install Puppeteer and commit both package.json and package-lock.json:
npm install --save-dev puppeteer
Create scripts/screenshot.mjs. This runnable example accepts a URL and output path, fixes the viewport and device scale factor, waits for the page to reach a useful state, and saves a full-page PNG.
import puppeteer from 'puppeteer';
const url = process.env.SCREENSHOT_URL ?? 'http://127.0.0.1:3000/';
const output = process.env.SCREENSHOT_PATH ?? 'artifacts/home.png';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({
width: 1440,
height: 900,
deviceScaleFactor: 1,
});
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60_000 });
await page.screenshot({ path: output, fullPage: true, type: 'png' });
} finally {
await browser.close();
}
The official screenshot guide documents Page.screenshot() and its capture options. Add a package script so local and CI runs use the same command:
{
"scripts": {
"screenshot": "node scripts/screenshot.mjs"
}
}
The target application must be running before this script executes. For a static page, serve the build locally; for an application, start its development or preview server and wait for its ready signal before capturing.
2. Run it in GitHub Actions
Create .github/workflows/screenshots.yml. This workflow checks out the repository, selects Node, installs from the lockfile, caches Puppeteer’s browser download, captures the page, and retains the image even when capture fails.
name: Screenshot
on:
push:
pull_request:
jobs:
screenshot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- name: Cache Puppeteer browser
uses: actions/cache@v4
with:
path: ~/.cache/puppeteer
key: ${{ runner.os }}-puppeteer-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-puppeteer-
- name: Install dependencies
run: npm ci
- name: Install Puppeteer browser
run: npx puppeteer browsers install chrome
- name: Build application
run: npm run build
- name: Start application and capture
env:
SCREENSHOT_URL: http://127.0.0.1:3000/
SCREENSHOT_PATH: artifacts/home.png
run: |
npm run preview -- --host 127.0.0.1 > /tmp/app.log 2>&1 &
echo $! > /tmp/app.pid
for attempt in $(seq 1 60); do
if curl --fail --silent http://127.0.0.1:3000/ > /dev/null; then
break
fi
sleep 1
done
curl --fail http://127.0.0.1:3000/ > /dev/null
mkdir -p artifacts
npm run screenshot
- name: Upload screenshot
if: always()
uses: actions/upload-artifact@v4
with:
name: page-screenshot
path: artifacts/
if-no-files-found: warn
retention-days: 7
Adjust the build and preview commands, port, URL, Node version, and action versions to match the project. Choose a Node version supported by the application and check current GitHub Actions and Puppeteer requirements when publishing or updating the workflow. Puppeteer’s own CI workflow is a first-party example of browser caching, Linux test execution, and artifact upload; its repository-specific commands and pins are not requirements for your project.
3. Make captures repeatable
A screenshot depends on more than the URL. Keep the following inputs explicit when they affect what the page displays:
- Viewport and device scale: fix the viewport dimensions and
deviceScaleFactor. Use a different preset only when the test is meant to represent that device. - Page readiness: select a readiness condition that matches the app.
networkidle2waits for network activity to quiet, but analytics, polling, or long-lived requests can prevent a network-idle condition. Waiting for a specific selector can be more reliable for dynamic applications. - Dynamic content: use stable test data, disable animations where appropriate, and wait for the exact content under test. Avoid capturing while timestamps, rotating content, or asynchronous images are still changing.
- Fonts and locale: install fonts the app uses if the runner lacks them. Set locale and timezone in the browser context when they change visible dates, number formatting, or text. These values are practical controls for repeatability, not India-specific workflow requirements.
- Browser and runner: browser and runner updates can change rendering. Pin project dependencies and use a consistent environment when visual comparisons need stable output; do not assume different browser or runner versions produce pixel-identical images.
For a focused capture, pass a selector instead of fullPage: true, or capture a viewport-only image. Full-page captures can be large and may expose lazy-loading behavior that a viewport capture does not. Puppeteer’s screenshot guide covers the API details.
4. Choose how the browser is installed
The standard setup lets Puppeteer manage a compatible browser download. This keeps the Puppeteer and browser pairing aligned, and the browser cache can avoid repeated downloads on later runs. An alternative is to provision a browser explicitly, but then the project must keep its executable path and browser version compatible with Puppeteer. Follow Puppeteer’s installation guidance for the chosen arrangement rather than assuming a system Chrome is present.
GitHub-hosted runners are the simplest starting point for most repositories. Self-hosted runners can provide a controlled environment, but the team then owns its installed browser, libraries, fonts, and maintenance. GitHub documents how to install additional software on hosted runners in its runner customization guide.
5. Inspect screenshots and add visual assertions
The artifact makes the actual output available from the workflow run. Open it when a capture fails or a page changes unexpectedly. Artifact retention is configurable; keep enough history for review without retaining files longer than the project needs.
Saving a screenshot is not itself a visual regression test. To fail a build on visual changes, compare the new image against an approved baseline with a visual-diff tool and define an acceptable difference threshold. Keep the browser, fonts, viewport, and input data consistent so the comparison measures application changes rather than environment drift. The cited Puppeteer workflow demonstrates artifact upload, but does not prescribe a particular visual-diff service.
Or skip the browser setup
If the goal is to capture a URL rather than run browser automation inside your project, ScreenshotNeo provides a screenshot API and MCP server. Its API returns a screenshot or PDF from one GET request; see the ScreenshotNeo documentation for its parameters.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture, and each of those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots.
Sign up for 1,000 free screenshots a month, with no card required.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| “Could not find Chrome” or browser executable missing | The Puppeteer install download did not run, or its cache is unavailable. | Check whether package installation scripts were disabled. Run npx puppeteer browsers install chrome in the workflow and confirm the cache path matches Puppeteer’s browser cache location. |
| Browser exits immediately or reports missing shared libraries | Linux launch requirements or runner dependencies are incomplete. | Use a supported runner and follow Puppeteer’s current system requirements and troubleshooting guide. Inspect browser stderr in the Actions log. |
| Navigation times out | The route is not ready, the server did not start, or a persistent request prevents the selected wait condition. | Verify the server URL and readiness loop first. Increase the navigation timeout only if the page legitimately needs longer; consider waiting for a page-specific selector instead of network idle. |
| Screenshot is blank or missing content | The app was not ready, the route redirected, or content is loaded after the chosen capture point. | Check the returned URL and app logs, then wait for a visible selector or other app-specific ready state before calling screenshot(). |
| Text or icons render differently in CI | A font available locally is absent on the Linux runner, or font loading had not finished. | Install the fonts the application actually uses and wait for fonts before capture, for example with await page.evaluate(() => document.fonts.ready). Puppeteer’s troubleshooting guide notes that some character sets require additional fonts. |
| Repeated runs differ | Browser or runner changed, or page state includes time, animation, randomness, or external data. | Fix relevant environment and page inputs, wait for stable content, and avoid relying on a moving external page for a regression baseline. |
| No artifact appears | The capture failed before creating a file, or the artifact path does not match the output path. | Confirm SCREENSHOT_PATH and upload path agree, preserve upload with if: always(), and review the preceding step logs. |
Performance, reliability, and cost
- Performance: browser downloads are a common avoidable cost on repeated jobs, so cache Puppeteer’s browser directory with a key tied to the lockfile and operating system. Reuse one browser process for several pages within a single script when appropriate, then close it in a
finallyblock. - Reliability: use
npm ciwith a committed lockfile, wait for the app’s actual readiness, and upload artifacts even after a failure. Keep a screenshot job isolated from unrelated tests if its browser setup or artifacts need separate diagnosis. - Cost: the dossier does not establish India-specific GitHub Actions pricing or runner allowances. Check the current GitHub plan and usage terms for the repository. Browser screenshots also consume workflow time and artifact storage, so use suitable capture scope and retention.
- India-specific setup: no special hosted-runner configuration follows from the developer’s physical location. Set locale and timezone only to match the product behavior being tested.
FAQ
Does a developer in India need a different GitHub Actions workflow?
No India-specific workflow is established by the cited documentation. The job runs on the runner selected in its workflow configuration.
Does saving a screenshot automatically test the design?
No. It creates an artifact for inspection. Failing on visual changes requires a baseline comparison and a defined threshold.
Can I use this for a page that requires authentication?
Yes, if the test supplies the required state securely, such as through test credentials or an authenticated browser session. Do not commit secrets; use repository or environment secrets and avoid including sensitive page data in retained artifacts.


