Skip to content

Capture an element inside an iframe

An iframe is a separate document, and a query does not cross into one on its own. This guide names the iframe so everything after it resolves inside — a payment field, an embedded checkout, a block editor's canvas.

Requires shotlist 0.4.0 or later.

Clip a region inside a frame

frame takes a query of its own, which finds the <iframe> element. Everything written beside it then resolves in that iframe's document:

screenshots/recipes/order-total.yaml
name: order-total

clip:
  frame: { css: 'iframe[title="Checkout"]' }
  css: '.order-total'
  pad: 16

Name the iframe by something it carries — a title, a name, an id. Those are attributes on the element in the page you opened, so an ordinary query finds them.

Mark something inside a frame

A mark takes frame the same way, and it does not have to be the region you clipped. Here the capture is a panel in the page and the callout points at a field inside an embedded payment form:

screenshots/recipes/checkout.yaml
name: checkout

clip: { css: '.checkout-panel' }

marks:
  card:
    frame: { css: 'iframe[title="Payment"]' }
    label: Card number

callouts:
  - { mark: card, text: Your card is never sent to us, place: right }

The rect comes back in page coordinates, so the callout lands where a reader sees the field rather than where the frame thinks it is.

Click and type inside a frame

Steps take queries, so nothing new is needed:

screenshots/recipes/checkout.yaml
setup:
  - fill:
      frame: { css: 'iframe[title="Payment"]' }
      label: Card number
    value: '4242 4242 4242 4242'
  - wait:
      frame: { css: 'iframe[title="Payment"]' }
      text: Card accepted

Cross-origin frames

Add the frame's destination to site.allow:

shotlist.config.yaml
site:
  allow:
    - checkout.example.com

Resolution goes through the browser rather than the page's own JavaScript, so a query can reach into the approved frame even though the page containing it cannot. The destination approval is separate because the frame makes its own network requests rather than inheriting permission from the page around it.

A frame inside a frame

Needs nothing extra. Name the inner one and the coordinates still come back against the page:

screenshots/recipes/order-total.yaml
marks:
  total:
    frame: { css: 'iframe[title="Checkout"]' }
    css: '.total'

When the frame is empty in the picture

An application that renders its own frame — a block editor, a live preview — often replaces it when its content changes. A query resolves against the frame that is there when it runs, and the replacement has not painted yet, so the capture comes out blank while the callout sits in exactly the right place.

Wait for the content, then let it settle:

screenshots/recipes/editor.yaml
setup:
  - click: { role: button, name: Add a block }
  - wait:
      frame: { css: 'iframe[name="editor-canvas"]' }
      text: Media Library
  - wait: 1500
    comment: the application rebuilt its frame, and the new one has not painted yet

site.settle does the same for every recipe in the project, which is the better home for it once more than one shot needs it.

What it does not handle

  • A transform or zoom on the iframe element is not compensated for, so a scaled preview places its callouts off the mark.
  • within naming a mark outside the frame is refused rather than quietly scoped to nothing.