ScreenshotNeo

BlogHow-to

How to capture Selenium screenshots in GitHub Actions with headless Chrome

Capture Selenium screenshots with headless Chrome in GitHub Actions, upload them as artifacts, and troubleshoot common CI failures.

By the ScreenshotNeo team4 October 20267 min read

To capture a Selenium screenshot in GitHub Actions, run Chrome with --headless, create a known output directory, and call Selenium’s screenshot API after the page reaches the state you want to inspect. Then upload that directory as a workflow artifact. The example below uses Python; the same flow works with other Selenium bindings.

1. Add a screenshot to a Python Selenium test

This complete script opens a page in headless Chrome, sets a repeatable viewport, saves the current browser window as a PNG, checks that the file was written, and always closes the browser:

from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

output = Path("artifacts")
output.mkdir(parents=True, exist_ok=True)

options = Options()
options.add_argument("--headless")

driver = webdriver.Chrome(options=options)
try:
    driver.set_window_size(1440, 1000)
    driver.get("https://example.com")

    screenshot_path = output / "page.png"
    saved = driver.save_screenshot(str(screenshot_path))
    if not saved:
        raise RuntimeError(f"Could not save screenshot to {screenshot_path}")
finally:
    driver.quit()

Install Selenium as part of the project’s normal dependency setup. For a local run, for example, install it with python -m pip install selenium, then run the script. In CI, use your project’s dependency file or lockfile so the job installs the same Selenium version as the rest of the test suite.

driver.save_screenshot(path) captures the current browser window and writes a PNG. Selenium’s API describes this as capturing the current browsing context. The API also has element screenshot methods; a window screenshot and an element screenshot have different scopes. The basic call does not promise a full-page capture. See the Selenium screenshot documentation and Python WebDriver API.

2. Upload screenshots from GitHub Actions

Save screenshots under a predictable workspace-relative directory and configure the workflow to upload that same path. The upload step should run even if tests fail, because a failed test is often when the screenshot is most useful.

name: Selenium tests

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.x"

      - name: Install dependencies
        run: python -m pip install selenium

      - name: Run browser test
        run: python screenshot.py

      - name: Upload screenshots
        if: ${{ always() }}
        uses: actions/upload-artifact@v4
        with:
          name: selenium-screenshots
          path: artifacts/

This shows the workflow shape. Check the official action documentation and your repository policy before choosing action versions, Python versions, or artifact retention settings; those values can change. GitHub describes artifacts as a way to preserve and share files produced by a workflow after a job completes, and lists screenshots among common artifact examples. See GitHub’s workflow artifact guide.

3. Capture on test failure

If screenshots are diagnostic evidence, take them in a test teardown or failure hook so they are created when an assertion fails. The artifact upload step must still run after the test step fails.

For a small standalone script, wrap navigation and assertions in try/finally as above and save the screenshot in an exception handler before quitting. In a test framework, put the capture in its failure hook and give each test a distinct filename; otherwise concurrent tests can overwrite one another.

from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

output = Path("artifacts")
output.mkdir(parents=True, exist_ok=True)
options = Options()
options.add_argument("--headless")
driver = webdriver.Chrome(options=options)

try:
    driver.set_window_size(1440, 1000)
    driver.get("https://example.com")
    # Run assertions here.
except Exception:
    driver.save_screenshot(str(output / "failure.png"))
    raise
finally:
    driver.quit()

A screenshot can only show a browser state reached before the error. If Chrome failed to start, navigation failed before a page loaded, or setup stopped before the capture code ran, there may be no screenshot to upload.

4. Wait for the intended page state

A successful screenshot call can still capture a loading screen or incomplete application. Navigation returning does not necessarily mean client-side rendering, API requests, or lazy content have finished. Wait for a condition that represents the state the test needs instead of adding an arbitrary long delay.

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

# After driver.get(url):
WebDriverWait(driver, 15).until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, "main"))
)
driver.save_screenshot("artifacts/page.png")

Choose a stable selector that appears only when the relevant content is ready. For a transient animation or delayed visual state, a short explicit delay may be appropriate, but a selector or application-ready condition is usually more reliable.

5. Choose the screenshot target and viewport

