Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/tui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ func (app AppModel) handleRune(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
app.helpVisible = !app.helpVisible
case " ", "s":
return app.togglePin()
case "k":
return app.killSession()
}
return app, nil
}
Expand Down
156 changes: 156 additions & 0 deletions internal/tui/app_kill_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package tui

import (
"testing"

tea "github.com/charmbracelet/bubbletea"
"github.com/robinojw/dj/internal/state"
)

const errExpectedRemaining = "expected %s remaining, got %s"

func TestAppKillSessionRemovesThread(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleThread1)
store.Add(testThreadID2, testTitleThread2)
app := NewAppModel(store)

kKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}
updated, _ := app.Update(kKey)
appModel := updated.(AppModel)

threads := store.All()
if len(threads) != 1 {
test.Fatalf("expected 1 thread after kill, got %d", len(threads))
}
if threads[0].ID != testThreadID2 {
test.Errorf(errExpectedRemaining, testThreadID2, threads[0].ID)
}
_ = appModel
}

func TestAppKillSessionStopsPTY(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleThread1)
app := NewAppModel(store, WithInteractiveCommand(testCommandCat))

enterKey := tea.KeyMsg{Type: tea.KeyEnter}
updated, _ := app.Update(enterKey)
app = updated.(AppModel)

escKey := tea.KeyMsg{Type: tea.KeyEsc}
updated, _ = app.Update(escKey)
app = updated.(AppModel)

kKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}
updated, _ = app.Update(kKey)
appModel := updated.(AppModel)

if len(appModel.ptySessions) != 0 {
test.Errorf("expected 0 PTY sessions after kill, got %d", len(appModel.ptySessions))
}
}

func TestAppKillSessionUnpins(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleThread1)
app := NewAppModel(store, WithInteractiveCommand(testCommandCat))

spaceKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}}
updated, _ := app.Update(spaceKey)
app = updated.(AppModel)

escKey := tea.KeyMsg{Type: tea.KeyEsc}
updated, _ = app.Update(escKey)
app = updated.(AppModel)

kKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}
updated, _ = app.Update(kKey)
appModel := updated.(AppModel)

if len(appModel.sessionPanel.PinnedSessions()) != 0 {
test.Errorf("expected 0 pinned after kill, got %d", len(appModel.sessionPanel.PinnedSessions()))
}
}

func TestAppKillSessionClampsSelection(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleThread1)
store.Add(testThreadID2, testTitleThread2)
app := NewAppModel(store)

rightKey := tea.KeyMsg{Type: tea.KeyRight}
updated, _ := app.Update(rightKey)
app = updated.(AppModel)

kKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}
updated, _ = app.Update(kKey)
appModel := updated.(AppModel)

if appModel.canvas.SelectedIndex() != 0 {
test.Errorf("expected selection clamped to 0, got %d", appModel.canvas.SelectedIndex())
}
}

func TestAppKillSessionWithNoThreadsDoesNothing(test *testing.T) {
store := state.NewThreadStore()
app := NewAppModel(store)

kKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}
updated, _ := app.Update(kKey)
appModel := updated.(AppModel)

if len(store.All()) != 0 {
test.Errorf("expected 0 threads, got %d", len(store.All()))
}
_ = appModel
}

func TestAppKillSessionReturnsFocusToCanvas(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleThread1)
app := NewAppModel(store, WithInteractiveCommand(testCommandCat))

enterKey := tea.KeyMsg{Type: tea.KeyEnter}
updated, _ := app.Update(enterKey)
app = updated.(AppModel)

escKey := tea.KeyMsg{Type: tea.KeyEsc}
updated, _ = app.Update(escKey)
app = updated.(AppModel)

kKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}
updated, _ = app.Update(kKey)
appModel := updated.(AppModel)

if appModel.FocusPane() != FocusPaneCanvas {
test.Errorf("expected canvas focus after killing last pinned, got %d", appModel.FocusPane())
}
}

func TestAppKillSessionInTreeMode(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testThreadTitle1)
store.Add(testThreadID2, testThreadTitle2)
app := NewAppModel(store)

tKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'t'}}
updated, _ := app.Update(tKey)
app = updated.(AppModel)

downKey := tea.KeyMsg{Type: tea.KeyDown}
updated, _ = app.Update(downKey)
app = updated.(AppModel)

kKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}
updated, _ = app.Update(kKey)
_ = updated.(AppModel)

threads := store.All()
if len(threads) != 1 {
test.Fatalf("expected 1 thread after kill in tree mode, got %d", len(threads))
}
if threads[0].ID != testThreadID1 {
test.Errorf(errExpectedRemaining, testThreadID1, threads[0].ID)
}
}
200 changes: 200 additions & 0 deletions internal/tui/app_prefix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package tui

import (
"strings"
"testing"

tea "github.com/charmbracelet/bubbletea"
"github.com/robinojw/dj/internal/state"
)

func TestAppCtrlBMOpensMenu(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleTest)

app := NewAppModel(store)

ctrlB := tea.KeyMsg{Type: tea.KeyCtrlB}
updated, _ := app.Update(ctrlB)
app = updated.(AppModel)

mKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}}
updated, _ = app.Update(mKey)
app = updated.(AppModel)

