Artifact c6186c54ef34eb0835a738309c33d1a0086694ec8ebaba706e97659e9771c784:
- File
internal/web/chrome_test.go
— part of check-in
[3a330c9560]
at
2026-08-17 18:49:56
on branch trunk
— web: repository nav shows counts and current section
Counts tell a reader where a store's content is — pikchr is 245 forum threads and one ticket, tk is 5,809 tickets and one post. The index memoizes them per fold; on tk they cost ~110ms cold and 3ms after.
Also: the current section is marked, empty sections are dimmed but stay linked, the repository outranks the forge name in the header, and timestamps render relative with the exact value in the title. (user: dmestas, size: 5495) [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) } } }