Files
arr-tui/internal/ui/cover_test.go
T

191 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("<html>not an image</html>")); 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 TestQuadCell(t *testing.T) {
red, blue, black := color.RGBA{255, 0, 0, 255}, color.RGBA{0, 0, 255, 255}, color.RGBA{0, 0, 0, 255}
for _, tc := range []struct {
name string
px [4]color.RGBA
wantCh rune
wantFg color.RGBA
wantBg color.RGBA
sameBoth bool
}{
{"flat", [4]color.RGBA{red, red, red, red}, ' ', red, red, true},
{"top half", [4]color.RGBA{red, red, blue, blue}, '▀', red, blue, false},
{"bottom half", [4]color.RGBA{blue, blue, red, red}, '▀', blue, red, false}, // same picture, canonical form
{"left half", [4]color.RGBA{red, blue, red, blue}, '▌', red, blue, false},
{"one corner", [4]color.RGBA{red, blue, blue, blue}, '▘', red, blue, false},
{"diagonal", [4]color.RGBA{red, black, black, red}, '▞', black, red, false},
} {
ch, fg, bg := quadCell(tc.px)
if ch != tc.wantCh || fg != tc.wantFg || bg != tc.wantBg {
t.Errorf("%s: %q fg=%v bg=%v, want %q fg=%v bg=%v", tc.name, ch, fg, bg, tc.wantCh, tc.wantFg, tc.wantBg)
}
}
// Nearly flat noise stays a flat cell instead of a distracting pattern.
n := color.RGBA{100, 100, 100, 255}
m := color.RGBA{102, 100, 101, 255}
if ch, _, _ := quadCell([4]color.RGBA{n, m, n, n}); ch != ' ' {
t.Errorf("noise rendered as %q", ch)
}
}
func TestCoverCells(t *testing.T) {
// A 2:3 poster in a roomy box is limited by the height: 10 rows are 20
// width units tall, so 13 columns.
if c, r := coverCells(40, 60, 30, 10); c != 13 || r != 10 {
t.Errorf("tall limit: %dx%d", c, r)
}
// In a narrow box the width limits it.
if c, r := coverCells(40, 60, 10, 20); c != 10 || r != 7 {
t.Errorf("narrow limit: %dx%d", c, r)
}
}
func TestCoverViewDraws(t *testing.T) {
screen := tcell.NewSimulationScreen("")
if err := screen.Init(); err != nil {
t.Fatal(err)
}
defer screen.Fini()
screen.SetSize(30, 10)
c := newCoverView()
c.setImage(stripes(40, 60)) // red above blue
c.SetRect(0, 0, 30, 10) // fits 13 columns × 10 rows
c.Draw(screen)
rgba := func(c tcell.Color) color.RGBA {
r, g, b := c.RGB()
return color.RGBA{uint8(r), uint8(g), uint8(b), 255}
}
cell := func(x, y int) (rune, color.RGBA, color.RGBA) {
ch, _, st, _ := screen.GetContent(x, y)
fg, bg, _ := st.Decompose()
return ch, rgba(fg), rgba(bg)
}
red, blue := color.RGBA{255, 0, 0, 255}, color.RGBA{0, 0, 255, 255}
if _, fg, bg := cell(0, 0); fg != red || bg != red {
t.Errorf("top row = fg %v bg %v, want red", fg, bg)
}
if _, fg, bg := cell(12, 9); fg != blue || bg != blue {
t.Errorf("bottom right = fg %v bg %v, want blue", fg, bg)
}
// Outside the poster's footprint nothing is drawn.
if ch, _, _, _ := screen.GetContent(13, 0); ch != ' ' {
t.Errorf("poster stretched beyond its aspect ratio: %q", ch)
}
}
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")
}
}