From 11bdd9751b860ba0c3b740a3e51114597b17229c Mon Sep 17 00:00:00 2001 From: mkelvers Date: Fri, 3 Jul 2026 16:16:12 +0200 Subject: [PATCH] test: add manifest icon and first-load asset size validation --- templates/brand_assets_test.go | 118 +++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 templates/brand_assets_test.go diff --git a/templates/brand_assets_test.go b/templates/brand_assets_test.go new file mode 100644 index 00000000..46ddeb52 --- /dev/null +++ b/templates/brand_assets_test.go @@ -0,0 +1,118 @@ +package templates + +import ( + "encoding/json" + "image/png" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +type manifestIcon struct { + Src string `json:"src"` + Sizes string `json:"sizes"` +} + +func TestManifestIconDimensions(t *testing.T) { + data, err := os.ReadFile("../static/assets/manifest.json") + if err != nil { + t.Fatalf("read manifest: %v", err) + } + + var manifest struct { + Icons []manifestIcon `json:"icons"` + } + if err := json.Unmarshal(data, &manifest); err != nil { + t.Fatalf("parse manifest: %v", err) + } + if len(manifest.Icons) == 0 { + t.Fatal("manifest has no icons") + } + + for _, icon := range manifest.Icons { + t.Run(icon.Sizes, func(t *testing.T) { + width, height := parseIconSize(t, icon.Sizes) + assetURL, err := url.Parse(icon.Src) + if err != nil { + t.Fatalf("parse icon URL: %v", err) + } + if assetURL.Query().Get("v") == "" { + t.Fatalf("icon URL is not versioned: %q", icon.Src) + } + + file, err := os.Open(filepath.Join("..", strings.TrimPrefix(assetURL.Path, "/"))) + if err != nil { + t.Fatalf("open icon: %v", err) + } + defer file.Close() + + config, err := png.DecodeConfig(file) + if err != nil { + t.Fatalf("decode icon: %v", err) + } + if config.Width != width || config.Height != height { + t.Fatalf("icon dimensions = %dx%d, manifest declares %s", config.Width, config.Height, icon.Sizes) + } + }) + } +} + +func TestFirstLoadBrandAssetSizes(t *testing.T) { + tests := []struct { + name string + path string + width int + height int + maxBytes int64 + }{ + {name: "navigation logo", path: "../static/assets/logo-128.png", width: 166, height: 128, maxBytes: 25 * 1024}, + {name: "32px favicon", path: "../static/assets/favicon-32.png", width: 32, height: 32, maxBytes: 20 * 1024}, + {name: "16px favicon", path: "../static/assets/favicon-16.png", width: 16, height: 16, maxBytes: 20 * 1024}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + file, err := os.Open(tt.path) + if err != nil { + t.Fatalf("open asset: %v", err) + } + defer file.Close() + + info, err := file.Stat() + if err != nil { + t.Fatalf("stat asset: %v", err) + } + if info.Size() > tt.maxBytes { + t.Fatalf("asset size = %d bytes, max %d", info.Size(), tt.maxBytes) + } + + config, err := png.DecodeConfig(file) + if err != nil { + t.Fatalf("decode asset: %v", err) + } + if config.Width != tt.width || config.Height != tt.height { + t.Fatalf("asset dimensions = %dx%d, want %dx%d", config.Width, config.Height, tt.width, tt.height) + } + }) + } +} + +func parseIconSize(t *testing.T, value string) (int, int) { + t.Helper() + widthValue, heightValue, ok := strings.Cut(value, "x") + if !ok { + t.Fatalf("invalid icon size %q", value) + } + width, err := strconv.Atoi(widthValue) + if err != nil { + t.Fatalf("parse icon width: %v", err) + } + height, err := strconv.Atoi(heightValue) + if err != nil { + t.Fatalf("parse icon height: %v", err) + } + return width, height +}