posit-dev / posit-dev/py-shiny

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

Open
#2,269 0 comments 0 reactions 1 assignee View on GitHub

@schloerke is already working on this.

Since Jun 2, 2026.

ai-triage:done Priority: Medium
Dominant language
Python
Stars
1.8k
Forks
135
Avg merge
2d 18h
Merged PRs (30d)
21

Description

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

  • #1156 (Add testing API for py-shiny — broader testing-namespace request)
  • #1281 (Epic — Testing shiny apps; Playwright controller work)
  • #1980 (Save and restore state — touches the same input _serialize path)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.