Skip to content

test: pin the render protocol between Rust and the preview - #378

Merged
PathGao merged 1 commit into
sftwrdotdev:masterfrom
PathGao:test/render-protocol-fixture
Aug 2, 2026
Merged

test: pin the render protocol between Rust and the preview#378
PathGao merged 1 commit into
sftwrdotdev:masterfrom
PathGao:test/render-protocol-fixture

Conversation

@PathGao

@PathGao PathGao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

The Rust renderer's output is a behavioural contract the frontend depends on. Four parts of it have regressed before:

  • task markers and the source positions clicks resolve through
  • the raw text kept inside $$…$$ so KaTeX still sees its own source
  • heading ids, and the fold anchors keyed to them
  • the difference between a Markdown task item and a hand-written HTML checkbox

markpad提升.md lists this fixture as the first item of the structural queue and as the safety net a later PreviewRenderer extraction would need. This is that net; it changes no implementation code.

Why not more source-regex tests

Most of scripts/ matches source text with regular expressions. That fails on rewrites which change nothing and stays green on behaviour changes that break users. Today alone: replacing lock().unwrap() with a recovering helper, and moving a localStorage.setItem into a persistence table, each turned an assertion red without altering behaviour.

These run processMarkdownHtml for real against 29 documents whose HTML is genuine convert_markdown output, then re-parse the result and query it structurally — so nothing depends on the harness's own serialisation conventions.

The two halves, and their honest status

Frontend: genuinely executed. No DOM shim precedent existed in scripts/ and no DOM library is in node_modules (adding one touches package.json and the lockfile, and node_modules is shared). So the harness ships a deliberately narrow parser, serializer, selector engine and TreeWalker. It is validated two ways: eight self-tests including parse→serialize→parse stability over all 29 fixtures, and a differential run against Python's html.parser, which yields identical trees for every fixture. That validates the tokenizer and tree builder — not browser DOM semantics, which only the targeted self-tests cover. The file is documented as "must not grow into a general parser".

Renderer HTML: captured, not called. convert_markdown is a private fn; src-tauri/tests/ cannot reach it. The HTML was not hand-written — it came from a throwaway crate carrying comrak 0.18, verbatim copies of protect_display_math_underscores, annotate_task_checkboxes, both task regexps and the exact ComrakOptions, validated by reproducing byte-for-byte the HTML literals that lib.rs's own passing tests assert.

So these fixtures are inputs. If comrak or convert_markdown drifts, nothing here goes red until someone regenerates them. Closing that gap needs the assertions to live in lib.rs's mod tests, where several already do. The fixture file header states this and how to regenerate.

Coverage

Protocol Notable cases
Task marker + sourcepos nested, > - [ ], 1. [ ] and 1) [ ], */+, prose either side, CRLF, loose lists, multi-line continuation. Expected lines are derived from the Markdown by the TASK_SOURCE_RE rule, then compared with what the viewer's own read (closest('li')/^(\d+):/) produces
Display math braced and plain subscripts, own-line blocks, escaped \_, inside a list item, two blocks on one line, math beside real emphasis. Asserts data-math-source equals the Markdown between delimiters, underscore count preserved, zero <em>, and that emphasis outside math still works
Heading id + fold anchor id promoted off a.anchor, no duplicate ids, chevron ↔ wrapper pairing, nesting. The sharpest: three identical # Titles with collapsedHeaders = {'title-1'} — only the second collapses, which fails outright if #371's id keying regresses
Raw HTML checkbox including - <input type="checkbox" disabled="" />, byte-identical to a real task item and separated only by source line; a raw <ul> whose li has no data-sourcepos at all; a checkbox inside a <p>

Failability, checked rather than argued

Sixteen mutations — frontend and simulated renderer drift — each turn the suite red: leaving checkboxes disabled, dropping id promotion, keying folds by text, dropping data-math-source, treating raw checkboxes as tasks, li losing data-sourcepos, 1.-概述 anchorization, <em> inside $$, duplicate headings sharing an id, an escaped underscore being eaten, and so on. Three initially came out green; all three were bugs in the mutation script (first-occurrence-only replace, or mutating markdown and HTML together) and went red once corrected.

Not covered

  • documentSession.toggleTaskCheckbox is not executed — the regex that rewrites the source line lives in a closure inside a runes module that cannot be imported under tsx. The tests stop at "a click resolves to line N"; that the rewrite of line N is correct is not covered. Prerequisite: extract that rewrite into a plain exported function.
  • MarkdownViewer.svelte's handlers are not executed either. Its data-sourcepos parse rule is restated here as the documented contract, so a change there will not red this suite.
  • The DOM shim is not validated against a real browser — localhost and file:// are both blocked by the browser pane in this environment.

Validation

  • npm test — 234/234 (29 new)
  • npm run check — 0 errors, 0 warnings
  • git status — only the four new files under scripts/

Worth knowing: scripts/ sits outside svelte-check's tsconfig include, as the whole existing suite does, so these files are not type-checked by CI.

The Rust renderer's output is a behavioural contract the frontend depends
on: task markers and their source positions, the raw text kept inside
display math, heading ids and the fold anchors keyed to them, and the
distinction between a Markdown task and a hand-written HTML checkbox. All
four have regressed before.

The existing suite mostly matches source text with regular expressions,
which fails on rewrites that change nothing and stays green on behaviour
changes that break users. Today alone, replacing `lock().unwrap()` with a
helper and moving a `localStorage.setItem` into a persistence table each
turned an assertion red without altering behaviour.

These tests run `processMarkdownHtml` for real against 29 documents whose
HTML is genuine `convert_markdown` output, then query the result
structurally rather than by string. Node has no DOM and adding one would
touch the lockfile, so the harness ships a deliberately narrow parser,
serializer, selector engine and TreeWalker — validated by self-tests and by
a differential run against Python's `html.parser`, which produces identical
trees for all 29 fixtures.

The renderer HTML is captured, not called: `convert_markdown` is private and
unreachable from an integration test. It was generated by a throwaway crate
carrying verbatim copies of the pre-passes and the exact comrak options, and
validated by reproducing byte-for-byte the HTML literals that lib.rs's own
tests already assert. So these fixtures are inputs — they cannot detect Rust
drift on their own, and the file header says so along with how to regenerate
them.

Failability was checked empirically, not argued: sixteen mutations of the
frontend and of simulated renderer drift each turn the suite red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao
PathGao force-pushed the test/render-protocol-fixture branch from a62d47a to 136b836 Compare August 2, 2026 19:58
@PathGao
PathGao merged commit bdbcf67 into sftwrdotdev:master Aug 2, 2026
4 checks passed
@PathGao
PathGao deleted the test/render-protocol-fixture branch August 2, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant