Gim/internal/motion/command_test.go
Hayden Hargreaves 3c98dca777 feat: implement command history, tested
The tests are starting to get messy, lots of duplication. Going to
resolve that. Lots of this is due to AI generation of tests.
2026-03-19 15:23:44 -07:00

450 lines
13 KiB
Go

package motion
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/style"
)
// ==================================================
// Mock Model Implementation
// ==================================================
type mockModel struct {
windows []*core.Window
activeWindow *core.Window
buffers []*core.Buffer
settings core.EditorSettings
mode core.Mode
registers map[rune]core.Register
insertKeys []string
command string
commandCursor int
commandOutput *core.CommandOutput
commandHistory []string
commandHistoryCursor int
lastFind core.LastFindCommand
styles style.Styles
}
// Core Data Access
func (m *mockModel) Windows() []*core.Window { return m.windows }
func (m *mockModel) ActiveWindow() *core.Window { return m.activeWindow }
func (m *mockModel) Buffers() []*core.Buffer { return m.buffers }
func (m *mockModel) SetBuffers(bufs []*core.Buffer) { m.buffers = bufs }
func (m *mockModel) ActiveBuffer() *core.Buffer { return m.activeWindow.Buffer }
// Insert Mode State
func (m *mockModel) InsertKeys() []string { return m.insertKeys }
func (m *mockModel) SetInsertKeys(keys []string) { m.insertKeys = keys }
func (m *mockModel) SetInsertRecording(count int, action action.Action) {}
func (m *mockModel) ExitInsertMode() {}
func (m *mockModel) SetLastFind(char string, forward, inclusive bool) {
m.lastFind = core.LastFindCommand{Char: char, Forward: forward, Inclusive: inclusive}
}
func (m *mockModel) GetLastFind() *core.LastFindCommand { return &m.lastFind }
// Command Mode State
func (m *mockModel) Command() string { return m.command }
func (m *mockModel) SetCommand(cmd string) { m.command = cmd }
func (m *mockModel) CommandCursor() int { return m.commandCursor }
func (m *mockModel) SetCommandCursor(cur int) { m.commandCursor = cur }
func (m *mockModel) CommandOutput() *core.CommandOutput { return m.commandOutput }
func (m *mockModel) SetCommandOutput(out *core.CommandOutput) { m.commandOutput = out }
func (m *mockModel) CommandHistory() []string { return m.commandHistory }
func (m *mockModel) SetCommandHistory(history []string) { m.commandHistory = history }
func (m *mockModel) CommandHistoryCursor() int { return m.commandHistoryCursor }
func (m *mockModel) SetCommandHistoryCursor(cur int) { m.commandHistoryCursor = cur }
// Editor-wide State
func (m *mockModel) Mode() core.Mode { return m.mode }
func (m *mockModel) SetMode(mode core.Mode) { m.mode = mode }
func (m *mockModel) Settings() core.EditorSettings { return m.settings }
func (m *mockModel) SetSettings(s core.EditorSettings) { m.settings = s }
func (m *mockModel) Styles() style.Styles { return m.styles }
func (m *mockModel) SetStyles(s style.Styles) { m.styles = s }
// Registers
func (m *mockModel) Registers() map[rune]core.Register { return m.registers }
func (m *mockModel) GetRegister(name rune) (core.Register, bool) {
reg, ok := m.registers[name]
return reg, ok
}
func (m *mockModel) SetRegister(name rune, t core.RegisterType, cnt []string) error {
m.registers[name] = core.Register{Type: t, Content: cnt}
return nil
}
func (m *mockModel) UpdateDefaultRegister(t core.RegisterType, cnt []string) {
m.registers['"'] = core.Register{Type: t, Content: cnt}
}
// ==================================================
// MoveCommandHistoryUp Tests
// ==================================================
func TestMoveCommandHistoryUp(t *testing.T) {
t.Run("navigate up from start", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 0,
}
action := MoveCommandHistoryUp{}
action.Execute(m)
// Should load first command in history
if m.Command() != "cmd3" {
t.Errorf("Command = %q, want %q", m.Command(), "cmd3")
}
// Cursor should move to end of command
if m.CommandCursor() != len("cmd3") {
t.Errorf("CommandCursor = %d, want %d", m.CommandCursor(), len("cmd3"))
}
// History cursor should advance
if m.CommandHistoryCursor() != 1 {
t.Errorf("CommandHistoryCursor = %d, want 1", m.CommandHistoryCursor())
}
})
t.Run("navigate up multiple times", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 0,
}
action := MoveCommandHistoryUp{}
// First up
action.Execute(m)
if m.Command() != "cmd3" {
t.Errorf("After 1st up: Command = %q, want 'cmd3'", m.Command())
}
// Second up
action.Execute(m)
if m.Command() != "cmd2" {
t.Errorf("After 2nd up: Command = %q, want 'cmd2'", m.Command())
}
// Third up
action.Execute(m)
if m.Command() != "cmd1" {
t.Errorf("After 3rd up: Command = %q, want 'cmd1'", m.Command())
}
if m.CommandHistoryCursor() != 3 {
t.Errorf("CommandHistoryCursor = %d, want 3", m.CommandHistoryCursor())
}
})
t.Run("cannot navigate past end of history", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 3, // Already at end
}
action := MoveCommandHistoryUp{}
action.Execute(m)
// Should not change command (still showing cmd1)
if m.Command() != "" {
t.Errorf("Command = %q, want empty (should not go past end)", m.Command())
}
// Cursor should not advance
if m.CommandHistoryCursor() != 3 {
t.Errorf("CommandHistoryCursor = %d, want 3 (should not advance)", m.CommandHistoryCursor())
}
})
t.Run("empty history does nothing", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "current",
commandHistory: []string{},
commandHistoryCursor: 0,
}
action := MoveCommandHistoryUp{}
action.Execute(m)
// Command should not change
if m.Command() != "current" {
t.Errorf("Command = %q, want 'current' (no history to navigate)", m.Command())
}
if m.CommandHistoryCursor() != 0 {
t.Errorf("CommandHistoryCursor = %d, want 0", m.CommandHistoryCursor())
}
})
t.Run("command cursor moves to end", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandCursor: 0,
commandHistory: []string{"long command here"},
commandHistoryCursor: 0,
}
action := MoveCommandHistoryUp{}
action.Execute(m)
expectedLen := len("long command here")
if m.CommandCursor() != expectedLen {
t.Errorf("CommandCursor = %d, want %d (end of command)", m.CommandCursor(), expectedLen)
}
})
}
// ==================================================
// MoveCommandHistoryDown Tests
// ==================================================
func TestMoveCommandHistoryDown(t *testing.T) {
t.Run("navigate down from middle of history", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "cmd2",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 2, // At cmd2
}
action := MoveCommandHistoryDown{}
action.Execute(m)
// Should load cmd3 (more recent)
if m.Command() != "cmd3" {
t.Errorf("Command = %q, want 'cmd3'", m.Command())
}
// Cursor should be at 1
if m.CommandHistoryCursor() != 1 {
t.Errorf("CommandHistoryCursor = %d, want 1", m.CommandHistoryCursor())
}
})
t.Run("navigate down to empty command", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "cmd3",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 1, // At first command
}
action := MoveCommandHistoryDown{}
action.Execute(m)
// Should clear command (back to current input)
if m.Command() != "" {
t.Errorf("Command = %q, want empty (back to current)", m.Command())
}
if m.CommandCursor() != 0 {
t.Errorf("CommandCursor = %d, want 0", m.CommandCursor())
}
if m.CommandHistoryCursor() != 0 {
t.Errorf("CommandHistoryCursor = %d, want 0", m.CommandHistoryCursor())
}
})
t.Run("navigate down from cursor 0 does nothing", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 0, // Already at bottom
}
action := MoveCommandHistoryDown{}
action.Execute(m)
// Should clear command
if m.Command() != "" {
t.Errorf("Command = %q, want empty", m.Command())
}
if m.CommandHistoryCursor() != 0 {
t.Errorf("CommandHistoryCursor = %d, want 0", m.CommandHistoryCursor())
}
})
t.Run("command cursor moves to end", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandCursor: 0,
commandHistory: []string{"cmd3", "long command here"},
commandHistoryCursor: 2,
}
action := MoveCommandHistoryDown{}
action.Execute(m)
expectedLen := len("cmd3")
if m.CommandCursor() != expectedLen {
t.Errorf("CommandCursor = %d, want %d (end of command)", m.CommandCursor(), expectedLen)
}
})
}
// ==================================================
// Integration Tests
// ==================================================
func TestCommandHistoryIntegration(t *testing.T) {
t.Run("up down up sequence", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 0,
}
upAction := MoveCommandHistoryUp{}
downAction := MoveCommandHistoryDown{}
// Up twice
upAction.Execute(m) // cursor=1, cmd=cmd3
upAction.Execute(m) // cursor=2, cmd=cmd2
// Down once
downAction.Execute(m) // cursor=1, cmd=cmd3
// Up again
upAction.Execute(m) // cursor=2, cmd=cmd2
if m.Command() != "cmd2" {
t.Errorf("Command = %q, want 'cmd2'", m.Command())
}
if m.CommandHistoryCursor() != 2 {
t.Errorf("CommandHistoryCursor = %d, want 2", m.CommandHistoryCursor())
}
})
t.Run("navigate up past end does not crash", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd1", "cmd2"},
commandHistoryCursor: 0,
}
upAction := MoveCommandHistoryUp{}
// Navigate to end
upAction.Execute(m) // cursor = 1, cmd = cmd1
upAction.Execute(m) // cursor = 2, cmd = cmd2
// Try to go past end
upAction.Execute(m) // Should do nothing
if m.CommandHistoryCursor() != 2 {
t.Errorf("CommandHistoryCursor = %d, want 2 (should stay at end)", m.CommandHistoryCursor())
}
})
t.Run("navigate down past bottom does not crash", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd1"},
commandHistoryCursor: 0,
}
downAction := MoveCommandHistoryDown{}
// Try to go down from bottom
downAction.Execute(m)
if m.CommandHistoryCursor() != 0 {
t.Errorf("CommandHistoryCursor = %d, want 0 (should stay at bottom)", m.CommandHistoryCursor())
}
// Try again
downAction.Execute(m)
if m.CommandHistoryCursor() != 0 {
t.Errorf("CommandHistoryCursor = %d, want 0 (should stay at bottom)", m.CommandHistoryCursor())
}
})
}
// ==================================================
// Long History Tests
// ==================================================
func TestCommandHistoryWithLongHistory(t *testing.T) {
t.Run("navigate through 20 commands", func(t *testing.T) {
history := make([]string, 20)
for i := 0; i < 20; i++ {
history[i] = string(rune('A' + i))
}
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: history,
commandHistoryCursor: 0,
}
upAction := MoveCommandHistoryUp{}
// Navigate to 10th command
for i := 0; i < 10; i++ {
upAction.Execute(m)
}
want := string(rune('A' + 9))
if m.Command() != want {
t.Errorf("Command after 10 ups = %q, want %q", m.Command(), want)
}
if m.CommandHistoryCursor() != 10 {
t.Errorf("CommandHistoryCursor = %d, want 10", m.CommandHistoryCursor())
}
})
t.Run("navigate to very end of long history", func(t *testing.T) {
history := make([]string, 50)
for i := 0; i < 50; i++ {
history[i] = string(rune('0' + (i % 10)))
}
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: history,
commandHistoryCursor: 0,
}
upAction := MoveCommandHistoryUp{}
// Navigate all the way to the end
for i := 0; i < 100; i++ { // Try to go past end
upAction.Execute(m)
}
// Should be at position 50 (end of 50-item array)
if m.CommandHistoryCursor() != 50 {
t.Errorf("CommandHistoryCursor = %d, want 50 (at end)", m.CommandHistoryCursor())
}
// Should be showing last command
want := string(rune('0' + 49%10))
if m.Command() != want {
t.Errorf("Command = %q, want %q (last in history)", m.Command(), want)
}
})
}