if !app.MenuVisible() {
test.Error("expected menu to be visible")
}
}

func TestAppMenuEscCloses(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleTest)

app := NewAppModel(store)

ctrlB := tea.KeyMsg{Type: tea.KeyCtrlB}
updated, _ := app.Update(ctrlB)
app = updated.(AppModel)

mKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}}
updated, _ = app.Update(mKey)
app = updated.(AppModel)

escKey := tea.KeyMsg{Type: tea.KeyEsc}
updated, _ = app.Update(escKey)
app = updated.(AppModel)

if app.MenuVisible() {
test.Error("expected menu hidden after Esc")
}
}

func TestAppCtrlBEscCancelsPrefix(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleTest)

app := NewAppModel(store)

ctrlB := tea.KeyMsg{Type: tea.KeyCtrlB}
updated, _ := app.Update(ctrlB)
app = updated.(AppModel)

escKey := tea.KeyMsg{Type: tea.KeyEsc}
updated, _ = app.Update(escKey)
app = updated.(AppModel)

if app.MenuVisible() {
test.Error("expected menu not visible after prefix cancel")
}
}

func TestAppMenuNavigation(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleTest)

app := NewAppModel(store)

ctrlB := tea.KeyMsg{Type: tea.KeyCtrlB}
updated, _ := app.Update(ctrlB)
app = updated.(AppModel)

mKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}}
updated, _ = app.Update(mKey)
app = updated.(AppModel)

downKey := tea.KeyMsg{Type: tea.KeyDown}
updated, _ = app.Update(downKey)
app = updated.(AppModel)

if app.menu.SelectedIndex() != 1 {
test.Errorf("expected menu index 1, got %d", app.menu.SelectedIndex())
}
}

func TestAppCtrlBXUnpinsSession(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleThread1)
app := NewAppModel(store, WithInteractiveCommand(testCommandEcho, testArgHello))

spaceKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}}
updated, _ := app.Update(spaceKey)
app = updated.(AppModel)

tabKey := tea.KeyMsg{Type: tea.KeyTab}
updated, _ = app.Update(tabKey)
app = updated.(AppModel)

ctrlB := tea.KeyMsg{Type: tea.KeyCtrlB}
updated, _ = app.Update(ctrlB)
app = updated.(AppModel)

xKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}}
updated, _ = app.Update(xKey)
app = updated.(AppModel)
defer app.StopAllPTYSessions()

if len(app.sessionPanel.PinnedSessions()) != 0 {
test.Errorf("expected 0 pinned after unpin, got %d", len(app.sessionPanel.PinnedSessions()))
}
if app.FocusPane() != FocusPaneCanvas {
test.Errorf("expected focus back to canvas, got %d", app.FocusPane())
}
}

func TestAppCtrlBZTogglesZoom(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleThread1)
app := NewAppModel(store, WithInteractiveCommand(testCommandEcho, testArgHello))

spaceKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}}
updated, _ := app.Update(spaceKey)
app = updated.(AppModel)

ctrlB := tea.KeyMsg{Type: tea.KeyCtrlB}
updated, _ = app.Update(ctrlB)
app = updated.(AppModel)

zKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'z'}}
updated, _ = app.Update(zKey)
app = updated.(AppModel)
defer app.StopAllPTYSessions()

if !app.sessionPanel.Zoomed() {
test.Error("expected zoomed after Ctrl+B z")
}
}

func TestAppCtrlBRightCyclesPaneRight(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleThread1)
store.Add(testThreadID2, testTitleThread2)
app := NewAppModel(store, WithInteractiveCommand(testCommandEcho, testArgHello))

space := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}}
updated, _ := app.Update(space)
app = updated.(AppModel)

app.canvas.MoveRight()
updated, _ = app.Update(space)
app = updated.(AppModel)

tab := tea.KeyMsg{Type: tea.KeyTab}
updated, _ = app.Update(tab)
app = updated.(AppModel)

if app.sessionPanel.ActivePaneIdx() != 0 {
test.Fatalf("expected active pane 0, got %d", app.sessionPanel.ActivePaneIdx())
}

ctrlB := tea.KeyMsg{Type: tea.KeyCtrlB}
updated, _ = app.Update(ctrlB)
app = updated.(AppModel)

rightKey := tea.KeyMsg{Type: tea.KeyRight}
updated, _ = app.Update(rightKey)
app = updated.(AppModel)
defer app.StopAllPTYSessions()

if app.sessionPanel.ActivePaneIdx() != 1 {
test.Errorf("expected active pane 1, got %d", app.sessionPanel.ActivePaneIdx())
}
}

func TestAppViewShowsDividerWhenPinned(test *testing.T) {
store := state.NewThreadStore()
store.Add(testThreadID1, testTitleThread1)
app := NewAppModel(store, WithInteractiveCommand(testCommandEcho, testArgHello))
app.width = testAppWidth
app.height = testAppHeight

spaceKey := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}}
updated, _ := app.Update(spaceKey)
app = updated.(AppModel)
defer app.StopAllPTYSessions()

view := app.View()
if !strings.Contains(view, "─") {
test.Error("expected divider line in view when sessions pinned")
}
}
Loading
Loading