Files
projax/web/markdown_test.go
mAi 2152c2d68e feat(web): Phase 8 Direction A — read-first detail page (markdown + edit mode)
Implements norman's approved Direction A (docs/plans/phase-8-detail-ux.md): a
projax item is a page you READ, with editing as a deliberate mode.

- READ MODE (default): content_md renders as MARKDOWN via goldmark (pure-Go,
  no cgo; SAFE-by-default — raw HTML omitted, no separate sanitizer needed;
  GFM for tables/strikethrough/autolinks). A compact chip header (status /
  management / tags / pinned / archived) + a single Edit toggle. Kills D1 (no
  read mode) and D7 (title/status rendered twice).
- EDIT MODE (?edit=1): the identity+content form (progressive-enhancement
  baseline — full-page toggle, no JS). Save → POST → back to read; Cancel
  returns to read. Rare settings (Public listing / Timeline behaviour) stay
  nested <details> disclosures inside the form (B's accordion discipline).
- WORK CARDS: Tasks (the Phase 7c unified list, dropped in UNCHANGED), Issues,
  Documents render as always-visible cards below the description, in BOTH modes
  (they were always HTMX-independent of the form). One visible card-title each;
  the in-partial h2 stays visually-hidden (Slice 0); #…-section ids + outerHTML
  swap targets preserved.
- Retired under A (Q6): the top-level proj-section collapse model + its
  localStorage persistence script + 'reset section state' link + the
  aux-divider/'Related' framing. Kept the secondary sub-disclosures (done
  tasks / closed issues) inside the partials.
- CSS: new .detail (760px reading column), .detail-card, .card-title,
  .markdown-body, .btn/.edit-toggle — all within the existing design tokens,
  no foreign system. Phone lands on description→tasks (settings behind Edit).

Tests: TestRenderMarkdown{,Empty,Safe,GFM}; rewrote the detail tests for the
read/edit split (TestDetailReadModeRendersMarkdown, TestDetailEditFieldsRender
InOrder, TestDetailWorkCardsAfterForm, TestDetailReadModeIsNotCollapsibles,
TestDetailEditModeKeepsSettingsDisclosures, updated TestDetailNoDoubleHeader +
public-listing). Only pre-existing failures remain (TestParityListAll,
TestProjectFilter*/TestTimeline* — route-drift on 6436b52).
2026-06-02 12:31:53 +02:00

46 lines
1.5 KiB
Go

package web
import (
"strings"
"testing"
)
func TestRenderMarkdown(t *testing.T) {
got := string(renderMarkdown("## Architecture\n\n1. Model first\n2. Interfaces second"))
if !strings.Contains(got, "<h2") || !strings.Contains(got, "Architecture</h2>") {
t.Errorf("expected ## to render as <h2>, got: %s", got)
}
if !strings.Contains(got, "<ol>") || !strings.Contains(got, "<li>Model first") {
t.Errorf("expected an ordered list, got: %s", got)
}
}
func TestRenderMarkdownEmpty(t *testing.T) {
if renderMarkdown("") != "" {
t.Errorf("empty input should render empty")
}
if renderMarkdown(" \n ") != "" {
t.Errorf("whitespace-only input should render empty")
}
}
// TestRenderMarkdownSafe proves goldmark's default omits raw HTML — a <script>
// in content_md must NOT survive into the rendered output (XSS guard).
func TestRenderMarkdownSafe(t *testing.T) {
got := string(renderMarkdown("ok\n\n<script>alert(1)</script>\n\n<img src=x onerror=alert(1)>"))
if strings.Contains(got, "<script>") {
t.Errorf("raw <script> must not pass through goldmark safe render, got: %s", got)
}
if strings.Contains(got, "onerror=") {
t.Errorf("raw event-handler HTML must not pass through, got: %s", got)
}
}
// TestRenderMarkdownGFM confirms the GFM extension renders tables + autolinks.
func TestRenderMarkdownGFM(t *testing.T) {
got := string(renderMarkdown("| a | b |\n|---|---|\n| 1 | 2 |"))
if !strings.Contains(got, "<table>") {
t.Errorf("expected GFM table render, got: %s", got)
}
}