carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package docsite
2
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "testing"
8)
9
10// TestInternalLinksResolve stops a docs link 404ing.
11//
12// It checks the fragment as well as the page, because a link to a
13// renamed heading is the failure that actually happens: the page still
14// exists, the link still looks right in review, and the reader lands at
15// the top of a long page wondering what they missed.
16func TestInternalLinksResolve(t *testing.T) {
17 site, err := Load(root)
18 if err != nil {
19 t.Fatalf("load: %v", err)
20 }
21 for _, p := range site.Pages() {
22 for _, l := range p.Links {
23 target, ok := site.BySlug[l.Target]
24 if !ok {
25 t.Errorf("%s.md:%d: links to /docs/%s, which is not a page",
26 p.Slug, l.Line, l.Target)
27 continue
28 }
29 if l.Fragment == "" {
30 continue
31 }
32 if !hasAnchor(target, l.Fragment) {
33 t.Errorf("%s.md:%d: links to /docs/%s#%s, but %s has no heading anchoring %q",
34 p.Slug, l.Line, l.Target, l.Fragment, l.Target, l.Fragment)
35 }
36 }
37 }
38}
39
40func hasAnchor(p *Page, anchor string) bool {
41 for _, h := range p.Headings {
42 if h.Anchor == anchor {
43 return true
44 }
45 }
46 return false
47}
48
49// TestLinkCheckCatchesADeadFragment proves the fragment half fires.
50func TestLinkCheckCatchesADeadFragment(t *testing.T) {
51 site, err := Load(testCorpus)
52 if err != nil {
53 t.Fatalf("load: %v", err)
54 }
55 cli := site.BySlug["reference/cli"]
56 if cli == nil {
57 t.Fatal("no reference/cli page")
58 }
59 if !hasAnchor(cli, "carlos-ship") {
60 t.Error("expected reference/cli to anchor carlos-ship")
61 }
62 if hasAnchor(cli, "carlos-teleport") {
63 t.Error("hasAnchor invented an anchor that is not there")
64 }
65}
66
67// TestAnchorsAreUnique matters because a duplicate anchor makes a link
68// to it ambiguous, and the renderer resolves that by silently suffixing
69// the second one. The link then goes to the wrong section rather than
70// nowhere, which is harder to notice.
71func TestAnchorsAreUnique(t *testing.T) {
72 site, err := Load(root)
73 if err != nil {
74 t.Fatalf("load: %v", err)
75 }
76 for _, p := range site.Pages() {
77 seen := map[string]string{}
78 for _, h := range p.Headings {
79 if h.Level == 1 {
80 continue
81 }
82 if prev, dup := seen[h.Anchor]; dup {
83 t.Errorf("%s.md: headings %q and %q both anchor to #%s",
84 p.Slug, prev, h.Text, h.Anchor)
85 }
86 seen[h.Anchor] = h.Text
87 }
88 }
89}
90
91// TestAnchorMatchesFixture pins the anchor rule itself. The website's
92// slugify reads the same file, so the two implementations assert
93// against a shared fixture rather than against each other.
94//
95// One direction of that sharing is not symmetric: the website's
96// hack/sync-docs.mjs injects a "//" notice key into its vendored copy,
97// saying the file came from here and must not be edited there, and its
98// checker skips that key. This file must never carry one — Anchor("//")
99// is "", so a "//" key here fails the test below rather than being
100// ignored.
101//
102// The non-ASCII cases are the ones worth keeping: Go's strings.ToLower
103// and JavaScript's String.prototype.toLowerCase do not agree on every
104// code point, and both implementations DROP non-ASCII runes rather than
105// transliterating them — "Montréal" anchors as montr-al in both. Pin
106// anything with an em dash or an accented letter here before it reaches
107// a heading.
108func TestAnchorMatchesFixture(t *testing.T) {
109 raw, err := os.ReadFile(filepath.Join(root, "anchors.json"))
110 if err != nil {
111 t.Fatalf("read fixture: %v", err)
112 }
113 var cases map[string]string
114 if err := json.Unmarshal(raw, &cases); err != nil {
115 t.Fatalf("parse fixture: %v", err)
116 }
117 if len(cases) < 5 {
118 t.Fatalf("fixture has %d cases — too few to pin anything", len(cases))
119 }
120 for in, want := range cases {
121 if got := Anchor(in); got != want {
122 t.Errorf("Anchor(%q) = %q, want %q", in, got, want)
123 }
124 }
125}
126