Skip to content

Repository files navigation

shotlist

Take annotated UI screenshots from YAML recipes, using Playwright.

shotlist opens your running site, drives it to the state you describe, clips a region, draws callouts, and writes the image where you want it. You describe each screenshot in a YAML recipe.

shotlist.dev/docs is the reference. It covers each key, step verb, and query primitive. This README covers setup and common commands.

Install

shotlist requires Node.js 20 or newer and a project with a package.json. Create one with npm init if needed.

Install shotlist and Playwright from the npm registry:

npm install -D shotlist playwright

Use the equivalent pnpm or Yarn command if your project uses one of them.

shotlist keeps Playwright optional because Playwright downloads browsers during package installation. Install it when shotlist writes an image, including runs with --check or a source: file recipe. shotlist draws file-recipe callouts in a page. The --init, --help, and recipe-listing commands do not launch a browser.

Quick start

npx shotlist --init

shotlist writes a commented shotlist.config.yaml and a starter recipe. You can create them by hand instead.

1. Configure the project once in shotlist.config.yaml at the project root:

site:
  url: http://localhost:3000
  viewport: { width: 1440, height: 900 }
  scale: 2
  theme: dark

install:
  guide: content/guide/images

2. Write a recipe in screenshots/recipes/order-row.yaml:

name: order-row
install: guide

setup:
  - click: { role: button, name: Orders }

clip:
  css: '.order-row'
  contains: Acme Corp
  pad: 20

marks:
  amount: { within: clip, text: $42.00 }
  status: { within: clip, text: Open }

callouts:
  - { mark: amount, text: What they owe }
  - { mark: status, text: Where it stands }

3. Shoot it:

npx shotlist order-row --install

shotlist writes the image to screenshots/out/order-row.png. With --install, it copies the image to content/guide/images/order-row.png. PNG is the default. Set image.format to jpeg or webp in the project config or recipe.

Documentation

Page What it covers
Your first screenshot A lesson: install, a recipe, a callout
Add shotlist to a project Setting up in an app you already have
Configuration file Every key, starting the site, style and fonts
Recipe file Every field, and annotating an existing image
Steps The step vocabulary
Queries Sources, filters, traversal, frames, finders
Callouts and masks Labels, numbered discs, masking
Macros and data files Sharing setup, driving a shot from a list
Command line Every flag, the API, editor and agent support
Keeping a screenshot current --check, diffs, and a shot that changes
Undo what a shot changed teardown, for a recipe that writes
What a configuration can do What a run is allowed to reach

Commands

npx shotlist --init               # write a starter config and recipe
npx shotlist                      # list every recipe
npx shotlist <name> [<name>…]     # shoot into paths.out
npx shotlist <name> --install     # …and copy to its install destination
npx shotlist --all --install      # shoot everything
npx shotlist --all --keep-going   # …carrying on past a recipe that fails
npx shotlist --check              # compare against committed images
npx shotlist --check --diff       # …and write a before/after/changed image
npx shotlist --check --json       # …and report it as JSON on stdout
npx shotlist --lint               # check every YAML; no browser, no site needed
npx shotlist --work-limit executedSteps=20000 <name>
                                  # let this Run execute more Steps
npx shotlist --login admin        # sign in by hand, and save the session
npx shotlist --help               # the full list, from the tool

Library API

Your caller grants Operator authority to openRun, then makes every Capture and Checking request through that Run:

import { openRun, type CaptureReport, type CheckReport, type RunProgress } from 'shotlist'

const run = openRun({ untrusted: false }, 'shotlist.config.yaml')

const one: CaptureReport = await run.capture({ recipes: ['order-row'] })
const group: CaptureReport = await run.capture({
  recipes: ['order-row', 'account-menu'],
  install: true,
})
const all: CaptureReport = await run.capture({ all: true })

const controller = new AbortController()
process.once('SIGINT', () => controller.abort('SIGINT'))
const checked: CheckReport = await run.check({
  all: true,
  diff: true,
  signal: controller.signal,
  onProgress: async (progress: RunProgress) => console.log(progress.type),
})

for (const result of checked.results) {
  if (result.status === 'changed') console.log(result.name, result.diff)
  if (result.status === 'failed') console.error(result.name, result.error)
}
for (const failure of checked.failures) console.error(failure.resource, failure.error)
for (const warning of checked.warnings ?? []) console.warn(warning)

A one-Recipe request uses the same interface and report as a group. Named Recipes keep caller order; { all: true } selects every Recipe in recipe-name order. Capture results are captured, failed, cancelled, or not-attempted; group.installation accounts for Committed images only after every selected Capture and required cleanup finishes. Checking retains same, changed, new, and skipped findings beside operational failures, cancellation, and unattempted Recipes. Its report also includes environment drift, Ignore-region counts, and optional diff-image paths.

Both reports are immutable. Request-level site, browser, and Baseline failures live in failures; progress-observer failures live in warnings; cancellation details live in cancellation. keepGoing: true attempts later Recipes after a Recipe failure. An awaited onProgress observer receives request, Recipe, retry, and installation facts in order. The Run remains reusable after a request settles and rejects overlapping requests.

shotlist rejects invalid requests with ShotlistError. It also rejects a hand-built Run before it touches the browser, filesystem, network, or a process. The caller grants Operator authority, including any numerical Work limit changes; the Project config does not.

Migrating pre-1.0 TypeScript callers

Replace shoot(run, recipe, options) with a name-based request such as run.capture({ recipes: [recipe.name] }). Replace check(run, recipes, options) with run.check({ recipes: names }), where names is a string array. Handle the returned report rather than a direct ShotResult or CheckResult[], and remove caller-owned browser setup. Use install: true on the Capture request; the Run defers Installation until Capture and cleanup finish.

This package-interface change does not change Recipe or config files, and it does not change CLI commands or flags.

Pass authority to lint because a malformed Project cannot open a complete Run: lint({ untrusted: false }, 'shotlist.config.yaml'). Use parseConfig, parseRecipe, parseMacro, parseLibrary, and parseQuery to parse in-memory values without Operator authority.

Every Run applies Work limits to Library document size, authored structure, Macro expansion, actual Steps, and elapsed Recipe work. A Project cannot raise them. An Operator may change a numerical limit for one command with --work-limit name=value, through protected SHOTLIST_WORK_LIMITS, or through OperatorAuthority.workLimits in TypeScript. shotlist --lint rejects predictable excess before shotlist starts a site or browser.

A recipe is data

Describe screenshots with recipe vocabulary rather than JavaScript. Contributors add a step verb or query primitive for screenshots the vocabulary cannot express. shotlist does not support eval: or other executable recipe fields. See CONTRIBUTING.md.

Running a config you did not write

Automation may run a config from a fork or another contributor. In every mode, shotlist blocks secret-looking paths such as .env, .git, and .ssh. It checks browser requests, redirects, WebSockets, and readiness probes against approved protocol, host, and port values. In --untrusted mode, shotlist starts no configured process, loads no stored Session, accepts no Project-provided Network destination approvals, and confines paths to approved roots. Use --allow or protected SHOTLIST_ALLOW settings to grant exact destinations, and keep hostile Projects inside an isolated runner.

Read the full policy and its limits in the security model.

Contributing

You can contribute code or documentation, from bug fixes and tests to new features and typo corrections.

CONTRIBUTING.md covers setup, commands, code style, and the completion criteria.

License

MIT © Nicola Mustone. See LICENSE.

About

Repeatable, annotated UI screenshots from declarative recipes. No code per shot.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages