The tests are starting to get messy, lots of duplication. Going to resolve that. Lots of this is due to AI generation of tests.
450 lines
13 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|