package web
import (
"fmt"
"strings"
"testing"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
"sourcecraft.dev/bigbes/sr-ht-spec/service"
)
// These tests are about the markup: the classes and attributes the stylesheet
// and the selection script are pinned to, and the escaping discipline. What a
// row *says* — which number lands in which track — is diffrows_test.go's, on
// the model rather than through a layer of angle brackets.
// render is renderDocDiff for a document nobody has commented on, which is what
// the presentation tests are about.
func render(oldSrc, newSrc []byte) diffView {
return renderDocDiff(docDiff{DocID: "SPEC-0007", Path: "specs/0007-storage.md", Base: oldSrc, Proposed: newSrc})
}
// blockIDs pulls the rendered block ids out of a diff, in document order, so a
// test can compare two renders without depending on the digest itself.
func blockIDs(html string) []string {
var ids []string
for _, rest := range strings.Split(html, ` id="b-`)[1:] {
ids = append(ids, "b-"+rest[:strings.IndexByte(rest, '"')])
}
return ids
}
// TestRenderDocDiffUnchanged proves a proposal that does not change a document
// is reported as unchanged rather than as an empty diff.
func TestRenderDocDiffUnchanged(t *testing.T) {
src := []byte("# Title\n\nOne paragraph.\n")
v := render(src, src)
if !v.Unchanged {
t.Fatalf("Unchanged = false, want true for identical input")
}
if v.HTML != "" {
t.Errorf("HTML = %q, want empty for an unchanged document", v.HTML)
}
}
// TestRenderDocDiffInlineWordChange proves a small edit renders inline with
// <del>/<ins> marks and not as two columns.
func TestRenderDocDiffInlineWordChange(t *testing.T) {
old := []byte("# Title\n\nThe quick brown fox jumps over the lazy dog.\n")
nw := []byte("# Title\n\nThe quick red fox jumps over the lazy dog.\n")
v := render(old, nw)
if v.Unchanged {
t.Fatalf("Unchanged = true, want a change")
}
html := string(v.HTML)
if !strings.Contains(html, "<del>brown</del>") {
t.Errorf("missing inline deletion of 'brown'; got:\n%s", html)
}
if !strings.Contains(html, "<ins>red</ins>") {
t.Errorf("missing inline insertion of 'red'; got:\n%s", html)
}
if strings.Contains(html, "ph-region") {
t.Errorf("a one-word edit fell back to a line range; got:\n%s", html)
}
}
// TestRenderDocDiffRegionFallbackBelowThreshold proves a block rewritten enough
// to fall below the similarity threshold renders as the paired old/new region
// rows — the Phase 0 verdict's hard requirement, and the one place the gutter
// states a range instead of a line number.
func TestRenderDocDiffRegionFallbackBelowThreshold(t *testing.T) {
// A block rewritten to ~0.58 similarity: paired as a modify (above the 0.40
// pairing floor) but shredded enough to fall below the 0.75 inline switch.
old := []byte("# Title\n\nThe committee approved the annual budget after a long and " +
"contentious debate that lasted well into the evening.\n")
nw := []byte("# Title\n\nThe committee rejected the annual budget after a brief and " +
"quiet discussion that ended early in the afternoon.\n")
v := render(old, nw)
html := string(v.HTML)
if !strings.Contains(html, `ph-r-del ph-blk-start ph-region"`) ||
!strings.Contains(html, `ph-r-ins ph-region"`) {
t.Fatalf("a wholesale rewrite did not render as a region pair; got:\n%s", html)
}
// The old side keeps its deletions and the new side its insertions, so each
// row is a paragraph a reviewer can read straight through.
if !strings.Contains(html, "<del>approved</del>") || !strings.Contains(html, "<ins>rejected</ins>") {
t.Errorf("the region rows lost their word marks; got:\n%s", html)
}
}
// TestRenderDocDiffEscapesContent proves document content is HTML-escaped: a
// document that contains markup cannot inject it into the review page.
func TestRenderDocDiffEscapesContent(t *testing.T) {
old := []byte("# Title\n\nplain text here.\n")
nw := []byte("# Title\n\nplain <script>alert(1)</script> text here.\n")
v := render(old, nw)
html := string(v.HTML)
if strings.Contains(html, "<script>") {
t.Fatalf("unescaped <script> reached the output; got:\n%s", html)
}
if !strings.Contains(html, "<script>") {
t.Errorf("expected the escaped script tag in the output; got:\n%s", html)
}
}
// TestRenderDocDiffEscapesUnchangedContent proves the context rows the review
// now renders are escaped too, not only the changed ones.
func TestRenderDocDiffEscapesUnchangedContent(t *testing.T) {
const untouched = "A <b>bold</b> claim nobody edited.\n"
old := []byte("# Title\n\n" + untouched + "\nfirst.\n")
nw := []byte("# Title\n\n" + untouched + "\nsecond.\n")
html := string(render(old, nw).HTML)
if strings.Contains(html, "<b>bold</b>") {
t.Fatalf("unescaped markup from an unchanged block reached the output; got:\n%s", html)
}
if want := strings.Count(html, "<b>bold</b>"); want != 1 {
t.Errorf("escaped markup appears %d times, want 1 (the context row); got:\n%s", want, html)
}
}
// TestRenderDocDiffInsertAndDelete proves an added and a removed line are each
// marked as such, in the class and in the sign column.
func TestRenderDocDiffInsertAndDelete(t *testing.T) {
old := []byte("# Title\n\nKept paragraph.\n\nDoomed paragraph.\n")
nw := []byte("# Title\n\nKept paragraph.\n\nBrand new paragraph.\n")
v := render(old, nw)
html := string(v.HTML)
if !strings.Contains(html, "ph-r-ins") {
t.Errorf("missing an inserted line; got:\n%s", html)
}
if !strings.Contains(html, "ph-r-del") {
t.Errorf("missing a deleted line; got:\n%s", html)
}
if !strings.Contains(html, `<td class="ph-sign">+</td>`) ||
!strings.Contains(html, `<td class="ph-sign">-</td>`) {
t.Errorf("the sign column does not say what happened; got:\n%s", html)
}
}
// TestUnchangedBlocksRenderAsSubordinateContext proves an unchanged block is on
// the page — every block of a proposed document must be commentable — but as
// context: no tint class of its own, no sign, so a changed row still reads as
// the thing that changed.
func TestUnchangedBlocksRenderAsSubordinateContext(t *testing.T) {
old := []byte("# Title\n\nUntouched paragraph.\n\nOld wording here.\n")
nw := []byte("# Title\n\nUntouched paragraph.\n\nNew wording here.\n")
html := string(render(old, nw).HTML)
if !strings.Contains(html, "Untouched paragraph.") {
t.Fatalf("the unchanged block is not on the page; got:\n%s", html)
}
if !strings.Contains(html, `class="ph-row ph-r-eq ph-blk-start" id="b-`) {
t.Errorf("the unchanged block is not rendered as a context row; got:\n%s", html)
}
// The changed block must stay plainly changed: its rows marked, its word
// marks intact, and both sides of the edit numbered from their own revision.
if !strings.Contains(html, "ph-r-del") || !strings.Contains(html, "ph-r-ins") {
t.Errorf("the changed block lost its own presentation; got:\n%s", html)
}
if !strings.Contains(html, "<ins>New</ins>") || !strings.Contains(html, "<del>Old</del>") {
t.Errorf("the changed block lost its inline marks; got:\n%s", html)
}
}
// TestLongContextRunCollapses proves the other half of "subordinate": a screen
// of unchanged lines folds away behind a checkbox, and the fold is a real form
// control so it opens with JavaScript off.
func TestLongContextRunCollapses(t *testing.T) {
var src strings.Builder
src.WriteString("# Title\n")
for i := 1; i <= 12; i++ {
fmt.Fprintf(&src, "\nContext paragraph %d.\n", i)
}
html := string(render([]byte(src.String()+"\nOld tail.\n"), []byte(src.String()+"\nNew tail.\n")).HTML)
if !strings.Contains(html, `<tbody class="ph-fold">`) {
t.Fatalf("a run of unchanged lines did not fold; got:\n%s", html)
}
if !strings.Contains(html, `<input type="checkbox" class="ph-fold-cb" id="fold-`) {
t.Errorf("the fold is not a checkbox, so it needs JavaScript to open; got:\n%s", html)
}
if !strings.Contains(html, "ph-folded") {
t.Errorf("the fold hides nothing; got:\n%s", html)
}
// Hidden is not absent: the text of a folded row is still in the document,
// so browser search and a text-mode reader still find it.
if !strings.Contains(html, "Context paragraph 6.") {
t.Errorf("a folded line was dropped from the page rather than hidden; got:\n%s", html)
}
}
// TestEverythingInAFoldIsHidden proves a collapsed run collapses whole. The
// owner gets a compose form on every block, context ones included, and a
// comment control drawn for lines the fold has just hidden is a control on
// nothing — on the page it appears as an orphan row under the fold's own
// toggle.
func TestEverythingInAFoldIsHidden(t *testing.T) {
var src strings.Builder
src.WriteString("# Title\n")
for i := 1; i <= 12; i++ {
fmt.Fprintf(&src, "\nContext paragraph %d.\n", i)
}
view := renderDocDiff(docDiff{
DocID: "SPEC-0007", Path: "specs/0007-storage.md",
Base: []byte(src.String() + "\nOld tail.\n"),
Proposed: []byte(src.String() + "\nNew tail.\n"),
Controls: reviewControls{Owner: true, Reply: true, ActionBase: "/s/p/1"},
})
html := string(view.HTML)
open := strings.Index(html, `<tbody class="ph-fold">`)
if open < 0 {
t.Fatalf("no fold in a diff with twelve unchanged paragraphs; got:\n%s", html)
}
fold := html[open : open+strings.Index(html[open:], "</tbody>")]
for _, piece := range strings.Split(fold, "<tr")[1:] {
row := "<tr" + piece[:strings.IndexByte(piece, '>')]
if strings.Contains(row, "ph-fold-head") {
continue // the toggle itself is the one row a fold shows
}
if !strings.Contains(row, "ph-folded") {
t.Errorf("a row inside the fold is not hidden by it: %s", row)
}
}
if !strings.Contains(fold, `class="ph-notes ph-folded"`) {
t.Errorf("no folded compose row in this fold, so the test proves nothing; got:\n%s", fold)
}
}
// TestFoldIDsAreScopedToTheirDocument proves two documents on one review page
// cannot toggle each other's folds. The ids are per-document by construction;
// this is what says so.
func TestFoldIDsAreScopedToTheirDocument(t *testing.T) {
var src strings.Builder
src.WriteString("# Title\n")
for i := 1; i <= 12; i++ {
fmt.Fprintf(&src, "\nContext paragraph %d.\n", i)
}
base, proposed := []byte(src.String()+"\nOld tail.\n"), []byte(src.String()+"\nNew tail.\n")
one := renderDocDiff(docDiff{DocID: "A", Path: "specs/a.md", Base: base, Proposed: proposed})
two := renderDocDiff(docDiff{DocID: "B", Path: "specs/b.md", Base: base, Proposed: proposed})
ids := func(html string) []string {
var out []string
for _, rest := range strings.Split(html, ` id="fold-`)[1:] {
out = append(out, rest[:strings.IndexByte(rest, '"')])
}
return out
}
a, b := ids(string(one.HTML)), ids(string(two.HTML))
if len(a) == 0 || len(b) == 0 {
t.Fatalf("expected a fold in each document, got %v and %v", a, b)
}
for _, x := range a {
for _, y := range b {
if x == y {
t.Fatalf("two documents share the fold id %q", x)
}
}
}
}
// TestEveryRenderedBlockCarriesAnID proves a comment has something to point at
// and a link has something to scroll to, on changed and unchanged blocks alike.
func TestEveryRenderedBlockCarriesAnID(t *testing.T) {
old := []byte("# Title\n\nUntouched paragraph.\n\nOld wording here.\n")
nw := []byte("# Title\n\nUntouched paragraph.\n\nNew wording here.\n")
html := string(render(old, nw).HTML)
ids := blockIDs(html)
// heading, untouched paragraph, modified paragraph.
if len(ids) != 3 {
t.Fatalf("rendered %d block ids, want 3; got:\n%s", len(ids), html)
}
seen := map[string]bool{}
for _, id := range ids {
if seen[id] {
t.Errorf("duplicate block id %q; got:\n%s", id, html)
}
seen[id] = true
}
}
// TestBlockIDIsStableAcrossARerender proves the id is a function of the block,
// not of anything the renderer accumulates as it goes.
func TestBlockIDIsStableAcrossARerender(t *testing.T) {
old := []byte("# Title\n\nUntouched paragraph.\n\nOld wording here.\n")
nw := []byte("# Title\n\nUntouched paragraph.\n\nNew wording here.\n")
first := blockIDs(string(render(old, nw).HTML))
second := blockIDs(string(render(old, nw).HTML))
if len(first) == 0 || !equalStrings(first, second) {
t.Fatalf("ids changed across a re-render:\n%v\n%v", first, second)
}
}
// TestBlockIDSurvivesAnUnrelatedEdit proves an id names its own block: editing
// a different section of the document leaves it alone, so a link written down
// yesterday still lands on the paragraph it was written for.
func TestBlockIDSurvivesAnUnrelatedEdit(t *testing.T) {
const base = "# Storage\n\nThe block under test.\n\n## Trade-offs\n\nSomething else entirely.\n"
const first = base + "\nA tail paragraph.\n"
const second = "# Storage\n\nThe block under test.\n\n## Trade-offs\n\nSomething else, rewritten.\n\nA tail paragraph.\n"
before := blockIDs(string(render([]byte(base), []byte(first)).HTML))
after := blockIDs(string(render([]byte(base), []byte(second)).HTML))
if len(before) < 2 || len(after) < 2 {
t.Fatalf("expected several blocks, got %d and %d", len(before), len(after))
}
// Index 1 is "The block under test." in both renders; the edit is two blocks
// further down, under a different heading.
if before[1] != after[1] {
t.Errorf("an edit elsewhere in the document changed a block's id: %q vs %q", before[1], after[1])
}
}
// TestThreadRendersOnItsOwnBlock proves an anchored comment is drawn against
// the block it anchors to and nowhere else.
func TestThreadRendersOnItsOwnBlock(t *testing.T) {
const src = "# Storage\n\nThe commented paragraph.\n\nAn innocent bystander.\n"
anchor, err := service.AnchorOf("SPEC-0007", []byte(src), 1, core.SideNew)
if err != nil {
t.Fatalf("AnchorOf: %v", err)
}
view := renderDocDiff(docDiff{
DocID: "SPEC-0007", Path: "specs/a.md",
Base: []byte("# Storage\n\nThe commented paragraph.\n"),
Proposed: []byte(src),
Threads: []service.Thread{{
Root: service.Comment{ID: 4, Author: "bigbes", Body: "this is wrong"},
Anchor: anchor, State: core.AnchorExact, Block: 1,
}},
})
html := string(view.HTML)
if len(view.Unplaced) != 0 {
t.Fatalf("an anchored thread came back unplaced: %+v", view.Unplaced)
}
// The comment must sit inside the block it anchors to: the bystander block
// opens after it, so the comment body has to appear before that boundary.
comment := strings.Index(html, "this is wrong")
bystander := strings.Index(html, "An innocent bystander")
if comment < 0 {
t.Fatalf("the comment is not on the page; got:\n%s", html)
}
if bystander > 0 && comment > bystander {
t.Errorf("the comment rendered after the following block, not on its own; got:\n%s", html)
}
}
// TestOutdatedThreadIsHandedBackNotAttached proves a comment whose anchor is
// lost is never drawn against some other block. It comes back for the page to
// show in its own area — visible, and attached to nothing.
func TestOutdatedThreadIsHandedBackNotAttached(t *testing.T) {
view := renderDocDiff(docDiff{
DocID: "SPEC-0007", Path: "specs/a.md",
Base: []byte("# Storage\n\nOne paragraph.\n"),
Proposed: []byte("# Storage\n\nOne paragraph, edited.\n"),
Threads: []service.Thread{{
Root: service.Comment{ID: 9, Author: "bigbes", Body: "orphaned critique"},
State: core.AnchorOutdated, Block: -1,
}},
})
if len(view.Unplaced) != 1 || view.Unplaced[0].Root.ID != 9 {
t.Fatalf("Unplaced = %+v, want the outdated thread", view.Unplaced)
}
if strings.Contains(string(view.HTML), "orphaned critique") {
t.Errorf("an outdated comment was rendered against a block; got:\n%s", view.HTML)
}
}
// TestThreadOnAnUnrenderedBlockIsHandedBack proves the invariant the renderer
// is built on: a thread comes out either on its block or in Unplaced, never
// neither. An old-side anchor that still resolves against the base but whose
// block the diff no longer renders — the agent restored the deleted paragraph —
// is the case that would otherwise vanish.
func TestThreadOnAnUnrenderedBlockIsHandedBack(t *testing.T) {
const src = "# Storage\n\nThe restored paragraph.\n"
anchor, err := service.AnchorOf("SPEC-0007", []byte(src), 1, core.SideOld)
if err != nil {
t.Fatalf("AnchorOf: %v", err)
}
view := renderDocDiff(docDiff{
DocID: "SPEC-0007", Path: "specs/a.md",
Base: []byte(src),
Proposed: []byte(src + "\nAnd a new one.\n"),
Threads: []service.Thread{{
Root: service.Comment{ID: 3, Body: "written when this was deleted"},
Anchor: anchor, State: core.AnchorExact, Block: 1,
}},
})
if len(view.Unplaced) != 1 {
t.Fatalf("Unplaced = %+v, want the thread whose block is not rendered", view.Unplaced)
}
if strings.Contains(string(view.HTML), "written when this was deleted") {
t.Errorf("the thread was rendered against some other block; got:\n%s", view.HTML)
}
}
// TestUnchangedDocumentKeepsItsThreads proves nothing is lost in the degenerate
// case: a document whose diff renders no blocks at all still hands its threads
// back rather than swallowing them.
func TestUnchangedDocumentKeepsItsThreads(t *testing.T) {
src := []byte("# Storage\n\nOne paragraph.\n")
view := renderDocDiff(docDiff{
DocID: "SPEC-0007", Path: "specs/a.md", Base: src, Proposed: src,
Threads: []service.Thread{{Root: service.Comment{ID: 1, Body: "still here"}, State: core.AnchorExact, Block: 1}},
})
if !view.Unchanged {
t.Fatalf("Unchanged = false for identical input")
}
if len(view.Unplaced) != 1 {
t.Errorf("Unplaced = %+v, want the thread of an unrendered document", view.Unplaced)
}
}
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}