Summary
Port R Shiny's test mode to py-shiny: a read-only per-session JSON snapshot endpoint exposing {input, output, export}, plus an export_test_values() API so app authors can surface internal reactive values. This is the server-side foundation for a future shinytest2-like snapshot-testing story in Python.
This is Phase A (server-side snapshot endpoint + API). First-class Playwright/automation access to these values is a later Phase B and is out of scope here.
Motivation
R Shiny's test mode powers shinytest2 snapshot testing: it registers a per-session /dataobj/shinytest JSON endpoint returning {input, output, export}, and exportTestValues() lets app authors expose internal reactive values for snapshotting. py-shiny has the building blocks (input serialization, cached output values, a per-session request router) but nothing is wired up, and there is no export_test_values() equivalent.
Scope (Phase A)
Enabling
- Enable via environment variable
SHINY_TESTMODE=1 only (mirrors the existing SHINY_DEV_MODE pattern in html_dependencies.py).
- A constructor argument (
App(..., test_mode=...)) is deferred to a later phase.
- When off: zero behavioral change — no value recording, the endpoint 404s,
export_test_values() is a cheap no-op.
export_test_values() API
- Module-level function and session method (
session.export_test_values(**kwargs)).
- Called as
export_test_values(my_val=lambda: total() + 1, count=my_calc) — each value is a zero-arg callable / reactive.calc, evaluated lazily inside a reactive context at snapshot time.
- Module-level form resolves the current session via
get_current_session().
- Export names are namespaced with the module's
ns prefix (consistent with inputs/outputs; note R does not namespace).
- Last-registration-wins on duplicate names.
Snapshot endpoint
- Auto-registered at
/session/<token>/dataobj/shinytest (R-compatible path) when test mode is on.
- Returns
{ "input": {...}, "output": {...}, "export": {...} } (R's singular keys, kept as-is).
- Handler wraps work in
session_context(self) + isolate() (same pattern as dynamic_route).
input — serialized current inputs (dedicated snapshot serialization path, not the bookmark _serialize, so it doesn't write files to a state_dir or silently drop unserializables).
output — last-known-value record: maintain a per-session dict of the most recent value computed for each output (recorded where output values are set). Outputs that never computed are absent. (Matches R semantics.)
export — each registered callable evaluated lazily at request time.
Serialization fidelity
- Best-effort coercion; values that can't be JSON-encoded are wrapped in a visible tagged marker (e.g.
{"__shiny_serialization_error__": "<message>"}) rather than silently stringified or hard-failing.
- Sort keys for deterministic, cleanly-diffable snapshots.
URL helper
- Add
session.get_test_snapshot_url() (R parity with session$getTestSnapshotUrl()), including a cache-busting nonce. Included in Phase A.
Out of scope (deferred to Phase B)
- Registering the built-but-unused
shiny/www/shared/shiny-testmode.js eval-driving helper (for driving an app from a parent frame).
- First-class Playwright controller /
page helper access to snapshot values.
App(test_mode=...) constructor argument.
Related
Summary
Port R Shiny's test mode to py-shiny: a read-only per-session JSON snapshot endpoint exposing
{input, output, export}, plus anexport_test_values()API so app authors can surface internal reactive values. This is the server-side foundation for a future shinytest2-like snapshot-testing story in Python.This is Phase A (server-side snapshot endpoint + API). First-class Playwright/automation access to these values is a later Phase B and is out of scope here.
Motivation
R Shiny's test mode powers
shinytest2snapshot testing: it registers a per-session/dataobj/shinytestJSON endpoint returning{input, output, export}, andexportTestValues()lets app authors expose internal reactive values for snapshotting. py-shiny has the building blocks (input serialization, cached output values, a per-session request router) but nothing is wired up, and there is noexport_test_values()equivalent.Scope (Phase A)
Enabling
SHINY_TESTMODE=1only (mirrors the existingSHINY_DEV_MODEpattern inhtml_dependencies.py).App(..., test_mode=...)) is deferred to a later phase.export_test_values()is a cheap no-op.export_test_values()APIsession.export_test_values(**kwargs)).export_test_values(my_val=lambda: total() + 1, count=my_calc)— each value is a zero-arg callable /reactive.calc, evaluated lazily inside a reactive context at snapshot time.get_current_session().nsprefix (consistent with inputs/outputs; note R does not namespace).Snapshot endpoint
/session/<token>/dataobj/shinytest(R-compatible path) when test mode is on.{ "input": {...}, "output": {...}, "export": {...} }(R's singular keys, kept as-is).session_context(self)+isolate()(same pattern asdynamic_route).input— serialized current inputs (dedicated snapshot serialization path, not the bookmark_serialize, so it doesn't write files to astate_diror silently drop unserializables).output— last-known-value record: maintain a per-session dict of the most recent value computed for each output (recorded where output values are set). Outputs that never computed are absent. (Matches R semantics.)export— each registered callable evaluated lazily at request time.Serialization fidelity
{"__shiny_serialization_error__": "<message>"}) rather than silently stringified or hard-failing.URL helper
session.get_test_snapshot_url()(R parity withsession$getTestSnapshotUrl()), including a cache-bustingnonce. Included in Phase A.Out of scope (deferred to Phase B)
shiny/www/shared/shiny-testmode.jseval-driving helper (for driving an app from a parent frame).pagehelper access to snapshot values.App(test_mode=...)constructor argument.Related
_serializepath)