Files
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

267 lines
6.6 KiB
Go

package model
import (
"fmt"
"regexp"
"strings"
"testing"
"charm.land/lipgloss/v2"
"github.com/bjarneo/cliamp/playlist"
"github.com/bjarneo/cliamp/ui"
)
func withFrameWidth(t *testing.T, width int) {
t.Helper()
prevFrameStyle := ui.FrameStyle
prevPanelWidth := ui.PanelWidth
ui.FrameStyle = ui.FrameStyle.Width(width)
ui.PanelWidth = max(0, width-2*ui.PaddingH)
t.Cleanup(func() {
ui.FrameStyle = prevFrameStyle
ui.PanelWidth = prevPanelWidth
})
}
func TestMainViewShrinksPlaylistForFooterMessages(t *testing.T) {
if sharedPlayer == nil {
t.Skip("audio hardware unavailable")
}
withFrameWidth(t, 80)
pl := playlist.New()
for i := range 12 {
pl.Add(playlist.Track{
Path: fmt.Sprintf("/tmp/track-%d.mp3", i),
Title: fmt.Sprintf("Track %d", i+1),
})
}
m := Model{
player: sharedPlayer,
playlist: pl,
vis: ui.NewVisualizer(float64(sharedPlayer.SampleRate())),
width: 80,
plVisible: 3,
}
m.vis.Mode = ui.VisNone
m.save.startDownload()
m.status.Show("Saved", statusTTLDefault)
m.height = m.mainFrameFixedLines(true) + 1
if got := m.effectivePlaylistVisible(); got != 1 {
t.Fatalf("effectivePlaylistVisible() = %d, want 1 with one row left after footer lines", got)
}
if got := lipgloss.Height(m.View().Content); got > m.height {
t.Fatalf("View() height = %d, want <= %d after footer lines shrink playlist", got, m.height)
}
}
func TestRenderPlaylistKeepsCursorVisibleWhenFooterShrinksBudget(t *testing.T) {
if sharedPlayer == nil {
t.Skip("audio hardware unavailable")
}
withFrameWidth(t, 80)
sharedPlayer.Stop()
pl := playlist.New()
for i := range 12 {
pl.Add(playlist.Track{
Path: fmt.Sprintf("/tmp/track-%d.mp3", i),
Title: fmt.Sprintf("Track %d", i+1),
})
}
m := Model{
player: sharedPlayer,
playlist: pl,
vis: ui.NewVisualizer(float64(sharedPlayer.SampleRate())),
width: 80,
focus: focusPlaylist,
plVisible: 3,
plScroll: 7,
plCursor: 9,
}
m.vis.Mode = ui.VisNone
m.save.startDownload()
m.status.Show("Saved", statusTTLDefault)
m.height = m.mainFrameFixedLines(true) + 2
if got := m.effectivePlaylistVisible(); got != 2 {
t.Fatalf("effectivePlaylistVisible() = %d, want 2 with footer-shrunk playlist", got)
}
out := m.renderPlaylist()
if !strings.Contains(out, "Track 10") {
t.Fatalf("renderPlaylist() = %q, want selected row to remain visible", out)
}
}
func TestViewConsumesInitialVisualizerRefresh(t *testing.T) {
if sharedPlayer == nil {
t.Skip("audio hardware unavailable")
}
withFrameWidth(t, 80)
m := Model{
player: sharedPlayer,
playlist: playlist.New(),
vis: ui.NewVisualizer(float64(sharedPlayer.SampleRate())),
width: 80,
height: 24,
}
if !m.vis.RefreshPending() {
t.Fatal("refreshPending = false on new visualizer, want initial refresh request")
}
_ = m.View()
if m.vis.RefreshPending() {
t.Fatal("refreshPending = true after first View(), want refresh consumed")
}
if m.vis.Frame() != 1 {
t.Fatalf("visualizer frame after first View() = %d, want 1", m.vis.Frame())
}
}
func TestOverlayViewIncludesFooterMessages(t *testing.T) {
if sharedPlayer == nil {
t.Skip("audio hardware unavailable")
}
withFrameWidth(t, 80)
sharedPlayer.Stop()
// Footer/transient messages are now rendered by the inline overlay layout
// (mainSectionsOverlay) rather than by each overlay renderer.
m := Model{
player: sharedPlayer,
playlist: playlist.New(),
vis: ui.NewVisualizer(float64(sharedPlayer.SampleRate())),
width: 80,
height: 24,
plVisible: 5,
}
m.vis.Mode = ui.VisNone
m.refreshChrome()
m.applyHeightMode()
m.save.startDownload()
out := m.View().Content
if !strings.Contains(out, "Downloading...") {
t.Fatalf("overlay view missing download footer: %q", out)
}
}
func TestKeymapRendersInline(t *testing.T) {
if sharedPlayer == nil {
t.Skip("audio hardware unavailable")
}
withFrameWidth(t, 80)
sharedPlayer.Stop()
m := Model{
player: sharedPlayer,
playlist: playlist.New(),
vis: ui.NewVisualizer(float64(sharedPlayer.SampleRate())),
width: 80,
height: 24,
plVisible: 5,
keymap: keymapOverlay{
visible: true,
entries: []keymapEntry{{key: "Space", action: "Play/Pause"}},
},
}
m.vis.Mode = ui.VisNone
out := m.View().Content
if got := lipgloss.Height(out); got > m.height {
t.Fatalf("View() height = %d, want <= %d for inline keymap", got, m.height)
}
// The keymap renders in the playlist region, so its entries appear inline
// rather than as a standalone centered overlay.
if !strings.Contains(out, "Play/Pause") {
t.Fatalf("View() missing inline keymap entry: %q", out)
}
}
func TestFullVisualizerViewFitsTerminalWidth(t *testing.T) {
if sharedPlayer == nil {
t.Skip("audio hardware unavailable")
}
withFrameWidth(t, 80)
sharedPlayer.Stop()
m := Model{
player: sharedPlayer,
playlist: playlist.New(),
vis: ui.NewVisualizer(float64(sharedPlayer.SampleRate())),
width: 80,
height: 24,
fullVis: true,
}
m.vis.Mode = ui.VisNone
if got := lipgloss.Width(m.View().Content); got > m.width {
t.Fatalf("View() width = %d, want <= %d in full visualizer mode", got, m.width)
}
}
var stripAnsiRegExp = regexp.MustCompile(`\x1b\[[0-9;]*[mK]`)
func stripAnsi(str string) string {
return stripAnsiRegExp.ReplaceAllString(str, "")
}
func TestRenderPlaylistAddsPaddingToTrackNumber(t *testing.T) {
if sharedPlayer == nil {
t.Skip("audio hardware unavailable")
}
withFrameWidth(t, 80)
sharedPlayer.Stop()
pl := playlist.New()
for i := range 120 {
pl.Add(playlist.Track{
Path: fmt.Sprintf("/tmp/track-%d.mp3", i),
Title: fmt.Sprintf("Track %d", i+1),
})
}
m := Model{
player: sharedPlayer,
playlist: pl,
vis: ui.NewVisualizer(float64(sharedPlayer.SampleRate())),
width: 80,
plVisible: 120,
}
m.vis.Mode = ui.VisNone
m.height = m.mainFrameFixedLines(false) + 120
out := m.renderPlaylist()
lines := strings.Split(out, "\n")
if len(lines) < 120 {
t.Fatalf("renderPlaylist() returned %d lines, want 120", len(lines))
}
line9 := stripAnsi(lines[8])
line99 := stripAnsi(lines[98])
line119 := stripAnsi(lines[118])
ninthLineTrackIndex := strings.Index(line9, "Track")
ninetyNinthLineTrackIndex := strings.Index(line99, "Track")
oneHundredNineteenthLineTrackIndex := strings.Index(line119, "Track")
if ninthLineTrackIndex != ninetyNinthLineTrackIndex || ninthLineTrackIndex != oneHundredNineteenthLineTrackIndex {
t.Errorf(`Track name alignment is off for 3-digit numbers.
Line 9: %q (index %d)
Line 99: %q (index %d)
Line 119: %q (index %d)`, line9, ninthLineTrackIndex, line99, ninetyNinthLineTrackIndex, line119, oneHundredNineteenthLineTrackIndex)
}
}