ScreenshotNeo

BlogHow-to

How to Capture an iPhone Website Screenshot with Selenium Grid

Use Appium to automate Mobile Safari on an iPhone, route the session through Selenium Grid 4, and capture the page with WebDriver.

By the ScreenshotNeo team4 October 202610 min read

To capture an iPhone website screenshot through Selenium Grid, use Appium to automate Mobile Safari and configure Selenium Grid 4 to relay WebDriver requests to the Appium server. Then create an iOS Safari session, navigate to the site, wait for the content you need, and call WebDriver’s screenshot API. Selenium’s Safari documentation directs iOS Safari automation to Appium. Selenium Safari documentation.

This workflow captures the current viewport in a web context; do not assume it creates a full-page image. Appium supports Mobile Safari on real iOS devices and simulators. A simulator may suit layout checks; use real hardware when the behavior under test depends on a physical device. Appium Mobile Safari documentation.

1. Choose a real iPhone or simulator

Use a simulator when you need a controlled iOS environment for layout or functional checks and have Xcode available. Use a connected iPhone when actual hardware behavior matters. Appium documents support for both. Apple’s documented real-device WebDriver setup uses a connected Mac; it is not a workflow in which an arbitrary remote Grid host can directly control an iPhone without device and host setup.

On a real device, enable Web Inspector and Remote Automation in Safari’s Advanced settings. On the Mac used to automate the connected device, enable WebDriver. Follow Apple’s current setup instructions for the device and macOS versions in use: Apple: enabling automation with WebDriver.

Install and configure Appium and its iOS driver on a Mac that can access the target device or simulator. Simulators require Xcode. Appium’s driver capability reference describes the platform, device, OS version, and simulator selection options; check it against your installed driver version before copying capability names: XCUITest driver capabilities.

2. Start Appium and make it reachable from Grid

Start an Appium server with the iOS driver installed and verify that it can create a Mobile Safari session locally. Grid must be able to reach the Appium server over the network. If Appium and Grid are on different machines, configure Grid with Appium’s reachable hostname or IP address. localhost and 127.0.0.1 refer to the Grid machine from Grid’s perspective, so they will not identify a separate Appium host.

Selenium Grid 4 supports relaying WebDriver requests to Appium instances. Configure a Grid node/relay for the Appium server using the relay options documented for your Grid version. The exact node configuration depends on how you run Grid and Appium, so use the official relay example as the source of truth rather than copying a configuration for a different release: Appium: Selenium Grid.

Before debugging Safari capabilities, check the network path from the Grid process to Appium: hostname resolution, port access, firewall rules, and whether the Appium listener accepts connections from the Grid host. A relay cannot forward a session request to an unreachable server.

3. Request an iOS Safari session through Grid

Send a W3C WebDriver request to the Grid URL with capabilities that match the device registered through the Appium relay. The example uses Selenium’s JavaScript bindings and a simulator. Replace the Grid URL, platform version, and device name with values supported by your Grid node and Appium installation.

import { Builder, Browser } from 'selenium-webdriver';

const driver = await new Builder()
  .usingServer('http://GRID_HOST:4444')
  .withCapabilities({
    browserName: 'safari',
    platformName: 'iOS',
    'appium:deviceName': 'iPhone 16',
    'appium:platformVersion': '18.0',
    'appium:automationName': 'XCUITest',
    'appium:udid': 'SIMULATOR_OR_DEVICE_UDID'
  })
  .forBrowser(Browser.SAFARI)
  .build();

try {
  await driver.get('https://example.com');
  await driver.wait(async () => {
    const state = await driver.executeScript('return document.readyState');
    return state === 'complete';
  }, 30000);

  // Save the current web viewport as a PNG.
  const pngBase64 = await driver.takeScreenshot();
  const fs = await import('node:fs/promises');
  await fs.writeFile('iphone-viewport.png', Buffer.from(pngBase64, 'base64'));
} finally {
  await driver.quit();
}

Install the Selenium JavaScript package in your project before running the example. For a real device, use its configured device name and UDID, and ensure the connected Mac and device are prepared as described above. Depending on the Appium and driver versions, a capability may be supplied by the Grid relay or need to match the node’s stereotype. Keep the requested capabilities aligned with the relay configuration.

4. Wait for the page state you need and capture

document.readyState === 'complete' is a useful baseline, but it does not guarantee that a single-page application, lazy-loaded image, animation, or client-rendered widget has finished. Prefer waiting for an application-specific element that indicates the content is ready:

const { By, until } = await import('selenium-webdriver');
await driver.wait(until.elementLocated(By.css('[data-testid="report-ready"]')), 30000);
const png = await driver.takeScreenshot();

Appium’s screenshot command returns the current viewport in a web context, or the current window in a native context. It does not establish universal full-page capture support. If you need an entire long page, verify the behavior of the exact Appium driver and browser version you use, or capture and stitch scroll positions as a separate process. Stitching can introduce overlap, sticky-header, and lazy-loading artifacts. Appium screenshot command reference.

5. Run a screenshot request with cURL, Python, or Node.js

These examples show the same Grid session request using Selenium’s WebDriver HTTP endpoint. They assume the Grid relay is already configured, the Appium server is reachable, and the requested capabilities match the registered iOS target. Adjust the endpoint path if your Grid deployment uses a different base path.

cURL

curl -sS -X POST 'http://GRID_HOST:4444/session' \
  -H 'Content-Type: application/json' \
  -d '{
    "capabilities": {
      "alwaysMatch": {
        "browserName": "safari",
        "platformName": "iOS",
        "appium:automationName": "XCUITest",
        "appium:deviceName": "iPhone 16",
        "appium:platformVersion": "18.0",
        "appium:udid": "SIMULATOR_OR_DEVICE_UDID"
      }
    }
  }'

Read the returned session ID, then use it for the WebDriver navigation and screenshot endpoints. The screenshot endpoint returns a base64-encoded PNG in the JSON response; decode that value to write the file. For repeatable automation, a Selenium binding handles session lifecycle and response parsing more safely than hand-building HTTP calls.

Python

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

options = webdriver.SafariOptions()
options.set_capability("platformName", "iOS")
options.set_capability("appium:automationName", "XCUITest")
options.set_capability("appium:deviceName", "iPhone 16")
options.set_capability("appium:platformVersion", "18.0")
options.set_capability("appium:udid", "SIMULATOR_OR_DEVICE_UDID")

driver = webdriver.Remote(
    command_executor="http://GRID_HOST:4444",
    options=options,
)
try:
    driver.get("https://example.com")
    WebDriverWait(driver, 30).until(
        lambda d: d.execute_script("return document.readyState") == "complete"
    )
    # Replace this with an application-specific ready selector when possible.
    driver.save_screenshot("iphone-viewport.png")
finally:
    driver.quit()

Install Selenium for Python in the environment running this script. If your Selenium version or Appium relay expects a different capability format, use the W3C capability syntax documented by those installed versions.

Node.js

import { Builder, Browser } from 'selenium-webdriver';
import { writeFile } from 'node:fs/promises';

const driver = await new Builder()
  .usingServer('http://GRID_HOST:4444')
  .withCapabilities({
    browserName: 'safari',
    platformName: 'iOS',
    'appium:automationName': 'XCUITest',
    'appium:deviceName': 'iPhone 16',
    'appium:platformVersion': '18.0',
    'appium:udid': 'SIMULATOR_OR_DEVICE_UDID'
  })
  .forBrowser(Browser.SAFARI)
  .build();

try {
  await driver.get('https://example.com');
  await driver.wait(
    () => driver.executeScript('return document.readyState').then(s => s === 'complete'),
    30000
  );
  const base64 = await driver.takeScreenshot();
  await writeFile('iphone-viewport.png', Buffer.from(base64, 'base64'));
} finally {
  await driver.quit();
}

6. Configure device, session, and capture behavior

Setting What it controls Practical guidance
platformName Requests the iOS platform. Use the platform name expected by the Appium driver and Grid relay.
appium:automationName Selects the Appium automation driver. For iOS Safari, configure the iOS driver and match its documented capability value.
appium:deviceName Describes the target device or simulator. Match the registered target. A name alone may not uniquely select a device.
appium:udid Identifies a specific connected device or simulator. Use the UDID for deterministic routing where the setup requires it.
appium:platformVersion Requests an iOS version. Request a version available on the target; do not assume Grid can provision it.
Simulator selection Chooses simulator operation where supported. Check current driver capability documentation and ensure Xcode and the simulator runtime are installed.
Wait condition Defines when the page is ready to capture. Wait for the actual content or state under test, not only navigation completion.

