~bigbes/sr-ht-spec

ref: e756d504ebc789e9e88f9bdd175d57afd4c87439 sr-ht-spec/web/diff_internal_test.go -rw-r--r-- 17.2 KiB
e756d504 — Eugene Blikh web: draw the whole web tier from sr-ht-ecore 9 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
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, "&lt;script&gt;") {
		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, "&lt;b&gt;bold&lt;/b&gt;"); 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
}