Skip to content

feat: Add test mode with export_test_values() and /dataobj/shinytest snapshot endpoint #2269

@schloerke

Description

@schloerke

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).
  • outputlast-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

Metadata

Metadata

Assignees

Labels

Priority: MediumValid bug or well-defined request with moderate impact or a workaround.ai-triage:doneMarks an issue whose AI triage workflow is complete.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions