Vellum/internal/modules/comics/comic_test.go
mithral c3b7db7114 Phase 4: CBZ comics module and reader
- comics: reads pages straight out of the archive on demand. No extraction
  pass, so a 200-page CBZ is not materialised for a reader who views three
  pages. Pages are read into memory and served via http.ServeContent, which
  gives Range for free.
- natural page ordering: page2 sorts before page10. Lexical ordering shuffles
  a numbered archive, which is the difference between a readable book and a
  scrambled one.
- skips __MACOSX and dotfiles; rejects archives with no images.
- cover is the first page.
- media.Item gained ModTime (from the backing file) so page responses carry a
  Last-Modified and support conditional GETs.
- web: ComicReader with a bounded prefetch window (two pages each way), page
  jump, keyboard nav, and progress stored as a page number.

Verified: 3-page CBZ served in natural order, out-of-range 404, cover from
page 1, progress round-trip. svelte-check 0/0.
2026-10-09 21:07:59 -04:00

102 lines
2.3 KiB
Go

package comics
import (
"archive/zip"
"os"
"path/filepath"
"testing"
)
func writeCBZ(t *testing.T, entries map[string]string) string {
t.Helper()
p := filepath.Join(t.TempDir(), "test.cbz")
f, err := os.Create(p)
if err != nil {
t.Fatal(err)
}
defer f.Close()
zw := zip.NewWriter(f)
for name, body := range entries {
w, err := zw.Create(name)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write([]byte(body)); err != nil {
t.Fatal(err)
}
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
return p
}
func TestListPagesNaturalOrder(t *testing.T) {
p := writeCBZ(t, map[string]string{
"page10.jpg": "ten",
"page2.jpg": "two",
"page1.jpg": "one",
"notes.txt": "not an image",
"__MACOSX/._page1.jpg": "junk",
})
pages, err := ListPages(p)
if err != nil {
t.Fatalf("ListPages: %v", err)
}
want := []string{"page1.jpg", "page2.jpg", "page10.jpg"}
if len(pages) != len(want) {
t.Fatalf("got %d pages, want %d: %+v", len(pages), len(want), pages)
}
for i, name := range want {
if pages[i].Name != name {
t.Errorf("page %d = %q, want %q", i+1, pages[i].Name, name)
}
if pages[i].Index != i+1 {
t.Errorf("page %d index = %d, want %d", i, pages[i].Index, i+1)
}
}
}
func TestReadPage(t *testing.T) {
p := writeCBZ(t, map[string]string{"page1.jpg": "hello-page"})
data, err := ReadPage(p, "page1.jpg")
if err != nil {
t.Fatalf("ReadPage: %v", err)
}
if string(data) != "hello-page" {
t.Errorf("page content = %q", data)
}
if _, err := ReadPage(p, "missing.jpg"); err == nil {
t.Error("ReadPage succeeded for a missing entry")
}
}
func TestListPagesRejectsArchiveWithoutImages(t *testing.T) {
p := writeCBZ(t, map[string]string{"readme.txt": "nothing to see"})
if _, err := ListPages(p); err == nil {
t.Error("ListPages succeeded on an archive with no images")
}
}
func TestNaturalLess(t *testing.T) {
cases := []struct {
a, b string
want bool
}{
{"page1.jpg", "page2.jpg", true},
{"page2.jpg", "page10.jpg", true},
{"page10.jpg", "page2.jpg", false},
{"page01.jpg", "page1.jpg", false}, // equal numerically
{"page1.jpg", "page01.jpg", false},
{"a.jpg", "b.jpg", true},
}
for _, c := range cases {
if got := naturalLess(c.a, c.b); got != c.want {
t.Errorf("naturalLess(%q, %q) = %v, want %v", c.a, c.b, got, c.want)
}
}
}