package main import ( "testing" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/x/exp/teatest" ) // sendKeys sends a sequence of keys to the test model func sendKeys(tm *teatest.TestModel, keys ...string) { for _, key := range keys { switch key { case "esc": tm.Send(tea.KeyMsg{Type: tea.KeyEscape}) case "enter": tm.Send(tea.KeyMsg{Type: tea.KeyEnter}) case "backspace": tm.Send(tea.KeyMsg{Type: tea.KeyBackspace}) case "ctrl+c": tm.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) case "ctrl+d": tm.Send(tea.KeyMsg{Type: tea.KeyCtrlD}) default: tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)}) } } } // newTestModel creates a test model with default content func newTestModel(t *testing.T) *teatest.TestModel { lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5", "line 6"} return teatest.NewTestModel(t, newModel(lines), teatest.WithInitialTermSize(80, 24)) } func newTestModelWithLines(t *testing.T, lines []string) *teatest.TestModel { return teatest.NewTestModel(t, newModel(lines), teatest.WithInitialTermSize(80, 24)) } func newTestModelWithCursorPos(t *testing.T, pos Position) *teatest.TestModel { lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5", "line 6"} return teatest.NewTestModel(t, newModelWithPos(lines, pos), teatest.WithInitialTermSize(80, 24)) } func newTestModelWithCursorPosAndLines(t *testing.T, lines []string, pos Position) *teatest.TestModel { return teatest.NewTestModel(t, newModelWithPos(lines, pos), teatest.WithInitialTermSize(80, 24)) } // getFinalModel extracts the final model state (sends ctrl+c to quit first) func getFinalModel(t *testing.T, tm *teatest.TestModel) model { tm.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) fm := tm.FinalModel(t, teatest.WithFinalTimeout(time.Second)) return fm.(model) } func TestMoveDown(t *testing.T) { t.Run("test 'j'", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "j") m := getFinalModel(t, tm) if m.cursor.y != 1 { t.Errorf("cursor.y = %d, want 1", m.cursor.y) } }) t.Run("test 'jjjj'", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "j", "j", "j", "j") m := getFinalModel(t, tm) if m.cursor.y != 4 { t.Errorf("cursor.y = %d, want 4", m.cursor.y) } }) t.Run("test 'jjjjjjjjj's with overflow", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "j", "j", "j", "j", "j", "j", "j", "j", "j") m := getFinalModel(t, tm) if m.cursor.y != 5 { t.Errorf("cursor.y = %d, want 5", m.cursor.y) } }) } func TestMoveDownWithCount(t *testing.T) { t.Run("test '3j'", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "3", "j") m := getFinalModel(t, tm) if m.cursor.y != 3 { t.Errorf("cursor.y = %d, want 3", m.cursor.y) } }) t.Run("test '10j' with overflow", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "1", "0", "j") m := getFinalModel(t, tm) if m.cursor.y != 5 { t.Errorf("cursor.y = %d, want 5", m.cursor.y) } }) } func TestMoveDownWithOverflow(t *testing.T) { lines := []string{"long line", "small"} t.Run("test 'j' with overflow", func(t *testing.T) { tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 8, Line: 0}) sendKeys(tm, "j") m := getFinalModel(t, tm) want := len(lines[1]) if m.cursor.x != want { t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) } }) t.Run("test 'j' without overflow", func(t *testing.T) { tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 3, Line: 0}) sendKeys(tm, "j") m := getFinalModel(t, tm) if m.cursor.x != 3 { t.Errorf("cursor.x = %d, want 3", m.cursor.x) } }) } func TestMoveUp(t *testing.T) { t.Run("test 'k'", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 0, Line: 2}) sendKeys(tm, "k") m := getFinalModel(t, tm) if m.cursor.y != 1 { t.Errorf("cursor.y = %d, want 1", m.cursor.y) } }) t.Run("test 'kkkk'", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 0, Line: 4}) sendKeys(tm, "k", "k", "k", "k") m := getFinalModel(t, tm) if m.cursor.y != 0 { t.Errorf("cursor.y = %d, want 0", m.cursor.y) } }) t.Run("test 'k' at top (no movement)", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "k", "k", "k") m := getFinalModel(t, tm) if m.cursor.y != 0 { t.Errorf("cursor.y = %d, want 0", m.cursor.y) } }) } func TestMoveUpWithCount(t *testing.T) { t.Run("test '3k'", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 0, Line: 5}) sendKeys(tm, "3", "k") m := getFinalModel(t, tm) if m.cursor.y != 2 { t.Errorf("cursor.y = %d, want 2", m.cursor.y) } }) t.Run("test '10k' with overflow", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 0, Line: 3}) sendKeys(tm, "1", "0", "k") m := getFinalModel(t, tm) if m.cursor.y != 0 { t.Errorf("cursor.y = %d, want 0", m.cursor.y) } }) } func TestMoveUpWithOverflow(t *testing.T) { lines := []string{"small", "long line"} t.Run("test 'k' with overflow", func(t *testing.T) { tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 10, Line: 1}) sendKeys(tm, "k") m := getFinalModel(t, tm) want := len(lines[0]) if m.cursor.x != want { t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) } }) t.Run("test 'k' without overflow", func(t *testing.T) { tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 3, Line: 1}) sendKeys(tm, "k") m := getFinalModel(t, tm) if m.cursor.x != 3 { t.Errorf("cursor.x = %d, want 3", m.cursor.x) } }) } func TestMoveRight(t *testing.T) { t.Run("test 'l'", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "l") m := getFinalModel(t, tm) if m.cursor.x != 1 { t.Errorf("cursor.x = %d, want 1", m.cursor.x) } }) t.Run("test 'llll'", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "l", "l", "l", "l") m := getFinalModel(t, tm) if m.cursor.x != 4 { t.Errorf("cursor.x = %d, want 4", m.cursor.x) } }) t.Run("test 'l' at end of line (no movement past end)", func(t *testing.T) { lines := []string{"abc"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "l", "l", "l", "l", "l", "l") m := getFinalModel(t, tm) want := len(lines[0]) if m.cursor.x != want { t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) } }) } func TestMoveRightWithCount(t *testing.T) { t.Run("test '3l'", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "3", "l") m := getFinalModel(t, tm) if m.cursor.x != 3 { t.Errorf("cursor.x = %d, want 3", m.cursor.x) } }) t.Run("test '10l' with overflow", func(t *testing.T) { lines := []string{"short"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "1", "0", "l") m := getFinalModel(t, tm) want := len(lines[0]) if m.cursor.x != want { t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) } }) } func TestMoveLeft(t *testing.T) { t.Run("test 'h'", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 3, Line: 0}) sendKeys(tm, "h") m := getFinalModel(t, tm) if m.cursor.x != 2 { t.Errorf("cursor.x = %d, want 2", m.cursor.x) } }) t.Run("test 'hhhh'", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 4, Line: 0}) sendKeys(tm, "h", "h", "h", "h") m := getFinalModel(t, tm) if m.cursor.x != 0 { t.Errorf("cursor.x = %d, want 0", m.cursor.x) } }) t.Run("test 'h' at start (no movement)", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "h", "h", "h") m := getFinalModel(t, tm) if m.cursor.x != 0 { t.Errorf("cursor.x = %d, want 0", m.cursor.x) } }) } func TestMoveLeftWithCount(t *testing.T) { t.Run("test '3h'", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 5, Line: 0}) sendKeys(tm, "3", "h") m := getFinalModel(t, tm) if m.cursor.x != 2 { t.Errorf("cursor.x = %d, want 2", m.cursor.x) } }) t.Run("test '10h' with overflow", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 3, Line: 0}) sendKeys(tm, "1", "0", "h") m := getFinalModel(t, tm) if m.cursor.x != 0 { t.Errorf("cursor.x = %d, want 0", m.cursor.x) } }) } // --- G and gg Tests --- func TestMoveToBottom(t *testing.T) { t.Run("test 'G' from top", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "G") m := getFinalModel(t, tm) if m.cursor.y != 5 { t.Errorf("cursor.y = %d, want 5", m.cursor.y) } }) t.Run("test 'G' from middle", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 0, Line: 2}) sendKeys(tm, "G") m := getFinalModel(t, tm) if m.cursor.y != 5 { t.Errorf("cursor.y = %d, want 5", m.cursor.y) } }) t.Run("test 'G' already at bottom", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 0, Line: 5}) sendKeys(tm, "G") m := getFinalModel(t, tm) if m.cursor.y != 5 { t.Errorf("cursor.y = %d, want 5", m.cursor.y) } }) t.Run("test 'G' clamps cursor.x", func(t *testing.T) { lines := []string{"long line here", "short"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 10, Line: 0}) sendKeys(tm, "G") m := getFinalModel(t, tm) if m.cursor.y != 1 { t.Errorf("cursor.y = %d, want 1", m.cursor.y) } want := len(lines[1]) if m.cursor.x != want { t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) } }) t.Run("test 'G' on single line file", func(t *testing.T) { lines := []string{"only line"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "G") m := getFinalModel(t, tm) if m.cursor.y != 0 { t.Errorf("cursor.y = %d, want 0", m.cursor.y) } }) } func TestMoveToTop(t *testing.T) { t.Run("test 'gg' from bottom", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 0, Line: 5}) sendKeys(tm, "g", "g") m := getFinalModel(t, tm) if m.cursor.y != 0 { t.Errorf("cursor.y = %d, want 0", m.cursor.y) } }) t.Run("test 'gg' from middle", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 0, Line: 3}) sendKeys(tm, "g", "g") m := getFinalModel(t, tm) if m.cursor.y != 0 { t.Errorf("cursor.y = %d, want 0", m.cursor.y) } }) t.Run("test 'gg' already at top", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "g", "g") m := getFinalModel(t, tm) if m.cursor.y != 0 { t.Errorf("cursor.y = %d, want 0", m.cursor.y) } }) t.Run("test 'gg' clamps cursor.x", func(t *testing.T) { lines := []string{"short", "long line here"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 10, Line: 1}) sendKeys(tm, "g", "g") m := getFinalModel(t, tm) if m.cursor.y != 0 { t.Errorf("cursor.y = %d, want 0", m.cursor.y) } want := len(lines[0]) if m.cursor.x != want { t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) } }) } // --- 0 and $ Tests --- func TestMoveToLineStart(t *testing.T) { t.Run("test '0' from middle of line", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 3, Line: 0}) sendKeys(tm, "0") m := getFinalModel(t, tm) if m.cursor.x != 0 { t.Errorf("cursor.x = %d, want 0", m.cursor.x) } }) t.Run("test '0' from end of line", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: len(lines[0]), Line: 0}) sendKeys(tm, "0") m := getFinalModel(t, tm) if m.cursor.x != 0 { t.Errorf("cursor.x = %d, want 0", m.cursor.x) } }) t.Run("test '0' already at start", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "0") m := getFinalModel(t, tm) if m.cursor.x != 0 { t.Errorf("cursor.x = %d, want 0", m.cursor.x) } }) t.Run("test '0' on empty line", func(t *testing.T) { lines := []string{""} tm := newTestModelWithLines(t, lines) sendKeys(tm, "0") m := getFinalModel(t, tm) if m.cursor.x != 0 { t.Errorf("cursor.x = %d, want 0", m.cursor.x) } }) t.Run("test '0' preserves line", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 3, Line: 2}) sendKeys(tm, "0") m := getFinalModel(t, tm) if m.cursor.y != 2 { t.Errorf("cursor.y = %d, want 2", m.cursor.y) } }) } func TestMoveToLineEnd(t *testing.T) { t.Run("test '$' from start of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "$") m := getFinalModel(t, tm) want := len(lines[0]) if m.cursor.x != want { t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) } }) t.Run("test '$' from middle of line", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 3, Line: 0}) sendKeys(tm, "$") m := getFinalModel(t, tm) want := len(lines[0]) if m.cursor.x != want { t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) } }) t.Run("test '$' already at end", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: len(lines[0]), Line: 0}) sendKeys(tm, "$") m := getFinalModel(t, tm) want := len(lines[0]) if m.cursor.x != want { t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) } }) t.Run("test '$' on empty line", func(t *testing.T) { lines := []string{""} tm := newTestModelWithLines(t, lines) sendKeys(tm, "$") m := getFinalModel(t, tm) if m.cursor.x != 0 { t.Errorf("cursor.x = %d, want 0", m.cursor.x) } }) t.Run("test '$' preserves line", func(t *testing.T) { tm := newTestModelWithCursorPos(t, Position{Col: 0, Line: 2}) sendKeys(tm, "$") m := getFinalModel(t, tm) if m.cursor.y != 2 { t.Errorf("cursor.y = %d, want 2", m.cursor.y) } }) } // --- Delete Char (x) Tests --- func TestDeleteChar(t *testing.T) { t.Run("test 'x' deletes character under cursor", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "x") m := getFinalModel(t, tm) if m.lines[0] != "ello" { t.Errorf("lines[0] = %q, want 'ello'", m.lines[0]) } }) t.Run("test 'x' in middle of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 2, Line: 0}) sendKeys(tm, "x") m := getFinalModel(t, tm) if m.lines[0] != "helo" { t.Errorf("lines[0] = %q, want 'helo'", m.lines[0]) } }) t.Run("test 'x' at end of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 4, Line: 0}) sendKeys(tm, "x") m := getFinalModel(t, tm) if m.lines[0] != "hell" { t.Errorf("lines[0] = %q, want 'hell'", m.lines[0]) } }) t.Run("test 'xx' deletes two characters", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "x", "x") m := getFinalModel(t, tm) if m.lines[0] != "llo" { t.Errorf("lines[0] = %q, want 'llo'", m.lines[0]) } }) } func TestDeleteCharWithCount(t *testing.T) { t.Run("test '3x' deletes three characters", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "3", "x") m := getFinalModel(t, tm) if m.lines[0] != "lo" { t.Errorf("lines[0] = %q, want 'lo'", m.lines[0]) } }) t.Run("test '10x' with overflow", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "1", "0", "x") m := getFinalModel(t, tm) if m.lines[0] != "" { t.Errorf("lines[0] = %q, want ''", m.lines[0]) } }) t.Run("test '2x' from middle", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 1, Line: 0}) sendKeys(tm, "2", "x") m := getFinalModel(t, tm) if m.lines[0] != "hlo" { t.Errorf("lines[0] = %q, want 'hlo'", m.lines[0]) } }) } // --- Insert Mode Tests --- func TestEnterInsert(t *testing.T) { t.Run("test 'i' enters insert mode", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "i") m := getFinalModel(t, tm) if m.mode != InsertMode { t.Errorf("mode = %d, want InsertMode (%d)", m.mode, InsertMode) } }) t.Run("test 'i' insert at beginning", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "i", "X", "esc") m := getFinalModel(t, tm) if m.lines[0] != "Xhello" { t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0]) } }) t.Run("test 'i' insert in middle", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 2, Line: 0}) sendKeys(tm, "i", "X", "esc") m := getFinalModel(t, tm) if m.lines[0] != "heXllo" { t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0]) } }) t.Run("test 'i' cursor moves back on esc", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 2, Line: 0}) sendKeys(tm, "i", "X", "esc") m := getFinalModel(t, tm) if m.cursor.x != 2 { t.Errorf("cursor.x = %d, want 2", m.cursor.x) } }) } func TestEnterInsertAfter(t *testing.T) { t.Run("test 'a' enters insert mode", func(t *testing.T) { tm := newTestModel(t) sendKeys(tm, "a") m := getFinalModel(t, tm) if m.mode != InsertMode { t.Errorf("mode = %d, want InsertMode (%d)", m.mode, InsertMode) } }) t.Run("test 'a' inserts after cursor", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "a", "X", "esc") m := getFinalModel(t, tm) if m.lines[0] != "hXello" { t.Errorf("lines[0] = %q, want 'hXello'", m.lines[0]) } }) t.Run("test 'a' from middle of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 2, Line: 0}) sendKeys(tm, "a", "X", "esc") m := getFinalModel(t, tm) if m.lines[0] != "helXlo" { t.Errorf("lines[0] = %q, want 'helXlo'", m.lines[0]) } }) } func TestEnterInsertLineStart(t *testing.T) { t.Run("test 'I' enters insert mode at line start", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 3, Line: 0}) sendKeys(tm, "I", "X", "esc") m := getFinalModel(t, tm) if m.lines[0] != "Xhello" { t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0]) } }) t.Run("test 'I' from end of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 5, Line: 0}) sendKeys(tm, "I", "X", "esc") m := getFinalModel(t, tm) if m.lines[0] != "Xhello" { t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0]) } }) } func TestEnterInsertLineEnd(t *testing.T) { t.Run("test 'A' enters insert mode at line end", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "A", "X", "esc") m := getFinalModel(t, tm) if m.lines[0] != "helloX" { t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0]) } }) t.Run("test 'A' from middle of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 2, Line: 0}) sendKeys(tm, "A", "X", "esc") m := getFinalModel(t, tm) if m.lines[0] != "helloX" { t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0]) } }) } // --- Open Line Tests --- func TestOpenLineBelow(t *testing.T) { t.Run("test 'o' creates line below", func(t *testing.T) { lines := []string{"line 1", "line 2"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "o", "n", "e", "w", "esc") m := getFinalModel(t, tm) if len(m.lines) != 3 { t.Errorf("len(lines) = %d, want 3", len(m.lines)) } if m.lines[1] != "new" { t.Errorf("lines[1] = %q, want 'new'", m.lines[1]) } }) t.Run("test 'o' from middle of file", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 0, Line: 1}) sendKeys(tm, "o", "n", "e", "w", "esc") m := getFinalModel(t, tm) if len(m.lines) != 4 { t.Errorf("len(lines) = %d, want 4", len(m.lines)) } if m.lines[2] != "new" { t.Errorf("lines[2] = %q, want 'new'", m.lines[2]) } }) t.Run("test 'o' at end of file", func(t *testing.T) { lines := []string{"line 1", "line 2"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 0, Line: 1}) sendKeys(tm, "o", "n", "e", "w", "esc") m := getFinalModel(t, tm) if len(m.lines) != 3 { t.Errorf("len(lines) = %d, want 3", len(m.lines)) } if m.lines[2] != "new" { t.Errorf("lines[2] = %q, want 'new'", m.lines[2]) } }) t.Run("test 'o' cursor moves to new line", func(t *testing.T) { lines := []string{"line 1", "line 2"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "o", "esc") m := getFinalModel(t, tm) if m.cursor.y != 1 { t.Errorf("cursor.y = %d, want 1", m.cursor.y) } if m.cursor.x != 0 { t.Errorf("cursor.x = %d, want 0", m.cursor.x) } }) } func TestOpenLineBelowWithCount(t *testing.T) { t.Run("test '3o' creates 3 lines", func(t *testing.T) { lines := []string{"line 1"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "3", "o", "x", "esc") m := getFinalModel(t, tm) if len(m.lines) != 4 { t.Errorf("len(lines) = %d, want 4", len(m.lines)) } for i := 1; i <= 3; i++ { if m.lines[i] != "x" { t.Errorf("lines[%d] = %q, want 'x'", i, m.lines[i]) } } }) t.Run("test '2o' with multiple chars", func(t *testing.T) { lines := []string{"line 1"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "2", "o", "a", "b", "esc") m := getFinalModel(t, tm) if len(m.lines) != 3 { t.Errorf("len(lines) = %d, want 3", len(m.lines)) } if m.lines[1] != "ab" { t.Errorf("lines[1] = %q, want 'ab'", m.lines[1]) } if m.lines[2] != "ab" { t.Errorf("lines[2] = %q, want 'ab'", m.lines[2]) } }) } func TestOpenLineAbove(t *testing.T) { t.Run("test 'O' creates line above", func(t *testing.T) { lines := []string{"line 1", "line 2"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 0, Line: 1}) sendKeys(tm, "O", "n", "e", "w", "esc") m := getFinalModel(t, tm) if len(m.lines) != 3 { t.Errorf("len(lines) = %d, want 3", len(m.lines)) } if m.lines[1] != "new" { t.Errorf("lines[1] = %q, want 'new'", m.lines[1]) } }) t.Run("test 'O' at top of file", func(t *testing.T) { lines := []string{"line 1", "line 2"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "O", "n", "e", "w", "esc") m := getFinalModel(t, tm) if len(m.lines) != 3 { t.Errorf("len(lines) = %d, want 3", len(m.lines)) } if m.lines[0] != "new" { t.Errorf("lines[0] = %q, want 'new'", m.lines[0]) } }) t.Run("test 'O' cursor at start of new line", func(t *testing.T) { lines := []string{"line 1", "line 2"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 3, Line: 1}) sendKeys(tm, "O", "esc") m := getFinalModel(t, tm) if m.cursor.x != 0 { t.Errorf("cursor.x = %d, want 0", m.cursor.x) } }) } func TestOpenLineAboveWithCount(t *testing.T) { t.Run("test '3O' creates 3 lines above", func(t *testing.T) { lines := []string{"line 1"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 0, Line: 0}) sendKeys(tm, "3", "O", "x", "esc") m := getFinalModel(t, tm) if len(m.lines) != 4 { t.Errorf("len(lines) = %d, want 4", len(m.lines)) } for i := 0; i < 3; i++ { if m.lines[i] != "x" { t.Errorf("lines[%d] = %q, want 'x'", i, m.lines[i]) } } }) } // --- Insert Mode Special Keys --- func TestInsertModeEnter(t *testing.T) { t.Run("test enter splits line", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 5, Line: 0}) sendKeys(tm, "i", "enter", "esc") m := getFinalModel(t, tm) if len(m.lines) != 2 { t.Errorf("len(lines) = %d, want 2", len(m.lines)) } if m.lines[0] != "hello" { t.Errorf("lines[0] = %q, want 'hello'", m.lines[0]) } if m.lines[1] != " world" { t.Errorf("lines[1] = %q, want ' world'", m.lines[1]) } }) t.Run("test enter at end of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 5, Line: 0}) sendKeys(tm, "i", "enter", "esc") m := getFinalModel(t, tm) if len(m.lines) != 2 { t.Errorf("len(lines) = %d, want 2", len(m.lines)) } if m.lines[0] != "hello" { t.Errorf("lines[0] = %q, want 'hello'", m.lines[0]) } if m.lines[1] != "" { t.Errorf("lines[1] = %q, want ''", m.lines[1]) } }) t.Run("test enter at start of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "i", "enter", "esc") m := getFinalModel(t, tm) if len(m.lines) != 2 { t.Errorf("len(lines) = %d, want 2", len(m.lines)) } if m.lines[0] != "" { t.Errorf("lines[0] = %q, want ''", m.lines[0]) } if m.lines[1] != "hello" { t.Errorf("lines[1] = %q, want 'hello'", m.lines[1]) } }) } func TestInsertModeBackspace(t *testing.T) { t.Run("test backspace deletes character", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 3, Line: 0}) sendKeys(tm, "i", "backspace", "esc") m := getFinalModel(t, tm) if m.lines[0] != "helo" { t.Errorf("lines[0] = %q, want 'helo'", m.lines[0]) } }) t.Run("test backspace at start of line joins lines", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 0, Line: 1}) sendKeys(tm, "i", "backspace", "esc") m := getFinalModel(t, tm) if len(m.lines) != 1 { t.Errorf("len(lines) = %d, want 1", len(m.lines)) } if m.lines[0] != "helloworld" { t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0]) } }) t.Run("test backspace at start of first line does nothing", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "i", "backspace", "esc") m := getFinalModel(t, tm) if m.lines[0] != "hello" { t.Errorf("lines[0] = %q, want 'hello'", m.lines[0]) } }) t.Run("test multiple backspaces", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithCursorPosAndLines(t, lines, Position{Col: 5, Line: 0}) sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc") m := getFinalModel(t, tm) if m.lines[0] != "he" { t.Errorf("lines[0] = %q, want 'he'", m.lines[0]) } }) }