> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.doc-detective.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.doc-detective.com/_mcp/server.

# screenshot

> Take a screenshot in PNG format and optionally perform visual regression testing.

The `screenshot` action captures a PNG image of the current browser viewport or a specific element. It can also perform visual regression testing by comparing the captured image against a previously saved reference image. Reference images can be local files or remote URLs, enabling teams to store reference images in cloud storage like S3.

You can specify the screenshot action in several ways:

* **Boolean Shorthand:** Set `screenshot: true` to capture the full viewport with default settings.
* **String Shorthand:** Provide a file path directly as the value for the `screenshot` key (for example, `screenshot: "my_screenshot.png"`). This saves the full viewport screenshot to the specified path.
* **Object Format:** Use an object for more control over the path, visual comparison, and cropping:
  * `path`: (Optional) The file path to save the screenshot to, or an `http(s)://` URL to a remote PNG image. It can include directories. Omitting it generates a default path. Given a URL, Doc Detective downloads the remote image and uses it as a read-only reference for comparison. It writes the new capture to `./doc-detective-runs/<run-timestamp>/` instead of overwriting the remote image.
  * `directory`: (Optional) The directory where the screenshot will be saved. If `path` includes a directory, this is ignored. Defaults to the configured output directory.
  * `maxVariation`: (Optional) The maximum acceptable visual difference (0 to 1) when comparing against a reference image. If the variation exceeds this threshold, the step returns WARNING. If set, visual comparison is enabled.
  * `overwrite`: (Optional) Controls when to overwrite the reference image. Ignored when `path` is a URL (remote storage is never mutated). Options:
    * `"false"`: Never overwrite the reference image.
    * `"true"`: Always overwrite the reference image with the new capture.
    * `"aboveVariation"`: Overwrite only if the visual difference exceeds `maxVariation` (default).
  * `crop`: (Optional) Crop the screenshot to a specific element. Provide a CSS/XPath selector string (for example, `"#myElement"`) or an object:
    * `selector`: (Required in object) The [CSS selector](/docs/selectors/css) or [XPath selector](/docs/selectors/xpath) of the element to crop to.
    * `elementText`: (Optional) Display text to further specify the element if the selector isn't unique.
    * `padding`: (Optional) Add padding around the element before cropping. Can be a single number (all sides) or an object `{ top, right, bottom, left }`.
  * `surface`: (Optional) The browser session, window, or tab to capture, such as `{ "browser": "chrome", "tab": "cart" }`. It can also be the [native app surface](/docs/actions/startsurface) to capture, such as `{ "app": "notepad" }` or a specific app window `{ "app": "notepad", "window": -1 }`. On macOS the capture is that window element, and on Windows the switched window. Omit it to capture the active surface, the most recently opened, focused, or explicitly targeted one, whether a browser tab or an app window. Specifying a surface switches the active surface for the steps that follow. See [Test across multiple tabs, windows, and browsers](/docs/test-docs/multiple-tabs-and-windows), [Target an app surface](/docs/actions/startsurface#target-an-app-surface), and [App windows](find#app-windows).
  * `annotations`: (Optional) A list of visual annotations to draw onto the screenshot. See [Annotations](#annotations).

## Annotations

Annotations mark up a screenshot as part of capturing it, so the callouts regenerate with the image instead of drifting from it. Each annotation names exactly one type, and the type's value is what it points at:

```json
{
  "screenshot": {
    "path": "login.png",
    "annotations": [
      { "outline": "#username" },
      { "badge": "#password", "label": "2" },
      { "blur": ".account-id" }
    ]
  }
}
```

Doc Detective composites annotations into the image instead of injecting them into the page. It never modifies the page under test. An annotated screenshot therefore can't disturb a test or leak into a concurrent recording.

### Types

| Type      | Draws                                                                  |
| --------- | ---------------------------------------------------------------------- |
| `outline` | A box around the target.                                               |
| `arrow`   | An arrow pointing at the target.                                       |
| `badge`   | A small marker on the target, showing `label`. Use for numbered steps. |
| `callout` | A text box with a leader line, showing `label`.                        |
| `blur`    | A blur over the target, to redact sensitive information.               |
| `text`    | A standalone text box, showing `label`.                                |

### Targets

A target can be:

* **A string**, a selector or display text, the same as `crop` accepts. For example, `{ "outline": "#submit" }`.
* **An object**, using the same element-finding fields as [`find`](/docs/actions/find). For example, `{ "outline": { "elementTestId": "submit" } }`.
* **A position**, a fixed spot for annotations that aren't tied to an element. For example, `{ "text": { "position": "top-right" }, "label": "Demo data" }`. Accepts a named region (`top`, `bottom`, `left`, `right`, `center`, `top-left`, `top-right`, `bottom-left`, `bottom-right`) or a point (`{ "x": 640, "y": 220 }`).

An element target waits up to 5000 milliseconds for its element to appear. Set `timeout` on the target to wait longer:

```json
{ "outline": { "elementTestId": "dashboard", "timeout": 20000 } }
```

`timeout` works the same way as [`find`](/docs/actions/find)'s. Like find's, it needs the object form of the target, because the string shorthand has nowhere to put it.

### Options

* `label`: Text to display. Used by `badge`, `callout`, and `text`.
* `position`: Where to place the annotation relative to its target. Accepts a named region, a point, or an offset (`{ "offset": { "x": -80, "y": -60 } }`).
* `style`: Visual overrides for this annotation: `color`, `background`, `strokeWidth`, `fontSize`, `fontFamily`, `opacity`, `radius`, `padding`, `maxWidth`, and `intensity` (blur strength).
* `all`: Annotate every matching element instead of only the first. Needs an object target, because a string could be a selector or display text.

> **Note**
>
> Use `all` for redaction. `{ "blur": { "selector": ".secret" } }` blurs only the first match. If the page shows several, the rest stay readable in an image that looks redacted.

Annotations resolve after `crop`, so you can crop to a panel and annotate elements inside it. An annotation whose target doesn't resolve fails the step rather than quietly producing an unannotated image.

### App surfaces

Annotations work on [native app surfaces](/docs/actions/startsurface) too, including element-anchored ones. Native surfaces match elements through the platform's accessibility layer rather than the DOM, so only four fields have a native equivalent: `elementText`, `elementId`, `elementTestId`, and `elementAria`. `selector`, `elementClass`, and `elementAttribute` are browser-only, and a bare string target isn't accepted on an app surface. Name the field explicitly instead:

```json
{
  "screenshot": {
    "path": "settings.png",
    "surface": { "app": "notepad" },
    "annotations": [
      { "outline": { "elementText": "Save" } },
      { "text": { "position": "top-left" }, "label": "Step 1" }
    ]
  }
}
```

Position-anchored annotations need no element lookup, so they work on every surface.

`all` isn't supported on app surfaces yet, because the native element lookup resolves a single match. Rather than annotate the first match and leave the rest, the step fails. Give each element its own annotation instead.

### Themes

Set `annotationDefaults` to style every annotation at once, so images across a doc set stay consistent. Set it on the config, a spec, or a test. A test's theme overrides a spec's, which overrides the config's, and an individual annotation's `style` overrides all of them.

```json
{
  "annotationDefaults": {
    "color": "#E11D48",
    "strokeWidth": 3,
    "fontFamily": "Inter, system-ui, sans-serif",
    "badge": { "background": "#E11D48", "color": "#FFFFFF" },
    "blur": { "intensity": 14 }
  }
}
```

Base properties apply to every type. A per-type block, such as `badge` or `callout`, overrides them for that type.

> **Warning**
>
> Annotations change the image's pixels. If you compare against a reference image with `maxVariation`, changing a theme or an annotation invalidates existing reference images.

**Visual Regression Testing:** If `maxVariation` is set and a reference image exists at the target `path`, the action compares the new screenshot to the reference. If the difference is within `maxVariation`, the step passes. If it exceeds the threshold, the step returns WARNING, and behavior depends on the `overwrite` setting.

**URL Paths:** When `path` is an `http(s)://` URL pointing to a PNG image, Doc Detective downloads that image once per run. It uses the image as a read-only reference for comparison. The new screenshot is saved to `./doc-detective-runs/<run-timestamp>/<stepId>_<filename>.png` for inspection. The `overwrite` setting is ignored, because remote storage is never mutated. The step result includes `outputs.referenceUrl` for traceability. This suits teams that store reference images in cloud storage like S3 without managing local files.

> **Note**
>
> URL paths must end in `.png` (or `.PNG`) and can include query strings for signed URLs. The step fails if the URL is unreachable or returns invalid image data.

**Private addresses:** remote reference URLs must resolve to a public host. Doc Detective won't fetch reference images from private, loopback, or link-local addresses, including their IPv4-mapped IPv6 forms. This guards against server-side request forgery (SSRF). This covers hosts like `localhost` and `127.0.0.1`. It also covers ranges such as `10.0.0.0/8`, `172.16.0.0/12`, and `192.168.0.0/16`, plus the `169.254.169.254` cloud-metadata endpoint. A blocked URL fails the step with a message that starts with `Couldn't fetch remote reference image`. The message notes it's `Refusing to fetch private/loopback address`. To compare against a reference image on an internal address, such as a local development server, set `DOC_DETECTIVE_ALLOW_LOCAL_URLS=true`.

**Outputs:** depending on which options you configure, the action exposes the following values in its `outputs` object. To capture any of them for later steps, use the step-level `variables` object with the matching expression. Each output is present only when its check applies:

* `$$width` and `$$height`: the saved image's actual pixel dimensions, after any crop. When the browser floors a requested viewport, these report the size the page rendered rather than the size you asked for. You can then reconcile a caption or report against the file. They're present when Doc Detective can read the saved image's dimensions. They're omitted when the metadata read fails or the image has no readable size.
* `$$cropElementFound`: whether the element to crop to was located (when `crop` is set).
* `$$fitsViewport`: whether the cropped element fits within the viewport (when `crop` is set).
* `$$annotationTargetsFound`: whether every annotation's target resolved (when `annotations` is set). The step fails if any target is missing.
* `$$annotationsInBounds`: whether every annotation landed inside the image (when `annotations` is set). The step returns WARNING if one falls outside, which usually means the crop left it out of frame.
* `$$aspectRatioMatch`: whether the new capture and the reference image share an aspect ratio (when comparing against a reference image).
* `$$variation`: the fractional difference between the new capture and the reference image (when comparing against a reference image).

> For comprehensive options, see the [`screenshot`](/reference/schemas/screenshot) reference.

## Examples

Here are a few ways you might use the `screenshot` action:

### Basic full viewport screenshot (boolean shorthand)

Saves to a default path like `output/screenshot_1745032062266.png`.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Take a basic screenshot.",
          "screenshot": true
        }
      ]
    }
  ]
}
```

### Full viewport screenshot to specific path (string shorthand)

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Save screenshot to a specific file.",
          "screenshot": "./output/images/login_page.png"
        }
      ]
    }
  ]
}
```

### Full viewport screenshot with visual comparison (object format)

Compares against `./output/reference_images/home_page.png`. Fails if difference > 5% (`0.05`). Overwrites reference if failure occurs.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Take screenshot and compare to reference.",
          "screenshot": {
            "path": "home_page.png",
            "directory": "./output/reference_images",
            "maxVariation": 0.05,
            "overwrite": "aboveVariation"
          }
        }
      ]
    }
  ]
}
```

