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) } }) }