Files
beardrive/internal/webapp/markdown_test.go
T
4f04b78c71 feat(webapp): frontmatter moves to a collapsible side panel (BEA-154) (#187)
A doc's YAML frontmatter rendered as a table pinned to the top of the
reading column, so on anything with more than two or three keys the
document itself started below the fold. It is a panel beside the prose
now — a sticky rail on a wide window, a closed disclosure above the body
on anything narrower — and the reading column starts with the document.

The table was built on the server and handed to the client inside one HTML
string, so this is not a CSS change: markdown.go splits the parse
(frontmatterPairs) from the markup, /api/render gains an ordered
`frontmatter` field, and the viewer switches to RenderMarkdownPairs.

RenderMarkdown keeps its exact output — it is the public share page, and
every /s/ link ever minted serves it. shares_test now pins that, because
nothing else would have failed if a later cleanup moved shares.go onto the
pairs path.

Values cross the wire as literal text plus a `code` flag rather than
pre-escaped HTML, so the panel is ordinary React text nodes and never
touches dangerouslySetInnerHTML: "a value containing markup renders as
text" holds by construction.

The rail's breakpoint is 1400px, not the 1180px the plan named — 768 of
prose + 28 + 240 of rail needs 1036px of column, and at 1280 the reading
measure lost 110px, which is the squeeze the panel exists to avoid.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 12:55:29 -07:00

156 lines
4.6 KiB
Go

package webapp
import (
"strings"
"testing"
)
// A leading YAML frontmatter block renders as a key/value table (author
// key order, escaped values) instead of goldmark's thematic-break soup;
// everything that isn't a clean frontmatter mapping renders exactly as
// before.
func TestRenderMarkdownFrontmatter(t *testing.T) {
src := `---
title: Q3 findings
tags: [churn, revenue]
owner: snow@runbear.io
meta:
reviewed: true
---
# Body
Hello.`
out, err := RenderMarkdown([]byte(src))
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
`<table class="frontmatter">`,
`<th scope="row">title</th><td>Q3 findings</td>`,
`<td>churn, revenue</td>`, // flat lists comma-join
`owner`, `snow@runbear.io`,
`<code>reviewed: true</code>`, // nested values as compact YAML
`<h1 id="body">Body</h1>`, // the body still renders
} {
if !strings.Contains(out, want) {
t.Errorf("missing %q in:\n%s", want, out)
}
}
if strings.Contains(out, "<hr") {
t.Errorf("frontmatter fences leaked as thematic breaks:\n%s", out)
}
// Key order preserved: title row precedes owner row.
if strings.Index(out, ">title<") > strings.Index(out, ">owner<") {
t.Errorf("frontmatter keys reordered:\n%s", out)
}
}
func TestRenderMarkdownFrontmatterEscapes(t *testing.T) {
out, err := RenderMarkdown([]byte("---\nnote: <script>alert(1)</script>\n---\nx"))
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "<script>") {
t.Fatalf("frontmatter value not escaped:\n%s", out)
}
if !strings.Contains(out, "&lt;script&gt;") {
t.Fatalf("escaped value missing:\n%s", out)
}
}
func TestRenderMarkdownFrontmatterFallthrough(t *testing.T) {
cases := map[string]struct {
src string
wantTable bool
want string
}{
"no frontmatter": {"# Hi\n\ntext", false, "<h1"},
"mid-doc fences": {"para\n\n---\n\nmore", false, "<hr"},
"unclosed fence": {"---\ntitle: x\n\nbody", false, ""},
"non-mapping yaml": {"---\n- just\n- a list\n---\nbody", false, ""},
"invalid yaml": {"---\n: : :\n---\nbody", false, ""},
"empty frontmatter hidden": {"---\n---\nbody", false, "<p>body</p>"},
}
for name, c := range cases {
out, err := RenderMarkdown([]byte(c.src))
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if got := strings.Contains(out, `class="frontmatter"`); got != c.wantTable {
t.Errorf("%s: table presence = %v, want %v\n%s", name, got, c.wantTable, out)
}
if c.want != "" && !strings.Contains(out, c.want) {
t.Errorf("%s: missing %q in:\n%s", name, c.want, out)
}
}
}
// The viewer's split: frontmatter comes back as ordered data and the HTML
// is body-only, with the same value rules the table has always applied.
func TestFrontmatterPairs(t *testing.T) {
src := `---
title: Q3 findings
tags: [churn, revenue]
owner: snow@runbear.io
meta:
reviewed: true
---
# Body
Hello.`
pairs, out, err := RenderMarkdownPairs([]byte(src))
if err != nil {
t.Fatal(err)
}
want := []FrontmatterPair{
{Key: "title", Value: "Q3 findings"},
{Key: "tags", Value: "churn, revenue"}, // flat lists comma-join
{Key: "owner", Value: "snow@runbear.io"},
{Key: "meta", Value: "reviewed: true", Code: true}, // nested: compact YAML
}
if len(pairs) != len(want) {
t.Fatalf("pairs = %+v, want %d", pairs, len(want))
}
for i, w := range want {
if pairs[i] != w { // index-wise: author order is the contract
t.Errorf("pair %d = %+v, want %+v", i, pairs[i], w)
}
}
if strings.Contains(out, `class="frontmatter"`) {
t.Errorf("table leaked into the viewer's html:\n%s", out)
}
if !strings.HasPrefix(strings.TrimSpace(out), `<h1 id="body">Body</h1>`) {
t.Errorf("body does not start with its heading:\n%s", out)
}
}
// Same tri-state as the table: anything that isn't a clean YAML mapping
// falls through with the source untouched, and empty frontmatter is hidden.
func TestFrontmatterPairsFallthrough(t *testing.T) {
cases := map[string]struct {
src string
want string
}{
"no frontmatter": {"# Hi\n\ntext", "<h1"},
"mid-doc fences": {"para\n\n---\n\nmore", "<hr"},
"unclosed fence": {"---\ntitle: x\n\nbody", ""},
"non-mapping yaml": {"---\n- just\n- a list\n---\nbody", ""},
"invalid yaml": {"---\n: : :\n---\nbody", ""},
"empty frontmatter hidden": {"---\n---\nbody", "<p>body</p>"},
}
for name, c := range cases {
pairs, out, err := RenderMarkdownPairs([]byte(c.src))
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if len(pairs) != 0 {
t.Errorf("%s: pairs = %+v, want none", name, pairs)
}
if c.want != "" && !strings.Contains(out, c.want) {
t.Errorf("%s: missing %q in:\n%s", name, c.want, out)
}
}
}