Do not treat a WebDriver screenshot as a screenshot of the physical phone’s entire screen. In web context it captures the browser’s current content viewport. If you switch into a native context, the screenshot semantics change to the current native window, and the page may no longer be addressable with web DOM commands.

7. Reliability, capacity, and cost

Plan concurrency around actual iOS device capacity. Apple documents that only one Safari browser instance and one WebDriver session can be attached at a time. A single iPhone or Safari endpoint should therefore be treated as a serialized resource; adding Grid routing does not remove that device-side limit. Apple Safari WebDriver behavior.

  • Always call quit() in a cleanup path so failed captures do not leave sessions occupying the device.
  • Use a bounded wait and capture diagnostic logs when session creation or navigation stalls.
  • Keep the Appium host address reachable from Grid and stable across runs.
  • For parallel runs, allocate distinct supported devices or simulators and confirm the relay can route each requested capability to the intended node.
  • Pin and review Selenium, Grid, Appium, and iOS driver versions together. The Appium mobile web and screenshot references include older documentation, so confirm details against the installed versions.

Self-managed cost includes the Mac host, device or simulator infrastructure, and engineering time to maintain Xcode, iOS runtimes, Appium, Grid, and network access. A simulator avoids buying a physical iPhone when hardware behavior is not part of the requirement, but still needs a compatible Mac and Xcode. A hosted device service can shift device and host maintenance, but compare its device access, concurrency, and pricing against your workload; this research does not establish a particular provider or rate.

8. Troubleshooting

Symptom Likely cause Fix
Grid reports that no node matches the request. Requested capabilities do not match the Appium relay stereotype or available device. Compare the request’s browser, platform, device name, OS version, and UDID with the registered node and current driver capability docs.
Grid cannot create a session through the relay. Appium is unreachable from the Grid host, or the relay points to localhost on the wrong machine. Use a Grid-reachable hostname or IP and verify routing, port access, and Appium’s listener configuration.
Appium cannot find or launch Safari on iOS. The iOS driver, Xcode tooling, simulator runtime, device setup, or WebDriver settings are incomplete. Confirm the iOS driver is installed, Xcode is configured for simulator use, or enable Apple’s documented Web Inspector and Remote Automation settings for a real device.
Session creation hangs or times out. The device is unavailable, another Safari/WebDriver session is attached, or host-device communication is unhealthy. Release stale sessions, serialize access to the device, inspect Appium and Grid logs, and confirm the Mac still sees the device.
Screenshot is blank or misses dynamic content. Capture happened before client rendering, navigation, or lazy content completed. Wait for an application-specific ready element or state; capture only after it appears.
Image contains only the visible portion of a long page. The screenshot command captured the web viewport. Use a driver-supported full-page mechanism only after verifying it for your versions, or implement scrolling and stitching with checks for sticky elements and overlap.
Capability is rejected as invalid. Legacy capability spelling, missing Appium namespace, or version mismatch. Use W3C capability names and the current Appium driver documentation; compare Grid relay configuration with the same installed versions.

9. Or skip the browser setup

For a rendered website image without managing a Mac, iPhone, Appium server, and Grid relay, ScreenshotNeo provides a website screenshot API and MCP server. It returns PNG, JPEG, WebP, or PDF output from one GET request. See the ScreenshotNeo website and API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

Cookie and consent banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and 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 a month with no card; paid plans start at $5 for 3,000. These captures render a website through the API and do not replace testing Safari behavior on a specific iPhone.

Sign up for 1,000 free screenshots a month, with no card required.

10. FAQ

Does Selenium Grid automate iPhone Safari by itself?

No. For iOS Safari, use Appium as the automation endpoint and configure Grid 4 to relay requests to it.

Can I use a simulator without owning an iPhone?

Yes. Appium supports iOS simulators, which require Xcode. Use a real device when your test depends on physical hardware behavior.

Will this save a full-page screenshot?

The documented screenshot command captures the current web viewport. Verify a full-page method for your specific driver and version before relying on it.

Can one iPhone run several Safari screenshot sessions at once?

Apple documents a limit of one attached Safari browser instance and one WebDriver session, so schedule work for that device accordingly.