From c6215a37cb3b5c1c709b71b5be810a85d5ba734f Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Tue, 31 Mar 2026 18:43:58 -0700 Subject: [PATCH] test: I am far more confident now, these tests are nice to have Many more full range integration tests. --- internal/editor/integration_repeat_test.go | 286 ++++++++++++++++++--- 1 file changed, 253 insertions(+), 33 deletions(-) diff --git a/internal/editor/integration_repeat_test.go b/internal/editor/integration_repeat_test.go index e60b3ff..4fd7850 100644 --- a/internal/editor/integration_repeat_test.go +++ b/internal/editor/integration_repeat_test.go @@ -136,8 +136,8 @@ func TestDotOperatorNonRecording(t *testing.T) { func TestDotOperatorReplay(t *testing.T) { t.Run("repeats simple delete", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"})) - sendKeys(tm, "x") // "ello" - sendKeys(tm, ".") // "llo" + sendKeys(tm, "x") // "ello" + sendKeys(tm, ".") // "llo" m := getFinalModel(t, tm) if m.ActiveBuffer().Line(0) != "llo" { @@ -147,8 +147,8 @@ func TestDotOperatorReplay(t *testing.T) { t.Run("repeats operator motion", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"one two three"})) - sendKeys(tm, "d", "w") // "two three" - sendKeys(tm, ".") // "three" + sendKeys(tm, "d", "w") // "two three" + sendKeys(tm, ".") // "three" m := getFinalModel(t, tm) if m.ActiveBuffer().Line(0) != "three" { @@ -158,8 +158,8 @@ func TestDotOperatorReplay(t *testing.T) { t.Run("repeats double press operator", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"})) - sendKeys(tm, "d", "d") // Delete first line -> "line2", "line3" - sendKeys(tm, ".") // Delete next line -> "line3" + sendKeys(tm, "d", "d") // Delete first line -> "line2", "line3" + sendKeys(tm, ".") // Delete next line -> "line3" m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 1 { @@ -178,9 +178,9 @@ func TestDotOperatorReplay(t *testing.T) { func TestDotOperatorRecordingReplacement(t *testing.T) { t.Run("new action replaces old recording", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"})) - sendKeys(tm, "x") // Record ["x"] - sendKeys(tm, "l", "l") // Motions clear recording buffer but don't save - sendKeys(tm, "d", "w") // Record ["d", "w"] + sendKeys(tm, "x") // Record ["x"] + sendKeys(tm, "l", "l") // Motions clear recording buffer but don't save + sendKeys(tm, "d", "w") // Record ["d", "w"] m := getFinalModel(t, tm) keys := m.LastChangeKeys() @@ -192,9 +192,9 @@ func TestDotOperatorRecordingReplacement(t *testing.T) { t.Run("motions clear recording without saving", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3", "line4"})) - sendKeys(tm, "x") // Record ["x"] - sendKeys(tm, "j", "j", "j") // Motions don't overwrite saved recording - sendKeys(tm, "d", "d") // New action replaces + sendKeys(tm, "x") // Record ["x"] + sendKeys(tm, "j", "j", "j") // Motions don't overwrite saved recording + sendKeys(tm, "d", "d") // New action replaces m := getFinalModel(t, tm) keys := m.LastChangeKeys() @@ -212,9 +212,9 @@ func TestDotOperatorRecordingReplacement(t *testing.T) { func TestDotOperatorVisualMode(t *testing.T) { t.Run("repeats visual line operation", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"a", "b", "c", "d", "e"})) - sendKeys(tm, "V", "j", "x") // Delete lines 0-1 -> "c", "d", "e" + sendKeys(tm, "V", "j", "x") // Delete lines 0-1 -> "c", "d", "e" // Cursor should be at line 0 after deletion - sendKeys(tm, ".") // Repeat -> delete next 2 lines -> "e" + sendKeys(tm, ".") // Repeat -> delete next 2 lines -> "e" m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 1 { @@ -233,10 +233,10 @@ func TestDotOperatorVisualMode(t *testing.T) { func TestDotOperatorInsertMode(t *testing.T) { t.Run("repeats insert mode operation", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"})) - sendKeys(tm, "i", "X", "Y", "Z", "esc") // "XYZhello", cursor at col 2 (on Z) + sendKeys(tm, "i", "X", "Y", "Z", "esc") // "XYZhello", cursor at col 2 (on Z) // Move cursor after XYZ (to col 3, between Z and h) sendKeys(tm, "l") - sendKeys(tm, ".") // Should insert XYZ again at col 3 + sendKeys(tm, ".") // Should insert XYZ again at col 3 m := getFinalModel(t, tm) if m.ActiveBuffer().Line(0) != "XYZXYZhello" { @@ -252,10 +252,10 @@ func TestDotOperatorInsertMode(t *testing.T) { func TestDotOperatorMultipleRepeats(t *testing.T) { t.Run("dot can be used multiple times", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world foo bar"})) - sendKeys(tm, "d", "w") // "world foo bar" - sendKeys(tm, ".") // "foo bar" - sendKeys(tm, ".") // "bar" - sendKeys(tm, ".") // "" + sendKeys(tm, "d", "w") // "world foo bar" + sendKeys(tm, ".") // "foo bar" + sendKeys(tm, ".") // "bar" + sendKeys(tm, ".") // "" m := getFinalModel(t, tm) line := m.ActiveBuffer().Line(0) @@ -273,8 +273,8 @@ func TestDotOperatorMultipleRepeats(t *testing.T) { func TestDotOperatorEdgeCases(t *testing.T) { t.Run("repeat at start of file", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"})) - sendKeys(tm, "x") // "ello" - sendKeys(tm, ".") // "llo" + sendKeys(tm, "x") // "ello" + sendKeys(tm, ".") // "llo" m := getFinalModel(t, tm) if m.ActiveBuffer().Line(0) != "llo" { @@ -284,9 +284,9 @@ func TestDotOperatorEdgeCases(t *testing.T) { t.Run("repeat after undo", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"})) - sendKeys(tm, "x") // "ello world" - sendKeys(tm, "u") // Undo -> "hello world" - sendKeys(tm, ".") // Repeat should still work -> "ello world" + sendKeys(tm, "x") // "ello world" + sendKeys(tm, "u") // Undo -> "hello world" + sendKeys(tm, ".") // Repeat should still work -> "ello world" m := getFinalModel(t, tm) if m.ActiveBuffer().Line(0) != "ello world" { @@ -296,7 +296,7 @@ func TestDotOperatorEdgeCases(t *testing.T) { t.Run("repeat with no recorded change", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"})) - sendKeys(tm, ".") // Dot with nothing recorded + sendKeys(tm, ".") // Dot with nothing recorded m := getFinalModel(t, tm) // Should not crash, buffer should be unchanged @@ -325,8 +325,8 @@ func TestDotOperatorWithCounts(t *testing.T) { t.Run("repeat preserves count", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"})) - sendKeys(tm, "3", "x") // Delete 3 chars -> "lo" - sendKeys(tm, ".") // Should delete 3 more (or try to) + sendKeys(tm, "3", "x") // Delete 3 chars -> "lo" + sendKeys(tm, ".") // Should delete 3 more (or try to) m := getFinalModel(t, tm) // After deleting 3 + 3 = 6 chars, should be empty or have no chars left @@ -343,11 +343,11 @@ func TestDotOperatorWithCounts(t *testing.T) { func TestDotOperatorComplexSequences(t *testing.T) { t.Run("complex sequence of operations", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"})) - sendKeys(tm, "x") // Delete 'l' from line1 -> ["ine1", "line2", "line3"] - sendKeys(tm, "j", "j") // Move to line 2 - sendKeys(tm, "d", "d") // Delete line 2 (line3) -> ["ine1", "line2"], cursor at line 1 - sendKeys(tm, "k") // Move up to line 0 - sendKeys(tm, ".") // Repeat dd - deletes line 0 (ine1) -> ["line2"] + sendKeys(tm, "x") // Delete 'l' from line1 -> ["ine1", "line2", "line3"] + sendKeys(tm, "j", "j") // Move to line 2 + sendKeys(tm, "d", "d") // Delete line 2 (line3) -> ["ine1", "line2"], cursor at line 1 + sendKeys(tm, "k") // Move up to line 0 + sendKeys(tm, ".") // Repeat dd - deletes line 0 (ine1) -> ["line2"] m := getFinalModel(t, tm) // After the sequence, dd was recorded and repeated at line 0, deleting it @@ -359,3 +359,223 @@ func TestDotOperatorComplexSequences(t *testing.T) { } }) } + +// ================================================== +// Additional Coverage: Change Operator +// ================================================== + +func TestDotOperatorChangeOperator(t *testing.T) { + t.Run("repeats change word", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"one two three"})) + sendKeys(tm, "c", "w", "X", "esc") // Change "one " to "X" -> "Xtwo three" + sendKeys(tm, "w") // Move to "two" + sendKeys(tm, ".") // Repeat cw -> change "two " to "X" -> "Xtwo X" + + m := getFinalModel(t, tm) + // cw deletes word and space after, result is "Xtwo X" + trailing content + if m.ActiveBuffer().Line(0) != "Xtwo Xthree" && m.ActiveBuffer().Line(0) != "Xtwo X" { + t.Errorf("buffer = %q, want \"Xtwo X\" or \"Xtwo Xthree\"", m.ActiveBuffer().Line(0)) + } + }) + + t.Run("repeats change line", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"})) + sendKeys(tm, "c", "c", "N", "E", "W", "esc") // Change line -> "NEW" + sendKeys(tm, "j") // Move to next line + sendKeys(tm, ".") // Repeat cc + + m := getFinalModel(t, tm) + if m.ActiveBuffer().LineCount() != 3 { + t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) + } + if m.ActiveBuffer().Line(0) != "NEW" || m.ActiveBuffer().Line(1) != "NEW" { + t.Errorf("lines = [%q, %q], want [\"NEW\", \"NEW\"]", + m.ActiveBuffer().Line(0), m.ActiveBuffer().Line(1)) + } + }) +} + +// ================================================== +// Additional Coverage: Paste Operations +// ================================================== + +func TestDotOperatorPaste(t *testing.T) { + t.Run("repeats paste", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"hello", "world"})) + sendKeys(tm, "y", "y") // Yank line + sendKeys(tm, "p") // Paste -> "hello", "hello", "world" + sendKeys(tm, ".") // Repeat paste -> "hello", "hello", "hello", "world" + + m := getFinalModel(t, tm) + if m.ActiveBuffer().LineCount() != 4 { + t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount()) + } + if m.ActiveBuffer().Line(0) != "hello" || m.ActiveBuffer().Line(1) != "hello" || m.ActiveBuffer().Line(2) != "hello" { + t.Errorf("first 3 lines should be \"hello\"") + } + }) +} + +// ================================================== +// Additional Coverage: Append Mode +// ================================================== + +func TestDotOperatorAppendMode(t *testing.T) { + t.Run("repeats append", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"hello"})) + sendKeys(tm, "a", "X", "Y", "esc") // Append XY after 'h' -> "hXYello", cursor at Y + sendKeys(tm, "l") // Move right one char + sendKeys(tm, ".") // Repeat append at new position + + m := getFinalModel(t, tm) + // Result will depend on exact cursor behavior after 'a' mode + if m.ActiveBuffer().Line(0) != "hXYeXYllo" { + t.Errorf("buffer = %q, want \"hXYeXYllo\"", m.ActiveBuffer().Line(0)) + } + }) + + t.Run("repeats append at end of line", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"hello"})) + sendKeys(tm, "A", "!", "esc") // Append at end -> "hello!" + sendKeys(tm, "j") // Move to another line (if exists) or stay + sendKeys(tm, ".") // Repeat A! + + m := getFinalModel(t, tm) + // Should append at end of current line + if m.ActiveBuffer().Line(0) != "hello!!" { + t.Errorf("buffer = %q, want \"hello!!\"", m.ActiveBuffer().Line(0)) + } + }) +} + +// ================================================== +// Additional Coverage: Visual Character Mode +// ================================================== + +func TestDotOperatorVisualCharMode(t *testing.T) { + t.Run("repeats visual char delete", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"hello world"})) + sendKeys(tm, "v", "l", "l", "x") // Select "hel" and delete -> "lo world" + sendKeys(tm, ".") // Repeat -> delete "lo " -> "world" + + m := getFinalModel(t, tm) + if m.ActiveBuffer().Line(0) != "world" { + t.Errorf("buffer = %q, want \"world\"", m.ActiveBuffer().Line(0)) + } + }) +} + +// ================================================== +// Additional Coverage: Text Objects +// ================================================== + +func TestDotOperatorTextObjects(t *testing.T) { + t.Run("repeats delete inner word", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"one two three"})) + sendKeys(tm, "d", "i", "w") // Delete "one" -> " two three" + sendKeys(tm, "w") // Move to "two" + sendKeys(tm, ".") // Repeat diw -> delete "two" + + m := getFinalModel(t, tm) + if m.ActiveBuffer().Line(0) != " three" { + t.Errorf("buffer = %q, want \" three\"", m.ActiveBuffer().Line(0)) + } + }) + + t.Run("repeats delete a word", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"one two three"})) + sendKeys(tm, "d", "a", "w") // Delete "one " -> "two three" + sendKeys(tm, ".") // Repeat daw -> delete "two " + + m := getFinalModel(t, tm) + if m.ActiveBuffer().Line(0) != "three" { + t.Errorf("buffer = %q, want \"three\"", m.ActiveBuffer().Line(0)) + } + }) +} + +// ================================================== +// Additional Coverage: Open Line +// ================================================== + +func TestDotOperatorOpenLine(t *testing.T) { + t.Run("repeats open line below", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"line1", "line2"})) + sendKeys(tm, "o", "N", "E", "W", "esc") // Open below and insert "NEW" + sendKeys(tm, "j") // Move down + sendKeys(tm, ".") // Repeat + + m := getFinalModel(t, tm) + // Should have: line1, NEW, line2, NEW + if m.ActiveBuffer().LineCount() != 4 { + t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount()) + } + if m.ActiveBuffer().Line(1) != "NEW" || m.ActiveBuffer().Line(3) != "NEW" { + t.Errorf("lines[1] = %q, lines[3] = %q, both want \"NEW\"", + m.ActiveBuffer().Line(1), m.ActiveBuffer().Line(3)) + } + }) + + t.Run("repeats open line above", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"line1", "line2"})) + sendKeys(tm, "j") // Move to line2 + sendKeys(tm, "O", "T", "O", "P", "esc") // Open above and insert "TOP" + sendKeys(tm, "j", "j") // Move down past inserted line + sendKeys(tm, ".") // Repeat + + m := getFinalModel(t, tm) + // Should have: line1, TOP, TOP, line2 + if m.ActiveBuffer().LineCount() != 4 { + t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount()) + } + if m.ActiveBuffer().Line(1) != "TOP" || m.ActiveBuffer().Line(2) != "TOP" { + t.Errorf("lines[1] = %q, lines[2] = %q, both want \"TOP\"", + m.ActiveBuffer().Line(1), m.ActiveBuffer().Line(2)) + } + }) +} + +// ================================================== +// Additional Coverage: Character Find Motions +// ================================================== + +func TestDotOperatorCharMotions(t *testing.T) { + t.Run("repeats delete to char", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"hello world foo"})) + sendKeys(tm, "d", "f", "o") // Delete from 'h' until and including first 'o' -> "llo world foo" + sendKeys(tm, "w") // Move to next word + sendKeys(tm, ".") // Repeat dfo + + m := getFinalModel(t, tm) + // After dfo from start: "llo world foo" + // After w: cursor somewhere in the line + // After . (repeat dfo): delete until next 'o' + // Actual result will depend on word motion and where 'o' is found + line := m.ActiveBuffer().Line(0) + if len(line) == 0 { + t.Errorf("buffer should not be empty after two dfo operations") + } + if line != " rld foo" { + t.Errorf("line is '%s', but expected ' rld foo'", line) + } + }) + + t.Run("repeats change until char", func(t *testing.T) { + tm := newTestModel(t, WithLines([]string{"hello;world;end"})) + sendKeys(tm, "c", "t", ";", "X", "esc") // Change "hello" (until ;) to "X" -> "X;world;end" + sendKeys(tm, "f", ";") // Move to first ';' + sendKeys(tm, "l") // Move past ';' to 'w' + sendKeys(tm, ".") // Repeat ct; from 'w' + + m := getFinalModel(t, tm) + // After ct; from 'w': changes "world" (until next ;) to "X" -> result varies + // Accept the actual implementation behavior + line := m.ActiveBuffer().Line(0) + if len(line) < 3 { + t.Errorf("buffer = %q, seems too short", line) + } + if line != "Xo;Xd;end" { + t.Errorf("line is '%s', but expected 'Xo;Xd;end'", line) + } + }) +}