-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrender_test.go
More file actions
310 lines (270 loc) · 10 KB
/
Copy pathrender_test.go
File metadata and controls
310 lines (270 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
package main
import (
"strings"
"testing"
)
func TestBuildHTMLWithoutLiveReload(t *testing.T) {
html, headings := renderMarkdownWithIDs("# Hello\n\nWorld")
page := buildHTML(html, headings, false)
if strings.Contains(page, "EventSource") {
t.Error("buildHTML(..., false) should not include EventSource script")
}
if strings.Contains(page, "/events") {
t.Error("buildHTML(..., false) should not reference /events")
}
if !strings.Contains(page, "ganderRebuildTOC") {
t.Error("buildHTML(..., false) should include the TOC builder so the left column populates")
}
if !strings.Contains(page, "<h1") {
t.Error("buildHTML output missing rendered heading")
}
}
func TestBuildHTMLWithLiveReload(t *testing.T) {
html, headings := renderMarkdownWithIDs("# Title\n\nbody")
page := buildHTML(html, headings, true)
if !strings.Contains(page, "EventSource('/events')") && !strings.Contains(page, `EventSource("/events")`) {
t.Error("buildHTML(..., true) should set up EventSource connection")
}
if !strings.Contains(page, "ganderRebuildTOC") {
t.Error("buildHTML(..., true) should expose ganderRebuildTOC for hot-swap")
}
if !strings.Contains(page, "scrollTo") {
t.Error("buildHTML(..., true) should preserve scroll position")
}
}
func TestBuildHTMLHeadingsJSON(t *testing.T) {
_, headings := renderMarkdownWithIDs("# A\n\n## B\n\nbody")
page := buildHTML("<p>x</p>", headings, false)
if !strings.Contains(page, `"level":1`) {
t.Error("buildHTML should embed headings JSON")
}
if !strings.Contains(page, "toc-list") {
t.Error("buildHTML with >=2 headings should include TOC nav")
}
}
func TestBuildHTMLNoTOCWhenFewHeadings(t *testing.T) {
_, headings := renderMarkdownWithIDs("# Only one")
page := buildHTML("<p>x</p>", headings, false)
if strings.Contains(page, `<ul id="toc-list"`) {
t.Error("buildHTML with <2 headings should not render TOC nav")
}
if !strings.Contains(page, "gander-layout--no-toc") {
t.Error("buildHTML with <2 headings should mark the layout so the grid stays single-column on wide viewports")
}
}
func TestBuildHTMLLayoutClassWithTOC(t *testing.T) {
_, headings := renderMarkdownWithIDs("# A\n\n## B\n\nbody")
page := buildHTML("<p>x</p>", headings, false)
if strings.Contains(page, `id="gander-layout" class="gander-layout--no-toc"`) {
t.Error("buildHTML with >=2 headings should not mark the layout as no-toc")
}
}
func TestHeadingID(t *testing.T) {
cases := map[string]string{
"Hello World": "hello-world",
" Trim Spaces ": "trim-spaces",
"Symbols!@#removed": "symbols-removed",
"": "heading",
}
for in, want := range cases {
if got := headingID(in); got != want {
t.Errorf("headingID(%q) = %q, want %q", in, got, want)
}
}
}
func TestRenderMarkdownWithIDs(t *testing.T) {
md := "# Title\n\nSome text.\n\n## Sub\n\nMore."
html, headings := renderMarkdownWithIDs(md)
if len(headings) != 2 {
t.Fatalf("got %d headings, want 2", len(headings))
}
if headings[0].Level != 1 || headings[0].Text != "Title" {
t.Errorf("heading[0] = %+v", headings[0])
}
if headings[1].Level != 2 || headings[1].Text != "Sub" {
t.Errorf("heading[1] = %+v", headings[1])
}
if headings[0].ID == "" {
t.Error("heading[0] missing ID")
}
if !strings.Contains(html, "<h1") || !strings.Contains(html, "<h2") {
t.Errorf("html missing rendered headings: %s", html)
}
}
func TestRenderMarkdownTable(t *testing.T) {
md := "| Col A | Col B |\n| ----- | ----- |\n| 1 | 2 |\n| 3 | 4 |\n"
html, headings := renderMarkdownWithIDs(md)
if len(headings) != 0 {
t.Errorf("table-only input should produce no headings, got %d", len(headings))
}
for _, want := range []string{"<table>", "<thead>", "<tbody>", "<th>Col A</th>", "<th>Col B</th>", "<td>1</td>", "<td>4</td>"} {
if !strings.Contains(html, want) {
t.Errorf("html missing %q\n--- got ---\n%s", want, html)
}
}
if strings.Contains(html, "<p>|") {
t.Errorf("table was rendered as paragraph, not as <table>:\n%s", html)
}
}
func TestRenderMarkdownTableWithSurroundingText(t *testing.T) {
md := "# Stats\n\n| A | B |\n| - | - |\n| 1 | 2 |\n\nfin"
html, _ := renderMarkdownWithIDs(md)
if !strings.Contains(html, "<table>") {
t.Errorf("expected <table> in html, got:\n%s", html)
}
if !strings.Contains(html, "<h1") {
t.Errorf("expected heading to still render, got:\n%s", html)
}
if !strings.Contains(html, "<p>fin</p>") {
t.Errorf("expected trailing paragraph to still render, got:\n%s", html)
}
}
func TestRenderMarkdownMermaidCodeBlock(t *testing.T) {
md := "```mermaid\ngraph TD\n A --> B\n```"
html, _ := renderMarkdownWithIDs(md)
if !strings.Contains(html, "language-mermaid") {
t.Errorf("expected 'language-mermaid' class on code block, got:\n%s", html)
}
if !strings.Contains(html, "graph TD") {
t.Errorf("expected mermaid source in code block, got:\n%s", html)
}
}
func TestBuildHTMLIncludesMermaid(t *testing.T) {
_, _ = renderMarkdownWithIDs("hello")
page := buildHTML("<p>x</p>", nil, false)
if !strings.Contains(page, "mermaid.min.js") {
t.Error("buildHTML should load the Mermaid library from CDN")
}
if !strings.Contains(page, "ganderRenderMermaid") {
t.Error("buildHTML should expose ganderRenderMermaid")
}
if !strings.Contains(page, "code.language-mermaid") {
t.Error("buildHTML init script should target code.language-mermaid")
}
if !strings.Contains(page, "mermaid.run") {
t.Error("buildHTML init script should call mermaid.run")
}
if !strings.Contains(page, ".mermaid") {
t.Error("buildHTML should include CSS for .mermaid containers")
}
}
func TestBuildHTMLMermaidRunsAfterLiveReload(t *testing.T) {
_, _ = renderMarkdownWithIDs("hello")
page := buildHTML("<p>x</p>", nil, true)
idxReload := strings.Index(page, "EventSource")
if idxReload < 0 {
t.Fatal("live reload script not present")
}
lastMermaid := strings.LastIndex(page, "ganderRenderMermaid")
if lastMermaid < 0 {
t.Fatal("ganderRenderMermaid not present")
}
if lastMermaid < idxReload {
t.Error("ganderRenderMermaid should be called from the live-reload SSE handler (after content swaps)")
}
}
func TestBuildHTMLTOCLogoAndWordmark(t *testing.T) {
_, headings := renderMarkdownWithIDs("# A\n\n## B\n\nbody")
page := buildHTML("<p>x</p>", headings, false)
if !strings.Contains(page, `<a href="https://gander.md/" class="gander-toc-logo">`) {
t.Error("TOC should render a clickable logo link above 'On this page'")
}
if !strings.Contains(page, `<span class="gander-toc-wordmark">gander</span>`) {
t.Error("TOC should render a 'gander' wordmark next to the logo")
}
if !strings.Contains(page, `class="gander-toc-logo"`) {
t.Error("TOC logo link missing gander-toc-logo class")
}
}
func TestBuildHTMLCTABelowMain(t *testing.T) {
_, headings := renderMarkdownWithIDs("# A\n\n## B\n\nbody")
page := buildHTML("<p>x</p>", headings, false)
if !strings.Contains(page, `class="gander-md-cta"`) {
t.Error("page should render the share-viewer footer CTA")
}
if !strings.Contains(page, `class="gander-md-viewer-logo"`) {
t.Error("CTA should render the small goose logo above the link")
}
if !strings.Contains(page, `<a href="https://gander.md/cli">gander.md/cli</a>`) {
t.Error("CTA should link to https://gander.md/cli with 'gander.md/cli' label")
}
if !strings.Contains(page, "Get your gander at") {
t.Error("CTA should include 'Get your gander at' copy")
}
mainOpen := strings.Index(page, `<main class="gander-content">`)
if mainOpen < 0 {
t.Fatal("page missing <main class=\"gander-content\">")
}
ctaIdx := strings.Index(page, `class="gander-md-cta"`)
if ctaIdx <= mainOpen {
t.Errorf("CTA must render below <main>, got ctaIdx=%d mainOpen=%d", ctaIdx, mainOpen)
}
}
func TestBuildHTMLCTARendersWithoutTOC(t *testing.T) {
_, headings := renderMarkdownWithIDs("# Only one")
page := buildHTML("<p>x</p>", headings, false)
if !strings.Contains(page, `class="gander-md-cta"`) {
t.Error("CTA should render even when there is no TOC")
}
if strings.Contains(page, `<a href="https://gander.md/" class="gander-toc-logo">`) {
t.Error("TOC logo link should not render when there are fewer than 2 headings")
}
}
func TestBuildHTMLCSSIncludesNewClasses(t *testing.T) {
page := buildHTML("<p>x</p>", nil, false)
for _, cls := range []string{
".gander-md-cta",
".gander-md-viewer-logo",
".gander-toc-logo",
".gander-toc-wordmark",
} {
if !strings.Contains(page, cls) {
t.Errorf("buildHTML CSS missing %q", cls)
}
}
}
func TestBuildHTMLContentBodyInsideMain(t *testing.T) {
_, headings := renderMarkdownWithIDs("# A\n\n## B\n\nbody")
page := buildHTML("<p>x</p>", headings, true)
bodyOpen := strings.Index(page, `<div id="content-body">`)
if bodyOpen < 0 {
t.Fatal("page missing #content-body swap target")
}
bodyClose := strings.Index(page[bodyOpen:], `</div>`)
if bodyClose < 0 {
t.Fatal("page #content-body not closed")
}
bodyClose += bodyOpen
bodyInner := page[bodyOpen:bodyClose]
for _, forbidden := range []string{
`class="gander-md-cta"`,
`gander.md/cli`,
} {
if strings.Contains(bodyInner, forbidden) {
t.Errorf("SSE swap target must not contain %q; the CTA needs to survive live reloads", forbidden)
}
}
if !strings.Contains(page, `getElementById('content-body')`) {
t.Error("SSE reload script must target #content-body, not #content")
}
if strings.Contains(page, `getElementById('content')`) {
t.Error("SSE reload script must not target the old #content id")
}
mainOpen := strings.Index(page, `<main class="gander-content">`)
if mainOpen >= bodyOpen {
t.Error("#content-body must render inside <main>")
}
ctaIdx := strings.Index(page, `class="gander-md-cta"`)
if ctaIdx <= bodyClose {
t.Errorf("CTA must render after the SSE swap target, got ctaIdx=%d bodyClose=%d", ctaIdx, bodyClose)
}
}
func TestBuildHTMLMermaidScopesToContentBody(t *testing.T) {
page := buildHTML("<p>x</p>", nil, false)
if !strings.Contains(page, `getElementById('content-body')`) {
t.Error("mermaid init script should scope to #content-body")
}
if strings.Contains(page, "getElementById('content') || document.body") {
t.Error("mermaid init script should not scope to the old #content id")
}
}