267 lines
6.6 KiB
Go
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)
|
|
}
|
|
}
|