| Title: | Scripted Demo Videos, Screenshots, and Tests for Web Pages |
|---|---|
| Description: | Script polished demo videos and screenshots of anything that runs in a web browser, including 'Shiny' apps, 'Quarto' documents, and live websites. A pipeable interface built on 'chromote' clicks, types, and waits the way a person would. While recording, a cursor glides between elements, text is typed one character at a time, and a camera zooms and pans to the action. Annotations, callouts, spotlights, captions, and redactions follow their elements as the page changes. Frame captures to an element or aspect ratio, emulate phones, dark mode, and other devices, and write MP4, WebM, or GIF files. Without a recording, the same scripts run at full speed as browser tests with auto-waiting actions and retrying expectations that work with 'testthat'. |
| Authors: | Garrick Aden-Buie [aut, cre] (ORCID: <https://orcid.org/0000-0002-7111-0077>), W.Y. [cph] (Copyright holder of the bundled html-to-image JavaScript library) |
| Maintainer: | Garrick Aden-Buie <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.0.0.9000 |
| Built: | 2026-10-16 21:34:05 UTC |
| Source: | https://github.com/posit-dev/paparazzi |
The contract shared by the pz_act_*() functions.
The pz_act_*() functions use the page the way a person would. They send
real browser input, such as pointer moves, clicks and key presses, or use
the browser's own focus and text-selection methods. They never set a form
value directly.
An action with a target looks for it in the current scope and waits until
it matches an element. Inside a scope, target = NULL acts on the scope's
element, and scrolling with by or to scrolls the scope's container. An
explicit target is scrolled into view first when needed. A few actions,
like pz_act_press(), take no target and act on the focused element
instead.
While the page is recording, actions are staged as pz_stage() sets them
up: the cursor glides to pointer targets, typing is paced, scrolls normally
use the mouse wheel, and the page holds for the staged pause afterward.
Without a recording, actions go straight to their final state.
To set a value directly instead, without staging, use pz_set_value(),
pz_set_files(), or pz_set_shiny_input().
Removes focus from the current scope's element, or at the root
context from whatever element currently has focus (document .activeElement). A no-op when the body is focused. Useful to clear
focus rings before a screenshot.
pz_act_blur(ctx, ...)pz_act_blur(ctx, ...)
ctx |
A paparazzi context. |
... |
Checked empty; reserved for future use. |
ctx, invisibly.
The pz_act_*() functions use the page the way a person would. They send
real browser input, such as pointer moves, clicks and key presses, or use
the browser's own focus and text-selection methods. They never set a form
value directly.
An action with a target looks for it in the current scope and waits until
it matches an element. Inside a scope, target = NULL acts on the scope's
element, and scrolling with by or to scrolls the scope's container. An
explicit target is scrolled into view first when needed. A few actions,
like pz_act_press(), take no target and act on the focused element
instead.
While the page is recording, actions are staged as pz_stage() sets them
up: the cursor glides to pointer targets, typing is paced, scrolls normally
use the mouse wheel, and the page holds for the staged pause afterward.
Without a recording, actions go straight to their final state.
To set a value directly instead, without staging, use pz_set_value(),
pz_set_files(), or pz_set_shiny_input().
page <- pz_open(pz_example("tasks")) page |> pz_act_focus("#task-title") # Remove the focus ring, e.g. before a screenshot page |> pz_act_blur() pz_expect_focused(page, target = "#task-title", not = TRUE) pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_act_focus("#task-title") # Remove the focus ring, e.g. before a screenshot page |> pz_act_blur() pz_expect_focused(page, target = "#task-title", not = TRUE) pz_close(page)
Auto-waits for the element to be actionable – visible with a
non-empty box, the same "visible" pz_expect_visible() uses, and
receiving pointer events at its center (not covered by another
element) – then scrolls it into view and clicks the center of it
with real browser input events: a mouse move to the point, then a
left-button press and release. The page sees a trusted pointer
sequence – exactly what a user's click produces – so :hover state, focus, and click
handlers all behave as they would live. While recording, the staging
settings (pz_stage()) animate the scroll, the cursor glide, and
the press; otherwise everything runs straight to the final state.
pz_act_click(ctx, target = NULL, ..., effect = NULL, effect_color = NULL)pz_act_click(ctx, target = NULL, ..., effect = NULL, effect_color = NULL)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
effect |
Click feedback while recording: |
effect_color |
CSS color of the |
ctx, invisibly.
The pz_act_*() functions use the page the way a person would. They send
real browser input, such as pointer moves, clicks and key presses, or use
the browser's own focus and text-selection methods. They never set a form
value directly.
An action with a target looks for it in the current scope and waits until
it matches an element. Inside a scope, target = NULL acts on the scope's
element, and scrolling with by or to scrolls the scope's container. An
explicit target is scrolled into view first when needed. A few actions,
like pz_act_press(), take no target and act on the focused element
instead.
While the page is recording, actions are staged as pz_stage() sets them
up: the cursor glides to pointer targets, typing is paced, scrolls normally
use the mouse wheel, and the page holds for the staged pause afterward.
Without a recording, actions go straight to their final state.
To set a value directly instead, without staging, use pz_set_value(),
pz_set_files(), or pz_set_shiny_input().
pz_act_hover(), pz_act_type(), pz_act_press()
page <- pz_open(pz_example("tasks")) page |> pz_act_click("#toggle-help") pz_get_text(page, target = "#toggle-help") # In a scoped context, target = NULL clicks the scope element page |> pz_find(pz_loc(".task-done", within = pz_loc(".task", has_text = "bank"))) |> pz_act_click() pz_get_count(page, target = ".task.done") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_act_click("#toggle-help") pz_get_text(page, target = "#toggle-help") # In a scoped context, target = NULL clicks the scope element page |> pz_find(pz_loc(".task-done", within = pz_loc(".task", has_text = "bank"))) |> pz_act_click() pz_get_count(page, target = ".task.done") pz_close(page)
Auto-waits for the source to be actionable: visible, non-empty, and
receiving pointer events at its center (not covered by another element).
With to, the destination is checked for visibility and that it receives
the drop at its center after the source is brought into view.
It then drags with real mouse input: press at the source's center,
move to the destination, release. When the source is a
real HTML5 drag source (draggable, including inherited
draggable or the image/<a href> defaults), the drag runs through
the browser's drag pipeline instead: the press and move start a
genuine dragstart (so dataTransfer holds whatever the page put
there), and the drop is delivered to the destination as trusted
dragenter/dragover/drop events with that payload.
While recording, the staging (pz_stage()) shows the press at the
source and glides the cursor while holding from the source to the
destination, streaming the path's real input events (held moves, or
dragenter/dragover on the HTML5 path) along the glide, so
pointer-following content tracks the cursor and intermediate
elements see the drag pass. The drop lands as the cursor arrives.
On the HTML5 path the carry runs between the page's dragstart and
the replayed drop, so dragstart styling stays visible through
the carry, and the real pointer stays by the source until the
release, so the destination shows no hover during the carry.
to names the element to drop onto; by = c(x, y) drops at that
offset in pixels from the source's center. Supply exactly one.
pz_act_drag(ctx, target, to = NULL, ..., by = NULL, preview = TRUE)pz_act_drag(ctx, target, to = NULL, ..., by = NULL, preview = TRUE)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
to |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
by |
Offset in pixels from the source's center, |
preview |
Whether to show a static source image during recorded HTML5 drags and a brief post-drop settling animation. Must be named. |
ctx, invisibly.
With preview = TRUE, paparazzi carries a static image of the HTML5 drag
source during an active, unpaused recording with a visible cursor. Paparazzi
captures the image before dragstart styling; a handle inside a draggable
element previews that element. After the drop, the image briefly settles to
the same source node's new bounds if it survives with a supported, unchanged
size. Paparazzi never hides or replaces the page's source. The preview is not
the browser's native drag image and does not reproduce custom
DataTransfer.setDragImage() feedback.
Paparazzi disables the optional preview with a warning in these cases. The drag continues without the preview.
Unreadable stylesheets, often on file:// pages: open the page over
HTTP. pz_open() serves local .html paths over HTTP when httpuv is
installed and loads. You can also serve the page with pz_serve_static()
and open the server handle with pz_open(). Cross-origin stylesheets must
also allow the browser to read their rules.
Iframes or SVG <use> references in the source: use a source without
these references; they can copy content outside the source subtree.
Unsupported CSS rule groups, imported stylesheets, or font sources:
move rules out of unsupported groups or imports and embed font sources as
data URLs in @font-face rules. Registered fonts also need a readable embedded
CSS representation.
Source-related or overlapping paparazzi redactions, or uncertain redaction state: keep redactions separate from the source or turn off the preview. Paparazzi permits capture only when it can verify that redactions are separate; do not remove redactions from sensitive content for a preview.
Unsupported source geometry or content: use a source without transforms, filters, clipping, overflowing content, shadow DOM, or generated content. Fixed or sticky descendants and out-of-box positioned content are also unsupported.
Other preview failures report the underlying error message. Use
preview = FALSE to turn off capture, carry imagery, and settling.
Unrecorded, paused, hidden-cursor, and ordinary mouse drags do no preview work.
The pz_act_*() functions use the page the way a person would. They send
real browser input, such as pointer moves, clicks and key presses, or use
the browser's own focus and text-selection methods. They never set a form
value directly.
An action with a target looks for it in the current scope and waits until
it matches an element. Inside a scope, target = NULL acts on the scope's
element, and scrolling with by or to scrolls the scope's container. An
explicit target is scrolled into view first when needed. A few actions,
like pz_act_press(), take no target and act on the focused element
instead.
While the page is recording, actions are staged as pz_stage() sets them
up: the cursor glides to pointer targets, typing is paced, scrolls normally
use the mouse wheel, and the page holds for the staged pause afterward.
Without a recording, actions go straight to their final state.
To set a value directly instead, without staging, use pz_set_value(),
pz_set_files(), or pz_set_shiny_input().
pz_act_click(), pz_act_hover()
page <- pz_open(pz_example("tasks")) pz_get_text(page, target = ".task-title") # The tasks are HTML5 drag sources; drop "Water the plants" on the first task page |> pz_act_drag( pz_loc(".task", has_text = "plants"), to = pz_loc(".task", which = "first") ) pz_get_text(page, target = ".task-title") pz_close(page)page <- pz_open(pz_example("tasks")) pz_get_text(page, target = ".task-title") # The tasks are HTML5 drag sources; drop "Water the plants" on the first task page |> pz_act_drag( pz_loc(".task", has_text = "plants"), to = pz_loc(".task", which = "first") ) pz_get_text(page, target = ".task-title") pz_close(page)
Scrolls the element into view (instantly) and focuses it via the browser's element focus method, so the page shows focus rings and enabled-input styles exactly as a user would see them. Focus is an element-state change, not an input event, so the direct method call is the faithful implementation (Playwright does the same).
pz_act_focus(ctx, target = NULL, ...)pz_act_focus(ctx, target = NULL, ...)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
ctx, invisibly.
The pz_act_*() functions use the page the way a person would. They send
real browser input, such as pointer moves, clicks and key presses, or use
the browser's own focus and text-selection methods. They never set a form
value directly.
An action with a target looks for it in the current scope and waits until
it matches an element. Inside a scope, target = NULL acts on the scope's
element, and scrolling with by or to scrolls the scope's container. An
explicit target is scrolled into view first when needed. A few actions,
like pz_act_press(), take no target and act on the focused element
instead.
While the page is recording, actions are staged as pz_stage() sets them
up: the cursor glides to pointer targets, typing is paced, scrolls normally
use the mouse wheel, and the page holds for the staged pause afterward.
Without a recording, actions go straight to their final state.
To set a value directly instead, without staging, use pz_set_value(),
pz_set_files(), or pz_set_shiny_input().
page <- pz_open(pz_example("tasks")) page |> pz_act_focus("#task-title") pz_expect_focused(page, target = "#task-title") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_act_focus("#task-title") pz_expect_focused(page, target = "#task-title") pz_close(page)
Auto-waits for the element to be actionable – visible with a
non-empty box and receiving pointer events at its center (not covered
by another element) – then scrolls it into view and moves the
pointer to the center of it with a real mousemove event, without
pressing any button. This is what drives :hover styles and
mouseenter/mouseover handlers.
pz_act_hover(ctx, target = NULL, ...)pz_act_hover(ctx, target = NULL, ...)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
ctx, invisibly.
The pz_act_*() functions use the page the way a person would. They send
real browser input, such as pointer moves, clicks and key presses, or use
the browser's own focus and text-selection methods. They never set a form
value directly.
An action with a target looks for it in the current scope and waits until
it matches an element. Inside a scope, target = NULL acts on the scope's
element, and scrolling with by or to scrolls the scope's container. An
explicit target is scrolled into view first when needed. A few actions,
like pz_act_press(), take no target and act on the focused element
instead.
While the page is recording, actions are staged as pz_stage() sets them
up: the cursor glides to pointer targets, typing is paced, scrolls normally
use the mouse wheel, and the page holds for the staged pause afterward.
Without a recording, actions go straight to their final state.
To set a value directly instead, without staging, use pz_set_value(),
pz_set_files(), or pz_set_shiny_input().
page <- pz_open(pz_example("tasks")) passport <- pz_loc(".task", has_text = "passport") # Tasks change colour on :hover pz_get_style(page, "background-color", target = passport) page |> pz_act_hover(passport) pz_get_style(page, "background-color", target = passport) pz_close(page)page <- pz_open(pz_example("tasks")) passport <- pz_loc(".task", has_text = "passport") # Tasks change colour on :hover pz_get_style(page, "background-color", target = passport) page |> pz_act_hover(passport) pz_get_style(page, "background-color", target = passport) pz_close(page)
Presses one or more key combinations against whatever the page
currently has focused, e.g. "Enter", "Control+A",
c("Shift+Tab", "Escape"). A vector presses each combination fully
(down then up) in order. Specs are "Mod+Mod+Key" strings with the
modifiers Control, Shift, Alt, Meta, and Mod (matched case-insensitively)
and a named key (Enter, Tab, Escape, Backspace, arrows, F1-F12, ...) or
a single printable character. Mod resolves to Meta on browsers reporting
a Mac platform (navigator.userAgentData.platform or navigator.platform),
and Control otherwise. Following Playwright, an uppercase
letter or shifted symbol implies Shift: "Control+A" sends
Control+Shift+A.
Keys only reach focused elements; call pz_act_click() or pz_act_focus()
first to focus the element you're typing into.
pz_act_press(ctx, key, ..., show_keys = NULL)pz_act_press(ctx, key, ..., show_keys = NULL)
ctx |
A paparazzi context. |
key |
A character vector of key specs. |
... |
Checked empty; reserved for future use. |
show_keys |
|
ctx, invisibly.
The pz_act_*() functions use the page the way a person would. They send
real browser input, such as pointer moves, clicks and key presses, or use
the browser's own focus and text-selection methods. They never set a form
value directly.
An action with a target looks for it in the current scope and waits until
it matches an element. Inside a scope, target = NULL acts on the scope's
element, and scrolling with by or to scrolls the scope's container. An
explicit target is scrolled into view first when needed. A few actions,
like pz_act_press(), take no target and act on the focused element
instead.
While the page is recording, actions are staged as pz_stage() sets them
up: the cursor glides to pointer targets, typing is paced, scrolls normally
use the mouse wheel, and the page holds for the staged pause afterward.
Without a recording, actions go straight to their final state.
To set a value directly instead, without staging, use pz_set_value(),
pz_set_files(), or pz_set_shiny_input().
pz_act_type() to insert text.
page <- pz_open(pz_example("tasks")) page |> pz_act_type("Buy milkk", target = "#task-title") |> pz_act_press("Backspace") pz_get_value(page, target = "#task-title") # Enter submits the form; the page shows "Saving..." before the task appears page |> pz_act_press("Enter") pz_expect_text(page, "Saved", target = "#status") pz_get_text(page, target = pz_loc(".task-title", which = "first")) # A vector presses keys in sequence; + joins keys pressed together page |> pz_act_press(c("Tab", "Shift+Tab")) pz_expect_focused(page, target = "#task-title") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_act_type("Buy milkk", target = "#task-title") |> pz_act_press("Backspace") pz_get_value(page, target = "#task-title") # Enter submits the form; the page shows "Saving..." before the task appears page |> pz_act_press("Enter") pz_expect_text(page, "Saved", target = "#status") pz_get_text(page, target = pz_loc(".task-title", which = "first")) # A vector presses keys in sequence; + joins keys pressed together page |> pz_act_press(c("Tab", "Shift+Tab")) pz_expect_focused(page, target = "#task-title") pz_close(page)
Exactly one of target, by, and to:
target: auto-waits for a match, then scrolls it into view
(instantly outside a recording).
by = c(x, y): scrolls the current scope's scroll container –
the scope element or its nearest scrollable ancestor, or the page
itself at the root context – by that many pixels.
to: scrolls that same container to an edge or corner from the
direction vocabulary: "top", "bottom", "left", "right",
the four corners, or "center" (e.g. "bottom" scrolls to the
end; "top right" to the top-right corner).
Scrolling is instant outside a recording; while recording it is staged as real mouse wheel events with the cursor over the container, so the video shows the scroll. Wheel scrolling is best-effort: if the page swallows the events, the instant scroll still guarantees the final position.
pz_act_scroll(ctx, target = NULL, ..., by = NULL, to = NULL, duration = NULL)pz_act_scroll(ctx, target = NULL, ..., by = NULL, to = NULL, duration = NULL)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
by |
Offset in pixels, |
to |
A direction string: the sides, the four corners, or
|
duration |
Seconds per staged wheel scroll; |
ctx, invisibly.
The pz_act_*() functions use the page the way a person would. They send
real browser input, such as pointer moves, clicks and key presses, or use
the browser's own focus and text-selection methods. They never set a form
value directly.
An action with a target looks for it in the current scope and waits until
it matches an element. Inside a scope, target = NULL acts on the scope's
element, and scrolling with by or to scrolls the scope's container. An
explicit target is scrolled into view first when needed. A few actions,
like pz_act_press(), take no target and act on the focused element
instead.
While the page is recording, actions are staged as pz_stage() sets them
up: the cursor glides to pointer targets, typing is paced, scrolls normally
use the mouse wheel, and the page holds for the staged pause afterward.
Without a recording, actions go straight to their final state.
To set a value directly instead, without staging, use pz_set_value(),
pz_set_files(), or pz_set_shiny_input().
page <- pz_open(pz_example("tasks"), height = 600) # With a target, scroll it into view page |> pz_act_scroll("#toggle-help") pz_expect_in_viewport(page, target = "#toggle-help") # With `to` or `by`, scroll the current scope's container: here the list page |> pz_find(".task-list") |> pz_act_scroll(to = "bottom") pz_expect_in_viewport(page, target = pz_loc(".task", which = "last")) # At the root, the container is the page itself page |> pz_act_scroll(to = "top") pz_evaluate(page, "window.scrollY") pz_close(page)page <- pz_open(pz_example("tasks"), height = 600) # With a target, scroll it into view page |> pz_act_scroll("#toggle-help") pz_expect_in_viewport(page, target = "#toggle-help") # With `to` or `by`, scroll the current scope's container: here the list page |> pz_find(".task-list") |> pz_act_scroll(to = "bottom") pz_expect_in_viewport(page, target = pz_loc(".task", which = "last")) # At the root, the container is the page itself page |> pz_act_scroll(to = "top") pz_evaluate(page, "window.scrollY") pz_close(page)
Auto-waits for a match, then highlights the exact text inside it.
paparazzi finds the substring among the element's text nodes and sets
the page's real window selection, even when the match spans inline tags
such as <em> and <strong>. Typing afterwards replaces it – call
pz_act_type() with target = NULL, which inserts into whatever has
focus.
pz_act_select_text(ctx, text, ..., target = NULL)pz_act_select_text(ctx, text, ..., target = NULL)
ctx |
A paparazzi context. |
text |
A string to select. Must appear exactly in the element, across tags if needed; not found is an error. |
... |
Checked empty; reserved for future use. |
target |
A CSS selector string, a |
While recording with a visible cursor, paparazzi animates a drag across the text, then sets the exact selection with the browser's Range API. The drag sends real mouse-move events, which can trigger page hover effects such as tooltips or hover menus. Outside a recording, selection is instant and leaves the cursor where it was, without sending mouse-move events.
A contenteditable target is focused too (dragging across editable text focuses it, and that focus is where the typing lands); a static target is not.
ctx, invisibly.
The pz_act_*() functions use the page the way a person would. They send
real browser input, such as pointer moves, clicks and key presses, or use
the browser's own focus and text-selection methods. They never set a form
value directly.
An action with a target looks for it in the current scope and waits until
it matches an element. Inside a scope, target = NULL acts on the scope's
element, and scrolling with by or to scrolls the scope's container. An
explicit target is scrolled into view first when needed. A few actions,
like pz_act_press(), take no target and act on the focused element
instead.
While the page is recording, actions are staged as pz_stage() sets them
up: the cursor glides to pointer targets, typing is paced, scrolls normally
use the mouse wheel, and the page holds for the staged pause afterward.
Without a recording, actions go straight to their final state.
To set a value directly instead, without staging, use pz_set_value(),
pz_set_files(), or pz_set_shiny_input().
page <- pz_open(pz_example("tasks")) # Typing replaces the selection page |> pz_find(pz_loc(".task", has_text = "Renew passport")) |> pz_act_click(".task-edit") |> pz_find(".task-title") |> pz_act_select_text("passport") |> pz_act_type("driving licence") |> pz_act_press("Enter") |> pz_get_text() pz_close(page)page <- pz_open(pz_example("tasks")) # Typing replaces the selection page |> pz_find(pz_loc(".task", has_text = "Renew passport")) |> pz_act_click(".task-edit") |> pz_find(".task-title") |> pz_act_select_text("passport") |> pz_act_type("driving licence") |> pz_act_press("Enter") |> pz_get_text() pz_close(page)
With a target, auto-waits for the element to be actionable –
visible with a non-empty box and receiving pointer events at its center
(not covered by another element) – then scrolls it into view, clicks
the center of it (real mouse events, so the element genuinely gains
focus), and inserts text at the caret – the caret lands where the
click lands, just like a real user. With target = NULL at the
root context, inserts into whatever element currently has focus;
if nothing editable is focused, the text goes nowhere, exactly like
typing into a page with no focused field.
Insertion is instant (one insertText), except while recording
with typing = "natural" (the default; see pz_stage()): one
insertText per character with randomized delays around
typing_speed, so the video shows the text appearing. While
recording, paparazzi fades the cursor out during typing so it doesn't
cover the text. The next pointer action fades it back in. For
a value-setting primitive that works on selects, checkboxes, and range
inputs, see pz_set_value().
pz_act_type(ctx, text, ..., target = NULL)pz_act_type(ctx, text, ..., target = NULL)
ctx |
A paparazzi context. |
text |
A string to type. |
... |
Checked empty; reserved for future use. |
target |
A CSS selector string, a |
ctx, invisibly.
The pz_act_*() functions use the page the way a person would. They send
real browser input, such as pointer moves, clicks and key presses, or use
the browser's own focus and text-selection methods. They never set a form
value directly.
An action with a target looks for it in the current scope and waits until
it matches an element. Inside a scope, target = NULL acts on the scope's
element, and scrolling with by or to scrolls the scope's container. An
explicit target is scrolled into view first when needed. A few actions,
like pz_act_press(), take no target and act on the focused element
instead.
While the page is recording, actions are staged as pz_stage() sets them
up: the cursor glides to pointer targets, typing is paced, scrolls normally
use the mouse wheel, and the page holds for the staged pause afterward.
Without a recording, actions go straight to their final state.
To set a value directly instead, without staging, use pz_set_value(),
pz_set_files(), or pz_set_shiny_input().
pz_act_press() for key combos (Enter, Control+A, ...) and
pz_act_click().
page <- pz_open(pz_example("tasks")) page |> pz_act_type("Buy milk", target = "#task-title") pz_get_value(page, target = "#task-title") # Typing fires input events, so the page reacts: Add is now enabled pz_expect_enabled(page, target = "#add-task") # At the root, target = NULL types into the focused element page |> pz_act_type(" and eggs") pz_get_value(page, target = "#task-title") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_act_type("Buy milk", target = "#task-title") pz_get_value(page, target = "#task-title") # Typing fires input events, so the page reacts: Add is now enabled pz_expect_enabled(page, target = "#add-task") # At the root, target = NULL types into the focused element page |> pz_act_type(" and eggs") pz_get_value(page, target = "#task-title") pz_close(page)
Draws an annotation for each element matched by target. Annotations
follow their elements as the page scrolls or changes layout, and appear
in screenshots and recordings until cleared with pz_annotate_clear().
Annotations belong to the current document; navigating away removes them.
Marks respect the target's overflow containers (axis-aligned clipping only,
not a custom overflow-clip-margin). Badges prefer a position above the
visible mark's top-left corner, outside those clips. For targets inside the
viewport, paparazzi shifts settled badges just enough to fit, overlapping
the target if needed at the top edge. Badges larger than the viewport align
with its top or left edge and may still be clipped; paparazzi does not resize
them. Off-viewport targets keep their badges attached, and a screenshot's
frame crop can still exclude a badge. Badges size to their labels, wrapping
long labels at 20em rather than at the target's width. On sides
where the target is not cut off, the clip leaves room for the mark's pad
so its outline stays intact. A mark and its badge hide while the target is
disconnected, not rendered, visibility: hidden with no visible descendants,
or entirely clipped, even if its padding would reach into view. Zero-size
targets on a clipping edge also stay hidden.
pz_annotate( ctx, target = NULL, ..., type = "box", label = NULL, pad = 0, reveal = c("auto", "fade", "draw", "pop", "slide", "wipe", "none"), id = NULL, color = NULL, label_fill = NULL, label_text_color = NULL, stroke_width = NULL, font_family = NULL, font_size = NULL )pz_annotate( ctx, target = NULL, ..., type = "box", label = NULL, pad = 0, reveal = c("auto", "fade", "draw", "pop", "slide", "wipe", "none"), id = NULL, color = NULL, label_fill = NULL, label_text_color = NULL, stroke_width = NULL, font_family = NULL, font_size = NULL )
ctx |
A paparazzi context. |
target |
A selector, |
... |
Checked empty; reserved for future use. |
type |
|
label |
Optional badge. |
pad |
Extra CSS pixels around each element: one number or
|
reveal |
|
id |
Optional nonempty id. Reusing it replaces its annotations;
|
color |
CSS accent color for the mark outline, or |
label_fill, label_text_color
|
CSS colors for the badge background
and text. |
stroke_width |
Mark stroke width in CSS pixels, or |
font_family |
CSS font family for the badge, or |
font_size |
Badge font size in CSS pixels, or |
ctx, invisibly.
pz_annotate_clear(), pz_stage_annotate()
Draws one callout per matching element, repeating text for every match.
Callouts follow their targets during scrolling and layout changes, and
appear in screenshots and recordings until cleared with
pz_annotate_clear(). They belong to the current document and disappear
on navigation. A bubble is kept within the viewport even if this moves it
away from the requested side; text that cannot fit a tiny viewport is
clipped. A viewport resize does not rewrap an existing callout. A
callout hides while its target isn't rendered, is visibility: hidden
with no visible descendants, or is entirely clipped by an overflow
container. When the target is partly clipped, the leader
points at its visible part; the bubble itself is never clipped.
pz_annotate_callout( ctx, text, ..., target = NULL, side = NULL, leader = TRUE, label = NULL, reveal = c("pop", "fade", "draw", "slide", "wipe", "none"), id = NULL, color = NULL, fill = NULL, text_color = NULL, stroke_width = NULL, distance = NULL, font_family = NULL, font_size = NULL )pz_annotate_callout( ctx, text, ..., target = NULL, side = NULL, leader = TRUE, label = NULL, reveal = c("pop", "fade", "draw", "slide", "wipe", "none"), id = NULL, color = NULL, fill = NULL, text_color = NULL, stroke_width = NULL, distance = NULL, font_family = NULL, font_size = NULL )
ctx |
A paparazzi context. |
text |
One nonempty string of literal text (not HTML). |
... |
Checked empty; reserved for future use. |
target |
A selector, |
side |
A side ( |
leader |
The line between the bubble and the target. |
label |
Optional badge; |
reveal |
|
id |
Optional nonempty id. Reusing it replaces its annotations;
|
color |
Accent color for the bubble border, leader line and
decorations; |
fill, text_color
|
CSS colors for the bubble background and text
(and the callout's badge); |
stroke_width |
Leader line width in CSS pixels, or |
distance |
Bubble-to-target gap in CSS pixels, or |
font_family |
CSS font family; |
font_size |
Font size in CSS pixels; |
ctx, invisibly.
pz_annotate(), pz_annotate_clear()
A caption stays on the page until replaced or cleared with
pz_annotate_clear(id = "caption"). It persists
through navigation and across recordings, and appears on stills. In a
recording, a caption fades out when it is cleared or replaced; one that
is still showing at pz_record_stop() stays on through the last frame.
pz_annotate_caption( ctx, text, ..., side = NULL, color = "white", font_family = NULL, font_size = 20 )pz_annotate_caption( ctx, text, ..., side = NULL, color = "white", font_family = NULL, font_size = 20 )
ctx |
A paparazzi context. |
text |
Nonempty caption text. Newlines are preserved. |
... |
Checked empty. |
side |
Placement: |
color |
Text color; defaults to |
font_family |
CSS font family. |
font_size |
Font size in CSS pixels; defaults to 20, independent of the annotation badge size. Captions scale with the output. |
ctx, invisibly.
pz_annotate_clear(), pz_record_start(), pz_screenshot()
Remove an annotation by id, or remove every annotation when id is
NULL. During an active recording, marks play their reveal in reverse
before removal; paused recordings and stills clear instantly.
pz_annotate_clear(ctx, id = NULL, ...)pz_annotate_clear(ctx, id = NULL, ...)
ctx |
A paparazzi context. |
id |
Id to clear; |
... |
Checked empty; reserved for future use. |
ctx, invisibly.
Covers every element matched by target with an instant opaque fill or
backdrop blur in screenshots and recordings. Fill is the safe choice for
secrets: blur can leave text partly legible. Redactions follow elements
as they move and respect axis-aligned overflow clipping, except custom
overflow-clip-margin. If a target disconnects, stops rendering, or is
entirely clipped, its redaction hides until it becomes visible again. They
belong to the current document and are lost on navigation. Redaction covers
each element's border box plus pad, not overflowing descendants; target
the overflowing element or add padding.
Later top-layer UI (modal dialogs and popovers) paints above redactions, so
targets inside an open modal or popover are rejected.
pz_annotate_redact( ctx, target = NULL, ..., method = c("fill", "blur"), pad = 0, id = NULL, color = NULL )pz_annotate_redact( ctx, target = NULL, ..., method = c("fill", "blur"), pad = 0, id = NULL, color = NULL )
ctx |
A paparazzi context. |
target |
A selector, |
... |
Checked empty; reserved for future use. |
method |
|
pad |
Extra CSS pixels around each element; defaults to zero. |
id |
Optional nonempty id; reusing it replaces the old annotation.
|
color |
CSS color for fill; |
ctx, invisibly.
pz_annotate(), pz_annotate_clear()
Dims the page except for a rounded cutout around each matched element.
The scrim covers the document, including below-fold areas captured in
screenshots. The spotlight follows its elements through scrolling and
layout changes. Disconnected or zero-size elements lose their cutouts,
and so do visibility: hidden elements with no visible descendants.
Cutouts are clipped by the target's overflow containers
(axis-aligned clipping only, not a custom overflow-clip-margin), so a
target scrolled out of a scrolling container gets no cutout. It appears
in screenshots and recordings until cleared, and is lost on navigation.
Only one spotlight exists per page: a new call replaces the previous one,
and pz_annotate_clear() removes it by the reserved id "spotlight" or
with a clear-all call. The cutout covers each target's padded border box;
overflowing descendants and top-layer dialogs/popovers are not covered.
pz_annotate_spotlight( ctx, target = NULL, ..., pad = 0, dim = 0.6, reveal = c("fade", "none") )pz_annotate_spotlight( ctx, target = NULL, ..., pad = 0, dim = 0.6, reveal = c("fade", "none") )
ctx |
A paparazzi context. |
target |
A selector, |
... |
Checked empty; reserved for future use. |
pad |
Extra CSS pixels around each element: one number or
|
dim |
Opacity of the black overlay outside the cutouts, from 0 (transparent) to 1 (black). Defaults to 0.6. |
reveal |
|
ctx, invisibly.
pz_annotate(), pz_annotate_clear()
Moves an encode-time camera over a recording. The shot comes from
frame: a pz_frame() spec, or a bare locator (a CSS selector
string, a pz_loc() spec, or a list of either) promoted to one,
whose union is the shot. In a scoped context frame may be omitted
or NULL to shoot the scope's box; at the root it is required
(pz_camera_reset() returns to the home view). frame = FALSE
shoots the scope's box, or the viewport at the root, unframed. The
camera does not change the live page or still screenshots. Outside a
recording it does nothing, and each pz_record_start() begins at
the recording frame.
pz_camera(ctx, frame, ..., duration = NULL, wait = FALSE)pz_camera(ctx, frame, ..., duration = NULL, wait = FALSE)
ctx |
A paparazzi context. |
frame |
The shot's what-and-how: a
|
... |
Checked empty; reserved for future use. |
duration |
Movement duration in seconds. |
wait |
Whether to wait for the move to finish before the next step.
|
ctx, invisibly.
Returns the camera to the recording frame (or the full viewport). Outside a recording it does nothing.
pz_camera_reset(ctx, ..., wait = FALSE)pz_camera_reset(ctx, ..., wait = FALSE)
ctx |
A paparazzi context. |
... |
Checked empty; reserved for future use. |
wait |
Whether to wait for the move to finish before the next step.
|
ctx, invisibly.
Escape hatch for raw Chrome DevTools Protocol calls.
pz_chromote(ctx)pz_chromote(ctx)
ctx |
A paparazzi context. |
The chromote::ChromoteSession backing the page.
page <- pz_open(pz_example("tasks")) session <- pz_chromote(page) # Make raw Chrome DevTools Protocol calls session$Browser$getVersion()$product session$Performance$enable() metrics <- session$Performance$getMetrics()$metrics Filter(function(m) m$name == "Nodes", metrics)[[1]]$value pz_close(page)page <- pz_open(pz_example("tasks")) session <- pz_chromote(page) # Make raw Chrome DevTools Protocol calls session$Browser$getVersion()$product session$Performance$enable() metrics <- session$Performance$getMetrics()$metrics Filter(function(m) m$name == "Nodes", metrics)[[1]]$value pz_close(page)
Closes the page's browser session and any server it started. A page opened from a shared serving handle leaves the server running. Closing an already-closed page is a no-op.
pz_close(page)pz_close(page)
page |
A |
The page, invisibly.
page <- pz_open(pz_example("tasks")) pz_close(page) # Closing a closed page does nothing pz_close(page)page <- pz_open(pz_example("tasks")) pz_close(page) # Closing a closed page does nothing pz_close(page)
Hides the cursor set up by pz_cursor_show() or shown implicitly
while recording. The cursor keeps its last position; showing it
again returns it there. The hide is explicit and sticky: the cursor
stays hidden until pz_cursor_show() or pz_cursor_move() shows it
again – neither a recorded pointer action nor the cursor staging
setting brings it back. That is what distinguishes
pz_cursor_hide() from pz_cursor_leave(), which keeps the cursor
visible and glides back in on the next action.
pz_cursor_hide(ctx, ...)pz_cursor_hide(ctx, ...)
ctx |
A paparazzi context. |
... |
Checked empty; reserved for future use. |
ctx, invisibly.
pz_cursor_show(), pz_cursor_leave()
page <- pz_open(pz_example("tasks")) page |> pz_cursor_show("#add-task") |> pz_cursor_hide() |> pz_screenshot(file.path(tempdir(), "no-cursor.png")) pz_screenshot(page, frame = pz_frame("#new-task", pad = 24)) pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_cursor_show("#add-task") |> pz_cursor_hide() |> pz_screenshot(file.path(tempdir(), "no-cursor.png")) pz_screenshot(page, frame = pz_frame("#new-task", pad = 24)) pz_close(page)
Sends the cursor out of the frame through side: while recording it
glides out, otherwise it leaves immediately. The cursor is still
visible (just off-frame); the next pointer action or cursor call
glides it back in from that side.
pz_cursor_leave(ctx, side = "right", icon = NULL)pz_cursor_leave(ctx, side = "right", icon = NULL)
ctx |
A paparazzi context. |
side |
A side ( |
icon |
CSS cursor keyword to show during the exit. |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "help.mp4") page |> pz_record(path, { page |> pz_act_click("#toggle-help") |> # Move the cursor out of the way so the help text is unobstructed pz_cursor_leave("right", icon = "default") |> pz_record_hold(1) }) pz_close(page)page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "help.mp4") page |> pz_record(path, { page |> pz_act_click("#toggle-help") |> # Move the cursor out of the way so the help text is unobstructed pz_cursor_leave("right", icon = "default") |> pz_record_hold(1) }) pz_close(page)
Moves the cursor over target, showing it first if hidden. While
recording the move is a glide whose duration scales with distance
(see pz_stage()); pass duration to override it. Without a
recording the cursor jumps.
pz_cursor_move( ctx, target, ..., duration = NULL, icon = NULL, offset = c(0, 0) )pz_cursor_move( ctx, target, ..., duration = NULL, icon = NULL, offset = c(0, 0) )
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
duration |
Glide duration in seconds; |
icon |
CSS cursor keyword to show for this call. |
offset |
Landing offset in viewport CSS pixels, |
ctx, invisibly.
pz_cursor_show(), pz_cursor_leave()
page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "tour.mp4") # While recording, the cursor glides between elements page |> pz_record(path, { page |> pz_cursor_show(".filters", from = "left") |> pz_cursor_move("#toggle-help", duration = 1) |> pz_cursor_move("#add-task", icon = "crosshair") }) pz_close(page)page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "tour.mp4") # While recording, the cursor glides between elements page |> pz_record(path, { page |> pz_cursor_show(".filters", from = "left") |> pz_cursor_move("#toggle-help", duration = 1) |> pz_cursor_move("#add-task", icon = "crosshair") }) pz_close(page)
Shows paparazzi's overlay cursor, optionally over a target element.
While recording, the cursor appears with staging: it glides from its
last position, fades in on the target when it has never been shown,
or glides in from from (or the enter side set with pz_stage())
when entering the frame. Without a recording the cursor appears
statically, which is how a cursor lands in a screenshot. Its default
size is 1.75 times the original artwork; set cursor_scale with
pz_stage() to change the size in videos and stills.
pz_cursor_show(ctx, target = NULL, ..., from = NULL, icon = NULL)pz_cursor_show(ctx, target = NULL, ..., from = NULL, icon = NULL)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
from |
A side ( |
icon |
CSS cursor keyword to show for this call. |
ctx, invisibly.
pz_stage() for cursor settings, pz_cursor_hide(),
pz_cursor_move(), pz_cursor_leave()
page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "add-button.png") # Outside a recording the cursor appears at once, ready for a still page |> pz_cursor_show("#add-task", icon = "pointer") |> pz_screenshot(path, frame = pz_frame("#new-task", pad = 24)) pz_screenshot(page, frame = pz_frame("#new-task", pad = 24)) pz_close(page)page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "add-button.png") # Outside a recording the cursor appears at once, ready for a still page |> pz_cursor_show("#add-task", icon = "pointer") |> pz_screenshot(path, frame = pz_frame("#new-task", pad = 24)) pz_screenshot(page, frame = pz_frame("#new-task", pad = 24)) pz_close(page)
Configures device emulation for the page: viewport size, device scale
factor, mobile-ness, zoom, color scheme, reduced motion, locale and
time zone. Only supplied arguments change state: NULL (the
default) means "leave this setting alone", so zoom = 1, not
zoom = NULL, disables an active zoom.
pz_device( ctx, ..., width = NULL, height = NULL, scale = NULL, mobile = NULL, zoom = NULL, zoom_method = NULL, color_scheme = NULL, reduced_motion = NULL, locale = NULL, timezone = NULL )pz_device( ctx, ..., width = NULL, height = NULL, scale = NULL, mobile = NULL, zoom = NULL, zoom_method = NULL, color_scheme = NULL, reduced_motion = NULL, locale = NULL, timezone = NULL )
ctx |
A paparazzi context. |
... |
Checked empty; reserved for future use. |
width, height
|
Viewport size in CSS pixels. Only the supplied one changes; the other keeps its current value. |
scale |
Device scale factor: how many screen pixels each CSS pixel
covers. |
mobile |
Emulate a mobile device (touch input, mobile viewport
semantics)? |
zoom |
Zoom factor; |
zoom_method |
|
color_scheme |
|
reduced_motion |
Emulate |
locale |
An ICU locale, e.g. |
timezone |
An IANA time zone, e.g. |
The two zoom methods trade off differently:
zoom_method = "viewport" (the default) shrinks the CSS viewport
and raises the device scale factor, so vh stays correct but
media queries can start (or stop) matching.
zoom_method = "css" applies a CSS zoom to the page instead –
on every document, so it survives navigation and reload: layout
and media queries are untouched, but vh-sized elements no longer
fit the viewport (a 50vh element covers the whole viewport at
zoom 2).
reduced_motion = TRUE gives deterministic stills (animations stop);
it's opt-in because it's usually wrong for videos.
ctx, invisibly.
pz_open() forwards its ... here.
page <- pz_open(pz_example("tasks")) # Only the settings you supply change page |> pz_device(width = 390, height = 844, mobile = TRUE) pz_evaluate(page, "[window.innerWidth, window.devicePixelRatio]") page |> pz_device(color_scheme = "dark") pz_get_style(page, "background-color", target = "body") pz_close(page)page <- pz_open(pz_example("tasks")) # Only the settings you supply change page |> pz_device(width = 390, height = 844, mobile = TRUE) pz_evaluate(page, "[window.innerWidth, window.devicePixelRatio]") page |> pz_device(color_scheme = "dark") pz_get_style(page, "background-color", target = "body") pz_close(page)
Evaluates expr in the page, optionally awaiting a returned promise, and
returns the resulting value. This is paparazzi's equivalent of Playwright's
page.evaluate() (and likewise Puppeteer and the CDP Runtime.evaluate).
It is the primitive all element work uses and an escape hatch for one-off
scripts. Unlike most pz_*() functions it
ends the chain: it returns the value, not the context.
pz_evaluate(ctx, expr, ..., await = TRUE, timeout = NULL)pz_evaluate(ctx, expr, ..., await = TRUE, timeout = NULL)
ctx |
A paparazzi context. |
expr |
A string of JavaScript to evaluate. |
... |
Checked empty; reserved for future use. |
await |
Await a promise returned by |
timeout |
Seconds before the evaluation fails; |
The value produced by expr (converted to R), or NULL.
page <- pz_open(pz_example("tasks")) pz_evaluate(page, "document.querySelectorAll('.task').length") # Arrays come back as lists pz_evaluate(page, "[...document.querySelectorAll('.task-priority')].map(el => el.textContent)") # Returned promises are awaited pz_evaluate(page, "new Promise(resolve => setTimeout(() => resolve('done'), 100))") pz_close(page)page <- pz_open(pz_example("tasks")) pz_evaluate(page, "document.querySelectorAll('.task').length") # Arrays come back as lists pz_evaluate(page, "[...document.querySelectorAll('.task-priority')].map(el => el.textContent)") # Returned promises are awaited pz_evaluate(page, "new Promise(resolve => setTimeout(() => resolve('done'), 100))") pz_close(page)
paparazzi ships a few small pages and apps for its examples and
articles. pz_example() returns the path to one of them, ready to pass to
pz_open() or pz_serve_shiny(). The examples are:
pz_example(name = NULL)pz_example(name = NULL)
name |
The name of an example, without a file extension. |
"tasks": a static task tracker page (an HTML file) with a form, a
scrollable list of draggable tasks, filter links that change the URL
fragment, inline title editing, an attachment input for new tasks, a
help panel that starts hidden, and a "Start over" link that loads the
page again. Adding a task shows "Saving..." for 400ms before the task
appears.
"tasks-app": a Shiny app directory with a text input, a selectize
priority input, an Add button, and a task list whose output takes half
a second to render. Running it requires the shiny package.
The absolute path to the example's file or directory, or a
character vector of example names when name is NULL.
pz_example() pz_example("tasks") page <- pz_open(pz_example("tasks")) pz_get_title(page) pz_close(page)pz_example() pz_example("tasks") page <- pz_open(pz_example("tasks")) pz_get_title(page) pz_close(page)
pz_expect_attr() passes when at least one element matches and every
matched element satisfies every named attribute/value pair in ....
Missing attributes satisfy no pair. Comparisons are exact by default;
.match = "contains" or "regex" applies to all pairs in the call.
A length-1 value applies to every element. A vector requires exactly
that many matches and compares pairwise, in order. With .not = TRUE,
the expectation passes when the combined condition does not hold,
including when no element matches.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_attr( ctx, .target = NULL, ..., .match = c("exact", "contains", "regex"), .not = FALSE, .timeout = NULL )pz_expect_attr( ctx, .target = NULL, ..., .match = c("exact", "contains", "regex"), .not = FALSE, .timeout = NULL )
ctx |
A paparazzi context. |
.target |
A CSS selector string, a |
... |
Named attribute/value pairs. Values are character vectors;
use backticks for names such as |
.match |
Comparison mode for all pairs: |
.not |
Invert the combined expectation? |
.timeout |
Seconds to wait; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) page |> pz_expect_attr( pz_loc(".task", has_text = "tax"), `data-priority` = "high" ) page |> pz_act_click("#toggle-help") |> pz_expect_attr("#toggle-help", `aria-expanded` = "true") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_expect_attr( pz_loc(".task", has_text = "tax"), `data-priority` = "high" ) page |> pz_act_click("#toggle-help") |> pz_expect_attr("#toggle-help", `aria-expanded` = "true") pz_close(page)
pz_expect_checked() passes when at least one element matches and
every match is checked (the browser's :checked selector, so
checkboxes, radios, and select options all count). Toggle with
pz_act_click() or pz_set_value().
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_checked(ctx, target = NULL, ..., not = FALSE, timeout = NULL)pz_expect_checked(ctx, target = NULL, ..., not = FALSE, timeout = NULL)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) page |> pz_expect_checked(target = "#task-urgent", not = TRUE) page |> pz_act_click("#task-urgent") |> pz_expect_checked(target = "#task-urgent") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_expect_checked(target = "#task-urgent", not = TRUE) page |> pz_act_click("#task-urgent") |> pz_expect_checked(target = "#task-urgent") pz_close(page)
pz_expect_class() passes when at least one element matches and every
match carries class. With not = TRUE it passes when no match does,
including when nothing matches. class is one class name, not a
space-separated list: expect each class with its own call.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_class(ctx, class, target = NULL, ..., not = FALSE, timeout = NULL)pz_expect_class(ctx, class, target = NULL, ..., not = FALSE, timeout = NULL)
ctx |
A paparazzi context. |
class |
A single class name to expect on every match. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) water <- pz_loc(".task", has_text = "Water") page |> pz_expect_class("done", target = water, not = TRUE) page |> pz_find(water) |> pz_act_click(".task-done") |> pz_expect_class("done") pz_close(page)page <- pz_open(pz_example("tasks")) water <- pz_loc(".task", has_text = "Water") page |> pz_expect_class("done", target = water, not = TRUE) page |> pz_find(water) |> pz_act_click(".task-done") |> pz_expect_class("done") pz_close(page)
pz_expect_count() passes when the number of elements matching
target satisfies the requirement: n is exact, or min and/or
max give an inclusive range (either may be NULL, meaning
unbounded). With not = TRUE it passes when the count does anything
else. Specify exactly one of n or min/max.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_count( ctx, n = NULL, target = NULL, ..., min = NULL, max = NULL, not = FALSE, timeout = NULL )pz_expect_count( ctx, n = NULL, target = NULL, ..., min = NULL, max = NULL, not = FALSE, timeout = NULL )
ctx |
A paparazzi context. |
n |
Exact expected count. Exclusive with |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
min |
Minimum count; with |
max |
Maximum count; with |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) page |> pz_expect_count(7, target = ".task") page |> pz_expect_count(min = 1, max = 2, target = ".task.done") # Expectations retry, so they wait for the page to catch up: the new task # appears after a short "Saving..." delay page |> pz_act_type("Buy milk", target = "#task-title") |> pz_act_click("#add-task") |> pz_expect_count(8, target = ".task") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_expect_count(7, target = ".task") page |> pz_expect_count(min = 1, max = 2, target = ".task.done") # Expectations retry, so they wait for the page to catch up: the new task # appears after a short "Saving..." delay page |> pz_act_type("Buy milk", target = "#task-title") |> pz_act_click("#add-task") |> pz_expect_count(8, target = ".task") pz_close(page)
pz_expect_enabled() passes when at least one element matches and
every match is enabled. An element counts as disabled when it matches
the browser's :disabled selector, so inputs inside a disabled
<fieldset> are disabled too.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_enabled(ctx, target = NULL, ..., not = FALSE, timeout = NULL)pz_expect_enabled(ctx, target = NULL, ..., not = FALSE, timeout = NULL)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) # Add is disabled until the title has text page |> pz_expect_enabled(target = "#add-task", not = TRUE) page |> pz_act_type("Buy milk", target = "#task-title") |> pz_expect_enabled(target = "#add-task") pz_close(page)page <- pz_open(pz_example("tasks")) # Add is disabled until the title has text page |> pz_expect_enabled(target = "#add-task", not = TRUE) page |> pz_act_type("Buy milk", target = "#task-title") |> pz_expect_enabled(target = "#add-task") pz_close(page)
pz_expect_exists() passes when at least one element matching target
is in the DOM, visible or not. It's the one expectation where multiple
matches don't all have to satisfy the check: existence needs only one.
With not = TRUE it passes when nothing matches.
Expectations retry until they pass or timeout elapses, then return
ctx invisibly. Outside of testthat, a failure aborts with a classed
error of class "paparazzi_expectation_failure" showing the target,
the last observed value, and the time waited; inside testthat, the
failure is instead reported as a test failure, and a pass counts as a
successful testthat expectation.
pz_expect_exists(ctx, target = NULL, ..., not = FALSE, timeout = NULL)pz_expect_exists(ctx, target = NULL, ..., not = FALSE, timeout = NULL)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) page |> pz_expect_exists(target = ".task") # The help panel is in the page even while it's hidden page |> pz_expect_exists(target = "#help") page |> pz_expect_exists(target = ".error-message", not = TRUE) # A failing expectation retries until the timeout, then errors try(pz_expect_exists(page, target = ".error-message", timeout = 0.5)) pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_expect_exists(target = ".task") # The help panel is in the page even while it's hidden page |> pz_expect_exists(target = "#help") page |> pz_expect_exists(target = ".error-message", not = TRUE) # A failing expectation retries until the timeout, then errors try(pz_expect_exists(page, target = ".error-message", timeout = 0.5)) pz_close(page)
pz_expect_focused() passes when at least one element matches and
every match is the page's focused element (document.activeElement).
Focus it with pz_act_focus() or a pz_act_click(), and remove it with
pz_act_blur().
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_focused(ctx, target = NULL, ..., not = FALSE, timeout = NULL)pz_expect_focused(ctx, target = NULL, ..., not = FALSE, timeout = NULL)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) page |> pz_act_click("#task-title") |> pz_expect_focused(target = "#task-title") page |> pz_act_press("Tab") |> pz_expect_focused(target = "#task-priority") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_act_click("#task-title") |> pz_expect_focused(target = "#task-title") page |> pz_act_press("Tab") |> pz_expect_focused(target = "#task-priority") pz_close(page)
pz_expect_in_viewport() passes when at least one element matches
and every match overlaps the viewport: its bounding box crosses the
visible area by any amount. Elements entirely above, below, or beside
the fold fail; visibility itself is a separate expectation,
pz_expect_visible().
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_in_viewport(ctx, target = NULL, ..., not = FALSE, timeout = NULL)pz_expect_in_viewport(ctx, target = NULL, ..., not = FALSE, timeout = NULL)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks"), height = 500) # The last task is inside the scrolling list, below its visible area last_task <- pz_loc(".task", which = "last") page |> pz_expect_in_viewport(target = last_task, not = TRUE) page |> pz_act_scroll(last_task) |> pz_expect_in_viewport(target = last_task) pz_close(page)page <- pz_open(pz_example("tasks"), height = 500) # The last task is inside the scrolling list, below its visible area last_task <- pz_loc(".task", which = "last") page |> pz_expect_in_viewport(target = last_task, not = TRUE) page |> pz_act_scroll(last_task) |> pz_expect_in_viewport(target = last_task) pz_close(page)
pz_expect_js() passes when at least one element matches and the
predicate holds for every match: expr is evaluated as a function
receiving the element, e.g. "el => el.scrollTop > 0". It's the
escape hatch for conditions the catalog doesn't cover. A predicate
that throws is a JavaScript error, not a failed check.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_js(ctx, expr, target = NULL, ..., not = FALSE, timeout = NULL)pz_expect_js(ctx, expr, target = NULL, ..., not = FALSE, timeout = NULL)
ctx |
A paparazzi context. |
expr |
A JavaScript function receiving the element, as a string,
e.g. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) # The predicate receives each matching element page |> pz_expect_js("el => el.scrollHeight > el.clientHeight", target = ".task-list") page |> pz_expect_js("el => el.draggable", target = ".task") pz_close(page)page <- pz_open(pz_example("tasks")) # The predicate receives each matching element page |> pz_expect_js("el => el.scrollHeight > el.clientHeight", target = ".task-list") page |> pz_expect_js("el => el.draggable", target = ".task") pz_close(page)
pz_expect_style() passes when at least one element matches
.target and every match has every named property set to the
expected value, comparing computed styles – what the browser
actually applied, the same values Playwright's toHaveCSS() and
jQuery's .css() read.
Property names accept snake_case, converted to the CSS kebab-case
spelling (font_size becomes font-size); custom properties
(`--bs-primary` = "#0d6efd") pass through unchanged. By
default the expected values are normalized in the browser: what you
write ("1.5rem", "#0d6efd", "50%", "currentColor") is
resolved against the same context the target sees, so it matches
the computed value the browser reports. Numeric pixel values
compare with a 0.5px tolerance.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for
the retry, timeout, and bridge behavior shared by all expectations.
pz_expect_style( ctx, .target = NULL, ..., .not = FALSE, .timeout = NULL, .normalize = TRUE )pz_expect_style( ctx, .target = NULL, ..., .not = FALSE, .timeout = NULL, .normalize = TRUE )
ctx |
A paparazzi context. |
.target |
A CSS selector string, a |
... |
Property/value pairs, e.g. |
.not |
Invert the combined expectation? |
.timeout |
Seconds to wait; |
.normalize |
Normalize expected values in the browser before comparing? See Details. |
With .not = TRUE the expectation passes when at least one
property/value pair doesn't match (including when nothing matches).
With .normalize = FALSE the expected values are compared as raw
strings against the browser's computed output, for contexts the
probe can't reproduce (unusual % cases, container query units);
the pixel tolerance still applies.
Shorthand properties (margin, border, background, ...) are
unreliable in computed styles and error with a longhand suggestion;
check the longhand properties instead. A value the browser rejects
errors immediately as invalid CSS, without retrying (only detected
with .normalize = TRUE).
ctx, invisibly.
page <- pz_open(pz_example("tasks")) # Expected values are written as you'd write them in CSS page |> pz_expect_style("#add-task", background_color = "#0d6efd") page |> pz_expect_style("h1", font_size = "1.5rem") page |> pz_expect_style("#help", display = "none") page |> pz_act_click("#toggle-help") |> pz_expect_style("#help", display = "none", .not = TRUE) pz_close(page)page <- pz_open(pz_example("tasks")) # Expected values are written as you'd write them in CSS page |> pz_expect_style("#add-task", background_color = "#0d6efd") page |> pz_expect_style("h1", font_size = "1.5rem") page |> pz_expect_style("#help", display = "none") page |> pz_act_click("#toggle-help") |> pz_expect_style("#help", display = "none", .not = TRUE) pz_close(page)
pz_expect_text() passes when at least one element matches and the
text of every match satisfies text. Whitespace collapses on both
sides before comparing, so "Save now" matches text reading
"Save now".
A length-1 text applies to every match. A length-n text requires
exactly n matches and compares pairwise, in order. With
not = TRUE, the expectation passes when no match satisfies text,
including when nothing matches.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_text( ctx, text, target = NULL, ..., match = c("contains", "exact", "regex"), not = FALSE, timeout = NULL )pz_expect_text( ctx, text, target = NULL, ..., match = c("contains", "exact", "regex"), not = FALSE, timeout = NULL )
ctx |
A paparazzi context. |
text |
A character vector of expected text: length 1 applies to
every match, length |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
match |
How to compare |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) page |> pz_expect_text("Tasks", target = "h1", match = "exact") page |> pz_expect_text("passport", target = pz_loc(".task", which = "first")) # A vector compares pairwise with the matches, in order page |> pz_expect_text(c("All", "Open", "Done"), target = ".filters a") # Regular expressions use R's syntax page |> pz_expect_text("^[A-Z]", target = ".task-title", match = "regex") # On failure, the error shows the target and the last text seen try(pz_expect_text(page, "otters", target = "h1", timeout = 0.5)) pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_expect_text("Tasks", target = "h1", match = "exact") page |> pz_expect_text("passport", target = pz_loc(".task", which = "first")) # A vector compares pairwise with the matches, in order page |> pz_expect_text(c("All", "Open", "Done"), target = ".filters a") # Regular expressions use R's syntax page |> pz_expect_text("^[A-Z]", target = ".task-title", match = "regex") # On failure, the error shows the target and the last text seen try(pz_expect_text(page, "otters", target = "h1", timeout = 0.5)) pz_close(page)
pz_expect_title() passes when the page's <title> satisfies
title. It works from any context, scoped or root: the title belongs
to the page, not to an element.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_title( ctx, title, ..., match = c("contains", "exact", "regex"), not = FALSE, timeout = NULL )pz_expect_title( ctx, title, ..., match = c("contains", "exact", "regex"), not = FALSE, timeout = NULL )
ctx |
A paparazzi context. |
title |
The expected page title, a single string. |
... |
Checked empty; reserved for future use. |
match |
How to compare |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) page |> pz_expect_title("Tasks", match = "exact") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_expect_title("Tasks", match = "exact") pz_close(page)
pz_expect_url() passes when the page's URL satisfies url. It works
from any context, scoped or root: the URL belongs to the page, not to
an element.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_url( ctx, url, ..., match = c("contains", "exact", "regex"), not = FALSE, timeout = NULL )pz_expect_url( ctx, url, ..., match = c("contains", "exact", "regex"), not = FALSE, timeout = NULL )
ctx |
A paparazzi context. |
url |
The expected URL, a single string. |
... |
Checked empty; reserved for future use. |
match |
How to compare |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) page |> pz_expect_url("tasks.html") page |> pz_act_click(pz_loc(".filters a", has_text = "Done")) |> pz_expect_url("#done$", match = "regex") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_expect_url("tasks.html") page |> pz_act_click(pz_loc(".filters a", has_text = "Done")) |> pz_expect_url("#done$", match = "regex") pz_close(page)
pz_expect_value() passes when at least one element matches and the
value property of every match satisfies value. Elements without a
value property (paragraphs, divs) read as missing and satisfy no
value. Checkbox and radio values come from their value attribute
(the default is "on"), whatever their checked state.
A length-1 value applies to every match. A length-n value
requires exactly n matches and compares pairwise, in order. With
not = TRUE, the expectation passes when no match satisfies value,
including when nothing matches.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_value( ctx, value, target = NULL, ..., match = c("contains", "exact", "regex"), not = FALSE, timeout = NULL )pz_expect_value( ctx, value, target = NULL, ..., match = c("contains", "exact", "regex"), not = FALSE, timeout = NULL )
ctx |
A paparazzi context. |
value |
A character vector of expected values: length 1 applies
to every match, length |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
match |
How to compare |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) page |> pz_expect_value("normal", target = "#task-priority", match = "exact") page |> pz_act_type("Buy milk", target = "#task-title") |> pz_expect_value("milk", target = "#task-title") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_expect_value("normal", target = "#task-priority", match = "exact") page |> pz_act_type("Buy milk", target = "#task-title") |> pz_expect_value("milk", target = "#task-title") pz_close(page)
pz_expect_visible() passes when at least one element matches and
every match is visible. pz_expect_hidden() is exactly
pz_expect_visible(not = TRUE): it passes when no match is visible,
including when nothing matches.
Visibility follows the browser's own checkVisibility()
(https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility)
with CSS checks, so display: none and visibility: hidden anywhere
up the ancestor chain count as hidden. Opacity and viewport position
are not considered; use pz_expect_in_viewport() for position.
Outside of testthat, a failure aborts with a classed error of class
"paparazzi_expectation_failure"; inside testthat, the failure is
reported as a test failure instead. See pz_expect_exists() for the
retry, timeout, and bridge behavior shared by all expectations.
pz_expect_visible(ctx, target = NULL, ..., not = FALSE, timeout = NULL) pz_expect_hidden(ctx, target = NULL, ..., not = FALSE, timeout = NULL)pz_expect_visible(ctx, target = NULL, ..., not = FALSE, timeout = NULL) pz_expect_hidden(ctx, target = NULL, ..., not = FALSE, timeout = NULL)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
not |
Invert the check. |
timeout |
Seconds to wait for the expectation to pass; |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) page |> pz_expect_hidden(target = "#help") page |> pz_act_click("#toggle-help") |> pz_expect_visible(target = "#help") # Every match must pass, so check one task or narrow the target page |> pz_expect_visible(target = pz_loc(".task", has_text = "passport")) pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_expect_hidden(target = "#help") page |> pz_act_click("#toggle-help") |> pz_expect_visible(target = "#help") # Every match must pass, so check one task or narrow the target page |> pz_expect_visible(target = pz_loc(".task", has_text = "passport")) pz_close(page)
pz_find() resolves target (auto-waiting for at least one match),
pins the matched set as the current scope, and returns a new context:
later calls on it operate inside that scope. Explicit targets resolve
lazily among the scope's descendants at use time – re-renders within
the scope are fine – and target = NULL means the scope itself for
the calls that accept one.
The pinned set is eager: it is fixed at find time, and a later
re-render that detaches any of its elements aborts with a classed
error on the next use – the scope is never silently re-queried (a
lazy, reusable target is what pz_loc() is for). Multi-match targets
pin the whole set.
The stack is immutable: pz_find*() never mutate the context they
are called on, and contexts derived from the same parent share its
pinned sets.
pz_find(ctx, target, ..., from_root = FALSE)pz_find(ctx, target, ..., from_root = FALSE)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
from_root |
Resolve the target from the page root instead of
the current scope? The new scope is still pushed on top of the
stack, so |
A new context.
pz_find_first(), pz_find_pop(), pz_find_reset()
page <- pz_open(pz_example("tasks")) # Inside a scope, targets resolve among the scope's descendants page |> pz_find(pz_loc(".task", has_text = "dentist")) |> pz_act_click(".task-done") pz_get_attr(page, "class", target = pz_loc(".task", has_text = "dentist")) # Assign a scoped context to start more than one chain from it task_list <- pz_find(page, ".task-list") task_list pz_get_count(task_list, target = ".task.done") # from_root looks outside the current scope, but still pushes a new scope task_list |> pz_find("#new-task", from_root = TRUE) |> pz_act_type("Buy milk", target = "#task-title") pz_get_value(page, target = "#task-title") pz_close(page)page <- pz_open(pz_example("tasks")) # Inside a scope, targets resolve among the scope's descendants page |> pz_find(pz_loc(".task", has_text = "dentist")) |> pz_act_click(".task-done") pz_get_attr(page, "class", target = pz_loc(".task", has_text = "dentist")) # Assign a scoped context to start more than one chain from it task_list <- pz_find(page, ".task-list") task_list pz_get_count(task_list, target = ".task.done") # from_root looks outside the current scope, but still pushes a new scope task_list |> pz_find("#new-task", from_root = TRUE) |> pz_act_type("Buy milk", target = "#task-title") pz_get_value(page, target = "#task-title") pz_close(page)
pz_find_first() is pz_find() with which = "first". With a
target, it pins the spec's first match. Without a target, it
narrows the current scope to its first element: one eager slice of
the pinned set, with no re-query and no waiting.
pz_find_first(ctx, target = NULL, ..., from_root = FALSE)pz_find_first(ctx, target = NULL, ..., from_root = FALSE)
ctx |
A paparazzi context. |
target |
A CSS selector string or a |
... |
Checked empty; reserved for future use. |
from_root |
Resolve the target from the page root instead of
the current scope? Requires a |
A new context.
pz_find(), pz_find_last(), pz_find_nth()
page <- pz_open(pz_example("tasks")) page |> pz_find_first(".task") |> pz_get_text(target = ".task-title") # Without a target, narrow the current scope to its first element page |> pz_find(".task[data-priority='normal']") |> pz_find_first() |> pz_get_text(target = ".task-title") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_find_first(".task") |> pz_get_text(target = ".task-title") # Without a target, narrow the current scope to its first element page |> pz_find(".task[data-priority='normal']") |> pz_find_first() |> pz_get_text(target = ".task-title") pz_close(page)
pz_find_last() is pz_find() with which = "last". With a
target, it pins the spec's last match. Without a target, it
narrows the current scope to its last element: one eager slice of
the pinned set, with no re-query and no waiting.
pz_find_last(ctx, target = NULL, ..., from_root = FALSE)pz_find_last(ctx, target = NULL, ..., from_root = FALSE)
ctx |
A paparazzi context. |
target |
A CSS selector string or a |
... |
Checked empty; reserved for future use. |
from_root |
Resolve the target from the page root instead of
the current scope? Requires a |
A new context.
pz_find(), pz_find_first(), pz_find_nth()
page <- pz_open(pz_example("tasks")) page |> pz_find_last(".task") |> pz_get_text(target = ".task-title") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_find_last(".task") |> pz_get_text(target = ".task-title") pz_close(page)
pz_find_nth() is pz_find() with which = n. With a target, it
pins the spec's nth match; an out-of-range n means no match, so
the call keeps auto-waiting like any pz_loc() resolution. Without
a target, it narrows the current scope to its nth element: one
eager slice of the pinned set, and an out-of-range n errors
immediately – the set was fixed at pin time, so there is nothing to
wait for.
pz_find_nth(ctx, n, target = NULL, ..., from_root = FALSE)pz_find_nth(ctx, n, target = NULL, ..., from_root = FALSE)
ctx |
A paparazzi context. |
n |
The match to pick, 1-based ( |
target |
A CSS selector string or a |
... |
Checked empty; reserved for future use. |
from_root |
Resolve the target from the page root instead of
the current scope? Requires a |
A new context.
pz_find(), pz_find_first(), pz_find_last()
page <- pz_open(pz_example("tasks")) # Mark the second task as done page |> pz_find_nth(2, target = ".task") |> pz_act_click(".task-done") pz_get_text(page, target = pz_loc(".task.done .task-title")) pz_close(page)page <- pz_open(pz_example("tasks")) # Mark the second task as done page |> pz_find_nth(2, target = ".task") |> pz_act_click(".task-done") pz_get_text(page, target = pz_loc(".task.done .task-title")) pz_close(page)
pz_find_pop() returns a context one scope level up the stack. The
popped pinned set is NOT released – a context derived before the
pop may still hold it – and popping never mutates the context it
was called on. At the root it is a no-op returning ctx unchanged.
pz_find_pop(ctx)pz_find_pop(ctx)
ctx |
A paparazzi context. |
A new context one level up, or ctx itself at the root.
page <- pz_open(pz_example("tasks")) page |> pz_find(".task-list") |> pz_find_last(".task") |> pz_act_click(".task-done") |> # Back to the whole list, to check the result pz_find_pop() |> pz_expect_count(2, target = ".task.done") |> pz_get_count(target = ".task.done") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_find(".task-list") |> pz_find_last(".task") |> pz_act_click(".task-done") |> # Back to the whole list, to check the result pz_find_pop() |> pz_expect_count(2, target = ".task.done") |> pz_get_count(target = ".task.done") pz_close(page)
pz_find_reset() returns a context with an empty scope stack. No
pinned set is released – contexts derived before the reset may
still hold them – and resetting never mutates the context it was
called on. At the root it is a no-op returning ctx unchanged.
pz_find_reset(ctx)pz_find_reset(ctx)
ctx |
A paparazzi context. |
A new root context, or ctx itself at the root.
page <- pz_open(pz_example("tasks")) page |> pz_find(".task-list") |> pz_find_first(".task") |> pz_act_click(".task-done") |> # The help toggle is outside the list, so start again from the root pz_find_reset() |> pz_act_click("#toggle-help") |> pz_expect_visible(target = "#help") |> pz_get_text(target = "#help") pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_find(".task-list") |> pz_find_first(".task") |> pz_act_click(".task-done") |> # The help toggle is outside the list, so start again from the root pz_find_reset() |> pz_act_click("#toggle-help") |> pz_expect_visible(target = "#help") |> pz_get_text(target = "#help") pz_close(page)
Declares a font face for pz_stage_fonts(), served by Bunny Fonts
(https://fonts.bunny.net), a GDPR-friendly foundry that mirrors the
Google Fonts collection and stylesheet format.
pz_font_bunny(family, weight = 400, style = "normal")pz_font_bunny(family, weight = 400, style = "normal")
family |
The font family name, exactly as Google Fonts spells it,
e.g. |
weight |
Font weight, a number from 1 to 1000 (400 is regular, 700 is bold). |
style |
|
A font object for pz_stage_fonts().
pz_stage_fonts(), pz_font_google(), pz_font_file()
pz_font_bunny("Inter", style = "italic")pz_font_bunny("Inter", style = "italic")
Declares a font face for pz_stage_fonts() from a local font file.
The bytes are read by R and handed to the page directly, so the file
works on http pages (which cannot load file:// URLs) and is not
subject to Content Security Policy font restrictions. You are
responsible for the license of fonts you use.
pz_font_file(family, path, weight = 400, style = "normal")pz_font_file(family, path, weight = 400, style = "normal")
family |
The font family name, exactly as Google Fonts spells it,
e.g. |
path |
Path to a |
weight |
Font weight, a number from 1 to 1000 (400 is regular, 700 is bold). |
style |
|
A font object for pz_stage_fonts().
pz_stage_fonts(), pz_font_google(), pz_font_bunny()
## Not run: pz_font_file("My Brand Sans", "~/fonts/my-brand-sans.woff2") ## End(Not run)## Not run: pz_font_file("My Brand Sans", "~/fonts/my-brand-sans.woff2") ## End(Not run)
Declares a font face for pz_stage_fonts(). The family is identified
by its Google Fonts name, and Chrome fetches the stylesheet and font
files at staging time; nothing is downloaded by R.
pz_font_google(family, weight = 400, style = "normal")pz_font_google(family, weight = 400, style = "normal")
family |
The font family name, exactly as Google Fonts spells it,
e.g. |
weight |
Font weight, a number from 1 to 1000 (400 is regular, 700 is bold). |
style |
|
A font object for pz_stage_fonts().
pz_stage_fonts(), pz_font_bunny(), pz_font_file()
pz_font_google("Inter", weight = 700)pz_font_google("Inter", weight = 700)
pz_frame() builds a lazy framing spec for a capture: the region is
computed at capture time, then padded, nudged, grown to an aspect
ratio, and clamped. Every capture takes one: screenshots through
pz_screenshot(frame =), recordings through
pz_record_start(frame =), and camera shots through
pz_camera(frame =). Wherever a frame is accepted, a bare locator
(a CSS selector string, a pz_loc() spec, or a list of either) is
promoted to pz_frame(<locator>).
Every field defaults to NULL, meaning "inherit": at capture time
an unset field takes the pz_stage_frame() value if one is staged,
else the built-in default. The built-in defaults are pad = 0,
offset = c(0, 0), anchor = "center", when = "stop", and
target_box = "element", with ratio, bounds, zoom, and
target unset. Camera shots never inherit the staged frame; their
built-in defaults are pad = 24 and target_box = "element".
The framed region is computed in this order:
Union the bounding boxes of the target's matched elements. With
target_box = "annotated", include their attached annotations.
Expand by pad.
With zoom = NULL, shift by offset, then grow the shorter side
to reach ratio (never shrink), placing the content by anchor.
With a numeric zoom, the region is the view divided by zoom
(the viewport for stills and recordings, the recording's home
frame for camera shots), with ratio setting its aspect if
given; the padded target is placed inside it by anchor, then
shifted by offset.
Clamp to bounds and to the page's rendered area.
Round to whole pixels.
pz_frame( target = NULL, ..., ratio = NULL, pad = NULL, offset = NULL, anchor = NULL, bounds = NULL, when = NULL, target_box = NULL, zoom = NULL )pz_frame( target = NULL, ..., ratio = NULL, pad = NULL, offset = NULL, anchor = NULL, bounds = NULL, when = NULL, target_box = NULL, zoom = NULL )
target |
The element(s) the frame is computed from: a CSS
selector string, a |
... |
Checked empty; reserved for future use. |
ratio |
Width/height ratio for the region, e.g. |
pad |
Padding in CSS pixels: one number for all sides, or
|
offset |
Nudge the region by |
anchor |
Where the content sits while the region grows around
it: |
bounds |
A target (a CSS selector string, a |
when |
When a recording measures the frame: |
target_box |
|
zoom |
Magnification relative to the full view (the viewport
for stills and recordings, the recording's home frame for camera
shots). |
An S3 object of class paparazzi_frame.
# Frames are specs: nothing is measured until a capture uses them pz_frame(pad = 32) pz_frame(ratio = 16/9, pad = 24, anchor = "top") pz_frame(".task-list", ratio = 4/3, bounds = "main") page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "tasks.png") # A locator target frames the union of its boxes page |> pz_screenshot(path, frame = pz_frame(list("h1", ".filters"), pad = 16)) # Unset fields inherit the staged framing page |> pz_stage_frame(pad = 16) |> pz_screenshot(path, frame = pz_frame("#new-task", ratio = 16/9)) pz_screenshot(page, frame = pz_frame("#new-task", ratio = 16/9)) pz_close(page)# Frames are specs: nothing is measured until a capture uses them pz_frame(pad = 32) pz_frame(ratio = 16/9, pad = 24, anchor = "top") pz_frame(".task-list", ratio = 4/3, bounds = "main") page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "tasks.png") # A locator target frames the union of its boxes page |> pz_screenshot(path, frame = pz_frame(list("h1", ".filters"), pad = 16)) # Unset fields inherit the staged framing page |> pz_stage_frame(pad = 16) |> pz_screenshot(path, frame = pz_frame("#new-task", ratio = 16/9)) pz_screenshot(page, frame = pz_frame("#new-task", ratio = 16/9)) pz_close(page)
pz_get_attr() returns the named attribute of every element matching
target, one entry per match. Missing attributes give NA.
pz_get_attr(ctx, name, target = NULL, ...)pz_get_attr(ctx, name, target = NULL, ...)
ctx |
A paparazzi context. |
name |
The attribute name. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
A character vector, one entry per match.
page <- pz_open(pz_example("tasks")) pz_get_attr(page, "data-priority", target = ".task") # Missing attributes are NA pz_get_attr(page, "aria-label", target = "select, input[type='text']") pz_close(page)page <- pz_open(pz_example("tasks")) pz_get_attr(page, "data-priority", target = ".task") # Missing attributes are NA pz_get_attr(page, "aria-label", target = "select, input[type='text']") pz_close(page)
pz_get_count() returns the number of elements matching target,
counted inside the current scope. Unlike the other getters it
doesn't wait for a match: 0 is a valid answer, so it resolves once
and returns immediately. One exception: on a scope whose pinned
elements have left the page it raises paparazzi_error_detached
instead of returning 0, because the pinned set promises a live set
and is never silently re-queried.
pz_get_count(ctx, target = NULL, ...)pz_get_count(ctx, target = NULL, ...)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
An integer.
page <- pz_open(pz_example("tasks")) pz_get_count(page, target = ".task") # Zero is an answer, so pz_get_count() never waits pz_get_count(page, target = ".error-message") # Counts are relative to the current scope page |> pz_find(".task-list") |> pz_get_count(target = ".task.done") pz_close(page)page <- pz_open(pz_example("tasks")) pz_get_count(page, target = ".task") # Zero is an answer, so pz_get_count() never waits pz_get_count(page, target = ".error-message") # Counts are relative to the current scope page |> pz_find(".task-list") |> pz_get_count(target = ".task.done") pz_close(page)
pz_get_elements() returns a summary of every element matching
target, one row per match in match order: the lowercased tag name,
the id and class attributes, and the whitespace-collapsed text.
id and class are NA when the attribute is absent.
pz_get_elements(ctx, target = NULL, ...)pz_get_elements(ctx, target = NULL, ...)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
A tibble with columns tag, id, class, text, one row
per match, plus an element list-column of contexts scoped to
each match, pinned at get time (see pz_get_rect()).
page <- pz_open(pz_example("tasks")) pz_get_elements(page, target = "#new-task > *") pz_close(page)page <- pz_open(pz_example("tasks")) pz_get_elements(page, target = "#new-task > *") pz_close(page)
pz_get_html() returns the outer HTML of every element matching
target, one entry per match.
pz_get_html(ctx, target = NULL, ...)pz_get_html(ctx, target = NULL, ...)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
A character vector, one entry per match.
page <- pz_open(pz_example("tasks")) pz_get_html(page, target = pz_loc(".task", which = "first")) pz_close(page)page <- pz_open(pz_example("tasks")) pz_get_html(page, target = pz_loc(".task", which = "first")) pz_close(page)
pz_get_rect() returns the bounding box of every element matching
target, one row per match in match order.
pz_get_rect(ctx, target = NULL, ...)pz_get_rect(ctx, target = NULL, ...)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
A tibble with columns x, y, width, height (doubles,
CSS pixels, viewport-relative), one row per match, plus an
element list-column. Each element entry is a context scoped
to that one match, pinned at get time, so a chain can continue
from it: rects$element[[2]] |> pz_act_hover().
page <- pz_open(pz_example("tasks")) rects <- pz_get_rect(page, target = ".filters a") rects # Each row's element column is a context scoped to that match rects$element[[3]] |> pz_act_click() |> pz_expect_url("#done") pz_close(page)page <- pz_open(pz_example("tasks")) rects <- pz_get_rect(page, target = ".filters a") rects # Each row's element column is a context scoped to that match rects$element[[3]] |> pz_act_click() |> pz_expect_url("#done") pz_close(page)
pz_get_style() returns the computed styles of every element
matching target – what the browser actually applied, the same
values Playwright's toHaveCSS() and jQuery's .css() read – one
row per match in match order. Like all getters it auto-waits for at
least one match.
pz_get_style(ctx, props = NULL, target = NULL, ...)pz_get_style(ctx, props = NULL, target = NULL, ...)
ctx |
A paparazzi context. |
props |
A character vector of CSS property names, or |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
Property names accept snake_case, converted to the CSS kebab-case
spelling (font_size becomes font-size, also the column name, so
select it with df$`font-size` ); custom properties
("--bs-primary") pass through unchanged. props = NULL returns
every computed property the browser reports (longhands only).
A tibble with one row per match, one character column per
property, plus an element list-column: each entry is a context
scoped to that one match, pinned at get time, so a chain can
continue from it (see pz_get_rect()).
page <- pz_open(pz_example("tasks")) pz_get_style(page, c("font-size", "font_weight"), target = "h1") # One row per match pz_get_style(page, "text-decoration-line", target = ".task-title") pz_close(page)page <- pz_open(pz_example("tasks")) pz_get_style(page, c("font-size", "font_weight"), target = "h1") # One row per match pz_get_style(page, "text-decoration-line", target = ".task-title") pz_close(page)
pz_get_text() returns the textContent of every element matching
target, one entry per match. By default runs of whitespace are
collapsed to single spaces and trimmed, matching pz_expect_text();
raw = TRUE returns the text exactly as the browser holds it.
pz_get_text(ctx, target = NULL, ..., raw = FALSE)pz_get_text(ctx, target = NULL, ..., raw = FALSE)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
raw |
Return the text without collapsing whitespace? |
A character vector, one entry per match.
page <- pz_open(pz_example("tasks")) pz_get_text(page, target = "h1") # One string per match pz_get_text(page, target = ".task[data-priority='high'] .task-title") # Whitespace is collapsed unless raw = TRUE pz_get_text(page, target = "#help") pz_get_text(page, target = "#help", raw = TRUE) pz_close(page)page <- pz_open(pz_example("tasks")) pz_get_text(page, target = "h1") # One string per match pz_get_text(page, target = ".task[data-priority='high'] .task-title") # Whitespace is collapsed unless raw = TRUE pz_get_text(page, target = "#help") pz_get_text(page, target = "#help", raw = TRUE) pz_close(page)
pz_get_title() returns the page's current title.
pz_get_title(ctx)pz_get_title(ctx)
ctx |
A paparazzi context. |
A character vector of length one.
page <- pz_open(pz_example("tasks")) pz_get_title(page) pz_close(page)page <- pz_open(pz_example("tasks")) pz_get_title(page) pz_close(page)
pz_get_url() returns the page's current URL.
pz_get_url(ctx)pz_get_url(ctx)
ctx |
A paparazzi context. |
A character vector of length one.
page <- pz_open(pz_example("tasks")) page |> pz_act_click(pz_loc(".filters a", has_text = "Open")) basename(pz_get_url(page)) pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_act_click(pz_loc(".filters a", has_text = "Open")) basename(pz_get_url(page)) pz_close(page)
pz_get_value() returns the value property of every element
matching target, one entry per match. Elements without a value
property (non-form elements) give NA.
pz_get_value(ctx, target = NULL, ...)pz_get_value(ctx, target = NULL, ...)
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
A character vector, one entry per match.
page <- pz_open(pz_example("tasks")) page |> pz_act_type("Buy milk", target = "#task-title") pz_get_value(page, target = list("#task-title", "#task-priority")) pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_act_type("Buy milk", target = "#task-title") pz_get_value(page, target = list("#task-title", "#task-priority")) pz_close(page)
Prints a console summary of the page: URL, device, recording and cursor
state, and – when target is given – the target's matches resolved
relative to the current scope without auto-waiting: a single
resolution pass, each match shown as a short opening tag with its
visibility, enabled state, and box (long lists are truncated).
The header names the context. At the root it reads paparazzi page.
In a scoped context from pz_find() it reads paparazzi scope, with
the scope stack right under it: live match counts for each level,
plus a warning when pinned elements have gone stale.
show = "screenshot" additionally captures an annotated screenshot:
dashed outlines over the scope's elements, solid numbered outlines over
the target's matches, drawn under paparazzi's shadow-root overlay, so
they never appear in pz_screenshot() output. show = "browser" draws
the same outlines in the live page, leaves them, and opens the browser
via the page's $view(). show = "auto" picks "screenshot" in
interactive sessions and "none" otherwise.
pz_inspect( ctx, target = NULL, ..., path = NULL, show = c("auto", "screenshot", "browser", "none") )pz_inspect( ctx, target = NULL, ..., path = NULL, show = c("auto", "screenshot", "browser", "none") )
ctx |
A paparazzi context. |
target |
A CSS selector string, a |
... |
Checked empty; reserved for future use. |
path |
Where to write the annotated screenshot; with |
show |
How to visualize: |
ctx, invisibly (visibly in an active recording chain). With
show = "screenshot" and path = NULL while knitting, returns ctx
visibly so knitr prints the summary and includes the annotated screenshot
once. A subsequent action or scope change discards the pending image;
a terminal getter leaves it for the next knitr print. If the context
belongs to an active recording chain, knitr stops and includes the
recording instead of the inspect image.
page <- pz_open(pz_example("tasks")) page |> pz_inspect() # With a target: its matches, resolved inside the current scope page |> pz_find(".task-list") |> pz_inspect(".task.done", show = "none") # An annotated screenshot outlines the scope and the numbered matches page |> pz_find(".task-list") |> pz_inspect(".task-done", show = "screenshot", path = file.path(tempdir(), "inspect.png")) pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_inspect() # With a target: its matches, resolved inside the current scope page |> pz_find(".task-list") |> pz_inspect(".task.done", show = "none") # An annotated screenshot outlines the scope and the numbered matches page |> pz_find(".task-list") |> pz_inspect(".task-done", show = "screenshot", path = file.path(tempdir(), "inspect.png")) pz_close(page)
pz_loc() builds a page-independent element spec: a CSS selector,
optionally narrowed by required text content (has_text), a match
position (which), and an ancestor scope (within). Specs are lazy –
they resolve at use time, so they follow DOM changes between uses –
and can be defined once and reused across sessions.
Wherever a spec is accepted, a bare string is promoted to
pz_loc(string), and a list of specs and strings is a union that
matches the elements of any member.
pz_loc(css, ..., has_text = NULL, which = NULL, within = NULL)pz_loc(css, ..., has_text = NULL, which = NULL, within = NULL)
css |
A CSS selector string. |
... |
Checked empty; reserved for future use. |
has_text |
Substring the element's text content must contain.
|
which |
Pick one match by position: |
within |
Only match descendants of elements matching this spec
(a string or |
An S3 object of class paparazzi_loc.
# A spec describes elements; nothing is looked up until you use it pz_loc(".task") pz_loc(".task", has_text = "passport") pz_loc(".task", which = "last") pz_loc(".task-done", within = pz_loc(".task", has_text = "dentist")) page <- pz_open(pz_example("tasks")) # Specs resolve each time they're used, so define them once and reuse them high <- pz_loc(".task-title", within = ".task[data-priority='high']") pz_get_text(page, target = high) # A list of specs and strings matches the elements of any member pz_get_count(page, target = list(".task.done", high)) pz_close(page)# A spec describes elements; nothing is looked up until you use it pz_loc(".task") pz_loc(".task", has_text = "passport") pz_loc(".task", which = "last") pz_loc(".task-done", within = pz_loc(".task", has_text = "dentist")) page <- pz_open(pz_example("tasks")) # Specs resolve each time they're used, so define them once and reuse them high <- pz_loc(".task-title", within = ".task[data-priority='high']") pz_get_text(page, target = high) # A list of specs and strings matches the elements of any member pz_get_count(page, target = list(".task.done", high)) pz_close(page)
Opens a URL, local file, local server, or existing
chromote::ChromoteSession as a paparazzi page: the root context that
starts every |> chain.
pz_open( x, ..., wait = c("auto", "load", "shiny", "none"), timeout = 10, shiny_options = list(), envvars = NULL )pz_open( x, ..., wait = c("auto", "load", "shiny", "none"), timeout = 10, shiny_options = list(), envvars = NULL )
x |
What to open:
Path detection checks Shiny first (directories containing |
... |
Forwarded to |
wait |
What to wait for before returning. |
timeout |
Session default timeout in seconds; defaults to 10.
Per-call |
shiny_options, envvars
|
Passed to |
A PaparazziPage (the root context).
pz_open() opens pages in chromote's default browser
(chromote::default_chromote_object()) and starts it if none is running.
A browser paparazzi starts keeps its profile in tempdir(), so the
profile is removed when R exits, unless a --user-data-dir is already set
with chromote::set_chrome_args(). A default set with
chromote::set_default_chromote_object() is used as-is.
# An HTML file is served over HTTP so the page gets bfcache and other # HTTP-only behavior page <- pz_open(pz_example("tasks")) pz_get_url(page) pz_close(page) # Named arguments in `...` set up the device before the page loads phone <- pz_open(pz_example("tasks"), width = 390, height = 844, mobile = TRUE) pz_evaluate(phone, "window.innerWidth") pz_screenshot(phone, frame = pz_frame("#new-task", pad = 16)) pz_close(phone) # An app directory runs in a background R process that the page owns. # pz_open() returns once Shiny has connected and gone idle. page <- pz_open(pz_example("tasks-app")) pz_get_text(page, target = "#summary") pz_close(page) # also stops the app# An HTML file is served over HTTP so the page gets bfcache and other # HTTP-only behavior page <- pz_open(pz_example("tasks")) pz_get_url(page) pz_close(page) # Named arguments in `...` set up the device before the page loads phone <- pz_open(pz_example("tasks"), width = 390, height = 844, mobile = TRUE) pz_evaluate(phone, "window.innerWidth") pz_screenshot(phone, frame = pz_frame("#new-task", pad = 16)) pz_close(phone) # An app directory runs in a background R process that the page owns. # pz_open() returns once Shiny has connected and gone idle. page <- pz_open(pz_example("tasks-app")) pz_get_text(page, target = "#summary") pz_close(page) # also stops the app
The block form of pz_record_start(): starts recording, evaluates
the embraced expression code, and stops and encodes on exit –
including on error, so a failed run still produces the video up to
the failure. Returns what pz_record_stop() returns, never the block's
value: ctx invisibly when a path was given outside knitting, media
while knitting, or a viewer preview when the path is NULL in an
interactive session. Write pz_record(code = { ... }) to omit the
path.
pz_record(ctx, path = NULL, code, ...)pz_record(ctx, path = NULL, code, ...)
ctx |
A paparazzi context. |
path |
Output file path; the extension ( |
code |
An expression to evaluate while recording. |
... |
Passed to |
ctx, invisibly, or printable media; see pz_record_stop().
page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "add-task.mp4") # The recording stops and is written when the block exits, even on error page |> pz_record(path, { page |> pz_act_type("Buy milk", target = "#task-title") |> pz_act_click("#add-task") |> pz_expect_count(8, target = ".task") }) |> pz_screenshot(file.path(tempdir(), "after.png")) file.exists(path) pz_close(page)page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "add-task.mp4") # The recording stops and is written when the block exits, even on error page |> pz_record(path, { page |> pz_act_type("Buy milk", target = "#task-title") |> pz_act_click("#add-task") |> pz_expect_count(8, target = ".task") }) |> pz_screenshot(file.path(tempdir(), "after.png")) file.exists(path) pz_close(page)
Lingers on the current frame for seconds in the output video.
Unlike pz_wait(), the hold advances only the video clock. Captions,
annotations, and keystroke callouts added afterward appear after the
held segment. When the page is not recording this is a no-op, so
debugging a chain with the recording commented out doesn't pay for
video-only pauses. A camera move still in progress (see pz_camera())
finishes before the hold begins.
pz_record_hold(ctx, seconds)pz_record_hold(ctx, seconds)
ctx |
A paparazzi context. |
seconds |
Seconds to hold the frame in the output. |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) # Without a recording, pz_record_hold() returns straight away system.time(pz_record_hold(page, 2)) path <- file.path(tempdir(), "help.mp4") page |> pz_record_start(path) |> pz_act_click("#toggle-help") |> # Give viewers a second to read the help text pz_record_hold(1) |> pz_record_stop() pz_close(page)page <- pz_open(pz_example("tasks")) # Without a recording, pz_record_hold() returns straight away system.time(pz_record_hold(page, 2)) path <- file.path(tempdir(), "help.mp4") page |> pz_record_start(path) |> pz_act_click("#toggle-help") |> # Give viewers a second to read the help text pz_record_hold(1) |> pz_record_stop() pz_close(page)
pz_record_pause() cuts the stretch up to the next
pz_record_resume() out of the recording: no frames are captured
and the video clock stops, so the pause leaves no trace in the
output.
pz_record_pause(ctx) pz_record_resume(ctx)pz_record_pause(ctx) pz_record_resume(ctx)
ctx |
A paparazzi context. |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "add-task.mp4") page |> pz_record_start(path) |> pz_act_click("#task-title") |> pz_record_pause() |> # Filling in the form is cut from the video pz_set_value("Buy milk", target = "#task-title") |> pz_set_value("high", target = "#task-priority") |> pz_record_resume() |> pz_act_click("#add-task") |> pz_expect_count(8, target = ".task") |> pz_record_stop() pz_close(page)page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "add-task.mp4") page |> pz_record_start(path) |> pz_act_click("#task-title") |> pz_record_pause() |> # Filling in the form is cut from the video pz_set_value("Buy milk", target = "#task-title") |> pz_set_value("high", target = "#task-priority") |> pz_record_resume() |> pz_act_click("#add-task") |> pz_expect_count(8, target = ".task") |> pz_record_stop() pz_close(page)
pz_record_start() begins recording the page to a video or GIF;
pz_record_stop() ends the recording, encodes it, and writes the
file. pz_record_pause()/pz_record_resume() cut stretches out of
the recording, and pz_record_hold() lingers on the current frame.
pz_record() is the block form: it records for the duration of an
expression and stops on exit, including on error.
The output format comes from the path extension: .mp4 (h264),
.webm, or .gif. Both methods produce a video at the requested
fps; when a new frame is not available, the previous frame is
repeated.
pz_record_start( ctx, path = NULL, ..., method = c("poll", "screencast"), frame = NULL, fps = 15, scale = NULL, hold = c(0.5, 1), keep_frames = FALSE, captions = c("burn", "vtt", "both"), format = c("auto", "mp4", "webm", "gif") )pz_record_start( ctx, path = NULL, ..., method = c("poll", "screencast"), frame = NULL, fps = 15, scale = NULL, hold = c(0.5, 1), keep_frames = FALSE, captions = c("burn", "vtt", "both"), format = c("auto", "mp4", "webm", "gif") )
ctx |
A paparazzi context. |
path |
Output file path; the extension ( |
... |
Checked empty; reserved for future use. |
method |
Capture method: |
frame |
Area to show in the finished recording: a |
fps |
Frames per second in the finished recording. Poll aims to capture at this rate; screencast capture depends on visual changes. |
scale |
Output size: |
hold |
Seconds to hold the first and last frame,
|
keep_frames |
Keep the captured PNG frames in a
|
captions |
|
format |
The format when |
A context in the recording chain, invisibly.
"poll" (the default) takes screenshots at roughly the requested
fps. Choose it when resolution matters: it captures at the page's
full pixel density. "screencast" records when the page changes,
which can capture more intermediate states of an animation. It does
not guarantee a capture rate and may have lower resolution. For
example, with a 640-by-480 viewport on a 2x display, poll frames
are 1280-by-960 pixels while screencast frames may be 640-by-480
pixels. The scale argument sets output size, but enlarging a
lower-resolution frame does not add detail.
Both methods record while paparazzi runs browser actions or
pz_wait(). Long-running R code between browser actions, including
Sys.sleep(), can cause intermediate changes to be missed;
use pz_wait() when you want a visible wait in the video.
Stopping always captures one final frame of the page state at
pz_record_stop(), so a change made just before the stop still
appears; the last-frame hold repeats that final frame.
Use pz_frame() (or the staged default set with
pz_stage_frame()) to crop the finished recording. By default the
frame follows the layout at stop; pz_frame(when = "start") fixes it
to the layout at start. If the page navigates, a frame tied to the
previous page falls back to the full viewport unless it was fixed at
the start. MP4 dimensions are rounded down to multiples of 4 and
WebM to even pixels; GIF keeps whole pixels.
Set the viewport size before pz_record_start(); resizing during a
framed or camera recording is not supported.
WebVTT captions (when requested) are written as a .vtt file next to
the MP4 or WebM. Cue times include the first/last holds and explicit
pz_record_hold() intervals but exclude pauses. Caption burn and VTT
use the current caption set by pz_annotate_caption().
MP4 and WebM recordings require av; GIF recordings require
gifski. Simple GIFs need only gifski, but two features call in
extra packages: framed GIFs require png, and GIFs with camera
movement or burned-in captions require av.
Packages are checked at pz_record_start(), or during a GIF
recording at the first camera move or caption.
pz_record_start() returns a context that belongs to the new
recording. Chainable functions return it (and contexts derived from
it with pz_find()) visibly while the recording runs, and printing it
stops the recording. So a chain that starts with pz_record_start()
needs no pz_record_stop() when it is printed: at the end of a
knitted chunk expression the recording appears in the document, and
in the console it is shown in the viewer. Assign the chain or call
pz_record_stop() to record across several statements. Chains that
don't come from pz_record_start(), such as page |> pz_act_click(),
keep returning invisibly and never stop a recording.
pz_record(), pz_record_hold(), pz_frame(),
pz_stage_frame()
page <- pz_open(pz_example("tasks"), width = 800, height = 900) path <- file.path(tempdir(), "help.mp4") # Record the help panel opening, cropped to the page's card page |> pz_record_start(path, frame = pz_frame("main")) |> pz_act_click("#toggle-help") |> pz_expect_visible(target = "#help") |> pz_record_stop() file.exists(path) pz_close(page)page <- pz_open(pz_example("tasks"), width = 800, height = 900) path <- file.path(tempdir(), "help.mp4") # Record the help panel opening, cropped to the page's card page |> pz_record_start(path, frame = pz_frame("main")) |> pz_act_click("#toggle-help") |> pz_expect_visible(target = "#help") |> pz_record_stop() file.exists(path) pz_close(page)
Ends the recording started with pz_record_start(), encodes the
captured frames, and writes the file given to pz_record_start().
pz_record_stop(ctx)pz_record_stop(ctx)
ctx |
A paparazzi context. |
ctx, invisibly, when the path was supplied outside knitting.
While knitting, returns a printable image for GIF, HTML video for
MP4/WebM, or a video link in non-HTML output, even when the path was
supplied explicitly. Without a path in an interactive session,
returns a preview that shows the recording in the viewer when
printed.
page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "done.gif") page |> pz_record_start(path, frame = pz_frame(".task-list", pad = 8)) |> pz_act_click(pz_loc(".task-done", which = "first")) |> pz_record_stop() file.exists(path) pz_close(page)page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "done.gif") page |> pz_record_start(path, frame = pz_frame(".task-list", pad = 8)) |> pz_act_click(pz_loc(".task-done", which = "first")) |> pz_record_stop() file.exists(path) pz_close(page)
Captures a PNG of the page. With an explicit path, returns the context
invisibly so screenshots slot into |> chains. Without a path, the
capture is a terminal step: a figure in a knitted document or a
printable preview in an interactive session.
The captured region comes from frame:
NULL (the default): the staged framing set with
pz_stage_frame(), or, with none staged, the current scope's box
(the viewport at the root), unframed.
A pz_frame() spec, or a bare locator promoted to one: a CSS
selector string, a pz_loc() spec, or a list of either, framing
the union of the matched elements' bounding boxes. A selector
matching several elements is a union, not an error. Fields the
spec leaves unset inherit the staged value, then the built-in
default.
FALSE: the scope's box or viewport, unframed.
What a frame without its own target measures resolves by precedence: the scope (scoped contexts), then the staged frame's target, then the viewport.
Screenshots are captured at the page's current device pixel ratio: the PNG's pixel dimensions are the captured CSS size multiplied by the dpr.
A screen-space caption set with pz_annotate_caption() is composited
onto the captured PNG after framing; captioned stills require png.
pz_screenshot(ctx, path = NULL, ..., frame = NULL)pz_screenshot(ctx, path = NULL, ..., frame = NULL)
ctx |
A paparazzi context. |
path |
File path the PNG is written to; an existing file is
overwritten. With |
... |
Checked empty; reserved for future use. |
frame |
What to capture and how to frame it: a |
With an explicit path, ctx, invisibly. Without a path while
knitting, a knitr image; without a path in an interactive session or in
pkgdown examples, an image preview. These image results are terminal,
not contexts.
page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "tasks.png") # At the root, frame = NULL captures the viewport page |> pz_screenshot(path) # A locator captures its box; a list captures the union of the boxes page |> pz_screenshot(path, frame = ".task-list") page |> pz_screenshot(path, frame = list("#new-task", ".filters")) # Padding, aspect ratio and anchoring come from pz_frame() page |> pz_screenshot(path, frame = pz_frame("#new-task", pad = 16)) file.exists(path) pz_screenshot(page, frame = pz_frame("#new-task", pad = 16)) pz_close(page)page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "tasks.png") # At the root, frame = NULL captures the viewport page |> pz_screenshot(path) # A locator captures its box; a list captures the union of the boxes page |> pz_screenshot(path, frame = ".task-list") page |> pz_screenshot(path, frame = list("#new-task", ".filters")) # Padding, aspect ratio and anchoring come from pz_frame() page |> pz_screenshot(path, frame = pz_frame("#new-task", pad = 16)) file.exists(path) pz_screenshot(page, frame = pz_frame("#new-task", pad = 16)) pz_close(page)
Starts a local quarto preview process for a .qmd or .Rmd file,
or a directory containing _quarto.yml. Quarto renders .Rmd files
with knitr. The Quarto CLI must be on PATH, or its executable path
must be set in QUARTO_PATH; the quarto R package is not required.
pz_serve_quarto(path, ..., render = FALSE)pz_serve_quarto(path, ..., render = FALSE)
path |
A path to a |
... |
Reserved; must be empty. |
render |
Whether to request a full render before previewing.
|
Input watching and automatic navigation are disabled. Quarto may
still reload pages after resource changes, such as CSS edits; keep
those resources unchanged during a capture. Interactive documents
(runtime: shiny or server: shiny) are not supported.
A PaparazziServe handle with $url, $port, $stop(),
$is_running(), and $logs(). Each handle owns its process, so
multiple previews can run at once. Standard output and errors go to
a temporary log file, readable during and after the preview.
Pass the handle to pz_open() to share it across pages; closing those
pages leaves it running. $stop() is idempotent; use
withr::defer(server$stop()) for cleanup. A finalizer stops the
preview as a last resort.
Starts a Shiny app – a directory or an app file – in a background R
process and returns a handle for its lifecycle. One app can back any
number of pages: pass the handle to pz_open() for each page. Closing
those pages leaves the app running.
pz_serve_shiny(app, ..., envvars = NULL, shiny_options = list(), timeout = 10)pz_serve_shiny(app, ..., envvars = NULL, shiny_options = list(), timeout = 10)
app |
A path to a Shiny app directory or an app file
(anything |
... |
Reserved; must be empty. |
envvars |
Named character vector of environment variables set in
the app process, on top of the inherited environment. |
shiny_options |
Additional options for |
timeout |
Seconds to wait for the app to start listening; defaults to 10. |
The handle has $stop() and $logs() methods and a print() method
showing the URL, port, and status – the one place the paparazzi API
uses methods rather than pz_*() functions. $stop() interrupts,
waits, then kills, and is idempotent, so withr::defer(app$stop())
is a safe cleanup. A finalizer stops the app as a last resort.
App stdout and stderr go to a temporary log file (never an undrained
pipe), readable mid-run with $logs().
A PaparazziServe handle with public fields url and port
and methods stop(), logs(), and is_running().
app <- pz_serve_shiny(pz_example("tasks-app")) app # Pages opened on a handle share the app, here at two screen sizes desktop <- pz_open(app, width = 1280) phone <- pz_open(app, width = 390, mobile = TRUE) pz_get_text(phone, target = "#summary") pz_close(desktop) pz_close(phone) # Closing the pages leaves the app running until you stop it app$is_running() head(app$logs()) app$stop()app <- pz_serve_shiny(pz_example("tasks-app")) app # Pages opened on a handle share the app, here at two screen sizes desktop <- pz_open(app, width = 1280) phone <- pz_open(app, width = 390, mobile = TRUE) pz_get_text(phone, target = "#summary") pz_close(desktop) pz_close(phone) # Closing the pages leaves the app running until you stop it app$is_running() head(app$logs()) app$stop()
Serves a directory or a single HTML file on a free local port. For a file, its directory is served and the handle URL points to that file. Other files in that directory are also accessible over HTTP.
pz_serve_static(path, ..., root = NULL)pz_serve_static(path, ..., root = NULL)
path |
A path to a directory or an |
... |
Reserved; must be empty. |
root |
The directory to serve as the server root. Defaults to the
file's directory for an |
A PaparazziServe handle with $url, $port, $stop(),
$is_running(), and $logs(). Static servers have no captured logs:
$logs() returns character(). Pass the handle to pz_open() to
share a server across pages. Closing those pages leaves it running.
$stop() is idempotent; use withr::defer(server$stop()) for cleanup.
A finalizer stops the server as a last resort.
# pz_open() serves HTML files over HTTP on its own; pz_serve_static() # creates a handle you can share across pages instead server <- pz_serve_static(pz_example("tasks")) page <- pz_open(server) pz_get_url(page) pz_close(page) server$stop()# pz_open() serves HTML files over HTTP on its own; pz_serve_static() # creates a handle you can share across pages instead server <- pz_serve_static(pz_example("tasks")) page <- pz_open(server) pz_get_url(page) pz_close(page) server$stop()
Auto-waits for a match, then sets the input's files to the given
local files through the browser's own file-input channel, so the
input's FileList holds the real names, sizes, and contents, and
change fires exactly as if the files had been picked in a dialog.
pz_set_files(ctx, files, ..., target = NULL)pz_set_files(ctx, files, ..., target = NULL)
ctx |
A paparazzi context. |
files |
A character vector of paths to existing local files. |
... |
Checked empty; reserved for future use. |
target |
A CSS selector string, a |
ctx, invisibly.
notes <- file.path(tempdir(), "meeting-notes.txt") writeLines("Agenda: plants, parcel, passport", notes) page <- pz_open(pz_example("tasks")) page |> pz_set_files(notes, target = "#attachment") pz_get_text(page, target = "#attachment-name") pz_close(page)notes <- file.path(tempdir(), "meeting-notes.txt") writeLines("Agenda: plants, parcel, passport", notes) page <- pz_open(pz_example("tasks")) page |> pz_set_files(notes, target = "#attachment") pz_get_text(page, target = "#attachment-name") pz_close(page)
Uses the input's registered Shiny binding to update the visible control
and notify the server. The id is the complete DOM ID, including any
module prefix (for example, "mod-text"); it is searched only in the
current scope, including the scope element itself. This does not set
unbound inputs, action buttons, or file inputs.
pz_set_shiny_input(ctx, id, value, ..., wait = TRUE)pz_set_shiny_input(ctx, id, value, ..., wait = TRUE)
ctx |
A paparazzi context. |
id |
Full DOM ID of a bound Shiny input. |
value |
Value accepted by the binding's |
... |
Checked empty; reserved for future use. |
wait |
If |
ctx, invisibly.
page <- pz_open(pz_example("tasks-app")) # Each call waits for Shiny to go idle, so the server has seen the value page |> pz_set_shiny_input("title", "Buy milk") |> pz_set_shiny_input("priority", "high") |> pz_act_click("#add") |> pz_expect_text("3 tasks", target = "#summary") pz_get_attr(page, "data-priority", target = ".task") pz_close(page)page <- pz_open(pz_example("tasks-app")) # Each call waits for Shiny to go idle, so the server has seen the value page |> pz_set_shiny_input("title", "Buy milk") |> pz_set_shiny_input("priority", "high") |> pz_act_click("#add") |> pz_expect_text("3 tasks", target = "#summary") pz_get_attr(page, "data-priority", target = ".task") pz_close(page)
Auto-waits for a match, then sets the value instantly – never staged
as typing, even while recording – and dispatches input and
change, so the page reacts exactly as if a user had made the edit.
Framework-controlled inputs (React and friends) notice the change:
the value is assigned through the browser's native value setter, not
the instance-level property those frameworks intercept.
pz_set_value(ctx, value, ..., target = NULL)pz_set_value(ctx, value, ..., target = NULL)
ctx |
A paparazzi context. |
value |
A string (most controls), a number (range, number), or
|
... |
Checked empty; reserved for future use. |
target |
A CSS selector string, a |
Covers text inputs and textareas (clear with pz_set_value(ctx, "")),
native <select> elements (matched by option value), checkboxes
and radios (TRUE/FALSE; a radio set to TRUE unchecks the others
in its group), and range, date, and number inputs. A contenteditable
element (plain or framework-driven, e.g. ProseMirror) has its
content replaced in one step.
ctx, invisibly.
pz_act_type() for visible, keystroke-by-keystroke input and
pz_set_files().
page <- pz_open(pz_example("tasks")) page |> pz_set_value("Buy milk", target = "#task-title") |> pz_set_value("high", target = "#task-priority") |> pz_set_value(TRUE, target = "#task-urgent") pz_get_value(page, target = list("#task-title", "#task-priority")) pz_expect_checked(page, target = "#task-urgent") # An empty string clears a text input page |> pz_set_value("", target = "#task-title") pz_expect_enabled(page, target = "#add-task", not = TRUE) pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_set_value("Buy milk", target = "#task-title") |> pz_set_value("high", target = "#task-priority") |> pz_set_value(TRUE, target = "#task-urgent") pz_get_value(page, target = list("#task-title", "#task-priority")) pz_expect_checked(page, target = "#task-urgent") # An empty string clears a text input page |> pz_set_value("", target = "#task-title") pz_expect_enabled(page, target = "#add-task", not = TRUE) pz_close(page)
Sets the staging options that animate pointer actions, typing, and
scrolling while the page is recording. Only supplied arguments
change – an omitted argument leaves its setting alone, an explicit
NULL restores the built-in default, and a value sets it (so
pz_stage(cursor = FALSE) can be undone with pz_stage(cursor = NULL)). Staged settings apply to the pipeline the staged context
flows through: pz_find*() contexts inherit them, and they persist
across recordings inside the chain. A fresh pipeline from the
original context starts from the defaults, so assign the result
(page <- page |> pz_stage(...)) to set page-wide defaults. Without a
recording, staging is skipped and the chain runs straight to its
final state – except
cursor = TRUE, which shows a static cursor in screenshots too.
pz_stage( ctx, ..., cursor = NULL, cursor_speed = NULL, cursor_scale = NULL, enter = NULL, typing = NULL, typing_speed = NULL, pause = NULL, camera_follow = NULL, show_keys = NULL, click_effect = NULL, click_effect_color = NULL )pz_stage( ctx, ..., cursor = NULL, cursor_speed = NULL, cursor_scale = NULL, enter = NULL, typing = NULL, typing_speed = NULL, pause = NULL, camera_follow = NULL, show_keys = NULL, click_effect = NULL, click_effect_color = NULL )
ctx |
A paparazzi context. |
... |
Checked empty; reserved for future use. |
cursor |
Cursor visibility: |
cursor_speed |
Staging speed in pixels per second for cursor glides
and recorded scrolls. The default, 500 px/s, gives viewers time to
follow the movement; 400-600 px/s is a useful range for deliberate
actions. Each glide uses its straight-line distance; each scroll
uses the largest axis delta. Duration is
|
cursor_scale |
Cursor size relative to the original 20px overlay
artwork, from greater than 0 to 5. The default is 1.75 (about 35px);
use 1 for the original size. Applies to recordings and stills.
Supply |
enter |
Where a cursor that has never been shown first appears:
|
typing |
Typing style while recording: |
typing_speed |
Natural typing speed in characters per second.
The default is 16. Supply |
pause |
Seconds to hold after each action while recording.
The default is 0. Supply |
camera_follow |
Whether pointer and typing actions automatically pan
a zoomed recording camera to keep their target in view. Defaults to
|
show_keys |
Keystroke callouts for |
click_effect |
Click feedback shown by |
click_effect_color |
CSS color of the |
ctx, invisibly.
pz_stage_frame(), pz_stage_annotate(), pz_cursor_show(),
pz_record_start()
page <- pz_open(pz_example("tasks")) # Staged settings reach the pipelines the staged context flows # through; assignment makes them page-wide defaults page <- page |> pz_stage(enter = "left", cursor_speed = 500, typing_speed = 20, pause = 0.3) # Recorded clicks show a custom-colored ring page <- page |> pz_stage(click_effect = "ring", click_effect_color = "#2563eb") path <- file.path(tempdir(), "add-task.mp4") page |> pz_record(path, { page |> pz_act_type("Buy milk", target = "#task-title") |> pz_act_click("#add-task") }) # NULL restores a setting's built-in default page <- page |> pz_stage(pause = NULL) # cursor = TRUE shows the cursor in screenshots too page |> pz_stage(cursor = TRUE) |> pz_act_hover("#task-title") |> pz_screenshot(file.path(tempdir(), "cursor.png"), frame = "#new-task") pz_close(page)page <- pz_open(pz_example("tasks")) # Staged settings reach the pipelines the staged context flows # through; assignment makes them page-wide defaults page <- page |> pz_stage(enter = "left", cursor_speed = 500, typing_speed = 20, pause = 0.3) # Recorded clicks show a custom-colored ring page <- page |> pz_stage(click_effect = "ring", click_effect_color = "#2563eb") path <- file.path(tempdir(), "add-task.mp4") page |> pz_record(path, { page |> pz_act_type("Buy milk", target = "#task-title") |> pz_act_click("#add-task") }) # NULL restores a setting's built-in default page <- page |> pz_stage(pause = NULL) # cursor = TRUE shows the cursor in screenshots too page |> pz_stage(cursor = TRUE) |> pz_act_hover("#task-title") |> pz_screenshot(file.path(tempdir(), "cursor.png"), frame = "#new-task") pz_close(page)
Sets style defaults for new annotations in screenshots and recordings.
An omitted argument leaves its setting alone, an explicit NULL
restores the built-in default, and a value sets it. The defaults
apply to the pipeline the staged context flows through: pz_find*()
contexts inherit them, while a fresh pipeline from the original
context starts from the defaults – assign the result
(page <- page |> pz_stage_annotate(...)) to set page-wide defaults.
Per-call annotation style arguments override these defaults; note
the per-call names on pz_annotate() differ
(label_fill/label_text_color) from the staged names
(fill/text_color), and the staged fill and text color style
callout chrome only, not mark badges.
pz_stage_annotate( ctx, ..., color = NULL, fill = NULL, text_color = NULL, stroke_width = NULL, distance = NULL, font_family = NULL, font_size = NULL, caption_side = NULL )pz_stage_annotate( ctx, ..., color = NULL, fill = NULL, text_color = NULL, stroke_width = NULL, distance = NULL, font_family = NULL, font_size = NULL, caption_side = NULL )
ctx |
A paparazzi context. |
... |
Checked empty; reserved for future use. |
color |
CSS accent color for new annotations: mark outlines, callout
bubble borders, leader lines and decorations. The default is |
fill |
CSS background color for new callout bubbles and their
badges. By default, the fill follows each callout's resolved |
text_color |
CSS text color for new callout bubbles and their
badges. By default, paparazzi chooses black or white for the higher
WCAG contrast ratio against the resolved fill. Supply |
stroke_width |
Stroke width in CSS pixels for new annotation marks
and callout leader lines; decoration sizes scale with it. The default
is 3. Supply |
distance |
Bubble-to-target gap in CSS pixels for new callouts,
with or without a leader. By default the gap is 24 with a leader and
8 without one. Supply |
font_family |
CSS font family for new annotation badges and key
callouts. The default is |
font_size |
Badge font size in CSS pixels. The default is 14.
Supply |
caption_side |
Default side for new |
ctx, invisibly.
pz_stage(), pz_stage_frame(), pz_annotate()
page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "tasks.png") # Assignment makes these styles page-wide defaults for new annotations page <- page |> pz_stage_annotate(color = "#2563eb", font_size = 16) page |> pz_annotate("#add-task", label = TRUE) |> pz_screenshot(path) # A one-off pipeline can undo a default with NULL: its callout uses # the built-in red again page |> pz_stage_annotate(color = NULL) |> pz_annotate("#task-title", label = TRUE) |> pz_screenshot(path, frame = pz_frame("#task-title", pad = 16, target_box = "annotated")) pz_close(page)page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "tasks.png") # Assignment makes these styles page-wide defaults for new annotations page <- page |> pz_stage_annotate(color = "#2563eb", font_size = 16) page |> pz_annotate("#add-task", label = TRUE) |> pz_screenshot(path) # A one-off pipeline can undo a default with NULL: its callout uses # the built-in red again page |> pz_stage_annotate(color = NULL) |> pz_annotate("#task-title", label = TRUE) |> pz_screenshot(path, frame = pz_frame("#task-title", pad = 16, target_box = "annotated")) pz_close(page)
Loads one or more fonts into the page so that everything Chrome draws
for it — annotation badges, callout bubbles, captions, and key
callouts — can use them. Declare fonts with pz_font_google(),
pz_font_bunny(), or pz_font_file() and pass them in ....
pz_stage_fonts( ctx, ..., on_error = getOption("paparazzi.stage_fonts.on_error", "stop") )pz_stage_fonts( ctx, ..., on_error = getOption("paparazzi.stage_fonts.on_error", "stop") )
ctx |
A paparazzi context. |
... |
Font objects from |
on_error |
What to do when a font fails to load: |
Staging is an ahead-of-time step: pz_stage_fonts() blocks until every
face has loaded (or fails per on_error), so annotating is never
slowed by a font load and never flashes a fallback font. Staged fonts
persist across recordings; after navigation they are re-added to the
new document lazily, the first time something is annotated there.
Repeated calls add fonts. A face whose family, weight, and style are already staged is replaced by the new one. There is no clear or reset.
Remote stylesheets and font files are fetched by Chrome, not R, and
stay in Chrome's HTTP cache. On pages whose Content Security Policy
blocks the fetch (connect-src for the stylesheet, font-src for the
font files) the load fails and is routed through on_error.
pz_font_file() sidesteps CSP entirely because its bytes are handed to
the page directly.
ctx, invisibly.
pz_stage_annotate(), whose font_family sets the default
family for annotations and key callouts
page <- pz_open(pz_example("tasks")) page |> # on_error = "warn" keeps unattended renders (CI, pkgdown) going when # the font CDN is unreachable; drop it to make a missing font fatal. pz_stage_fonts(pz_font_google("Silkscreen"), on_error = "warn") |> # font_family takes a CSS family string; use the staged family with # a generic fallback pz_stage_annotate(font_family = '"Silkscreen", sans-serif') |> pz_annotate("#add-task", label = "New") |> pz_screenshot(file.path(tempdir(), "tasks.png")) pz_close(page)page <- pz_open(pz_example("tasks")) page |> # on_error = "warn" keeps unattended renders (CI, pkgdown) going when # the font CDN is unreachable; drop it to make a missing font fatal. pz_stage_fonts(pz_font_google("Silkscreen"), on_error = "warn") |> # font_family takes a CSS family string; use the staged family with # a generic fallback pz_stage_annotate(font_family = '"Silkscreen", sans-serif') |> pz_annotate("#add-task", label = "New") |> pz_screenshot(file.path(tempdir(), "tasks.png")) pz_close(page)
pz_stage_frame() stores a default pz_frame() on the context: every
screenshot and recording started from the staged context resolves its
own frame = field by field, and an unset field takes the staged
value if one is set, else the built-in default. frame = FALSE
disables framing for a single call. The staged frame applies to the
pipeline the staged context flows through – pz_find*() contexts
inherit it – while a fresh pipeline from the original context starts
from the defaults; assign the result
(page <- page |> pz_stage_frame(...)) to set page-wide defaults.
pz_stage_frame(NULL) clears an inherited staged frame for that
pipeline only, and a new pz_stage_frame() call replaces the staged
frame wholesale (unset fields fall back to built-in defaults, they do
not merge with a previously staged frame). Camera shots ignore the
staged frame; see pz_camera().
Unlike pz_stage()'s animation settings, framing (like
pz_stage_annotate()'s styles) applies to screenshots as well as
recordings. An annotated
staged frame measures attached annotations in stills; recordings use
only its element boxes for the home frame.
pz_stage_frame( ctx, ..., ratio = NULL, pad = NULL, offset = NULL, anchor = NULL, bounds = NULL, target_box = NULL, zoom = NULL )pz_stage_frame( ctx, ..., ratio = NULL, pad = NULL, offset = NULL, anchor = NULL, bounds = NULL, target_box = NULL, zoom = NULL )
ctx |
A paparazzi context. |
... |
The default frame's target: a CSS selector string, a
|
ratio, pad, offset, anchor, bounds, target_box, zoom
|
Framing
settings, as in |
ctx, invisibly.
pz_stage(), pz_stage_annotate(), pz_frame(), pz_screenshot()
page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "tasks.png") page <- page |> pz_stage_frame(pad = 24) # Framed by the default: the form plus 24px of padding page |> pz_screenshot(path, frame = "#new-task") # Fields not set here still come from the staged default page |> pz_screenshot(path, frame = pz_frame("#new-task", ratio = 16/9)) # FALSE turns framing off for one call page |> pz_screenshot(path, frame = FALSE) # NULL clears the staged default for this pipeline only page |> pz_stage_frame(NULL) |> pz_screenshot(path, frame = "#new-task") pz_close(page)page <- pz_open(pz_example("tasks")) path <- file.path(tempdir(), "tasks.png") page <- page |> pz_stage_frame(pad = 24) # Framed by the default: the form plus 24px of padding page |> pz_screenshot(path, frame = "#new-task") # Fields not set here still come from the staged default page |> pz_screenshot(path, frame = pz_frame("#new-task", ratio = 16/9)) # FALSE turns framing off for one call page |> pz_screenshot(path, frame = FALSE) # NULL clears the staged default for this pipeline only page |> pz_stage_frame(NULL) |> pz_screenshot(path, frame = "#new-task") pz_close(page)
Pauses for seconds, driving chromote's child event loop so timers
scheduled on it (e.g. recording capture) keep firing during the wait.
Never sleeps without pumping the loop.
pz_wait(ctx, seconds)pz_wait(ctx, seconds)
ctx |
A paparazzi context. |
seconds |
Number of seconds to wait. |
ctx, invisibly.
page <- pz_open(pz_example("tasks")) # A fixed pause; prefer an expectation or wait_for function when you # know what you're waiting for page |> pz_act_click("#toggle-help") |> pz_wait(0.5) pz_close(page)page <- pz_open(pz_example("tasks")) # A fixed pause; prefer an expectation or wait_for function when you # know what you're waiting for page |> pz_act_click("#toggle-help") |> pz_wait(0.5) pz_close(page)
Polls expr until it evaluates truthy, then returns ctx invisibly.
Each poll awaits a promise expr returns, so async conditions work;
JavaScript truthiness applies (a non-empty string or a non-zero
number counts). An expr that throws is a JavaScript error, not a
failed poll.
pz_wait_for_js(ctx, expr, ..., timeout = NULL)pz_wait_for_js(ctx, expr, ..., timeout = NULL)
ctx |
A paparazzi context. |
expr |
A string of JavaScript that evaluates truthy when the condition holds. A returned promise is awaited first. |
... |
Checked empty; reserved for future use. |
timeout |
Seconds before giving up; |
There is deliberately no element-state wait (pz_wait_for(target, state =)): pz_expect_visible(), pz_expect_hidden(), and
pz_expect_exists() with not = TRUE already retry, so they wait.
ctx, invisibly.
pz_wait_for_stable(), pz_wait_for_navigation()
page <- pz_open(pz_example("tasks")) page |> pz_act_type("Buy milk", target = "#task-title") |> pz_act_press("Enter") |> pz_wait_for_js("document.querySelectorAll('.task').length === 8") pz_get_text(page, target = pz_loc(".task-title", which = "first")) pz_close(page)page <- pz_open(pz_example("tasks")) page |> pz_act_type("Buy milk", target = "#task-title") |> pz_act_press("Enter") |> pz_wait_for_js("document.querySelectorAll('.task').length === 8") pz_get_text(page, target = pz_loc(".task-title", which = "first")) pz_close(page)
Waits for the Shiny connection, for <html> to lose shiny-busy, and
for every .recalculating output to finish. All three conditions must
hold continuously for at least 200ms, including brief busy/recalculating
transitions. A page without the Shiny global errors instead of waiting;
a page whose Shiny app never connects times out.
pz_wait_for_shiny_idle(ctx, ..., timeout = NULL)pz_wait_for_shiny_idle(ctx, ..., timeout = NULL)
ctx |
A paparazzi context. |
... |
Checked empty; reserved for future use. |
timeout |
Seconds before giving up; |
ctx, invisibly.
page <- pz_open(pz_example("tasks-app")) # Clicking Add makes the server re-render the task list, which takes a moment page |> pz_set_value("Buy milk", target = "#title") |> pz_act_click("#add") |> pz_wait_for_shiny_idle() pz_get_text(page, target = "#summary") pz_close(page)page <- pz_open(pz_example("tasks-app")) # Clicking Add makes the server re-render the task list, which takes a moment page |> pz_set_value("Buy milk", target = "#title") |> pz_act_click("#add") |> pz_wait_for_shiny_idle() pz_get_text(page, target = "#summary") pz_close(page)
Samples the prop property of every element matching target (or
the current context) until the sampled value has held still for
for_ms milliseconds: text has stopped arriving, or an animation
has come to rest. prop = "rect" samples the bounding box instead
of a property, rounded to whole pixels.
pz_wait_for_stable( ctx, ..., target = NULL, prop = "textContent", for_ms = 500, timeout = NULL )pz_wait_for_stable( ctx, ..., target = NULL, prop = "textContent", for_ms = 500, timeout = NULL )
ctx |
A paparazzi context. |
... |
Checked empty; reserved for future use. |
target |
A CSS selector string, a |
prop |
The element property to sample: any single JavaScript
property name ( |
for_ms |
Milliseconds the sampled value must hold still before the wait passes. |
timeout |
Seconds before giving up; |
The wait first waits (up to timeout) for target to match, then
samples within a second, separate timeout budget of its own: a
target that appears near the locator deadline still gets its full
for_ms window.
There is deliberately no element-state wait: pz_expect_visible(),
pz_expect_hidden(), and pz_expect_exists() with not = TRUE already
retry, so they wait.
ctx, invisibly.
pz_wait_for_js(), pz_wait_for_navigation()
page <- pz_open(pz_example("tasks")) # The status line reads "Saving..." and then "Saved"; wait for it to settle page |> pz_act_type("Buy milk", target = "#task-title") |> pz_act_click("#add-task") |> pz_wait_for_stable(target = "#status", for_ms = 500) pz_get_text(page, target = "#status") pz_close(page)page <- pz_open(pz_example("tasks")) # The status line reads "Saving..." and then "Saved"; wait for it to settle page |> pz_act_type("Buy milk", target = "#task-title") |> pz_act_click("#add-task") |> pz_wait_for_stable(target = "#status", for_ms = 500) pz_get_text(page, target = "#status") pz_close(page)
pz_with_page() evaluates code with the page open and closes it on exit,
including on error. pz_local_page() opens a page and closes it when the
calling frame (e.g. a test) exits, via withr::defer(). Both accept an
already-open page or context (e.g. the staged result of pz_stage()) or
anything pz_open() accepts, and always close the underlying page on
exit: the block owns the resource. A context is passed through as itself,
so its staged settings reach the code it flows into.
pz_with_page(x, code, ...) pz_local_page(x, ..., .env = caller_env())pz_with_page(x, code, ...) pz_local_page(x, ..., .env = caller_env())
x |
An open page or context, or anything |
code |
Code to run while the page is open. An expression (evaluated
as-is; useful when |
... |
Passed to |
.env |
The frame whose exit closes the page. |
pz_with_page() returns the page (or context) invisibly;
pz_local_page() returns it visibly.
path <- file.path(tempdir(), "task-list.png") # The page closes when the function returns, even if it errors pz_with_page(pz_example("tasks"), function(page) { pz_screenshot(page, path, frame = ".task-list") }) file.exists(path) # pz_local_page() ties the page to the calling function, e.g. a test count_tasks <- function() { page <- pz_local_page(pz_example("tasks")) pz_get_count(page, target = ".task") } count_tasks()path <- file.path(tempdir(), "task-list.png") # The page closes when the function returns, even if it errors pz_with_page(pz_example("tasks"), function(page) { pz_screenshot(page, path, frame = ".task-list") }) file.exists(path) # pz_local_page() ties the page to the calling function, e.g. a test count_tasks <- function() { page <- pz_local_page(pz_example("tasks")) pz_get_count(page, target = ".task") } count_tasks()