### Visual comparison against a remote URL

Compares against a reference image stored in S3 or other cloud storage. The new capture is saved locally under `./doc-detective-runs/<run-timestamp>/` for inspection.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Compare against reference image in S3.",
          "screenshot": {
            "path": "https://my-bucket.s3.amazonaws.com/reference-images/home_page.png",
            "maxVariation": 0.05
          }
        }
      ]
    }
  ]
}
```

### Screenshot cropped to an element (object format)

Captures only the element matching the `#main-content` selector.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Screenshot only the main content area.",
          "screenshot": {
            "path": "main_content_area.png",
            "directory": "./output/element_shots",
            "crop": "#main-content"
          }
        }
      ]
    }
  ]
}
```

### Screenshot cropped to an element with padding and visual comparison

Captures the element with text "Submit Button", adds 10px padding, and compares against a reference image.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Screenshot a button with padding and compare.",
          "screenshot": {
            "path": "submit_button_padded.png",
            "directory": "./output/reference_elements",
            "crop": {
              "elementText": "Submit Button",
              "padding": 10
            },
            "maxVariation": 0.01,
            "overwrite": "aboveVariation"
          }
        }
      ]
    }
  ]
}
```

### Annotated screenshot

Numbers the fields of a form and redacts an email address, all as part of the capture.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Capture an annotated screenshot of the sign-in form.",
          "screenshot": {
            "path": "sign_in_annotated.png",
            "directory": "./output/images",
            "annotations": [
              { "badge": "#username", "label": "1" },
              { "badge": "#password", "label": "2" },
              {
                "callout": "#submit",
                "label": "Sends you a one-time code",
                "position": "right"
              },
              { "blur": { "selector": ".account-email" }, "all": true }
            ]
          }
        }
      ]
    }
  ]
}
```

## Automatic screenshots

To capture a screenshot after every browser-based step, use the `autoScreenshot` option. This is useful for:

* Building a visual record of test runs
* Detecting UI regressions by comparing screenshots between runs
* Debugging failures by inspecting the state at each step

Enable at the config, spec, or test level (test overrides spec, which overrides config):

```json
{
  "autoScreenshot": true,
  "output": "./test-results"
}
```

Or use the CLI flag:

```bash
doc-detective --auto-screenshot
```

Doc Detective saves auto screenshots under `<output>/.doc-detective/runs/<runId>/`, nested in the REST-style resource tree. The path structure uses stable IDs derived from file paths and content hashes:

```
.doc-detective/runs/<runId>/         # <runId> is an ISO timestamp, for example 2026-06-27T04-55-00-000Z
├── testResults.json                 # or coverageResults.json for coverage runs
├── testResults.html                 # or coverageResults.html for coverage runs
└── specs/
    └── <specId>/
        └── tests/
            └── <testId>/
                └── contexts/
                    └── <contextId>/
                        └── screenshots/
                            └── 01-goTo-s4f2a91c.png
```

The HTML report provides a human-readable view of the same results. Run folders therefore become self-contained artifacts you can share or compare without the standalone `html` reporter.

Doc Detective generates deterministic IDs based on content rather than random UUIDs, so the same test step produces the same relative path in every run. This lets you diff two run folders and spot UI changes over time.

> **Note**
>
> Auto screenshot capture skips steps that already define an explicit `screenshot`, since those steps already produce an image. Doc Detective still captures failed steps, because the failure state is often the most useful frame for debugging.