Screenshots
xa11y can capture pixel-level snapshots of the screen alongside its accessibility APIs. This is useful for attaching screenshots to test failures, feeding vision-language models with the same frame the agent is reasoning about, or visually diffing a region around a specific control.
The entry points are:
- Full screen captures the entire primary display.
- Region captures an explicit rectangle in logical screen coordinates.
- Element captures the pixels under an accessibility element’s current bounds.
Screenshots are decoupled from the accessibility and input layers. Capture never raises or activates the target window. Whatever pixels sit at the requested coordinates are returned, including any occluding windows. Bring the target to the foreground yourself if you need a clean capture.
Permissions
Section titled “Permissions”| Platform | Requirement |
|---|---|
| macOS | Screen & System Audio Recording for the terminal/IDE |
| Windows | None for normal user sessions |
| Linux (X11) | DISPLAY set to a reachable X server |
| Linux (Wayland) | xdg-desktop-portal with screenshot support (user consent dialog may appear) |
Capturing
Section titled “Capturing”use xa11y::*;
fn main() -> Result<()> { // Full primary display let shot = screenshot()?; shot.save_png("full.png")?;
// Explicit region let shot = screenshot_region(Rect { x: 0, y: 0, width: 400, height: 300 })?; shot.save_png("corner.png")?;
// Pixels under an accessibility element let app = App::by_name("Calculator", std::time::Duration::from_secs(5))?; let display = app.locator("static_text[name='Result']").element()?; let shot = screenshot_element(&display)?; shot.save_png("result.png")?;
Ok(())}import xa11y
# Full primary displayshot = xa11y.screenshot()shot.save_png("full.png")
# Explicit region: (x, y, width, height)shot = xa11y.screenshot(region=(0, 0, 400, 300))shot.save_png("corner.png")
# Pixels under an accessibility elementapp = xa11y.App.by_name("Calculator")display = app.locator("static_text[name='Result']").element()shot = xa11y.screenshot(element=display)shot.save_png("result.png")Passing both element and region raises ValueError.
import { screenshot, App } from '@crowecawcaw/xa11y';
// Full primary displayconst full = await screenshot();await full.savePng('full.png');
// Explicit region: { x, y, width, height }const region = await screenshot({ region: { x: 0, y: 0, width: 400, height: 300 } });await region.savePng('corner.png');
// Pixels under an accessibility elementconst app = await App.byName('Calculator');const display = await app.locator("static_text[name='Result']").element();const elShot = await screenshot({ element: display });await elShot.savePng('result.png');Passing both element and region throws InvalidActionDataError.
Working with the pixel buffer
Section titled “Working with the pixel buffer”A Screenshot carries raw RGBA8 pixels along with dimensions and a scale factor. width and height are physical (device) pixels, the same resolution the compositor renders at, so on HiDPI displays they exceed the logical bounds passed in. scale records the physical-to-logical ratio (1.0 on standard displays, 2.0 on typical Retina, 1.5 / 1.75 / 2.0 on common Windows and Linux HiDPI configurations). pixels.len() equals width * height * 4.
let shot = screenshot()?;println!("{} x {} @ {}x", shot.width, shot.height, shot.scale);
// Encode as PNG bytes (e.g. to upload or embed)let png_bytes: Vec<u8> = shot.to_png()?;
// Or write directly to diskshot.save_png("out.png")?;
// Raw RGBA for custom processinglet rgba: &[u8] = &shot.pixels;shot = xa11y.screenshot()print(f"{shot.width} x {shot.height} @ {shot.scale}x")
# Encode as PNG bytes (e.g. to upload or embed)png_bytes: bytes = shot.to_png()
# Or write directly to disk (accepts str, bytes, or os.PathLike)shot.save_png("out.png")
# Raw RGBA for custom processingrgba: bytes = shot.pixelsconst shot = await screenshot();console.log(`${shot.width} x ${shot.height} @ ${shot.scale}x`);
// Encode as PNG bytes (e.g. to upload or embed)const pngBytes = await shot.toPng(); // Buffer
// Or write directly to diskawait shot.savePng('out.png');
// Raw RGBA for custom processingconst rgba = shot.pixels; // BufferExample: screenshot on test failure
Section titled “Example: screenshot on test failure”A common pattern is to capture the full screen (or the app under test) whenever an assertion fails, and attach it to the test report.
fn capture_on_fail(name: &str, test: impl FnOnce() -> Result<()>) -> Result<()> { match test() { Ok(()) => Ok(()), Err(e) => { if let Ok(shot) = screenshot() { let _ = shot.save_png(format!("target/failures/{name}.png")); } Err(e) } }}import pytest
@pytest.hookimpl(hookwrapper=True)def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: try: xa11y.screenshot().save_png(f"artifacts/{item.name}.png") except xa11y.XA11yError: passimport { screenshot } from '@crowecawcaw/xa11y';
afterEach(async function () { if (this.currentTest?.state === 'failed') { try { const shot = await screenshot(); await shot.savePng(`artifacts/${this.currentTest.title}.png`); } catch { /* capture failure shouldn't mask the test failure */ } }});Error handling
Section titled “Error handling”| Error | When |
|---|---|
PermissionDenied |
macOS Screen Recording not granted, or Wayland portal denied consent |
Unsupported |
No capture backend available for the session (no DISPLAY, no working Wayland portal, etc.) |
NoElementBounds |
The element passed to screenshot_element has no bounds |
Platform |
Raw OS / FFI failure during capture or PNG encode |