Files
bjarneo--cliamp/ui/model/stream_seek_keys_test.go
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

189 lines
6.6 KiB
Go

package model
import (
"testing"
"time"
tea "charm.land/bubbletea/v2"
"github.com/bjarneo/cliamp/internal/playback"
"github.com/bjarneo/cliamp/ipc"
)
type fakeEngine struct {
streamSeek bool
seekCalls []time.Duration
position time.Duration
}
func (f *fakeEngine) Play(string, time.Duration) error { return nil }
func (f *fakeEngine) PlayYTDL(string, time.Duration) error { return nil }
func (f *fakeEngine) Preload(string, time.Duration) error { return nil }
func (f *fakeEngine) PreloadYTDL(string, time.Duration) error { return nil }
func (f *fakeEngine) ClearPreload() {}
func (f *fakeEngine) Stop() {}
func (f *fakeEngine) Close() {}
func (f *fakeEngine) TogglePause() {}
func (f *fakeEngine) Seek(d time.Duration) error { f.seekCalls = append(f.seekCalls, d); return nil }
func (f *fakeEngine) SeekYTDL(time.Duration) error { return nil }
func (f *fakeEngine) CancelSeekYTDL() {}
func (f *fakeEngine) IsPlaying() bool { return true }
func (f *fakeEngine) IsPaused() bool { return false }
func (f *fakeEngine) Drained() bool { return false }
func (f *fakeEngine) HasPreload() bool { return false }
func (f *fakeEngine) Seekable() bool { return f.streamSeek }
func (f *fakeEngine) IsStreamSeek() bool { return f.streamSeek }
func (f *fakeEngine) IsYTDLSeek() bool { return false }
func (f *fakeEngine) GaplessAdvanced() bool { return false }
func (f *fakeEngine) Position() time.Duration { return f.position }
func (f *fakeEngine) Duration() time.Duration { return time.Hour }
func (f *fakeEngine) PositionAndDuration() (time.Duration, time.Duration) { return 0, time.Hour }
func (f *fakeEngine) SetVolumeMin(float64) {}
func (f *fakeEngine) VolumeMin() float64 { return -50 }
func (f *fakeEngine) SetVolume(float64) {}
func (f *fakeEngine) Volume() float64 { return 0 }
func (f *fakeEngine) SetSpeed(float64) {}
func (f *fakeEngine) Speed() float64 { return 1 }
func (f *fakeEngine) ToggleMono() {}
func (f *fakeEngine) Mono() bool { return false }
func (f *fakeEngine) SetEQBand(int, float64) {}
func (f *fakeEngine) EQBands() [10]float64 { return [10]float64{} }
func (f *fakeEngine) StreamErr() error { return nil }
func (f *fakeEngine) StreamTitle() string { return "" }
func (f *fakeEngine) StreamBytes() (downloaded, total int64) { return 0, 0 }
func (f *fakeEngine) SamplesInto([]float64) int { return 0 }
func (f *fakeEngine) SampleRate() int { return 44100 }
func assertStreamSeekCmd(t *testing.T, eng *fakeEngine, cmd tea.Cmd, want time.Duration) {
t.Helper()
if cmd == nil {
t.Fatal("cmd = nil, want seek cmd for HTTP stream")
}
msg := cmd()
if _, ok := msg.(seekTickMsg); !ok {
t.Fatalf("cmd() msg = %T, want seekTickMsg", msg)
}
if len(eng.seekCalls) != 1 {
t.Fatalf("Seek call count after cmd() = %d, want 1", len(eng.seekCalls))
}
if got := eng.seekCalls[0]; got != want {
t.Fatalf("Seek arg = %v, want %v", got, want)
}
}
func assertDeferredStreamSeek(t *testing.T, eng *fakeEngine, cmd tea.Cmd, position, want time.Duration) {
t.Helper()
if len(eng.seekCalls) != 0 {
t.Fatalf("Seek call count before cmd() = %d, want 0", len(eng.seekCalls))
}
eng.position = position
assertStreamSeekCmd(t, eng, cmd, want)
}
func assertImmediateStreamSeek(t *testing.T, eng *fakeEngine, cmd tea.Cmd, want time.Duration) {
t.Helper()
if cmd != nil {
t.Fatalf("cmd = %v, want nil for synchronous seek", cmd)
}
if len(eng.seekCalls) != 1 {
t.Fatalf("Seek call count = %d, want 1", len(eng.seekCalls))
}
if got := eng.seekCalls[0]; got != want {
t.Fatalf("Seek arg = %v, want %v", got, want)
}
}
func TestDeferredHTTPStreamSeek(t *testing.T) {
cases := []struct {
name string
initialPos time.Duration
settlePos time.Duration
want time.Duration
invoke func(*Model) tea.Cmd
}{
{
name: "right key",
settlePos: 8 * time.Second,
want: 5 * time.Second,
invoke: func(m *Model) tea.Cmd {
return m.handleKey(tea.KeyPressMsg{Code: tea.KeyRight})
},
},
{
name: "set position update",
initialPos: 3 * time.Second,
settlePos: 5 * time.Second,
want: 5 * time.Second,
invoke: func(m *Model) tea.Cmd {
_, cmd := m.Update(playback.SetPositionMsg{Position: 10 * time.Second})
return cmd
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
eng := &fakeEngine{streamSeek: true, position: tt.initialPos}
m := Model{player: eng}
cmd := tt.invoke(&m)
assertDeferredStreamSeek(t, eng, cmd, tt.settlePos, tt.want)
})
}
}
func TestImmediateHTTPStreamSeek(t *testing.T) {
cases := []struct {
name string
want time.Duration
invoke func(*Model) tea.Cmd
check func(*testing.T, *Model)
}{
{
name: "jump enter",
want: 7 * time.Second,
invoke: func(m *Model) tea.Cmd {
m.jumping = true
m.jumpInput = "10"
return m.handleJumpKey(tea.KeyPressMsg{Code: tea.KeyEnter})
},
check: func(t *testing.T, m *Model) {
t.Helper()
if m.jumping {
t.Fatal("jump mode remained active after enter")
}
if m.jumpInput != "" {
t.Fatalf("jump input = %q, want empty", m.jumpInput)
}
},
},
{
name: "ipc seek",
want: 4 * time.Second,
invoke: func(m *Model) tea.Cmd {
_, cmd := m.Update(ipc.SeekMsg{Offset: 4 * time.Second})
return cmd
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
eng := &fakeEngine{streamSeek: true, position: 3 * time.Second}
m := Model{player: eng}
cmd := tt.invoke(&m)
if tt.check != nil {
tt.check(t, &m)
}
assertImmediateStreamSeek(t, eng, cmd, tt.want)
})
}
}