Download
149 lines 6.3 KiBGoModified 2026-10-18 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
package render_test

import (
	"fmt"
	"strings"
	"testing"
)

// TestStatsIncremental: an incremental push updates the root's line counts.
func TestStatsIncremental(t *testing.T) {
	e := newEnv(t, "proj", "")
	e.seed()
	e.render()
	e.write("src/extra.c", "int x;\n\n  \nint y;\n")
	e.commit("more C")
	e.push(false)
	e.render()
	mustContain(t, "root stats", e.read(e.page("")), `>C <b>7</b></a>`)
	mustContain(t, "C page", e.read("types/proj/code/c.html"), `href="/code/proj/src/extra.c">extra.c</a>`, "3 files")
	mustNotContain(t, "file stats", e.read(e.page("src/extra.c")), `class="langs"`)
}

// TestSizeBars: each entry gets a Lines and a Size cell: its lines of
// text (as wc -l counts them) and its bytes, each with a ring filled as
// far as its share of the folder, lines split into code and docs.
func TestSizeBars(t *testing.T) {
	e := newEnv(t, "proj", "")
	// Root: 897 lines and 5129 bytes in all.
	e.write("top.c", strings.Repeat("ab;\n", 512))                // 512 lines, 2 KiB
	e.write("mixed/m.c", strings.Repeat("ab;\n", 256))            // 256 lines, 1 KiB
	e.write("mixed/m.md", strings.Repeat("abcdefg\n", 128))       // 128 lines, 1 KiB
	e.write("mixed/m.bin", "\x00"+strings.Repeat("z\n", 511)+"z") // no lines, 1 KiB
	e.write("lib.min.js", "excluded\n")                           // counted all the same
	e.commit("sizes")
	e.push(false)
	e.render()
	cell := func(col, url, share, text, arcs string) string {
		return fmt.Sprintf(`<td class="bar %s" title="%s">`+
			`<a class="bar-link" href="%s" tabindex="-1" aria-hidden="true"><svg class="ring" viewBox="0 0 16 16" aria-hidden="true">`+
			`<circle class="track" cx="8" cy="8" r="6"/>%s</svg></a>`+
			`<span class="amount">%s</span><span class="vis">%s</span></td>`, col, share, url, arcs, text, share)
	}
	arc := func(class, length, off string) string {
		if off != "" {
			off = ` stroke-dashoffset="` + off + `"`
		}
		return fmt.Sprintf(`<circle class="%s" cx="8" cy="8" r="6" pathLength="100" stroke-dasharray="%s 100"%s/>`, class, length, off)
	}
	root := e.read(e.page(""))
	mustContain(t, "root", root,
		// Code then docs, each in its color, the docs arc starting where
		// the code arc ends.
		cell("lines", "/code/proj/mixed", "43% of this folder&#39;s lines: 256 lines of code, 128 lines of docs", "384 lines",
			arc("code", "28.54", "")+arc("docs", "14.27", "-28.54"))+"\n"+
			cell("size", "/code/proj/mixed", "60% of this folder&#39;s bytes", "3 KiB", arc("size", "59.89", "")),
		cell("lines", "/code/proj/top.c", "57% of this folder&#39;s lines", "512 lines", arc("code", "57.08", ""))+"\n"+
			cell("size", "/code/proj/top.c", "40% of this folder&#39;s bytes", "2 KiB", arc("size", "39.93", "")),
		// A tiny share still gets a sliver.
		cell("lines", "/code/proj/lib.min.js", "&lt;1% of this folder&#39;s lines", "1 line", arc("code", "2.00", ""))+"\n"+
			cell("size", "/code/proj/lib.min.js", "&lt;1% of this folder&#39;s bytes", "9 B", arc("size", "2.00", "")))
	// Binary data has a size but no lines: "0 lines" by an empty ring.
	mustContain(t, "mixed", e.read(e.page("mixed")),
		cell("lines", "/code/proj/mixed/m.bin", "0% of this folder&#39;s lines", "0 lines", "")+"\n"+cell("size", "/code/proj/mixed/m.bin", "33% of this folder&#39;s bytes", "1 KiB", arc("size", "33.33", "")))
}

// TestStatsEmptyRows: a row is left out when it has nothing to count, and a
// language whose files are all blank gets no pill.
func TestStatsEmptyRows(t *testing.T) {
	for _, tc := range []struct {
		name        string
		files       map[string]string
		code, docs  bool
		binary      bool
		notContains []string
	}{
		{"docs only", map[string]string{"README.md": "# Hi\n\nSome words.\n"}, false, true, false, nil},
		{"code only", map[string]string{"main.c": "int x;\n"}, true, false, false, nil},
		{"binary only", map[string]string{"bin.dat": "\x00\x01"}, false, false, true, nil},
		{"none", map[string]string{"blank.c": "\n  \n"}, false, false, false, nil},
		{"blank language", map[string]string{"main.c": "int x;\n", "blank.lua": "\n\n", "x.md": "Hi\n"}, true, true, false, []string{"Lua"}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			e := newEnv(t, "proj", "")
			for p, c := range tc.files {
				e.write(p, c)
			}
			e.commit("files")
			e.push(false)
			e.render()
			root := e.read(e.page(""))
			check := func(has bool, s string) {
				if has {
					mustContain(t, tc.name, root, s)
				} else {
					mustNotContain(t, tc.name, root, s)
				}
			}
			check(tc.code, `<div class="langs code"`)
			check(tc.docs, `<div class="langs docs"`)
			check(tc.binary, `<div class="langs binary"`)
			if !tc.code && !tc.docs && !tc.binary {
				mustNotContain(t, tc.name, root, `class="langs`)
			}
			for _, s := range tc.notContains {
				mustNotContain(t, tc.name, root, `>`+s+` <b>`)
			}
		})
	}
}

// TestTypePagesFollowTheTree: a type whose last file goes loses its page,
// and types folded past the first eight share an "Other" page that names
// each file's type.
func TestTypePagesFollowTheTree(t *testing.T) {
	e := newEnv(t, "proj", "")
	exts := []string{"png", "gif", "bmp", "ico", "webp", "tiff", "jpeg", "pdf", "zip", "bin"}
	for i, ext := range exts {
		e.write("f."+ext, strings.Repeat("\x00", 100-i))
	}
	e.write("main.c", "int x;\n")
	e.commit("files")
	e.push(false)
	e.render()
	other := e.read("types/proj/binary/other.html")
	mustContain(t, "other page", other, `f.zip</a> <span class="target">ZIP</span>`, `f.bin</a> <span class="target">BIN</span>`)
	mustNotContain(t, "other page", other, "f.png")
	mustContain(t, "other pill", e.read(e.page("")), `title="183 B of other formats across 2 files">Other <b>183 B</b>`)
	e.mustExist("types/proj/binary/png.html", "types/proj/code/c.html")

	e.rm("main.c")
	e.commit("no C")
	e.push(false)
	e.render()
	e.mustNotExist("types/proj/code/c.html", "types/proj/code/c.html.gz", "types/proj/code")
	e.mustExist("types/proj/binary/png.html")
}

// TestTypePageNestedPath: every folder on the way to a file is its own link.
func TestTypePageNestedPath(t *testing.T) {
	e := newEnv(t, "proj", "")
	e.write("a/b c/x.c", "int x;\n")
	e.commit("nested")
	e.push(false)
	e.render()
	mustContain(t, "C page", e.read("types/proj/code/c.html"),
		`<a class="path-dir" href="/code/proj/a">a</a><span class="sep" aria-hidden="true">/</span>`+
			`<a class="path-dir" href="/code/proj/a/b%20c">b c</a><span class="sep" aria-hidden="true">/</span>`+
			`<a href="/code/proj/a/b%20c/x.c">x.c</a>`)
}