- 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.
102 lines
2.3 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|