package ui import ( "bytes" "context" "image" "image/color" "image/jpeg" "image/png" "testing" "time" "github.com/gdamore/tcell/v2" ) // stripes returns a w×h image whose top half is red and bottom half blue. func stripes(w, h int) *image.RGBA { img := image.NewRGBA(image.Rect(0, 0, w, h)) for y := 0; y < h; y++ { for x := 0; x < w; x++ { c := color.RGBA{255, 0, 0, 255} if y >= h/2 { c = color.RGBA{0, 0, 255, 255} } img.SetRGBA(x, y, c) } } return img } func TestBoxScaleAverages(t *testing.T) { src := image.NewRGBA(image.Rect(0, 0, 2, 2)) src.SetRGBA(0, 0, color.RGBA{200, 0, 0, 255}) src.SetRGBA(1, 0, color.RGBA{0, 0, 0, 255}) src.SetRGBA(0, 1, color.RGBA{0, 0, 0, 255}) src.SetRGBA(1, 1, color.RGBA{0, 0, 0, 255}) got := boxScale(src, 1, 1).RGBAAt(0, 0) if got.R < 49 || got.R > 51 || got.G != 0 || got.A != 255 { t.Errorf("average = %+v, want R≈50", got) } // Upscaling must not leave holes. up := boxScale(stripes(1, 2), 3, 4) if up.RGBAAt(2, 0).R != 255 || up.RGBAAt(2, 3).B != 255 { t.Errorf("upscaled corners: %+v %+v", up.RGBAAt(2, 0), up.RGBAAt(2, 3)) } } func TestDecodeCover(t *testing.T) { var buf bytes.Buffer if err := jpeg.Encode(&buf, stripes(300, 450), nil); err != nil { t.Fatal(err) } img, err := decodeCover(buf.Bytes()) if err != nil { t.Fatal(err) } if b := img.Bounds(); b.Dy() != storedCoverHeight || b.Dx() != 106 { t.Errorf("stored size %v, want 106x%d", b, storedCoverHeight) } buf.Reset() if err := png.Encode(&buf, stripes(20, 30)); err != nil { t.Fatal(err) } if img, err := decodeCover(buf.Bytes()); err != nil || img.Bounds().Dy() != 30 { t.Errorf("small png kept as is: %v %v", img, err) } if _, err := decodeCover([]byte("not an image")); err == nil { t.Error("garbage must be rejected") } // A header that promises a huge image is refused before decoding it. var huge bytes.Buffer huge.Write([]byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x27\x10\x00\x00\x27\x10\x08\x02\x00\x00\x00")) if _, err := decodeCover(huge.Bytes()); err == nil { t.Error("oversized image must be rejected") } } func TestCoverViewDrawsHalfBlocks(t *testing.T) { screen := tcell.NewSimulationScreen("") if err := screen.Init(); err != nil { t.Fatal(err) } defer screen.Fini() screen.SetSize(20, 10) c := newCoverView() c.setImage(stripes(40, 60)) // 2:3 poster c.SetRect(0, 0, 20, 10) // 20 columns × 20 pixel rows: a 13×20 pixel poster c.Draw(screen) // Row 0 holds pixel rows 0 (top) and 1: both red. The middle cell row // (index 4/5) crosses into blue; the last row is blue on both halves. rgba := func(c tcell.Color) color.RGBA { r, g, b := c.RGB() return color.RGBA{uint8(r), uint8(g), uint8(b), 255} } check := func(row int, wantFg, wantBg color.RGBA) { t.Helper() ch, _, style, _ := screen.GetContent(0, row) fg, bg, _ := style.Decompose() if ch != '▀' || rgba(fg) != wantFg || rgba(bg) != wantBg { t.Errorf("row %d: %q fg=%v bg=%v", row, ch, rgba(fg), rgba(bg)) } } red, blue := color.RGBA{255, 0, 0, 255}, color.RGBA{0, 0, 255, 255} check(0, red, red) check(9, blue, blue) // Beyond the poster's width nothing is drawn. if ch, _, _, _ := screen.GetContent(19, 0); ch == '▀' { t.Error("poster stretched beyond its aspect ratio") } } func TestLoadCoverCachesAndDedupes(t *testing.T) { a := newTestApp(t) fetches := 0 var buf bytes.Buffer png.Encode(&buf, stripes(10, 20)) ref := coverRef{key: "k", fetch: func(context.Context) ([]byte, error) { fetches++ return buf.Bytes(), nil }} // Run posted callbacks inline instead of through the (not running) event loop. delivered := make(chan error, 2) a.loadCover(ref, func(_ *image.RGBA, err error) { delivered <- err }) a.loadCover(ref, func(_ *image.RGBA, err error) { delivered <- err }) // joins the request in flight if len(a.coverCache.waiting["k"]) != 2 { t.Fatalf("waiters = %d, want 2", len(a.coverCache.waiting["k"])) } if fetches > 1 { t.Errorf("fetched %d times", fetches) } } func TestLoadCoverRemembersFailures(t *testing.T) { a := newTestApp(t) a.coverCache.failed["bad"] = time.Now() var gotErr error a.loadCover(coverRef{key: "bad", fetch: func(context.Context) ([]byte, error) { t.Error("a recent failure must not be retried") return nil, nil }}, func(_ *image.RGBA, err error) { gotErr = err }) if gotErr == nil { t.Error("expected an error for a recently failed cover") } }