posit-dev / posit-dev/shinyreact
Audit findings: parity gaps, latent bugs, and test holes found auditing FEATURES.md
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 14
- Forks
- 3
- Avg merge
- 9h 12m
- Merged PRs (30d)
- 74
Description
FEATURES.md (the behavior tree) was audited leaf-by-leaf against all three packages, in both directions: is every claim true, and does the code do things no claim covers. The tree itself has been corrected. This issue tracks the code-level findings, which are real and unfixed.
Grouped by severity. Every item was verified against the source; the ones marked verified live were reproduced by running code.
Probably-wrong code (not just undocumented)
-
[r]the#shinyreact-configtag renders in<body>, not<head>, underpage_react()— verified live:renderTags(page_react())puts it in$html, not$head.shinyreact_dep_page()returns a baretagList(pkg-r/R/dep.R:28-30), whereas Python wraps it inhead_content()(_bookmark.py:84). R's other path (page_react_html()→config_head_dep()) is head-correct, so R is inconsistent with itself as well as with Python. It works — the client finds the tag by id either way — but it is an unrecorded divergence. -
[r]page_react_html()does not preserve the document body verbatim — verified live:htmlTemplate()evaluates every{{ ... }}in the document as R code withparent = globalenv(), so a body containing{{ 6*7 }}renders42. Any Vite/Handlebars document with{{ }}in the body is rewritten or errors. Python's marker handling is a plain replace and has no such behavior. -
[js]a forgedwindow.shinyreact._restoresilently disables restore and the protocol handshake —bookmark.ts:36-40returns early on a truthy"-applied", beforeassertProtocolCompatible()runs, with no warning. Intended as idempotence; the side effect is a kill switch. -
[js]@posit/shinyreact(npm build) ships no CSS —npm.tshas no stylesheet import andpackage.jsonexposes no./stylesexport orstylefield, so consumers get no@keyframes spinandImageOutput's placeholder spinner does not animate. The IIFE bundle imports the CSS as a side effect; the ESM build does not. -
[js]output cleanup can remove the wrong DOM node —output-registry.ts:207looks the div up with a document-globalgetElementById(outputId), so an id collision with user markup removes the user's node. -
[js]initializeMessageRegistry()is a silent no-op withoutwindow.Shiny, with no retry — so if Shiny loads after the first hook call, message handlers never register. The input registry deliberately tolerates a missing Shiny; this asymmetry looks unintended.message-registry.tshas zero tests. -
[r]utilsis used but declared nowhere —getFromNamespace()(bookmark.R:40,49,57) andpackageVersion()(dep.R:11) with noDESCRIPTIONorNAMESPACEentry.
Parity gaps
-
input-handler argument order differs:
[py](value, name, session)(Shiny'sInputHandlerType),[r](value, session, name). Both ignore the last two, so nothing catches it — but the signatures are documented as if shared. -
page_react_html()file encoding:[r]decodes UTF-8 explicitly (viahtmlTemplate());[py]usesPath.read_text()with noencoding=, i.e. the platform locale (_page.py:356,450). -
[r]restore precision is globally overridable:shiny:::toJSON'sdigitsisgetOption("shiny.json.digits", I(16))— verified live:options(shiny.json.digits = 2)truncates3.14159265358979to3.14in the restore payload. Python has no equivalent global. -
[py]private-API use has no containment policy: readssession.output._outputs(_page.py:407) and importsshiny.bookmark._restore_state(_bookmark.py:6-9) inline. R confines its internals to namedshiny___*wrappers with ashiny (>= 1.13.0)pin — though not totally:ctx$input$asList()(bookmark.R:81) calls a private R6 method directly.
Robustness / rough edges
[r]a missingsrc_dirdegrades to"."— verified live: nowww/yields<title>.</title>and a dependency named., served at/lib/.-0/. No error, no test.[r]the marker check isfixed = TRUE, so{{headContent()}}is rejected even thoughhtmlTemplate()would accept it.- the marker check is not
<head>-scoped in either language: a{{ headContent() }}in<body>passes and deps render there, though the error message asserts<head>. [py]ReactApplatches its mode at construction (_app.py:81), so creating/deletingwww/index.htmlafter startup never switches modes — even though the UI is per-request. Relatedly, the per-request UI re-runspage_react's existence checks and re-emits the missing-ui.jswarning on every request.[py]static_assetsedges, both untested: explicitstatic_assets=Nonestill auto-mounts (the guard isis None), and a passed value replaces rather than merges, leaving the document's directory unserved.[js]useShinyOutputValuedoes not reset its held value when the id/namespace changes, so the old id's data stays visible.[js]MISSINGdoes not cancel a pending debounced send, so a real value written just before it still lands on the server.[js]conflictingpriorityacross mounts is silently last-writer-wins, while conflictingtypethrows.[js]initializeReactRegistry()is not idempotent (a second call discards all values);getReactRegistry()returnsundefinedunchecked when Shiny is present but init never ran, and throws otherwise; the output container is created with nodocumentguard, so init throws in a DOM-less environment — the opposite of the lifecycle store's stated policy.[js]every manufactured output div ships the literal textThis is the output div for <id>into the page.[js]ShinyReactComponentElement.disconnectedCallbackcallsunbindAll(this)withoutincludeSelf, so the element itself is never unbound — the inverse ofShinyOutput's carefully reasoned choice.[js]the protocol handshake is skipped whenprotocolVersionis absent or falsy (config.ts:16-20,bookmark.ts:50), so a tagged page carrying no version silently skips version checking.
Test holes worth closing
shiny-bound-outputis asserted nowhere — it appears only as a comment inpkg-py/tests/playwright/test_shiny_output.py:24. One-line fix:expect(out).to_have_class(re.compile(r"\bshiny-bound-output\b")).[r]the bundle script'sdeferattribute is unpinned — Python pins its script attrs; R pins onlysrc. This is precisely the #182 failure mode.- the bundle-version fallback divergence is tested in neither language —
[py]"0.1.0"vs[r]packageVersion("shinyreact"). .tagify()in the renderer harvest is unpinned —test_react_page_harvests_session_output_depsstill passes with the call deleted.page_react()'s "no body HTML" is asserted in neither language — and it is false in R (finding 1).__all__/NAMESPACEexport sets are pinned by no test in either language.[js]the 15-itemwindow.shinyreactsurface has no test — the object literal and theWindowinterface would drift together silently.- Also unpinned:
page_bare()emitting no config tag,set_react_page's explicit-missing-path error, renderer harvest in discovered mode,[r]send_message'sinvisible(NULL),[r]force = TRUEin.onLoad(),[js]debounceMs's numeric default, and everyImageOutputleaf except namespace precedence.
Not actionable, recorded for honesty
Twelve leaves are UNVERIFIABLE as written — rationale and policy claims with no code to check: the protocol's "bumps only when a boundary shape changes" (no test fails when it does not), <script defer> throwing on a module import, htmltools dep de-duplication, MISSING → SilentException (cross-language, unpinned), and "re-binding the parent is safe because Shiny skips bound elements" (mocked in unit tests). They are marked in the tree rather than dropped.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by splitting the findings by package and subsystem, then read the named entry points such as bookmark.ts, output-registry.ts, message-registry.ts, _page.py, and bookmark.R. Run the referenced tests, including pkg-py/tests/playwright/test_shiny_output.py, and add focused regression coverage for one selected finding. Done means the chosen behavior is corrected and its regression test passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, python, r, typescript
- Domain
- backend, frontend, testing
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 28/100