🎉 live server seems to be working now

This commit is contained in:
2026-05-14 14:29:57 +02:00
commit d72e439fd9
181 changed files with 47406 additions and 0 deletions

View File

@@ -0,0 +1,274 @@
// ABOUTME: Tests for the device-picker state machine — pure functions, no TUI loop required
package ui
import (
"errors"
"strings"
"testing"
"github.com/Sendspin/sendspin-go/pkg/audio/output"
)
func fixtureDevices() []output.PlaybackDevice {
return []output.PlaybackDevice{
{Name: "USB Audio"},
{Name: "Built-in Analog", IsDefault: true},
{Name: "HDMI"},
}
}
func listOK() ([]output.PlaybackDevice, error) {
return fixtureDevices(), nil
}
func TestNewDevicePicker_SeedsIndexFromCurrent(t *testing.T) {
s := newDevicePicker(listOK, "HDMI", true)
if s.index != 2 {
t.Errorf("index = %d, want 2 (HDMI)", s.index)
}
}
func TestNewDevicePicker_SeedsIndexFromDefaultWhenCurrentMissing(t *testing.T) {
s := newDevicePicker(listOK, "No such device", true)
if s.index != 1 {
t.Errorf("index = %d, want 1 (the IsDefault entry)", s.index)
}
}
func TestNewDevicePicker_SeedsZeroWhenNoDefaultAndNoCurrent(t *testing.T) {
listNoDefault := func() ([]output.PlaybackDevice, error) {
return []output.PlaybackDevice{
{Name: "One"},
{Name: "Two"},
}, nil
}
s := newDevicePicker(listNoDefault, "", true)
if s.index != 0 {
t.Errorf("index = %d, want 0", s.index)
}
}
func TestNewDevicePicker_PropagatesLoadError(t *testing.T) {
listErr := func() ([]output.PlaybackDevice, error) {
return nil, errors.New("no audio stack")
}
s := newDevicePicker(listErr, "", true)
if s.loadErr == nil {
t.Error("loadErr should be set when enumerator fails")
}
if s.selected() != nil {
t.Error("selected() should be nil when load failed")
}
}
func TestHandlePickerKey_UpDownClamp(t *testing.T) {
s := newDevicePicker(listOK, "USB Audio", true) // index 0
// Up at the top clamps.
s, act := s.handlePickerKey("up")
if s.index != 0 || act != pickerNoop {
t.Errorf("up at top: index=%d act=%d", s.index, act)
}
// Two downs.
s, _ = s.handlePickerKey("down")
s, _ = s.handlePickerKey("down")
if s.index != 2 {
t.Fatalf("after 2 downs, index = %d, want 2", s.index)
}
// Down at the bottom clamps.
s, _ = s.handlePickerKey("down")
if s.index != 2 {
t.Errorf("down at bottom: index = %d, want 2", s.index)
}
// One up.
s, _ = s.handlePickerKey("up")
if s.index != 1 {
t.Errorf("up from 2: index = %d, want 1", s.index)
}
}
func TestHandlePickerKey_EnterSavesWhenEnabled(t *testing.T) {
s := newDevicePicker(listOK, "", true)
_, act := s.handlePickerKey("enter")
if act != pickerSave {
t.Errorf("enter action = %d, want pickerSave", act)
}
}
func TestHandlePickerKey_EnterNoopWhenSaveDisabled(t *testing.T) {
s := newDevicePicker(listOK, "", false)
_, act := s.handlePickerKey("enter")
if act != pickerNoop {
t.Errorf("enter with save disabled action = %d, want pickerNoop", act)
}
}
func TestHandlePickerKey_EnterNoopOnEmptyList(t *testing.T) {
s := newDevicePicker(func() ([]output.PlaybackDevice, error) { return nil, nil }, "", true)
_, act := s.handlePickerKey("enter")
if act != pickerNoop {
t.Errorf("enter on empty action = %d, want pickerNoop", act)
}
}
func TestHandlePickerKey_EscAndQClose(t *testing.T) {
for _, key := range []string{"esc", "q"} {
s := newDevicePicker(listOK, "", true)
_, act := s.handlePickerKey(key)
if act != pickerClose {
t.Errorf("%q action = %d, want pickerClose", key, act)
}
}
}
func TestHandlePickerKey_UnknownPropagates(t *testing.T) {
s := newDevicePicker(listOK, "", true)
_, act := s.handlePickerKey("x")
if act != pickerPropagate {
t.Errorf("unknown key action = %d, want pickerPropagate", act)
}
}
func TestSelected_ReturnsCurrentRow(t *testing.T) {
s := newDevicePicker(listOK, "HDMI", true)
got := s.selected()
if got == nil {
t.Fatal("selected() = nil")
}
if got.Name != "HDMI" {
t.Errorf("selected().Name = %q, want HDMI", got.Name)
}
}
func TestRenderPicker_ShowsDefaultAndCurrentAnnotations(t *testing.T) {
s := newDevicePicker(listOK, "HDMI", true)
out := renderPicker(s, 80)
// Built-in Analog carries (default), HDMI carries (current).
if !strings.Contains(out, "Built-in Analog (default)") {
t.Errorf("expected '(default)' annotation; got:\n%s", out)
}
if !strings.Contains(out, "HDMI (current)") {
t.Errorf("expected '(current)' annotation on HDMI; got:\n%s", out)
}
}
func TestRenderPicker_AnnotatesBothWhenCurrentIsDefault(t *testing.T) {
s := newDevicePicker(listOK, "Built-in Analog", true)
out := renderPicker(s, 80)
if !strings.Contains(out, "Built-in Analog (default, current)") {
t.Errorf("expected combined annotation; got:\n%s", out)
}
}
func TestRenderPicker_EmptyAndErrorStates(t *testing.T) {
empty := newDevicePicker(func() ([]output.PlaybackDevice, error) { return nil, nil }, "", true)
emptyOut := renderPicker(empty, 80)
if !strings.Contains(emptyOut, "no playback devices found") {
t.Errorf("empty state did not render expected message; got:\n%s", emptyOut)
}
errState := newDevicePicker(func() ([]output.PlaybackDevice, error) {
return nil, errors.New("enumeration boom")
}, "", true)
errOut := renderPicker(errState, 80)
if !strings.Contains(errOut, "enumeration boom") {
t.Errorf("error state did not render error; got:\n%s", errOut)
}
}
func TestRenderPicker_SaveUnavailableHint(t *testing.T) {
s := newDevicePicker(listOK, "", false)
out := renderPicker(s, 80)
if !strings.Contains(out, "save unavailable") {
t.Errorf("expected 'save unavailable' hint when saveEnabled=false; got:\n%s", out)
}
}
// The two tests below integrate picker state with the Model's key dispatch
// to verify the end-to-end "press o → navigate → save" flow without
// standing up a full bubbletea runtime.
func TestModel_OKeyOpensPicker(t *testing.T) {
m := NewModel(Config{ConfigPath: "/tmp/test-player.yaml"})
m.listPlaybackDevices = listOK
m.width = 80
next, _ := m.handleKey(fakeKeyMsg("o"))
nm := next.(Model)
if nm.mode != modeDevicePicker {
t.Errorf("mode = %v, want modeDevicePicker", nm.mode)
}
if len(nm.picker.devices) != 3 {
t.Errorf("picker.devices len = %d, want 3", len(nm.picker.devices))
}
}
func TestModel_EnterInPickerSavesAndClosesModal(t *testing.T) {
var savedPath, savedKey, savedValue string
writer := func(path, key, value string) error {
savedPath, savedKey, savedValue = path, key, value
return nil
}
// Seed the current device so the picker opens highlighted on "USB Audio"
// (index 0). One "down" then lands on the IsDefault entry at index 1.
m := NewModel(Config{ConfigPath: "/tmp/test-player.yaml", AudioDevice: "USB Audio"})
m.listPlaybackDevices = listOK
m.writeConfigKey = writer
m.width = 80
// Open picker.
next, _ := m.handleKey(fakeKeyMsg("o"))
m = next.(Model)
// Navigate down to Built-in Analog (index 1).
next, _ = m.handleKey(fakeKeyMsg("down"))
m = next.(Model)
// Enter saves. Returns a Cmd (the 3s expire tick); we don't invoke it.
next, cmd := m.handleKey(fakeKeyMsg("enter"))
m = next.(Model)
if m.mode != modeNormal {
t.Errorf("mode after save = %v, want modeNormal", m.mode)
}
if savedPath != "/tmp/test-player.yaml" || savedKey != "audio_device" || savedValue != "Built-in Analog" {
t.Errorf("write recorded (%q, %q, %q); want (/tmp/test-player.yaml, audio_device, Built-in Analog)",
savedPath, savedKey, savedValue)
}
if m.transientMsg == "" {
t.Error("transient banner should be set after save")
}
if cmd == nil {
t.Error("save should schedule a transient-expire tick")
}
}
func TestModel_EnterWhenSelectionMatchesSkipsWrite(t *testing.T) {
writeCalled := false
writer := func(path, key, value string) error {
writeCalled = true
return nil
}
// Current is the HDMI entry; picker seeds selection there.
m := NewModel(Config{ConfigPath: "/tmp/test.yaml", AudioDevice: "HDMI"})
m.listPlaybackDevices = listOK
m.writeConfigKey = writer
m.width = 80
next, _ := m.handleKey(fakeKeyMsg("o"))
m = next.(Model)
next, _ = m.handleKey(fakeKeyMsg("enter"))
m = next.(Model)
if writeCalled {
t.Error("writeConfigKey should not be called when selection matches current")
}
if m.mode != modeNormal {
t.Errorf("mode = %v, want modeNormal", m.mode)
}
}