Artifact 1c2ecc156b6b65122c9ee1655f05b48b59bb0ce87fefee1167392c5f878cbb13:
- File
internal/web/chrome_test.go
— part of check-in
[15c39013de]
at
2026-08-18 13:40:30
on branch trunk
— nav: pills with compact counts; stop long tokens overflowing
The repo nav's width was a function of its numbers — tk's 25557 and 5809 made a visibly different nav from a small repo's, wrapping to three lines on a phone. Counts go compact (25K, 5.8K, 1.2M, four characters max) with the exact figure in the badge title, and each entry is a pill rather than a word beside a loose number. Nav width is now identical across repositories.
Separately: sqlite has wiki pages named after check-in hashes, 40-char unbreakable tokens that pushed the page 300px past a phone viewport. main now breaks over-long words; pre keeps its own scroll. (user: dmestas, size: 7362) [annotate] [blame] [check-ins using]
package web import ( "strings" "testing" "time" ) // The layouts are what real corpora actually contain — fossil writes // millisecond UTC with no zone, and an imported store carries the // second-precision form too. A layout this misses renders the raw stamp, // which is ugly but honest; a layout it MISPARSES would silently date // every page wrong, so the parse is worth pinning. func TestParseArtifactTimeCoversRealCorpusShapes(t *testing.T) { want := time.Date(2025, 8, 25, 21, 25, 59, 0, time.UTC) for _, s := range []string{ "2025-08-25T21:25:59.000", // fossil forum post "2025-08-25T21:25:59", // second precision "2025-08-25T21:25:59Z", // RFC3339, quarry's own stamps "2025-08-25 21:25:59", // space-separated } { got, ok := parseArtifactTime(s) if !ok { t.Errorf("%q did not parse", s) continue } if !got.Equal(want) { t.Errorf("%q parsed to %v, want %v", s, got, want) } } if _, ok := parseArtifactTime("last tuesday"); ok { t.Error("nonsense parsed as a time") } } // An unparseable stamp must be shown, not swallowed. A blank cell where // a date belongs reads as "no date recorded", which is a different and // wrong claim about the artifact. func TestAgoShowsWhatItCannotParse(t *testing.T) { if got := string(ago("not a date")); got != "not a date" { t.Errorf("ago(unparseable) = %q, want it passed through", got) } if got := string(ago("")); got != "" { t.Errorf("ago(empty) = %q, want empty", got) } // A parseable one keeps the exact value reachable, because a // relative time is not something you can quote in a bug report. got := string(ago("2025-08-25T21:25:59.000")) if !strings.Contains(got, `datetime="2025-08-25T21:25:59Z"`) { t.Errorf("ago dropped the machine-readable value: %s", got) } if !strings.Contains(got, `title="2025-08-25 21:25:59 UTC"`) { t.Errorf("ago dropped the absolute value: %s", got) } } func TestRelativeWording(t *testing.T) { now := time.Date(2026, 8, 17, 12, 0, 0, 0, time.UTC) for _, tc := range []struct { at time.Time want string }{ {now.Add(-30 * time.Second), "just now"}, {now.Add(-time.Minute), "1 minute ago"}, {now.Add(-5 * time.Minute), "5 minutes ago"}, {now.Add(-time.Hour), "1 hour ago"}, {now.Add(-3 * time.Hour), "3 hours ago"}, {now.Add(-24 * time.Hour), "1 day ago"}, {now.Add(-13 * 24 * time.Hour), "13 days ago"}, // Past a month a date beats a day count; past a year the year is // the part that carries the information. {now.Add(-60 * 24 * time.Hour), "18 Jun"}, {now.Add(-400 * 24 * time.Hour), "13 Jul 2025"}, // Clock skew: a future stamp must not read as "in 3 minutes". {now.Add(3 * time.Minute), "2026-08-17 12:03"}, } { if got := relative(tc.at, now); got != tc.want { t.Errorf("relative(%v) = %q, want %q", tc.at, got, tc.want) } } } // The nav's job is orientation, so the two states that carry it — where // you are, and where there is nothing to find — must both survive into // the markup, and must not collide into one attribute. func TestNavlinkStates(t *testing.T) { current := string(navlink("alpha", "forum", "forum", "forum", true, 12)) if !strings.Contains(current, `class="here"`) || !strings.Contains(current, `aria-current="page"`) { t.Errorf("current section unmarked: %s", current) } if !strings.Contains(current, `>12</span>`) { t.Errorf("count missing: %s", current) } empty := string(navlink("alpha", "wiki", "wiki", "forum", true, 0)) if !strings.Contains(empty, `class="empty"`) { t.Errorf("empty section unmarked: %s", empty) } if !strings.Contains(empty, `href="/r/alpha/wiki"`) { t.Errorf("empty section stopped being a link: %s", empty) } // Current AND empty is one class attribute, not two — a browser // honours only the first, so the second state would vanish. both := string(navlink("alpha", "wiki", "wiki", "wiki", true, 0)) // Count within the ANCHOR tag only; the count badge carries its own // class and would otherwise be mistaken for a duplicate. anchor, _, _ := strings.Cut(both, ">") if strings.Count(anchor, "class=") != 1 { t.Errorf("expected exactly one class attribute on the anchor: %s", both) } if !strings.Contains(anchor, "here") || !strings.Contains(anchor, "empty") { t.Errorf("both states not preserved: %s", both) } // A section with nothing countable shows no badge rather than "0". files := string(navlink("alpha", "tree", "files", "tree", false, false)) if strings.Contains(files, "<span") { t.Errorf("uncountable section rendered a badge: %s", files) } } // The marker has to follow a reader INTO a section, not just light up on // its index page — otherwise it goes dark exactly when orientation // matters most. func TestSectionOfFollowsDeepPages(t *testing.T) { for path, want := range map[string]string{ "/r/alpha": "tree", "/r/alpha/tree": "tree", "/r/alpha/file/trunk/README.md": "tree", "/r/alpha/history/trunk/README.md": "tree", "/r/alpha/timeline": "timeline", "/r/alpha/checkin/abc123": "timeline", "/r/alpha/diff": "timeline", "/r/alpha/wiki": "wiki", "/r/alpha/wiki/Home/history": "wiki", "/r/alpha/ticket/abc/edit": "ticket", "/r/alpha/forum/abc": "forum", "/fleet": "", } { if got := sectionOf(path); got != want { t.Errorf("sectionOf(%q) = %q, want %q", path, got, want) } } } // Badges exist so the numbers line up down the column, and a compact // duration is what makes that possible — "34 minutes ago" is a different // width in every row. func TestAgoShortIsCompact(t *testing.T) { for _, tc := range []struct{ d, want string }{ {"30s", "now"}, {"34m", "34m"}, {"12h", "12h"}, {"72h", "3d"}, {"9000h", "1y"}, } { dur, err := time.ParseDuration(tc.d) if err != nil { t.Fatal(err) } stamp := time.Now().UTC().Add(-dur).Format("2006-01-02T15:04:05") got := string(agoShort(stamp)) if !strings.Contains(got, ">"+tc.want+"<") { t.Errorf("agoShort(-%s) = %q, want %q", tc.d, got, tc.want) } // The exact instant stays reachable; a badge is a summary, not a // replacement for the value. if !strings.Contains(got, "datetime=") || !strings.Contains(got, "title=") { t.Errorf("agoShort dropped the precise value: %q", got) } } if agoShort("not a date") != "" { t.Error("agoShort invented a duration for an unparseable stamp") } } // The nav's width was a function of its numbers: a repository with // 25,557 check-ins rendered a visibly different nav from one with none, // and on a phone it wrapped to three lines. Four characters is the // budget; the exact figure lives in the badge's title. func TestCompactCountIsBounded(t *testing.T) { for _, tc := range []struct { n int want string }{ {0, "0"}, {19, "19"}, {503, "503"}, {999, "999"}, {1000, "1K"}, {1100, "1.1K"}, {5809, "5.8K"}, {9999, "10K"}, {25557, "25K"}, {42118, "42K"}, {999999, "999K"}, {1000000, "1M"}, {1234567, "1.2M"}, {25000000, "25M"}, {-5, "0"}, } { if got := compactCount(tc.n); got != tc.want { t.Errorf("compactCount(%d) = %q, want %q", tc.n, got, tc.want) } if len(compactCount(tc.n)) > 4 { t.Errorf("compactCount(%d) = %q, over the four-character budget", tc.n, compactCount(tc.n)) } } }