feat(network-activity-plugin): XML response renderer + JSON Raw view#277
Merged
V3RON merged 6 commits intoMay 19, 2026
Conversation
10 tasks
V3RON
approved these changes
May 19, 2026
4eef05a to
aa9cc98
Compare
`xmlRenderer` matches XML content-types via `isXmlContentType` —
`application/xml`, `text/xml`, and any `*/foo+xml` per RFC 7303,
covering Atom, RSS, SOAP, XHTML, XSLT, and any future composite
type without an enumeration to maintain. The matcher is case-
insensitive and parameter-tolerant via the existing
`normalizeContentType` helper. `image/svg+xml` also matches the
suffix rule but `svgRenderer`'s earlier registry slot keeps it
claiming SVG first.
Preview renders the parsed body as a collapsible tree via a new
`XmlTree` component. The tree is a recursive `XmlNode` switching
on `nodeType`: elements render as `<tag attr="value">` with a
chevron toggle and a copy button that serializes the subtree via
`XMLSerializer`; non-whitespace text nodes render in-place;
whitespace-only text nodes (DOM-pretty-print noise between
sibling elements) are filtered; CDATA sections render with
explicit `<![CDATA[ … ]]>` markers and a copy button on their
value. Comments, processing instructions, and DOCTYPE are
deliberately not rendered in the tree — they're rare in API
responses and the Raw view shows them verbatim.
Collapse uses `style={{ display: 'none' }}` rather than
conditional unmounting so a nested node's per-element `useState`
survives a parent collapse/expand cycle — collapse a sub-element,
collapse its parent, re-expand the parent, the sub-element is
still collapsed. Cost is hidden DOM, acceptable for typical XML.
Raw renders the body verbatim via the existing `CodeBlock` —
same shape as text-fallback and the SVG Raw view.
Parse-error detection covers cross-engine variance: Chrome / Safari
return a `Document` whose `documentElement` IS the `<parsererror>`
element; Firefox nests it under the
`http://www.mozilla.org/newlayout/xml/parsererror.xml` namespace.
`hasParseError` checks both paths in three lines. On parse failure
the renderer falls back to source + a "Failed to parse as XML"
warning — same fallback shape as `jsonRenderer`'s `catch` branch.
`views` stays static `['preview', 'raw']`. On malformed input the
fallback ignores the active view and renders source-with-warning;
the toggle stays visible but no-ops. Making `views` dynamic would
require a `ResponseRenderer` type change; this trade matches the
existing `jsonRenderer` behavior.
The tree's color palette uses Tailwind utility classes inline (no
`react-json-tree` wrapper): blue for tag names matching the
existing Content-Type label color, amber for attribute names,
green for attribute values, purple for CDATA wrapper markers,
muted gray for punctuation. Element-header copy reuses the
existing `JsonTreeCopyableItem` so the hover-revealed icon
behavior matches the JSON tree.
Tests:
- `XmlTree.test.tsx` covers tag-name rendering, attribute
inline-ness, text and CDATA rendering, whitespace filtering,
recursive nesting, `display: none` collapse mechanism, chevron
aria-label toggling, self-closing form, and namespace-prefix
preservation (`media:thumbnail` survives the walk).
- `xml.test.tsx` covers renderer attributes, Preview vs Raw
branching, malformed XML fallback for both the Chrome / Safari
shape (parsererror as `documentElement`) and a Firefox-like
shape (parsererror in the FF namespace), and `application/xhtml+xml`
rendering as a tree.
- `getContentTypeMimeType.test.ts` adds `isXmlContentType`
positive cases (plain, charset-parameterized, atom / rss / soap
/ xhtml / svg via +xml) and negative cases (text/html, JSON,
`application/xmlfoo`, null / undefined / empty).
- `dispatch.test.ts` flips `application/xml` from `text-fallback`
to `xml`, adds `text/xml`, atom, rss, soap, xhtml routing
assertions, an SVG-still-wins regression guard, and order
invariants `svg < xml` and `xml < text-fallback`.
`jsonRenderer` grows a Raw view alongside the existing tree Preview. The Preview/Raw toggle is now visible on every JSON response — it was previously hidden because `views` declared only `['preview']`. Raw shows the body pretty-printed via `JSON.stringify(parsed, null, 2)`, not the literal wire string. APIs commonly ship minified JSON, and a Raw view of minified JSON is unreadable; re-serializing with 2-space indent matches the convention in Chrome DevTools' Response panel. The small byte-fidelity loss versus the source is the correct trade for a debugging surface — anyone needing the exact bytes can copy from the override editor or hit the response directly. Implementation parses the body once at the top of `render` into a `let parsed`, then branches on `view`. Previously the body was parsed inside the Preview branch only; with two branches needing the same parsed value, extracting it is both shorter and avoids parsing twice on toggle. Malformed-JSON behavior is unchanged: when `JSON.parse` throws, the renderer falls back to source plus a warning regardless of the active view, just as before. `views` stays static `['preview', 'raw']` even on malformed input — the toggle is visible but no-ops because the fallback path returns the warning instead of branching. Same trade-off as `xmlRenderer`. Adds `__tests__/json.test.tsx` (this renderer had no test file before — it was covered only by the dispatcher routing test). Coverage: renderer attributes, Preview tree contains parsed values not the source, Raw produces multi-line pretty-printed output with exact 2-space indent at nested levels and round-trips through `JSON.parse` to the original value, malformed JSON falls back in both Preview and Raw views.
The XHR capture path in getResponseBody only routed text-shaped blobs to the text branch when the content-type was text/*, JSON (via isJsonContentType), or image/svg+xml. Anything application/xml or a +xml composite type (application/atom+xml, application/rss+xml, application/soap+xml, ...) fell through to the image-binary check, didn't match there either, and ended up returning null. The captured request reached the panel with no body, surfacing as "No response body available for this request" in the response tab — even though the body was right there on the wire. Add isXmlContentType to the text branch alongside isJsonContentType. The predicate uses the RFC 7303 structural rule (mimeType === 'application/xml' || mimeType === 'text/xml' || mimeType.endsWith( '+xml')), which sweeps in every XML composite type with no enumeration to maintain. The previously-explicit image/svg+xml startsWith check is removed because isXmlContentType now covers it (svg's content-type ends with +xml). The comment is updated so the reader sees that XML composite types and SVG all flow through this branch for the same reason — they're strings, not bytes, and the renderer parses them client-side. Tests in http-utils.test.ts cover the three new cases: plain application/xml, text/xml with a charset parameter, and application/atom+xml as a representative +xml composite type.
A new "XML" main-tab on NetworkTestScreen, modeled after the existing Images tab. Two buttons each fetch a known XML endpoint so the captured response exercises a different surface of the renderer: - "Sample XML" — https://httpbin.org/xml (200 OK, ~522 B application/xml slideshow). Exercises basic tree rendering, attributes, and nested elements. - "Atom feed" — https://github.com/callstackincubator/rozenite/ commits/main.atom (200 OK, application/atom+xml; charset=utf-8, ~52 KB). Exercises namespaces (xmlns, xmlns:media), multiple nested entry blocks, and CDATA-wrapped content fields — a representative real-world feed.
Minor bump for @rozenite/network-activity-plugin. The Response Viewing section in network-activity.mdx gains a new XML bullet enumerating the matched content-types (application/xml, text/xml, and RFC 7303 +xml composites including xhtml+xml) and describing the tree / Raw / malformed-fallback shape. The JSON bullet is extended to mention the new Raw view (pretty-printed with 2-space indent). The "Text and other source formats" bullet is tightened — the broad text/* glob is replaced with concrete examples and application/xml is dropped from the list because the xml renderer claims it now.
aa9cc98 to
9b76285
Compare
…mo copy The HTML response test's narrative still mentioned the status banner that was removed before callstackincubator#275 merged. The renderer hasn't shown a banner on error responses for a while now — the demo description referenced a feature that no longer exists. Updated the card body and the 404 hint to reflect what the demo actually exercises: the renderer handling an error response in addition to 2xx ones.
V3RON
pushed a commit
that referenced
this pull request
May 19, 2026
Part of #244 — "Improve response viewing in Network Activity." Makes the response panel's CodeBlock threshold-aware so multi-megabyte text bodies don't pin hundreds of thousands of DOM nodes. ## Summary - **`CodeBlock` becomes threshold-aware.** When `children` is a string longer than 50,000 characters, the body renders inside a `react-virtuoso` `<Virtuoso>` and only the rows currently visible inside a 500 px window stay in the DOM. Below the threshold (and for any non-string children — `<JsonTree>`, `<XmlTree>` wrapped in CodeBlock for the monospace-on-dark frame), rendering is unchanged: a flat `<pre>`. The dispatch is a runtime `typeof children === 'string'` branch — zero changes required at any of the 11+ existing CodeBlock call sites (json / xml / svg / text-fallback renderers, malformed-fallback paths, RequestBody). - **Virtuoso configuration.** Fixed `style={{ height: 500 }}` matches the visual scale of HexView and the iframe elsewhere in the response panel. `<div>` rows (Virtuoso positions rows absolutely and needs block-level items). Per-row `whitespace-pre-wrap wrap-anywhere` preserves today's wrapping UX so long URLs / base64 / minified-on-one-line content wrap visually instead of forcing horizontal scroll. Outer styling reuses the existing `codeBlockClassNames` constant so the dark bg / monospace / border / padding are identical between the two branches. - **Known shape limitation.** A 50 KB body with no newlines virtualizes to a single row containing the entire payload. Browser-level wrapping keeps the layout sane but there's no real virtualization benefit for that shape — the perf win shows up once the body has many lines. - **UX trade-off worth knowing.** Today's `<pre>` expands to content (height grows with body length). After this change, content above 50 KB scrolls inside a 500 px window. Deliberate, for snappy rendering on multi-megabyte responses. - **Adds `react-virtuoso ^4.6.0`** to `@rozenite/network-activity-plugin`'s `dependencies`. - **Playground.** A new "Large text" main-tab on `NetworkTestScreen` with two pre-verified URLs straddling the threshold so reviewers can see both branches: Alice in Wonderland from Gutenberg (~174 KB, virtualizes) and the GPL-3 license text (~35 KB, stays flat). ## Test plan - [ ] `pnpm --filter @rozenite/network-activity-plugin test` — all tests pass (8 new in `CodeBlock.test.tsx`). - [ ] `pnpm --filter @rozenite/network-activity-plugin typecheck` — clean. - [ ] `pnpm --filter @rozenite/network-activity-plugin lint` — clean. - [ ] `npx prettier --check` on changed files — clean. - [ ] Playground (`pnpm --filter @rozenite/playground ios` or `:android`) → Network Test → Large text tab: - [ ] **Alice in Wonderland (~174 KB)** — Response body scrolls inside a 500 px window. NOT an unbounded `<pre>`. Scrolling is smooth at the end of the ~3300-line file. Dark bg / monospace / border look identical to a small `<pre>`. - [ ] **GPL-3 license (~35 KB)** — Response body renders as a flat `<pre>`, height grows with content, no internal scrolling. (Control: confirms the threshold gating works.) - [ ] **JSON tree responses unchanged** — open any JSON response (HTTP Test → User-list). Renders as JsonTree wrapped in CodeBlock as before. No virtualization (children is a React element, not a string). - [ ] **SVG Raw view unchanged** — Images tab → SVG → toggle to Raw. Renders as flat `<pre>` with SVG source (SVG bodies are well under threshold). - [ ] **Toggle behavior unchanged** — sticky Preview/Raw preference, override button presence, content-type label all work as before. ## New test surface `components/__tests__/CodeBlock.test.tsx` (8 tests): - Small string content renders as `<pre>` with no virtuoso-mock present. - Exactly 50,000 chars stays on the flat path (inclusive-boundary semantics — the branch condition is `>`, not `>=`). - 50,001 chars switches to the virtualized rendering. - Content survives the threshold split (recognizable head and tail tokens both visible in the DOM after virtualization). - Newlines in virtualized content produce one row per line. - React-element children stay on the `<pre>` path even when the wrapped element contains a 50,001-char string internally (the `typeof children === 'string'` check takes precedence). - `className` forwarding on both the flat and virtualized branches. `vitest.setup.ts` adds a `vi.mock('react-virtuoso')` passthrough so jsdom-based tests can assert content correctness. jsdom can't drive Virtuoso's resize observation otherwise — without the stub any test importing a virtualizing component fails to render rows. Production code is unaffected. ## Followups (deliberately not in this PR) - Search inside virtualized content. - Truncation modes. - Virtualizing trees (`JsonTree`, `XmlTree`) — would require forking `react-json-tree` or rewriting from scratch. - Per-row line numbers in the virtualized branch. - Syntax highlighting (cross-cutting feature; would also benefit XML / JSON / HTML Raw views). - Programmatic line-wrapping for single-line content longer than the threshold (would require choosing a column width). ## Coordination notes - **`react-virtuoso` dep + `vi.mock('react-virtuoso')` test stub** are also added by #274 (binary hex viewer, ready for review). The lines are identical on both branches; whichever lands second will see a clean auto-merge (or a one-line trivial conflict on the dependencies block). - **`network-activity.mdx`** Response Viewing section is also edited by #275 (HTML) and #277 (XML + JSON Raw). This PR appends a separate paragraph at the end of the section rather than touching their bullets — additive, no overlap.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #244 — "Improve response viewing in Network Activity." Two complementary additions to the response renderer registry: a custom XML tree viewer for
application/xml/text/xml/*/foo+xmlresponses, and a Raw view alongside the existing JSON tree.Summary
xmlRenderermatches XML content-types via a newisXmlContentTypehelper —application/xml,text/xml, and any*/foo+xmlper RFC 7303. The structural suffix rule sweeps in Atom, RSS, SOAP, XHTML, XSLT, and any future composite without an enumeration to maintain. Case-insensitive and parameter-tolerant via the existingnormalizeContentTypehelper.image/svg+xmlalso matches the suffix butsvgRenderer's earlier registry position keeps SVG claiming it first.XmlTreecomponent — custom, ~160 lines, DOMParser-based. Renders Element nodes as<tag attr="value">with a chevron toggle, attributes inline, non-whitespace text nodes in-place, CDATA sections with explicit<![CDATA[ … ]]>markers (purple), and recursive children. Whitespace-only text nodes (DOM-pretty-print noise) are filtered. Namespaces render vianodeNameso the prefix is visible (atom:entry,media:thumbnail). Comments / processing instructions / DOCTYPE are intentionally not rendered in the tree — they're rare in API responses and the Raw view shows them verbatim.display: none, not unmount — each<XmlNode>owns its ownuseState<boolean>(true)for expand state, and toggling setsstyle.displayrather than conditionally rendering children. Nested collapse choices survive a parent collapse/expand cycle; matches DevTools' XML/HTML inspector behavior.JsonTreeCopyableItem. Element open tag copies the entire subtree viaXMLSerializer; text and CDATA leaves copy their literal value.DOMParserdoesn't throw on malformed XML; instead Chrome/Safari return a Document whosedocumentElementIS the<parsererror>element, while Firefox nests it in a dedicated namespace. ThehasParseErrorhelper covers both paths in three lines. On parse failure the renderer falls back to source + "Failed to parse as XML" warning, mirroringjsonRenderer'scatchbranch.jsonRendererextendsviewsto['preview', 'raw']. Preview keeps the existing tree; Raw shows the body pretty-printed viaJSON.stringify(parsed, null, 2). APIs commonly ship minified JSON, and a literal Raw view of minified JSON is unreadable; re-serializing with 2-space indent matches Chrome DevTools' Response panel convention. The Preview/Raw toggle is now visible on every JSON response for the first time.getResponseBodyinhttp/http-utils.tsonly routed text-shaped blobs to the text branch fortext/*, JSON, andimage/svg+xml.application/xmland+xmlcomposite types fell through tonulland never crossed the bridge. AddedisXmlContentTypeto the text branch; the now-subsumed explicitimage/svg+xmlcheck was removed sinceisXmlContentTypecovers it via the+xmlsuffix.NetworkTestScreenwith two demos:httpbin.org/xml(sampleapplication/xml, ~522 B) andgithub.com/callstackincubator/rozenite/commits/main.atom(Atom feed with namespaces, nested entries, CDATA in<content>).Test plan
pnpm --filter @rozenite/network-activity-plugin test— all tests pass (29 new across the new test files + dispatch / content-type additions).pnpm --filter @rozenite/network-activity-plugin typecheck— clean.pnpm --filter @rozenite/network-activity-plugin lint— clean.npx prettier --checkon changed files — clean.pnpm --filter @rozenite/playground iosor:android) → Network Test → XML tab:<slideshow>tree (title, date, author attributes visible in amber/green; nested<slide>elements with chevrons). Toggle to Raw — CodeBlock with the XML source. Toggle back — tree.<feed>), multiple<entry>blocks, CDATA-wrapped<content>rendered with purple<![CDATA[ … ]]>markers. Copy on an<entry>header (hover) serializes the whole subtree.display: nonerather than unmount).New test surface
utils/__tests__/getContentTypeMimeType.test.ts(+4 cases) —isXmlContentTypepositives (plain, parameterized, RFC 7303 composites including Atom, RSS, SOAP, XHTML, SVG-via-suffix), case-insensitive matching, and negatives (text/html, JSON,application/xmlfoo, null/undefined/empty).components/__tests__/XmlTree.test.tsx(10 tests) — tag/attr/text rendering, CDATA marker wrapping, whitespace text filtering, recursive nesting,display: nonecollapse mechanism (asserted at DOM level), chevronaria-labeltoggle, self-closing form, closing-tag rendering, namespace-prefix preservation (media:thumbnail).response-renderers/__tests__/xml.test.tsx(6 tests) — renderer attributes, Preview tree vs Raw source, malformed XML fallback for both Chrome-style (parsererror as documentElement) and Firefox-style (parsererror in the FF namespace),application/xhtml+xmlrendering as a tree.response-renderers/__tests__/json.test.tsx(new file, 7 tests) — renderer attributes (this renderer previously had no test file of its own — only dispatcher coverage), Preview tree contains parsed values not source, Raw produces multi-line output with exact 2-space indent at top and 4-space at nested levels and round-trips throughJSON.parse, malformed JSON falls back in both Preview and Raw views.response-renderers/__tests__/dispatch.test.ts(+2 it blocks) — XML routing matrix (application/xml,text/xml, atom+xml, rss+xml, soap+xml, xhtml+xml),image/svg+xml-still-wins regression guard, and order invariantssvg < xmlandxml < text-fallback.react-native/http/__tests__/http-utils.test.ts(+3 cases) — capture path returns the body verbatim forapplication/xml,text/xml; charset=utf-8, andapplication/atom+xmlblobs.Followups (deliberately not in this PR)
JsonTreeandXmlTreetogether in a separate PR so the trees stay consistent.Coordination note
A small mechanical mdx conflict is possible with #275 (HTML renderer, currently draft) if that one lands second — both PRs edit the "Response Viewing" bullet list in
website/src/docs/official-plugins/network-activity.mdx. Additive bullets only; trivial rebase.