diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-12 20:57:12 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-12 20:57:12 -0800 |
| commit | cfd583e9166330f053fa7cc28258b4467c48459c (patch) | |
| tree | bdea6e91c25e04b6e483a58040d386876593a457 | |
| parent | fa037e748bb2ba65f75ba104e18d460a8fbcd49b (diff) | |
| download | neko-cfd583e9166330f053fa7cc28258b4467c48459c.tar.gz neko-cfd583e9166330f053fa7cc28258b4467c48459c.tar.bz2 neko-cfd583e9166330f053fa7cc28258b4467c48459c.zip | |
Fix nil-pointer panics in TUI and add unit tests
- Initialized list models in NewModel to prevent panic on WindowSizeMsg
- Added nil check for selectedFeed in itemsMsg handler
- Created tui/tui_test.go with unit tests for state transitions and resizing
- Verified all tests pass
| -rw-r--r-- | tui/tui.go | 12 | ||||
| -rw-r--r-- | tui/tui_test.go | 58 |
2 files changed, 68 insertions, 2 deletions
@@ -54,9 +54,13 @@ type Model struct { } func NewModel() Model { - return Model{ + m := Model{ state: viewFeeds, } + // Initialize lists with empty items to avoid nil dereference in SetSize + m.feedList = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) + m.itemList = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) + return m } func (m Model) Init() tea.Cmd { @@ -113,7 +117,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { items[i] = itemString(it.Title) } m.itemList = list.New(items, list.NewDefaultDelegate(), m.width, m.height-4) - m.itemList.Title = m.selectedFeed.Title + if m.selectedFeed != nil { + m.itemList.Title = m.selectedFeed.Title + } else { + m.itemList.Title = "Items" + } m.state = viewItems case errMsg: diff --git a/tui/tui_test.go b/tui/tui_test.go new file mode 100644 index 0000000..6ae8d73 --- /dev/null +++ b/tui/tui_test.go @@ -0,0 +1,58 @@ +package tui + +import ( + "testing" + + "adammathes.com/neko/models/feed" + "adammathes.com/neko/models/item" + tea "github.com/charmbracelet/bubbletea" +) + +func TestNewModel(t *testing.T) { + m := NewModel() + if m.state != viewFeeds { + t.Errorf("Expected initial state viewFeeds, got %v", m.state) + } +} + +func TestUpdateWindowSizeNoPanic(t *testing.T) { + m := NewModel() + // This should not panic even if lists are empty + msg := tea.WindowSizeMsg{Width: 80, Height: 24} + newModel, _ := m.Update(msg) + tm := newModel.(Model) + if tm.width != 80 || tm.height != 24 { + t.Errorf("Expected size 80x24, got %dx%d", tm.width, tm.height) + } +} + +func TestStateTransitions(t *testing.T) { + m := NewModel() + + // Feed loaded + feeds := []*feed.Feed{{Id: 1, Title: "Test Feed"}} + m2, _ := m.Update(feedsMsg(feeds)) + tm2 := m2.(Model) + if len(tm2.feeds) != 1 { + t.Fatal("Expected 1 feed") + } + + // Items loaded + items := []*item.Item{{Id: 1, Title: "Test Item", Description: "Desc"}} + tm2.selectedFeed = feeds[0] // Simulate selection + m3, _ := tm2.Update(itemsMsg(items)) + tm3 := m3.(Model) + if tm3.state != viewItems { + t.Errorf("Expected state viewItems, got %v", tm3.state) + } + if len(tm3.items) != 1 { + t.Fatal("Expected 1 item") + } + + // Back to feeds + m4, _ := tm3.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")}) + tm4 := m4.(Model) + if tm4.state != viewFeeds { + t.Errorf("Expected back to viewFeeds, got %v", tm4.state) + } +} |
