oficina / estilo Public

Clone
git clone https://amadan.net/oficina/estilo

Plain git — no account needed to clone.

Download

Download this file

1package sky
2
3import (
4 "bytes"
5 "encoding/base64"
6 "encoding/xml"
7 "flag"
8 "html/template"
9 "image/png"
10 "io"
11 "net/http"
12 "net/http/httptest"
13 "os"
14 pathpkg "path"
15 "path/filepath"
16 "strings"
17 "testing"
18)
19
20func layers(id string) map[string]string {
21 out := map[string]string{}
22 for _, f := range []string{"haze-light.svg", "haze-dark.svg", "clouds-light.svg", "clouds-dark.svg", "grain.svg", "filaments.svg"} {
23 out[f] = draw(id, f)
24 }
25 return out
26}
27
28// A team's sky is the same every time it is drawn, anywhere, and not the
29// same as another team's.
30func TestSkiesAreSeededAndDistinct(t *testing.T) {
31 a, again, b := layers(ID("acme")), layers(ID("acme")), layers(ID("orchard"))
32 for f := range a {
33 if a[f] != again[f] {
34 t.Errorf("%s is not deterministic", f)
35 }
36 if a[f] == b[f] {
37 t.Errorf("%s is the same for two teams", f)
38 }
39 }
40}
41
42func TestEveryLayerIsWellFormedSVG(t *testing.T) {
43 for _, seed := range []string{SuiteSeed, "acme", "a team with spaces", ""} {
44 for f, body := range layers(ID(seed)) {
45 if strings.Contains(body, "NaN") || strings.Contains(body, "Inf") {
46 t.Errorf("%s for %q contains a non-number", f, seed)
47 }
48 d := xml.NewDecoder(strings.NewReader(body))
49 for {
50 _, err := d.Token()
51 if err == io.EOF {
52 break
53 }
54 if err != nil {
55 t.Errorf("%s for %q is not well-formed: %v", f, seed, err)
56 break
57 }
58 }
59 }
60 }
61}
62
63// Every app on the ring has a place in every sky.
64func TestEveryAppGathersInTheSky(t *testing.T) {
65 if got := len(anchors(ID(SuiteSeed))); got != 9 {
66 t.Errorf("the suite sky has %d anchors, want one per app (9)", got)
67 }
68}
69
70func TestIDIsClosedAndOpaque(t *testing.T) {
71 id := ID("acme")
72 if !idShape.MatchString(id) || strings.Contains(id, "acme") {
73 t.Errorf("ID(%q) = %q", "acme", id)
74 }
75}
76
77func TestHandlerServesOnlyTheSixLayers(t *testing.T) {
78 h := Handler()
79 id := ID("acme")
80 for path, want := range map[string]int{
81 Prefix + id + "/clouds-dark.svg": 200,
82 Prefix + id + "/filaments.svg": 200,
83 Prefix + id + "/grain.svg": 200,
84 Prefix + id + "/clouds.svg": 404,
85 Prefix + id + "/haze-light.svg/x": 404,
86 Prefix + "ACME0000000000000/haze-light.svg": 404,
87 Prefix + "acme/haze-light.svg": 404,
88 Prefix + id: 404,
89 "/static/estilo/sky/v0/" + id + "/haze-light.svg": 404,
90 } {
91 rec := httptest.NewRecorder()
92 h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
93 if rec.Code != want {
94 t.Errorf("GET %s = %d, want %d", path, rec.Code, want)
95 }
96 if want == 200 && (rec.Header().Get("Content-Type") != "image/svg+xml" || !strings.Contains(rec.Header().Get("Cache-Control"), "immutable")) {
97 t.Errorf("GET %s headers = %v", path, rec.Header())
98 }
99 }
100 rec := httptest.NewRecorder()
101 h.ServeHTTP(rec, httptest.NewRequest(http.MethodDelete, Prefix+id+"/filaments.svg", nil))
102 if rec.Code != http.StatusMethodNotAllowed {
103 t.Errorf("DELETE = %d, want 405", rec.Code)
104 }
105}
106
107func TestTemplateRendersEveryLayerDecoratively(t *testing.T) {
108 tmpl := template.Must(template.ParseFS(Templates(), "templates/*.html"))
109 var b bytes.Buffer
110 if err := tmpl.ExecuteTemplate(&b, "estilo-sky", For("acme")); err != nil {
111 t.Fatal(err)
112 }
113 html := b.String()
114 if !strings.Contains(html, `aria-hidden="true"`) || strings.Count(html, `alt=""`) != 4 || strings.Count(html, `estilo-sky-tone=`) != 2 ||
115 strings.Count(html, `src="data:image/png;base64,`) != 2 || strings.Contains(html, "ZgotmplZ") || strings.Contains(html, `loading="lazy"`) {
116 t.Errorf("unexpected markup:\n%s", html)
117 }
118 for _, l := range For("acme").Layers {
119 if l.Src != "" && !strings.Contains(html, `src="`+l.Src+`"`) {
120 t.Errorf("layer %s missing", l.Src)
121 }
122 }
123}
124
125var update = flag.Bool("update", false, "rewrite sky/testdata goldens (and bump Prefix's version)")
126
127// TestSkyGoldens pins what one ID draws. A drawing change — to generate.go,
128// or to the ring's hues in estilo's theme, which the sky is made of — fails
129// here, and a failure here means Prefix's version must change: browsers
130// hold each file for a year under its versioned URL.
131func TestSkyGoldens(t *testing.T) {
132 for file, body := range layers(ID("golden")) {
133 path := filepath.Join("testdata", pathpkg.Base(Prefix)+"-"+file)
134 if *update {
135 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
136 t.Fatal(err)
137 }
138 continue
139 }
140 want, err := os.ReadFile(path)
141 if err != nil {
142 t.Fatalf("%v (run with -update after bumping Prefix)", err)
143 }
144 if string(want) != body {
145 t.Errorf("%s draws differently from its golden: bump Prefix's version, then -update", file)
146 }
147 }
148}
149
150// The placeholder is a small, deterministic PNG per sky and scheme, and it
151// is the sky it stands in for: not blank, and not another team's.
152func TestPlaceholderIsTheSkyInAFewPixels(t *testing.T) {
153 id := ID("acme")
154 for _, tone := range []Tone{Light, Dark} {
155 uri := Placeholder(id, tone)
156 if uri != Placeholder(id, tone) || uri == Placeholder(ID("orchard"), tone) {
157 t.Errorf("%s placeholder is not deterministic per sky", tone)
158 }
159 raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(uri, "data:image/png;base64,"))
160 if err != nil {
161 t.Fatal(err)
162 }
163 img, err := png.Decode(bytes.NewReader(raw))
164 if err != nil {
165 t.Fatal(err)
166 }
167 if b := img.Bounds(); b.Dx() != phW || b.Dy() != phH {
168 t.Errorf("placeholder is %v", b)
169 }
170 var opaque int
171 for y := range phH {
172 for x := range phW {
173 if _, _, _, a := img.At(x, y).RGBA(); a > 0x4000 {
174 opaque++
175 }
176 }
177 }
178 if opaque < phW*phH/4 {
179 t.Errorf("%s placeholder is mostly empty (%d of %d pixels)", tone, opaque, phW*phH)
180 }
181 if len(uri) > 4096 {
182 t.Errorf("%s placeholder is %d bytes inline; keep it small", tone, len(uri))
183 }
184 }
185}
186