Files
arr-tui/internal/ui/detail_test.go
T
nilsandClaude Opus 5.5 a71ca6e3fb Posters wait while the selection moves quickly
On a terminal zoomed far out a poster is tens of thousands of cells, and
redrawing it on every step of a held arrow key was more output than the
terminal could draw (about 100 KB per step at 600x180), so the list
lagged. A selection that changes again within 120 ms keeps the poster's
place empty and shows the poster once it rests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
2026-10-21 11:31:51 +02:00

140 lines
4.7 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 (
"context"
"image"
"strings"
"testing"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
func rowText(screen tcell.SimulationScreen, y, x0, x1 int) string {
var b strings.Builder
for x := x0; x < x1; x++ {
ch, _, _, _ := screen.GetContent(x, y)
b.WriteRune(ch)
}
return b.String()
}
func TestFlowWrap(t *testing.T) {
text := "one two three four five six seven eight nine ten"
// Narrow (10) for the first two lines, wide (25) afterwards.
got := flowWrap(text, 10, 25, 2)
want := []string{"one two", "three four", "five six seven eight nine", "ten"}
if strings.Join(got, "|") != strings.Join(want, "|") {
t.Errorf("got %q, want %q", got, want)
}
// Without a poster everything is wide.
if got := flowWrap(text, 10, 25, 0); got[0] != "one two three four five" {
t.Errorf("no poster: %q", got)
}
// Newlines are kept, blank lines too; trailing blank lines are dropped.
got = flowWrap("head\nfacts\n\nplot words\n", 20, 20, 0)
if strings.Join(got, "|") != "head|facts||plot words" {
t.Errorf("newlines: %q", got)
}
// Color tags have no width and escaped brackets count as one character.
got = flowWrap("[red]ab[-] cd ef", 5, 5, 0)
if strings.Join(got, "|") != "[red]ab[-] cd|ef" {
t.Errorf("tags: %q", got)
}
if got := flowWrap(tview.Escape("[1080p]")+" x", 9, 9, 0); len(got) != 1 {
t.Errorf("escaped brackets: %q", got)
}
// A word wider than the column is not split or lost.
if got := flowWrap("a verylongwordhere b", 5, 5, 0); strings.Join(got, "|") != "a|verylongwordhere|b" {
t.Errorf("long word: %q", got)
}
}
func TestDetailPaneFlowsAroundPoster(t *testing.T) {
a := newTestApp(t)
screen := tcell.NewSimulationScreen("")
if err := screen.Init(); err != nil {
t.Fatal(err)
}
defer screen.Fini()
screen.SetSize(60, 40)
plot := strings.Repeat("plot ", 220) // long enough to run past the poster
d := newDetailPane(a)
d.SetRect(0, 0, 60, 40)
d.Show(detailText{head: "[::b]The Matrix (1999)[::-]\nStatus: released\n", plot: plot}, coverRef{})
// Without a poster the text uses the full width from the top.
d.Draw(screen)
if got := rowText(screen, 1, 1, 30); !strings.HasPrefix(got, "The Matrix (1999)") {
t.Errorf("first line = %q", got)
}
if got := strings.TrimSpace(rowText(screen, 5, 1, 59)); len(got) < 50 {
t.Errorf("without a poster the text should use the full width: %q", got)
}
// With a poster (23×17 cells at the top right) the text runs beside it.
d.hasCover = true
d.cover.setImage(stripes(40, 60))
d.Draw(screen)
if d.coverW != 23 || d.coverH != 17 {
t.Fatalf("poster size %dx%d, want 23x17", d.coverW, d.coverH)
}
if got := rowText(screen, 1, 1, 30); !strings.HasPrefix(got, "The Matrix (1999)") {
t.Errorf("head moved: %q", got)
}
if got := rowText(screen, 4, 1, 30); !strings.HasPrefix(got, "plot plot") {
t.Errorf("the plot starts beside the poster, right after the head: %q", got)
}
// Beside the poster the text stays left of it with a gap of two columns
// (58-23-2 = 33 columns), and the row right under the poster is kept clear too.
for y := 1; y <= d.coverH+coverGapRows; y++ {
if got := strings.TrimRight(rowText(screen, y, 1, 36), " "); len([]rune(got)) > 33 {
t.Errorf("row %d runs into the poster's margin: %q", y, got)
}
}
// Further down it uses the full width.
if got := strings.TrimSpace(rowText(screen, d.coverH+3, 1, 59)); len(got) < 50 {
t.Errorf("below the poster the text should use the full width: %q", got)
}
// The poster sits against the right edge of the pane.
if ch, _, st, _ := screen.GetContent(58, 1); ch == ' ' && st == tcell.StyleDefault {
t.Error("nothing drawn at the poster's right edge")
}
// A pane too short for the poster falls back to plain text.
d.SetRect(0, 0, 60, 10)
d.Draw(screen)
if d.coverW != 0 {
t.Error("a short pane must drop the poster")
}
}
func TestPosterWaitsWhileTheSelectionMoves(t *testing.T) {
a := newTestApp(t)
img := image.NewRGBA(image.Rect(0, 0, 20, 30))
a.coverCache.imgs["one"], a.coverCache.imgs["two"] = img, img
ref := func(key string) coverRef {
return coverRef{key: key, fetch: func(context.Context) ([]byte, error) { return nil, nil }}
}
d := newDetailPane(a)
d.Show(detailText{head: "One"}, ref("one"))
if d.cover.img != img {
t.Fatal("the first selection should show its poster at once")
}
d.Show(detailText{head: "Two"}, ref("two"))
if d.cover.img != nil || !d.hasCover {
t.Errorf("a quick move should keep the poster's place empty: img %v, hasCover %v", d.cover.img != nil, d.hasCover)
}
d.settle.Stop() // the test has no event loop to deliver it
// The same title again (a refresh) is not a move.
d.lastShow = time.Now().Add(-time.Second)
d.Show(detailText{head: "Two"}, ref("two"))
if d.cover.img != img {
t.Error("a selection at rest should show its poster")
}
}