ScreenshotNeo

BlogHow-to

How to Take Multiple Screenshots in Appium Hybrid Apps

Capture reliable screenshots at native and webview checkpoints in one Appium test, with context switching, file naming, troubleshooting, and automation code.

By the ScreenshotNeo team1 October 20268 min read

To take multiple screenshots in an Appium hybrid app, keep one session open, move the app to each meaningful checkpoint, select the correct native or webview context, call the screenshot operation, and save every image with a unique filename. Appium’s WebDriver endpoint captures the current browsing context and returns a base64-encoded PNG.

Core workflow

  1. Start an Appium session.
  2. Navigate to the first state worth recording.
  3. List available contexts with getContextHandles() or your client equivalent.
  4. Switch to the intended context. Use the native context for native UI and the matching webview context for embedded web content.
  5. Capture a screenshot and save it with the test name, step number, context, and timestamp.
  6. Continue the test, switching contexts whenever the next action requires it.

Appium models native UI and embedded web content as separate contexts. Hybrid apps can expose more than one webview, and locator strategies can differ between contexts. See Appium’s context commands documentation and screenshot command reference.

Python example: capture native and webview checkpoints

The following example uses the Appium Python client API. Adjust capabilities, package names, and the webview name for your application.

from datetime import datetime, timezone
from pathlib import Path
import time

from appium import webdriver
from appium.options.android import UiAutomator2Options

OUT = Path("artifacts/screenshots")
OUT.mkdir(parents=True, exist_ok=True)

options = UiAutomator2Options()
options.platform_name = "Android"
options.automation_name = "UiAutomator2"
options.device_name = "Android Emulator"
options.app = "/absolute/path/to/your-app.apk"
options.auto_webview = False

# Keep one driver session for the whole test.
driver = webdriver.Remote("http://127.0.0.1:4723", options=options)


def save_checkpoint(step: str):
    stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ")
    context = driver.current_context.replace("/", "_")
    path = OUT / f"checkout_{step}_{context}_{stamp}.png"
    driver.save_screenshot(str(path))
    return path


def switch_to_webview(timeout_seconds=20):
    deadline = time.time() + timeout_seconds
    while time.time() < deadline:
        contexts = driver.contexts
        for context in contexts:
            if "WEBVIEW" in context.upper():
                driver.switch_to.context(context)
                return context
        time.sleep(0.5)
    raise RuntimeError(f"No webview context found. Available contexts: {driver.contexts}")

try:
    # Native launch screen.
    save_checkpoint("01_native_launch")

    # Perform native actions here, then open the embedded web page.
    # driver.find_element(...).click()
    webview = switch_to_webview()
    save_checkpoint("02_webview_loaded")

    # Web interactions use web locators after switching context.
    # driver.find_element("css selector", "button.submit").click()
    save_checkpoint("03_webview_after_submit")

    driver.switch_to.context("NATIVE_APP")
    save_checkpoint("04_native_confirmation")
finally:
    driver.quit()

JavaScript and WebdriverIO pattern

With a WebdriverIO Appium client, the same sequence is expressed with getContexts(), switchContext(), and saveScreenshot(). Keep the context name returned by the running device instead of hard-coding a webview identifier.

import { remote } from "webdriverio";
import fs from "node:fs/promises";

const driver = await remote({
  hostname: "127.0.0.1",
  port: 4723,
  path: "/",
  capabilities: {
    platformName: "Android",
    "appium:automationName": "UiAutomator2",
    "appium:deviceName": "Android Emulator",
    "appium:app": "/absolute/path/to/your-app.apk"
  }
});

await fs.mkdir("artifacts/screenshots", { recursive: true });
let number = 0;
async function checkpoint(name) {
  number += 1;
  const context = (await driver.getContext()).replaceAll("/", "_");
  await driver.saveScreenshot(`artifacts/screenshots/${String(number).padStart(2, "0")}_${name}_${context}.png`);
}

try {
  await checkpoint("native-launch");
  const contexts = await driver.getContexts();
  const webview = contexts.find((value) => value.toUpperCase().includes("WEBVIEW"));
  if (!webview) throw new Error(`No webview context. Available: ${contexts.join(", ")}`);
  await driver.switchContext(webview);
  await checkpoint("webview-loaded");
  await driver.switchContext("NATIVE_APP");
  await checkpoint("native-confirmation");
} finally {
  await driver.deleteSession();
}

Calling the Appium endpoint directly

The WebDriver route is GET /session/:sessionId/screenshot. The response contains a base64 PNG in the JSON field value. This is useful when your test harness is not using a language client.

curl -sS \
  "http://127.0.0.1:4723/session/SESSION_ID/screenshot" \
  | python -c 'import sys, json, base64; print(base64.b64decode(json.load(sys.stdin)["value"]), end="")' \
  > artifacts/screenshots/checkpoint.png

Python HTTP example

import base64
import requests

session_id = "SESSION_ID"
r = requests.get(
    f"http://127.0.0.1:4723/session/{session_id}/screenshot",
    timeout=30,
)
r.raise_for_status()
with open("artifacts/screenshots/checkpoint.png", "wb") as image:
    image.write(base64.b64decode(r.json()["value"]))

Node.js HTTP example

import { writeFile } from "node:fs/promises";

const sessionId = "SESSION_ID";
const response = await fetch(`http://127.0.0.1:4723/session/${sessionId}/screenshot`);
if (!response.ok) throw new Error(`Appium returned ${response.status}`);
const body = await response.json();
await writeFile("artifacts/screenshots/checkpoint.png", Buffer.from(body.value, "base64"));

