e2e/diffweb_test.go

v1.27.0
gitbay/e2e/diffweb_test.go history · blame · raw

141 lines · 5217 bytes

  1package e2e
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"strings"
  7	"testing"
  8)
  9
 10// TestDiffRendering covers the shared diff view: per-file folds with stats,
 11// line-number gutters, syntax highlighting, and binary files declared
 12// rather than dumped.
 13func TestDiffRendering(t *testing.T) {
 14	inst := startInstance(t)
 15	aliceKey := inst.newKey(t, "alice")
 16	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 17	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
 18		t.Fatalf("repo create: %s", errOut)
 19	}
 20
 21	work := t.TempDir()
 22	env := inst.gitEnv(aliceKey)
 23	mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
 24	dir := filepath.Join(work, "w")
 25	write := func(name, body string) {
 26		os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644)
 27	}
 28	write("main.go", "package main\n\nfunc greet() string {\n\treturn \"hello\"\n}\n")
 29	write("notes.txt", "old title\n")
 30	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
 31	mustGit(t, dir, env, "add", ".")
 32	mustGit(t, dir, env, "commit", "-q", "-m", "base")
 33	mustGit(t, dir, env, "push", "-q", "origin", "main")
 34
 35	// One commit touching three files in three different ways.
 36	write("main.go", "package main\n\nfunc greet() string {\n\t// now with feeling\n\treturn \"HELLO\"\n}\n")
 37	os.Rename(filepath.Join(dir, "notes.txt"), filepath.Join(dir, "README.md"))
 38	os.WriteFile(filepath.Join(dir, "logo.png"), []byte("\x89PNG\r\n\x1a\n\x00\x00binary"), 0o644)
 39	mustGit(t, dir, env, "add", "-A")
 40	mustGit(t, dir, env, "commit", "-q", "-m", "rework")
 41	mustGit(t, dir, env, "push", "-q", "origin", "main")
 42
 43	out, _, _ := inst.ssh(t, aliceKey, "", "repo", "log", "alice/app", "--json")
 44	sha := jsonField(out, "sha")
 45	if sha == "" {
 46		t.Fatalf("no sha in log: %s", out)
 47	}
 48
 49	status, body := inst.get(t, "/alice/app/commit/"+sha)
 50	if status != 200 {
 51		t.Fatalf("commit page: %d", status)
 52	}
 53
 54	// A fold per file, each naming its path.
 55	for _, path := range []string{"main.go", "logo.png"} {
 56		if !strings.Contains(body, ">"+path+"<") {
 57			t.Errorf("no section for %s", path)
 58		}
 59	}
 60	if strings.Count(body, `<details class="difffold"`) != 3 {
 61		t.Errorf("want 3 file sections, got %d", strings.Count(body, `<details class="difffold"`))
 62	}
 63	// The rename is shown as one, not as an add plus a delete.
 64	if !strings.Contains(body, "notes.txt") || !strings.Contains(body, "renamed") {
 65		t.Error("rename not shown as a rename")
 66	}
 67	// Binary content is declared, never dumped into the page.
 68	if !strings.Contains(body, "Binary file not shown") {
 69		t.Error("binary file not declared")
 70	}
 71	if strings.Contains(body, "\x89PNG") {
 72		t.Error("binary content leaked into the diff")
 73	}
 74	// Line-number gutters and per-file stats.
 75	if !strings.Contains(body, `<td class="ln">`) {
 76		t.Error("no line-number gutter")
 77	}
 78	if !strings.Contains(body, `<span class="add">+`) || !strings.Contains(body, `<span class="del">−`) {
 79		t.Error("no per-file stat")
 80	}
 81	// Go is a type chroma knows, so the added line carries token markup.
 82	if !strings.Contains(body, "class=\"k\"") && !strings.Contains(body, "class=\"kd\"") {
 83		t.Error("diff content is not syntax highlighted")
 84	}
 85	// Token classes alone prove nothing: chroma scopes every colour rule
 86	// under .chroma, and the per-line markup has no wrapper of its own, so
 87	// the cell has to be that wrapper or the classes are inert.
 88	if !strings.Contains(body, `<td class="src chroma">`) {
 89		t.Error("diff cell is not a chroma wrapper, so token classes go unstyled")
 90	}
 91	css := stylesheet(t, inst)
 92	if !strings.Contains(css, ".chroma .k") {
 93		t.Error("stylesheet has no chroma token rules to match")
 94	}
 95	// Each palette is scoped to its own scheme. Unscoped, the light one
 96	// leaks into dark mode for every token the dark palette does not name.
 97	if !strings.Contains(css, "@media (prefers-color-scheme: light)") ||
 98		!strings.Contains(css, "@media (prefers-color-scheme: dark)") {
 99		t.Error("chroma palettes are not both scheme-scoped")
100	}
101	if i := strings.Index(css, ".chroma .na"); i >= 0 {
102		if j := strings.LastIndex(css[:i], "prefers-color-scheme"); j < 0 {
103			t.Error("a chroma token rule sits outside any scheme scope")
104		}
105	}
106	// The +/- markers are CSS, so a copied selection is real source.
107	if strings.Contains(body, `<td class="src">+`) {
108		t.Error("diff markers are in the markup, not the stylesheet")
109	}
110	// The cell must not use .code: that class carries its own background,
111	// which would paint over the add/delete row tint.
112	if strings.Contains(body, `<td class="code">`) {
113		t.Error("diff cells use the blob code class, whose background hides the row tint")
114	}
115}
116
117// jsonField pulls the first "name":"value" string out of a JSON blob.
118func jsonField(blob, name string) string {
119	key := `"` + name + `":"`
120	i := strings.Index(blob, key)
121	if i < 0 {
122		return ""
123	}
124	rest := blob[i+len(key):]
125	j := strings.IndexByte(rest, '"')
126	if j < 0 {
127		return ""
128	}
129	return rest[:j]
130}
131
132// stylesheet fetches the served CSS, so a test can check that a rule the
133// markup depends on actually exists.
134func stylesheet(t *testing.T, inst *instance) string {
135	t.Helper()
136	status, body := inst.get(t, "/static/style.css")
137	if status != 200 {
138		t.Fatalf("stylesheet: %d", status)
139	}
140	return body
141}