package tui import ( "strings" "testing" "time" "github.com/charmbracelet/bubbletea" "gitea.oblak.solutions/dimitar/gitFlow/internal/config" "gitea.oblak.solutions/dimitar/gitFlow/internal/presenter" "gitea.oblak.solutions/dimitar/gitFlow/pkg/status" ) func newTestModel() Model { return newTestModelWith(status.ScanResult{ ScannedAt: time.Now(), ParentDir: "/tmp/test", Repos: []status.RepoInfo{ {Path: "/tmp/test/a", Name: "a", Branch: "main", Status: status.StatusClean}, {Path: "/tmp/test/b", Name: "b", Branch: "feat/x", Status: status.StatusModified, ModifiedFiles: []string{"f.go"}}, {Path: "/tmp/test/c", Name: "c", Status: status.StatusError, Error: "boom"}, }, }) } func newTestModelWith(result status.ScanResult) Model { return Model{ app: nil, cfg: &config.Config{Dir: "/tmp/test", Format: "table", Color: "auto", Theme: "dark", Workers: 4}, opts: presenter.Options{Color: presenter.ColorNever, Theme: presenter.ThemeDark}, result: result, cursor: 0, first: true, styles: NewStyles(presenter.Options{Theme: presenter.ThemeDark}), } } func TestModelNavigation(t *testing.T) { m := newTestModel() // Move down m2, _ := m.Update(tea.KeyMsg{Type: tea.KeyDown}) if m2.(Model).cursor != 1 { t.Errorf("cursor = %d after down, want 1", m2.(Model).cursor) } // Move down again m3, _ := m2.Update(tea.KeyMsg{Type: tea.KeyDown}) if m3.(Model).cursor != 2 { t.Errorf("cursor = %d after down, want 2", m3.(Model).cursor) } // At bottom: cursor stays m4, _ := m3.Update(tea.KeyMsg{Type: tea.KeyDown}) if m4.(Model).cursor != 2 { t.Errorf("cursor = %d after down (at bottom), want 2", m4.(Model).cursor) } // Move up m5, _ := m4.Update(tea.KeyMsg{Type: tea.KeyUp}) if m5.(Model).cursor != 1 { t.Errorf("cursor = %d after up, want 1", m5.(Model).cursor) } // At top: cursor stays m6, _ := m5.Update(tea.KeyMsg{Type: tea.KeyUp}) m6, _ = m6.Update(tea.KeyMsg{Type: tea.KeyUp}) if m6.(Model).cursor != 0 { t.Errorf("cursor = %d after up (at top), want 0", m6.(Model).cursor) } } func TestModelJKKeys(t *testing.T) { m := newTestModel() m2, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}}) if m2.(Model).cursor != 1 { t.Errorf("cursor = %d after j, want 1", m2.(Model).cursor) } m3, _ := m2.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}) if m3.(Model).cursor != 0 { t.Errorf("cursor = %d after k, want 0", m3.(Model).cursor) } } func TestModelDetailToggle(t *testing.T) { m := newTestModel() if m.detail { t.Error("detail = true initially, want false") } m2, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter}) if !m2.(Model).detail { t.Error("detail = false after Enter, want true") } m3, _ := m2.Update(tea.KeyMsg{Type: tea.KeySpace}) if m3.(Model).detail { t.Error("detail = true after Space, want false") } } func TestModelHelpToggle(t *testing.T) { m := newTestModel() if m.help { t.Error("help = true initially, want false") } m2, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}}) if !m2.(Model).help { t.Error("help = false after ?, want true") } } func TestModelQuit(t *testing.T) { m := newTestModel() for _, key := range []tea.KeyType{tea.KeyCtrlC} { _, cmd := m.Update(tea.KeyMsg{Type: key}) if cmd == nil { t.Errorf("no quit cmd for key %v", key) continue } // tea.Quit is a function that returns a tea.QuitMsg if cmd() == nil { t.Errorf("quit cmd produced nil msg") } } // 'q' quit _, cmd := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}}) if cmd == nil { t.Error("no quit cmd for 'q'") } } func TestModelViewRendersRepos(t *testing.T) { m := newTestModel() view := m.View() for _, want := range []string{"a", "b", "c", "feat/x", "STATUS", "BRANCH"} { if !strings.Contains(view, want) { t.Errorf("view missing %q:\n%s", want, view) } } } func TestModelViewShowsHelp(t *testing.T) { m := newTestModel() m.help = true view := m.View() if !strings.Contains(view, "KEYBINDINGS") { t.Errorf("help view missing KEYBINDINGS:\n%s", view) } } func TestModelViewShowsDetail(t *testing.T) { m := newTestModel() m.cursor = 1 // select repo 'b' m.detail = true view := m.View() if !strings.Contains(view, "modified:") || !strings.Contains(view, "f.go") { t.Errorf("detail view missing file:\n%s", view) } } func TestModelViewLoading(t *testing.T) { m := newTestModel() m.loading = true view := m.View() if !strings.Contains(view, "scanning") { t.Errorf("loading view missing 'scanning':\n%s", view) } } func TestModelViewEmpty(t *testing.T) { m := newTestModelWith(status.ScanResult{Repos: nil}) view := m.View() if !strings.Contains(view, "no repositories") { t.Errorf("empty view missing 'no repositories':\n%s", view) } }