Choosing the correct context

Situation Context What the screenshot represents
Native activity, alert, keyboard, or system-facing screen NATIVE_APP The native browsing context exposed by the driver
Embedded HTML page A matching WEBVIEW_... context The web context handled by the driver
Multiple embedded pages The specific webview returned by the device Only the selected webview's current state

Do not assume that a webview is always named exactly WEBVIEW; log the complete context list and select the one belonging to the page under test. After web interactions, switch back to NATIVE_APP before locating native elements.

XCUITest screenshot scope and quality

XCUITest documents three web-context screenshot modes: native captures the full device screen including status bars, page attempts the entire active web page, and viewport captures the visible viewport. The documented default is native. These mode names are XCUITest-specific; verify equivalent behavior for your Android driver.

XCUITest also documents quality values that trade speed, size, and fidelity: 0 for lossless PNG, 1 for high-quality JPEG, 2 for low-quality JPEG, and 3 for lossless HEIC with PNG fallback when hardware HEIC encoding is unavailable. Orientation is heuristic and can vary with OS version, device model, and simulator versus real device. Validate visual comparisons on the device matrix you support. See the XCUITest driver settings.

Android hybrid-app requirements

  • For Android webviews, the app must expose a debuggable webview and the selected driver must be able to attach to it. Espresso documentation describes ChromeDriver-backed web contexts; use the requirements for your selected driver.
  • Use Chrome remote debugging tools to confirm that the webview is visible when Appium lists only NATIVE_APP.
  • On UiAutomator2 virtual displays, Android API 34 and newer can limit screenshots to the virtual display. Confirm which display is under test before comparing images.
  • Apps or platforms can restrict screenshots for security. Android's FLAG_SECURE is one documented example; confirm current behavior for the app and driver versions in use.

File naming and artifact layout

Appium does not impose a filename convention. A useful name includes the test, step number, context, device, and timestamp:

artifacts/
└── checkout/
    ├── 01_native_launch_NATIVE_APP_pixel7_20261001T120000Z.png
    ├── 02_webview_loaded_WEBVIEW_com_example_20261001T120004Z.png
    └── 03_native_confirmation_NATIVE_APP_pixel7_20261001T120009Z.png

Keep screenshots as test artifacts, write metadata beside them, and avoid overwriting files when retries run. Store the context name and device identifier with each image so a failed visual comparison can be reproduced.

Timing, performance, and reliability

  • Capture after the state is stable, not immediately after a tap. Wait for a known element, animation completion, or a short bounded delay.
  • Prefer event-based waits over long fixed sleeps. A screenshot taken during a transition creates flaky visual diffs.
  • Each capture adds device and transport work. Capture checkpoints that answer a debugging or visual-regression question rather than every command.
  • Use lossless PNG for pixel comparisons. Use the driver-supported JPEG or HEIC settings when storage and transfer time matter more than exact pixels.
  • Run the same orientation, OS version, device model, display scale, and context when comparing runs.
  • On failure, attempt one diagnostic screenshot in the current context, then preserve the original exception and session logs.
  • For parallel tests, isolate output directories and session IDs. Never let workers write the same filename.

Troubleshooting

Symptom Likely cause Fix
Only NATIVE_APP appears Webview is not debuggable, has not loaded, or the driver cannot attach Enable webview debugging for the build, wait for the page, inspect remote-debugging availability, and check the selected driver requirements.
Web element commands fail after a screenshot The driver is still in the native context or the wrong webview Log contexts, switch to the matching WEBVIEW_... value, then use web locators.
Native element commands fail after web interactions The session remains in a webview Switch explicitly to NATIVE_APP.
Screenshot is blank or stale Capture occurred before rendering completed, or the page is on another context Wait for a visible state marker, confirm the current context, and capture again.
Landscape image is rotated or cropped Orientation handling is heuristic and driver/device dependent Set orientation before capture, record device details, and validate on the target matrix.
Virtual-display screenshot does not match the full emulator Android API 34+ UiAutomator2 virtual-display behavior Confirm the display under test and compare like-for-like display captures.
Screenshot command returns an error Expired session, wrong session ID, unsupported driver operation, or security restriction Check session health and driver logs, verify the endpoint and ID, and test whether the app blocks screenshots.
Images overwrite each other Static filenames or parallel workers sharing a directory Include test, step, context, worker, and timestamp in the filename; isolate worker directories.

Or skip the browser setup

If the thing you need to capture is a public or authenticated web page rather than the native device UI, ScreenshotNeo provides a website screenshot API. It does not replace Appium for native screens, but it can remove browser automation from web-page capture workflows.

One GET request returns PNG, JPEG, WebP, or PDF. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents.

See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The Free plan includes 1,000 screenshots 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.

FAQ

Does one Appium screenshot call capture every context?

No. It captures the current browsing context. Switch contexts and call it again for native and webview checkpoints.

Can I take screenshots without ending the session?

Yes. Repeated screenshot calls are intended to run during one active session.

Does Appium choose the webview automatically?

Some capabilities can start in a webview, but robust tests should inspect the available contexts and select the intended one explicitly.

Are screenshots always PNG files?

The WebDriver screenshot response is a base64-encoded PNG. Driver-specific settings can offer other quality or encoding behaviors, such as the XCUITest options described above.

Should I use screenshots for pixel-perfect regression tests?

Only after controlling orientation, device, OS, display, context, font availability, and timing. Otherwise use screenshots primarily as diagnostic artifacts and validate visual diffs on a stable device matrix.