package docsite import ( "encoding/json" "os" "path/filepath" "testing" ) // TestInternalLinksResolve stops a docs link 404ing. // // It checks the fragment as well as the page, because a link to a // renamed heading is the failure that actually happens: the page still // exists, the link still looks right in review, and the reader lands at // the top of a long page wondering what they missed. func TestInternalLinksResolve(t *testing.T) { site, err := Load(root) if err != nil { t.Fatalf("load: %v", err) } for _, p := range site.Pages() { for _, l := range p.Links { target, ok := site.BySlug[l.Target] if !ok { t.Errorf("%s.md:%d: links to /docs/%s, which is not a page", p.Slug, l.Line, l.Target) continue } if l.Fragment == "" { continue } if !hasAnchor(target, l.Fragment) { t.Errorf("%s.md:%d: links to /docs/%s#%s, but %s has no heading anchoring %q", p.Slug, l.Line, l.Target, l.Fragment, l.Target, l.Fragment) } } } } func hasAnchor(p *Page, anchor string) bool { for _, h := range p.Headings { if h.Anchor == anchor { return true } } return false } // TestLinkCheckCatchesADeadFragment proves the fragment half fires. func TestLinkCheckCatchesADeadFragment(t *testing.T) { site, err := Load(testCorpus) if err != nil { t.Fatalf("load: %v", err) } cli := site.BySlug["reference/cli"] if cli == nil { t.Fatal("no reference/cli page") } if !hasAnchor(cli, "carlos-ship") { t.Error("expected reference/cli to anchor carlos-ship") } if hasAnchor(cli, "carlos-teleport") { t.Error("hasAnchor invented an anchor that is not there") } } // TestAnchorsAreUnique matters because a duplicate anchor makes a link // to it ambiguous, and the renderer resolves that by silently suffixing // the second one. The link then goes to the wrong section rather than // nowhere, which is harder to notice. func TestAnchorsAreUnique(t *testing.T) { site, err := Load(root) if err != nil { t.Fatalf("load: %v", err) } for _, p := range site.Pages() { seen := map[string]string{} for _, h := range p.Headings { if h.Level == 1 { continue } if prev, dup := seen[h.Anchor]; dup { t.Errorf("%s.md: headings %q and %q both anchor to #%s", p.Slug, prev, h.Text, h.Anchor) } seen[h.Anchor] = h.Text } } } // TestAnchorMatchesFixture pins the anchor rule itself. The website's // slugify reads the same file, so the two implementations assert // against a shared fixture rather than against each other. // // One direction of that sharing is not symmetric: the website's // hack/sync-docs.mjs injects a "//" notice key into its vendored copy, // saying the file came from here and must not be edited there, and its // checker skips that key. This file must never carry one — Anchor("//") // is "", so a "//" key here fails the test below rather than being // ignored. // // The non-ASCII cases are the ones worth keeping: Go's strings.ToLower // and JavaScript's String.prototype.toLowerCase do not agree on every // code point, and both implementations DROP non-ASCII runes rather than // transliterating them — "Montréal" anchors as montr-al in both. Pin // anything with an em dash or an accented letter here before it reaches // a heading. func TestAnchorMatchesFixture(t *testing.T) { raw, err := os.ReadFile(filepath.Join(root, "anchors.json")) if err != nil { t.Fatalf("read fixture: %v", err) } var cases map[string]string if err := json.Unmarshal(raw, &cases); err != nil { t.Fatalf("parse fixture: %v", err) } if len(cases) < 5 { t.Fatalf("fixture has %d cases — too few to pin anything", len(cases)) } for in, want := range cases { if got := Anchor(in); got != want { t.Errorf("Anchor(%q) = %q, want %q", in, got, want) } } }