paparazzi drives a headless Chromium browser from R. One chain of actions and expectations can be a browser test, a screenshot script or a recorded demo. It works with static sites, running web apps, Shiny apps on disk and Quarto documents.
The core loop
-
Open a page with
pz_open(): a URL, an app or document path, or a serving handle. Set the viewport when opening so the first render already uses it. -
Find elements with CSS strings or
pz_loc()specs.pz_find()returns a new scoped context; assign it to work inside that scope. -
Act, then expect. Send input with
pz_act_*()orpz_set_*(), then wait for the result with apz_expect_*()call. -
Capture the verified state with
pz_screenshot(), or wrap the steps inpz_record()to capture them as video. -
Close the page.
pz_with_page()closes it when its code finishes, andpz_local_page()closes it when the calling test or function exits.
This script adds a task to the bundled task tracker, checks that it appears first, and saves a screenshot of the list. Opening the HTML example serves it over HTTP when httpuv is installed, giving it the origin and history behavior of a deployed site.
library(paparazzi)
image <- tempfile(fileext = ".png")
pz_with_page(pz_example("tasks"), function(page) {
page |>
pz_act_type("Repot the fern", target = "#task-title") |>
pz_act_click("#add-task") |>
pz_expect_text(
"Repot the fern",
target = pz_loc(".task-title", which = "first"),
match = "exact"
) |>
pz_screenshot(image, frame = pz_frame(".task-list", pad = 16))
}, width = 1000, height = 720, color_scheme = "light")To record the same interaction, stage the page first and run the
chain inside pz_record(). Assigning the staged page carries
the settings into every later chain, the recording included. Staging
animates the cursor and typing only while a recording runs. This GIF
needs the gifski package.
video <- tempfile(fileext = ".gif")
pz_with_page(pz_example("tasks"), function(page) {
page <- page |> pz_stage(enter = "bottom", pause = 0.5)
page |> pz_record(video, code = {
page |>
pz_act_type("Repot the fern", target = "#task-title") |>
pz_act_click("#add-task") |>
pz_expect_visible(pz_loc(".task", has_text = "Repot the fern")) |>
pz_record_hold(0.5)
}, fps = 8, scale = 0.5)
}, width = 800, height = 600)Best practices
-
Read the page before choosing selectors. Look at
the app’s HTML, or call
pz_get_elements()andpz_inspect(). Target IDs, data attributes and meaningful classes; addhas_textwhen text identifies a row. -
Keep the root page and scopes apart.
row <- pz_find(page, spec)leavespageunchanged. Userowfor row-level steps andpagefor whole-page checks. -
Keep specs, refind scopes. A
pz_loc()spec is resolved each time it’s used, so it survives re-renders and navigation. After the page changes, expect the new content, then callpz_find()again. - Expect the result of every action. An action waits for its own target, not for what it causes. Follow it with an expectation on the outcome before reading values or taking a screenshot. Expectations retry until they pass or time out.
-
Pin down the device. Pass
width,heightandcolor_schemetopz_open(), pluslocaleandtimezonewhen the page formats dates or numbers. Usereduced_motion = TRUEfor stills. -
Serve pages over HTTP when they navigate.
pz_open()serves HTML files and directories over HTTP itself when httpuv is installed; callpz_serve_static()explicitly for a shared server or a custom root,pz_serve_quarto()for Quarto sources andpz_serve_shiny()for apps. -
Give screenshots a path in scripts. With a path,
pz_screenshot()writes the file and the chain continues. Without one it returns an image for interactive use or knitted documents. - Keep expectations in demos. A recording that runs its expectations is also evidence that the demo reached the state it shows.
- Pace videos for a first-time viewer. High-quality videos move slowly: give most pauses about 1 second, in 0.5-second steps, and never less than 0.5.
- Know who owns each server. A page opened from a path owns the server it started, and closing the page stops it. A server handle you created is yours to stop after its pages close.
References
Read the reference that matches the task.
-
Pages and serving: what
pz_open()accepts, device settings, HTTP servers, navigation and cleanup. -
Locators and scopes:
pz_loc()specs, text and position filters, scoped contexts and moving between scopes. - Testing: choosing expectations, waiting on conditions, testthat integration and inspecting failures.
- Screenshots and annotations: framing captures, marks, callouts, spotlights, redactions and fonts.
- Recording and staging: recording formats, cursor and typing settings, camera moves and captions.
- Shiny: app processes, Shiny readiness, setting bound inputs, module IDs and sharing one app across pages.
