e2e/diffweb_test.go
104 lines · 3643 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 // The +/- markers are CSS, so a copied selection is real source.
86 if strings.Contains(body, `<td class="code">+`) {
87 t.Error("diff markers are in the markup, not the stylesheet")
88 }
89}
90
91// jsonField pulls the first "name":"value" string out of a JSON blob.
92func jsonField(blob, name string) string {
93 key := `"` + name + `":"`
94 i := strings.Index(blob, key)
95 if i < 0 {
96 return ""
97 }
98 rest := blob[i+len(key):]
99 j := strings.IndexByte(rest, '"')
100 if j < 0 {
101 return ""
102 }
103 return rest[:j]
104}