Artifact eb615b9480d850c5d7b85841dd38040b0e8a84539af549f68c04b1d1723621c4:
- File
internal/web/timeline_test.go
— part of check-in
[6f34d948a0]
at
2026-08-23 12:31:59
on branch trunk
— timeline: the mainline claims rail 0 only when it is free
A merge whose two parents are both on trunk puts two chains in front of one column. Claiming rail 0 anyway stopped drawing the line that was already travelling there — the gap — and overwrote the commit it was heading to. Test asserts every head resumes its predecessor's tail. (user: dmestas, size: 10282) [annotate] [blame] [check-ins using]
package web import ( "fmt" "regexp" "strconv" "strings" "testing" ) // A branch tip is drawn as the quarry mark is: an open ring with the // check-in dot at its centre. The ring alone only said "nothing follows // this"; the dot says "and this is where the branch is now", which is // the fact a reader is actually scanning the graph for. // // Every other check-in stays a solid node — if the two ever render the // same, the graph stops answering "where is each branch now?" and the // only way to find a tip is to read the branch list instead. func TestBranchTipsCarryTheCentreDot(t *testing.T) { days := buildTimeline("r", "trunk", []timelineEntry{ {Kind: "checkin", Hash: "aaa", Date: "2026-08-18 10:00:00", Branch: "trunk", IsLeaf: true, Parents: []string{"bbb"}}, {Kind: "checkin", Hash: "bbb", Date: "2026-08-18 09:00:00", Branch: "trunk"}, }) var tip, mid string for _, d := range days { for _, row := range d.Rows { switch row.Hash { case "aaa": tip = string(row.Head) case "bbb": mid = string(row.Head) } } } if tip == "" || mid == "" { t.Fatalf("timeline dropped a row: tip=%q mid=%q", tip, mid) } if !strings.Contains(tip, `r="4.85"`) || !strings.Contains(tip, `fill:var(--bg)`) { t.Errorf("the tip is not an open ring:\n%s", tip) } if !strings.Contains(tip, `r="1.25"`) { t.Errorf("the tip has no centre dot, so it reads as a dead end:\n%s", tip) } // The ring must stay hollow: a filled ring plus a dot is just a // slightly larger solid node and tells the reader nothing. if strings.Count(tip, "<circle") != 2 { t.Errorf("a tip should be exactly ring + dot, got:\n%s", tip) } if strings.Contains(mid, `r="1.25"`) || strings.Contains(mid, `fill:var(--bg)`) { t.Errorf("an ordinary check-in is being drawn as a tip:\n%s", mid) } if strings.Count(mid, "<circle") != 1 { t.Errorf("an ordinary check-in should be one solid node, got:\n%s", mid) } } // railOf reports which rail column a row's node was drawn on, by // reading the node dot's x back out of the head SVG. func railOf(t *testing.T, row tlRow) int { t.Helper() head := string(row.Head) i := strings.Index(head, `<circle cx="`) if i < 0 { t.Fatalf("row %q drew no node", row.Title) } rest := head[i+len(`<circle cx="`):] x, err := strconv.Atoi(rest[:strings.Index(rest, `"`)]) if err != nil { t.Fatal(err) } return (x - railW/2) / railW } // The mainline owns the leftmost rail whenever it is on the page, even // when a branch holds the newest commit — otherwise a branch that // forked yesterday takes the column a reader scans for trunk. func TestMainlineKeepsTheLeftmostRail(t *testing.T) { // Newest first: the branch commits, then trunk's. days := buildTimeline("r", "trunk", []timelineEntry{ {Kind: "ci", Hash: "b2", Date: "2026-08-22T16:50:00", Branch: "ticket-list", Parents: []string{"b1"}, Comment: "branch tip"}, {Kind: "ci", Hash: "b1", Date: "2026-08-22T16:46:00", Branch: "ticket-list", Parents: []string{"t1"}, Comment: "branch base"}, {Kind: "ci", Hash: "t1", Date: "2026-08-22T16:35:00", Branch: "trunk", Parents: []string{"t0"}, Comment: "trunk work"}, {Kind: "ci", Hash: "t0", Date: "2026-08-22T16:30:00", Branch: "trunk", Comment: "trunk root"}, }) var rows []tlRow for _, d := range days { rows = append(rows, d.Rows...) } if len(rows) != 4 { t.Fatalf("rows = %d", len(rows)) } for _, row := range rows { got := railOf(t, row) if row.Branch == "trunk" && got != 0 { t.Errorf("trunk commit %q drew on rail %d, want 0", row.Title, got) } if row.Branch != "trunk" && got == 0 { t.Errorf("branch commit %q took the mainline's rail", row.Title) } } } // With the mainline absent from the page entirely, nothing is reserved // and the visible lines close up to the left edge. func TestWithoutMainlineTheLinesCloseUp(t *testing.T) { days := buildTimeline("r", "trunk", []timelineEntry{ {Kind: "ci", Hash: "b2", Date: "2026-08-22T16:50:00", Branch: "ticket-list", Parents: []string{"b1"}, Comment: "tip"}, {Kind: "ci", Hash: "b1", Date: "2026-08-22T16:46:00", Branch: "ticket-list", Comment: "base"}, }) for _, d := range days { for _, row := range d.Rows { if got := railOf(t, row); got != 0 { t.Errorf("with no mainline in view, %q drew on rail %d, want 0", row.Title, got) } } } } // Two branch lines on one page must never be the same colour. A hash // alone cannot promise that — `ticket-list` and `fossil-caps` hashed to // near-identical magentas — so the palette resolves collisions among // the branches actually in view. func TestBranchColoursAreDistinctOnAPage(t *testing.T) { names := []string{"ticket-list", "fossil-caps", "mirror-posture", "ticket-editor", "ticket-paging"} var entries []timelineEntry for i, n := range names { entries = append(entries, timelineEntry{ Kind: "ci", Hash: fmt.Sprintf("h%d", i), Date: "2026-08-22T16:00:00", Branch: n, Comment: n, }) } pal := newPalette("trunk", entries) seen := map[string]string{} for _, n := range names { c := pal.stroke(n) if other, dup := seen[c]; dup { t.Fatalf("%q and %q share the colour %s", other, n, c) } seen[c] = n } // None of them may take the mainline's blue or read as the accent. for _, n := range names { if h := pal.hueOf(n); h > 193 && h < 233 { t.Errorf("%q (hue %d) sits in the mainline's blue band", n, h) } else if h < 38 || h > 340 { t.Errorf("%q (hue %d) sits in the accent's orange band", n, h) } } } // A branch keeps its colour across pages when nothing contests its // slot: the hue is hashed first and only moved on an actual collision. func TestUncontestedBranchKeepsItsColour(t *testing.T) { one := newPalette("trunk", []timelineEntry{ {Kind: "ci", Hash: "a", Branch: "solo", Date: "2026-08-22T16:00:00"}, }) two := newPalette("trunk", []timelineEntry{ {Kind: "ci", Hash: "a", Branch: "solo", Date: "2026-08-22T16:00:00"}, {Kind: "ci", Hash: "b", Branch: "zzz-unrelated", Date: "2026-08-22T15:00:00"}, }) if one.stroke("solo") != two.stroke("solo") { t.Fatalf("an uncontested branch changed colour: %s vs %s", one.stroke("solo"), two.stroke("solo")) } } // Assignment cannot depend on the order commits happen to arrive in, // or the same page would repaint itself differently after one push. func TestColourAssignmentIsOrderIndependent(t *testing.T) { mk := func(bs ...string) []timelineEntry { var out []timelineEntry for i, b := range bs { out = append(out, timelineEntry{Kind: "ci", Hash: fmt.Sprint(i), Branch: b, Date: "2026-08-22T16:00:00"}) } return out } a := newPalette("trunk", mk("alpha", "beta", "gamma")) b := newPalette("trunk", mk("gamma", "alpha", "beta")) for _, n := range []string{"alpha", "beta", "gamma"} { if a.stroke(n) != b.stroke(n) { t.Fatalf("%q changed colour with commit order: %s vs %s", n, a.stroke(n), b.stroke(n)) } } } // Nothing follows a leaf, so nothing may be drawn above it: the head // band's upper segment is the claim "a line arrives here from above", // and on a tip that claim is false. func TestNothingIsDrawnAboveALeaf(t *testing.T) { days := buildTimeline("r", "trunk", []timelineEntry{ {Kind: "ci", Hash: "tip", Date: "2026-08-23T00:33:00", Branch: "wiki-read", Parents: []string{"base"}, Comment: "tip", IsLeaf: true}, {Kind: "ci", Hash: "base", Date: "2026-08-23T00:20:00", Branch: "trunk", Comment: "base"}, }) head := string(days[0].Rows[0].Head) if strings.Contains(head, `y1="0"`) && strings.Contains(head, `y2="24"`) { t.Fatalf("a line was drawn above a leaf:\n%s", head) } if !strings.Contains(head, `y1="24"`) { t.Fatalf("the tip lost its line downward:\n%s", head) } } // railsFromZero reports which rail columns a head band draws from its // top edge — as a straight segment or as the start of a curve. func railsFromZero(head string) map[int]bool { out := map[int]bool{} for _, m := range regexp.MustCompile(`<line x1="(\d+)" y1="0"`).FindAllStringSubmatch(head, -1) { x, _ := strconv.Atoi(m[1]) out[x] = true } for _, m := range regexp.MustCompile(`<path d="M (\d+) 0`).FindAllStringSubmatch(head, -1) { x, _ := strconv.Atoi(m[1]) out[x] = true } return out } // railsInTail reports which columns a tail band draws. func railsInTail(tail string) map[int]bool { out := map[int]bool{} for _, m := range regexp.MustCompile(`<line x1="(\d+)"`).FindAllStringSubmatch(tail, -1) { x, _ := strconv.Atoi(m[1]) out[x] = true } return out } // Every row is drawn as two bands — a fixed head and a stretchy tail — // so a line is continuous only if each head resumes every column its // predecessor's tail was still drawing. Miss one and the graph shows a // gap: the line simply stops for the height of a row and starts again. // // The case that broke it: a merge whose second parent is an older // MAINLINE commit. That parent's row puts the node on rail 0 (the // mainline always draws in its own column) while the rail that was // heading there is rail 1. The upper segment was decided by "did a rail // point at this node", which was rail 1 — so rail 0, still carrying the // mainline down the page, was left undrawn above the node. func TestRailsAreContinuousAcrossRowBoundaries(t *testing.T) { days := buildTimeline("r", "trunk", []timelineEntry{ {Kind: "ci", Hash: "m", Date: "2026-08-23T10:37:00", Branch: "trunk", Parents: []string{"a", "z"}, Comment: "merge"}, {Kind: "ci", Hash: "a", Date: "2026-08-23T10:36:30", Branch: "trunk", Parents: []string{"x"}, Comment: "work"}, {Kind: "ci", Hash: "z", Date: "2026-08-23T10:36:00", Branch: "trunk", Parents: []string{"y"}, Comment: "the merged parent"}, {Kind: "ci", Hash: "x", Date: "2026-08-23T10:35:00", Branch: "trunk", Comment: "older"}, }) var rows []tlRow for _, d := range days { rows = append(rows, d.Rows...) } if len(rows) != 4 { t.Fatalf("rows = %d", len(rows)) } for i := 1; i < len(rows); i++ { prev := railsInTail(string(rows[i-1].Tail)) here := railsFromZero(string(rows[i].Head)) for x := range prev { if !here[x] { t.Errorf("gap at row %d (%q): the row above draws column x=%d to its bottom edge, this row does not resume it\nabove tail: %s\nthis head: %s", i, rows[i].Title, x, rows[i-1].Tail, rows[i].Head) } } } }