/manage/delete/{slug} asks for confirmation, then removes the image,
its sidecar and everything generated from them. Builds now drop day
pages of days without photos and pager pages past the last one, so a
delete or a date change leaves nothing behind.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
159 lines
4.8 KiB
Go
159 lines
4.8 KiB
Go
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 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")
|
|
}
|
|
}
|
|
|
|
func TestShrinkingSiteDropsLeftoverPages(t *testing.T) {
|
|
output := t.TempDir()
|
|
buildSite(t, output, viewTimeline) // three photos, page size two
|
|
|
|
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)
|
|
}
|
|
writeFile(t, filepath.Join(content, "one.jpg"), img.Bytes())
|
|
writeFile(t, filepath.Join(content, "one.toml"),
|
|
[]byte("caption = \"one\"\ndate = \"2026-06-14\"\n"))
|
|
|
|
// The same output directory, now fed a single photo: one page per view,
|
|
// one day.
|
|
g := NewGenerator(content, output, "https://photos.example.com",
|
|
"Ada", "", "", "", "", "Crimson Pro", "Manjari", 2, viewTimeline)
|
|
if err := g.Build(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, gone := range []string{"page/2", "grid/page/2", "day/2026-06-10"} {
|
|
if _, err := os.Stat(filepath.Join(output, filepath.FromSlash(gone))); !os.IsNotExist(err) {
|
|
t.Errorf("leftover %s survived", gone)
|
|
}
|
|
}
|
|
for _, kept := range []string{"index.html", "grid/index.html", "day/2026-06-14/index.html"} {
|
|
if _, err := os.Stat(filepath.Join(output, filepath.FromSlash(kept))); err != nil {
|
|
t.Errorf("%s is missing: %v", kept, err)
|
|
}
|
|
}
|
|
}
|