Need Approach Watch for
Capture what a user can currently see Set the window size, then call save_screenshot The result is the current browser window, not necessarily the full document
Capture one component Locate the element and use Selenium’s element screenshot method The element must exist and be visible; selectors should be stable
Compare screenshots across runs Keep viewport, browser version, page state, and test data consistent Fonts, animations, dynamic content, and runner updates can change pixels

Set the viewport before capture, and preferably before navigation when the page’s layout responds to viewport dimensions. The 1440×1000 size in the example is illustrative, not a Selenium or GitHub requirement. A full-page image requires a capture method that explicitly supports full-page capture; do not assume the standard WebDriver window screenshot includes content below the viewport.

6. Browser and driver versions on hosted runners

GitHub-hosted runner images include browser automation software, but the installed versions follow the selected image and can change as images are updated. The runner-images project currently maps ubuntu-latest to Ubuntu 24.04 and explains that the alias follows the latest general-availability image over time. Its Ubuntu 24.04 software inventory is a dated snapshot, not a promise about future jobs. Inspect the job setup log when debugging and record the actual Chrome and driver versions if reproducibility matters. See the runner-images project and the Ubuntu 24.04 software inventory.

Selenium bindings use Selenium Manager for automated browser and driver management by default. On a hosted runner, first check which browser and driver were resolved and whether their versions and paths agree. Avoid assuming that a path or version from an old tutorial applies to the current image. The Selenium Manager documentation describes its browser and driver management behavior.

7. Troubleshooting

Symptom Likely cause Fix
No screenshot appears in the artifact The output directory was not created, the code wrote somewhere else, or the test failed before saving Create the directory before capture, use a workspace-relative path, and make the artifact path match it exactly
Artifact upload says no files were found The test did not produce a file, or the upload step points to the wrong directory Inspect the test logs and paths; use an unconditional upload step such as if: ${{ always() }}
Screenshot shows a loader or missing content The capture happened before the application reached the expected state Wait for a meaningful element or readiness condition before saving
Screenshot dimensions differ between runs Window size was not set consistently, or browser/image versions changed Set a deliberate viewport and log the actual runner browser and driver versions
Chrome fails to start Browser or driver resolution failed, versions mismatch, or the selected environment differs from assumptions Read the driver startup logs, inspect installed versions and paths, and use Selenium Manager or a compatible browser/driver pair
Screenshot exists but assertion output is missing An exception during capture or cleanup obscured the original error Preserve the original exception, keep capture in a failure hook, and ensure driver.quit() runs in finally
Screenshot is blank or unexpectedly simple The page may have failed, shown a bot check, or not rendered the expected content Inspect the page state and browser logs; wait for a target element and verify navigation reached the intended URL

8. Performance, reliability, and artifact handling

  • Keep captures purposeful. PNG screenshots add workflow output and upload time. Capture only the states needed for debugging or visual checks.
  • Make names unique. Include a test name or stable identifier when a suite can create multiple screenshots, especially when tests run concurrently.
  • Always close the driver. Use a test fixture or finally block so browser processes are cleaned up after navigation or screenshot errors.
  • Expect environment changes. Hosted images and browser builds evolve. Pin an explicit runner label where suitable, inspect setup logs, and revisit the choice as supported images change.
  • Protect screenshot contents. Images may expose account information, customer data, tokens rendered in a page, or other sensitive details. Limit what the test page displays and follow the repository’s artifact access and retention policy.

Or skip the browser setup

For a one-off page capture, a separate CI browser install and artifact workflow may be more machinery than the screenshot requires. ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo. A GET request returns an image or PDF, and its API documentation describes the available options.

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

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 screenshots. Start with 1,000 free screenshots a month, no card required.

FAQ

Does Selenium save screenshots as PNG or JPEG?

The WebDriver screenshot-to-file API writes a PNG. The code above uses a .png filename.

Can I download the screenshot after the GitHub Actions job ends?

Yes, when the workflow uploads the file as an artifact. Open the completed workflow run and download its artifact, subject to the repository’s artifact access and retention settings.

Will the screenshot include the entire page?

Not with the basic current-window screenshot call. It captures the browser window’s current view; use an explicit full-page capture capability when that is required.

Should I capture screenshots for every passing test?

Usually only if they are required visual test outputs. For debugging, capturing on failure often keeps artifacts useful while limiting unnecessary files and upload time.