package tui import ( "os" "path/filepath" "strings" "testing" tea "charm.land/bubbletea/v2" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/modernzconf" ) // selectModernZOption moves the options list cursor to the item with the given key. func selectModernZOption(t *testing.T, m *Model, key string) { t.Helper() for i, item := range m.modernzList.Items() { if item.(modernzOptionItem).key == key { m.modernzList.Select(i) return } } t.Fatalf("modernz option %q not found in list", key) } func getModernZItem(t *testing.T, m *Model, key string) modernzOptionItem { t.Helper() for _, item := range m.modernzList.Items() { if it := item.(modernzOptionItem); it.key == key { return it } } t.Fatalf("modernz option %q not found in list", key) return modernzOptionItem{} } func newModernZModel(t *testing.T) (*Model, string) { t.Helper() sandboxConfigHome(t) m := NewModel() m.setupModernZOptionsList() m.state = StateModernZOptions if m.modernzConf == nil { t.Fatal("expected modernz.conf to be loaded (defaults)") } return m, filepath.Join(config.GetMPVConfigPath(), "script-opts", "modernz.conf") } func TestModernZListShowsDefaultsUnmodified(t *testing.T) { m, _ := newModernZModel(t) if len(m.modernzList.Items()) != len(modernzconf.KnownOptions()) { t.Fatalf("expected %d options, got %d", len(modernzconf.KnownOptions()), len(m.modernzList.Items())) } item := getModernZItem(t, m, "showwindowed") if item.value != "yes" || item.modified { t.Errorf("expected default unmodified showwindowed=yes, got %+v", item) } } func TestModernZBoolToggle(t *testing.T) { m, confPath := newModernZModel(t) selectModernZOption(t, m, "showwindowed") m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) // Value toggled, saved per-edit, and marked modified item := getModernZItem(t, m, "showwindowed") if item.value != "no" { t.Errorf("expected showwindowed toggled to no, got %q", item.value) } if !item.modified { t.Error("expected showwindowed marked modified") } if !strings.HasPrefix(m.modernzStatusMsg, "✓") { t.Errorf("expected success status, got %q", m.modernzStatusMsg) } if content := readFile(t, confPath); !strings.Contains(content, "showwindowed=no") { t.Errorf("expected showwindowed=no in file, got %q", content) } // Toggling again flips back to yes selectModernZOption(t, m, "showwindowed") m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) if item := getModernZItem(t, m, "showwindowed"); item.value != "yes" { t.Errorf("expected showwindowed toggled back to yes, got %q", item.value) } } func TestModernZChoiceCycle(t *testing.T) { m, confPath := newModernZModel(t) selectModernZOption(t, m, "layout") m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) // default -> compact (next choice) item := getModernZItem(t, m, "layout") if item.value != "compact" { t.Errorf("expected layout cycled to compact, got %q", item.value) } if content := readFile(t, confPath); !strings.Contains(content, "layout=compact") { t.Errorf("expected layout=compact in file, got %q", content) } } func TestModernZScalingOptionsAreUserFriendly(t *testing.T) { m, _ := newModernZModel(t) if err := m.setModernZValue("vidscale", modernzconf.ManagedVidscaleDefault); err != nil { t.Fatalf("failed to set recommended vidscale: %v", err) } m.rebuildModernZList() item := getModernZItem(t, m, "vidscale") if item.label != "Scaling behavior" { t.Errorf("vidscale label = %q, want user-friendly label", item.label) } if item.recommended != modernzconf.ManagedVidscaleDefault { t.Errorf("vidscale recommended = %q, want %q", item.recommended, modernzconf.ManagedVidscaleDefault) } if !strings.Contains(item.Title(), "Recommended") { t.Errorf("recommended scaling title missing badge: %q", item.Title()) } if !strings.Contains(item.Description(), "Consistent size") { t.Errorf("scaling description does not use friendly choice label: %q", item.Description()) } if !strings.Contains(item.FilterValue(), "vidscale") || !strings.Contains(item.FilterValue(), "Scaling behavior") { t.Errorf("filter value should include raw key and friendly label: %q", item.FilterValue()) } } func TestModernZIntEditValidation(t *testing.T) { m, confPath := newModernZModel(t) selectModernZOption(t, m, "hidetimeout") m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) if m.state != StateModernZOptionEdit { t.Fatalf("expected StateModernZOptionEdit, got %v", m.state) } // Invalid integer is rejected inline: state and file stay untouched m.modernzTextInput.SetValue("abc") m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) if m.state != StateModernZOptionEdit { t.Fatalf("expected to stay in StateModernZOptionEdit, got %v", m.state) } if !strings.HasPrefix(m.modernzStatusMsg, "✗") { t.Errorf("expected error status, got %q", m.modernzStatusMsg) } if _, err := os.Stat(confPath); !os.IsNotExist(err) { t.Errorf("expected no modernz.conf written for invalid input") } // Out-of-range value is also rejected m.modernzTextInput.SetValue("999999") m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) if m.state != StateModernZOptionEdit { t.Fatalf("expected to stay in StateModernZOptionEdit, got %v", m.state) } if _, err := os.Stat(confPath); !os.IsNotExist(err) { t.Errorf("expected no modernz.conf written for out-of-range input") } // Valid value saves and returns to the list m.modernzTextInput.SetValue("2000") m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) if m.state != StateModernZOptions { t.Fatalf("expected StateModernZOptions, got %v", m.state) } item := getModernZItem(t, m, "hidetimeout") if item.value != "2000" || !item.modified { t.Errorf("expected hidetimeout=2000 modified, got %+v", item) } if content := readFile(t, confPath); !strings.Contains(content, "hidetimeout=2000") { t.Errorf("expected hidetimeout=2000 in file, got %q", content) } } func TestModernZResetToDefault(t *testing.T) { m, confPath := newModernZModel(t) // Set an explicit value first selectModernZOption(t, m, "showwindowed") m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) if item := getModernZItem(t, m, "showwindowed"); !item.modified { t.Fatal("expected showwindowed modified after toggle") } // d resets it to default selectModernZOption(t, m, "showwindowed") m = pressKey(t, m, keyRunes("d")) item := getModernZItem(t, m, "showwindowed") if item.modified { t.Error("expected showwindowed no longer marked modified after reset") } if item.value != "yes" { t.Errorf("expected showwindowed back to default yes, got %q", item.value) } if m.modernzConf.IsSet("showwindowed") { t.Error("expected showwindowed no longer explicitly set") } if content := readFile(t, confPath); strings.Contains(content, "showwindowed") { t.Errorf("expected showwindowed line removed from file, got %q", content) } } func TestModernZEscReturnsToConfigOptions(t *testing.T) { m, _ := newModernZModel(t) m = pressKey(t, m, tea.KeyPressMsg{Code: tea.KeyEsc}) if m.state != StateConfigOptions { t.Fatalf("expected StateConfigOptions after Esc, got %v", m.state) } } func TestModernZScalingMigrationIsActionableInTUI(t *testing.T) { sandboxConfigHome(t) if _, err := config.Reload(); err != nil { t.Fatalf("reload config: %v", err) } if err := config.SetInstalledUIType(constants.UITypeModernZ); err != nil { t.Fatalf("set installed UI: %v", err) } confPath := filepath.Join(config.GetMPVConfigPath(), constants.ScriptOptsDir, "modernz.conf") if err := os.MkdirAll(filepath.Dir(confPath), 0755); err != nil { t.Fatalf("create script-opts: %v", err) } if err := os.WriteFile(confPath, []byte("vidscale=auto\nscalewindowed=1.5\n"), 0644); err != nil { t.Fatalf("write legacy config: %v", err) } m := NewModel() m.setupModernZOptionsList() m.state = StateModernZOptions if m.uiConfigMigration == nil { t.Fatal("expected pending ModernZ scaling migration") } if view := m.modernzOptionsListView(); !strings.Contains(view, "Review ModernZ scaling") { t.Fatalf("expected migration guidance in TUI view, got %q", view) } m = pressKey(t, m, keyRunes("a")) if m.uiConfigMigration != nil { t.Fatal("expected migration to be resolved") } conf, err := modernzconf.ParseModernZConf(confPath) if err != nil { t.Fatalf("parse migrated config: %v", err) } if got := conf.GetValue("vidscale"); got != modernzconf.ManagedVidscaleDefault { t.Errorf("vidscale=%q, want %q", got, modernzconf.ManagedVidscaleDefault) } if got := conf.GetValue("scalewindowed"); got != "1.5" { t.Errorf("custom scaling multiplier changed to %q", got) } }