package main import ( "bytes" "image" "image/jpeg" "os" "path/filepath" "strings" "testing" ) // buildSite builds a site of three photos spread over two days, with a page // size of two so both views need a second page, and returns the output dir. func buildSite(t *testing.T, output, defaultView string) { t.Helper() content := t.TempDir() var img bytes.Buffer if err := jpeg.Encode(&img, image.NewRGBA(image.Rect(0, 0, 8, 8)), nil); err != nil { t.Fatal(err) } for slug, date := range map[string]string{ "one": "2026-06-14", "two": "2026-06-14", "three": "2026-06-10", } { writeFile(t, filepath.Join(content, slug+".jpg"), img.Bytes()) writeFile(t, filepath.Join(content, slug+".toml"), []byte("caption = \""+slug+"\"\ndate = \""+date+"\"\n")) } g := NewGenerator(content, output, "https://photos.example.com", "Ada", "", "", "", "", "Crimson Pro", "Manjari", 2, defaultView) if err := g.Build(); err != nil { t.Fatal(err) } } func writeFile(t *testing.T, path string, data []byte) { t.Helper() if err := os.WriteFile(path, data, 0644); err != nil { t.Fatal(err) } } func readPage(t *testing.T, output, rel string) string { t.Helper() data, err := os.ReadFile(filepath.Join(output, filepath.FromSlash(rel), "index.html")) if err != nil { t.Fatal(err) } return string(data) } // Timeline pages carry day sections, grid pages do not. func isTimeline(page string) bool { return strings.Contains(page, `class="tl-day"`) } func TestDefaultViewOwnsSiteRoot(t *testing.T) { tests := []struct { defaultView string gridAt string // URL of the grid's first page timelineAt string // URL of the timeline's first page }{ {viewTimeline, "/grid/", "/"}, {viewGrid, "/", "/timeline/"}, {"nonsense", "/grid/", "/"}, // unknown values fall back to the timeline } for _, tt := range tests { t.Run(tt.defaultView, func(t *testing.T) { output := t.TempDir() buildSite(t, output, tt.defaultView) timeline := readPage(t, output, tt.timelineAt) grid := readPage(t, output, tt.gridAt) if !isTimeline(timeline) { t.Errorf("%s is not the timeline", tt.timelineAt) } if isTimeline(grid) { t.Errorf("%s is not the grid", tt.gridAt) } // The views link to each other, and to their own second page. for page, want := range map[string]string{ timeline: `href="` + tt.gridAt + `" class="rss-a">Grid`, grid: `href="` + tt.timelineAt + `" class="rss-a">Timeline`, } { if !strings.Contains(page, want) { t.Errorf("missing link %q", want) } } for first, base := range map[string]string{ timeline: strings.TrimSuffix(tt.timelineAt, "/"), grid: strings.TrimSuffix(tt.gridAt, "/"), } { next := base + "/page/2/" if !strings.Contains(first, `href="`+next+`" data-load-more`) { t.Errorf("first page does not link to %s", next) } second := readPage(t, output, next) if !strings.Contains(second, `class="pager-newer" href="`+base+`/"`) { t.Errorf("%s does not link back to %s/", next, base) } } // Day pages lead back to the timeline, wherever it lives. day := readPage(t, output, "/day/2026-06-14/") if !strings.Contains(day, `href="`+tt.timelineAt+`" class="rss-a">← Timeline`) { t.Errorf("day page does not link to the timeline at %s", tt.timelineAt) } }) } } func TestSwitchingDefaultViewDropsOldViewDir(t *testing.T) { output := t.TempDir() buildSite(t, output, viewTimeline) if _, err := os.Stat(filepath.Join(output, "grid", "index.html")); err != nil { t.Fatalf("grid view missing before the switch: %v", err) } buildSite(t, output, viewGrid) if _, err := os.Stat(filepath.Join(output, "grid")); !os.IsNotExist(err) { t.Errorf("stale /grid/ survived the switch to -default-view grid") } if isTimeline(readPage(t, output, "/")) { t.Errorf("site root is still the timeline") } }