diff options
| author | spiperac <[email protected]> | 2026-06-15 06:42:13 +0200 |
|---|---|---|
| committer | spiperac <[email protected]> | 2026-06-15 06:42:13 +0200 |
| commit | 0a477fd288d064d265395c88dbd0f261b723804e (patch) | |
| tree | 9b129f8f41c907285b89ed8385679934bb59c14f /internal/ui/widgets/textviewentry_test.go | |
Initial release
Diffstat (limited to 'internal/ui/widgets/textviewentry_test.go')
| -rw-r--r-- | internal/ui/widgets/textviewentry_test.go | 381 |
1 files changed, 381 insertions, 0 deletions
diff --git a/internal/ui/widgets/textviewentry_test.go b/internal/ui/widgets/textviewentry_test.go new file mode 100644 index 0000000..bbb20ef --- /dev/null +++ b/internal/ui/widgets/textviewentry_test.go @@ -0,0 +1,381 @@ +package widgets + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTVE() *TextViewEntry { + e := &TextViewEntry{} + e.ExtendBaseWidget(e) + return e +} + +// ── splitRaw ───────────────────────────────────────────────────────────────── + +func TestSplitRaw(t *testing.T) { + tests := []struct { + input string + want []string + }{ + {"", []string{""}}, + {"a\nb", []string{"a", "b"}}, + {"a\r\nb", []string{"a", "b"}}, + {"a\rb", []string{"a", "b"}}, + {"a\nb\nc", []string{"a", "b", "c"}}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := splitRaw(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} + +// ── buildVisualLines ───────────────────────────────────────────────────────── + +func TestBuildVisualLines_NoWrap(t *testing.T) { + lines := buildVisualLines([]string{"GET /foo HTTP/1.1", "Host: example.com"}, 80) + assert.Len(t, lines, 2) +} + +func TestBuildVisualLines_Wraps(t *testing.T) { + long := strings.Repeat("x", 100) + lines := buildVisualLines([]string{long}, 40) + + assert.Greater(t, len(lines), 2, "expected ≥3 visual lines for 100-char line at width 40") + + for _, vl := range lines { + assert.Equal(t, 0, vl.logicalIdx, "all chunks should map to logicalIdx 0") + } + + expected := 0 + for _, vl := range lines { + assert.Equal(t, expected, vl.colOffset) + expected += len([]rune(vl.raw)) + } +} + +func TestBuildVisualLines_EmptyLine(t *testing.T) { + lines := buildVisualLines([]string{"GET /foo HTTP/1.1", "", `{"key":"val"}`}, 80) + assert.Len(t, lines, 3) +} + +func TestBuildVisualLines_EmptyInput(t *testing.T) { + lines := buildVisualLines([]string{""}, 80) + assert.Len(t, lines, 1) +} + +// ── logicalToVisual ────────────────────────────────────────────────────────── + +func TestLogicalToVisual_Simple(t *testing.T) { + visual := buildVisualLines([]string{"hello", "world"}, 80) + + v, col := logicalToVisual(visual, 0, 0) + assert.Equal(t, 0, v) + assert.Equal(t, 0, col) + + v, col = logicalToVisual(visual, 1, 3) + assert.Equal(t, 1, visual[v].logicalIdx) + assert.Equal(t, 3, col) +} + +func TestLogicalToVisual_EndOfLine(t *testing.T) { + visual := buildVisualLines([]string{"hello"}, 80) + + v, col := logicalToVisual(visual, 0, 5) + assert.Equal(t, 0, v) + assert.Equal(t, 5, col) +} + +func TestLogicalToVisual_WrappedLine(t *testing.T) { + // "01234567890123456789" wrapped at 10 → chunks [0:10] [10:20] + visual := buildVisualLines([]string{"01234567890123456789"}, 10) + + v, col := logicalToVisual(visual, 0, 11) + assert.Equal(t, 10, visual[v].colOffset, "col 11 should land in chunk starting at offset 10") + assert.Equal(t, 1, col) +} + +func TestLogicalToVisual_EndOfWrappedLine(t *testing.T) { + // "01234567890123456789" wrapped at 10 → chunks [0:10] [10:20] + visual := buildVisualLines([]string{"01234567890123456789"}, 10) + + v, _ := logicalToVisual(visual, 0, 20) + assert.Equal(t, visual[len(visual)-1].colOffset, visual[v].colOffset, "col at end should land in last chunk") +} + +// ── normaliseSelection ─────────────────────────────────────────────────────── + +func TestNormaliseSelection_Forward(t *testing.T) { + e := newTVE() + e.selAnchorLine, e.selAnchorCol = 0, 2 + e.selLine, e.selCol = 1, 5 + + sl, sc, el, ec := e.normaliseSelection() + assert.Equal(t, 0, sl) + assert.Equal(t, 2, sc) + assert.Equal(t, 1, el) + assert.Equal(t, 5, ec) +} + +func TestNormaliseSelection_Reversed(t *testing.T) { + e := newTVE() + e.selAnchorLine, e.selAnchorCol = 1, 5 + e.selLine, e.selCol = 0, 2 + + sl, sc, el, ec := e.normaliseSelection() + assert.Equal(t, 0, sl) + assert.Equal(t, 2, sc) + assert.Equal(t, 1, el) + assert.Equal(t, 5, ec) +} + +func TestNormaliseSelection_SamePosition(t *testing.T) { + e := newTVE() + e.selAnchorLine, e.selAnchorCol = 0, 3 + e.selLine, e.selCol = 0, 3 + + sl, sc, el, ec := e.normaliseSelection() + assert.Equal(t, 0, sl) + assert.Equal(t, 3, sc) + assert.Equal(t, 0, el) + assert.Equal(t, 3, ec) +} + +// ── selectedText ───────────────────────────────────────────────────────────── + +func TestSelectedText_SameLine(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "hello world" + e.mu.Unlock() + e.selAnchorLine, e.selAnchorCol = 0, 0 + e.selLine, e.selCol = 0, 5 + e.hasSelection = true + + assert.Equal(t, "hello", e.selectedText()) +} + +func TestSelectedText_MultiLine(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "line one\nline two" + e.mu.Unlock() + e.selAnchorLine, e.selAnchorCol = 0, 5 + e.selLine, e.selCol = 1, 4 + e.hasSelection = true + + assert.Equal(t, "one\nline", e.selectedText()) +} + +func TestSelectedText_NoSelection(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "hello" + e.mu.Unlock() + e.hasSelection = false + + assert.Equal(t, "", e.selectedText()) +} + +// ── deleteSelection ────────────────────────────────────────────────────────── + +func TestDeleteSelection_SameLine(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "hello world" + e.mu.Unlock() + e.selAnchorLine, e.selAnchorCol = 0, 6 + e.selLine, e.selCol = 0, 11 + e.hasSelection = true + + lines := e.logicalLines() + lines = e.deleteSelection(lines) + e.commitLines(lines) + + assert.Equal(t, "hello ", e.GetText()) + assert.Equal(t, 6, e.cursorCol) + assert.False(t, e.hasSelection) +} + +func TestDeleteSelection_MultiLine(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "line one\nline two\nline three" + e.mu.Unlock() + e.selAnchorLine, e.selAnchorCol = 0, 5 + e.selLine, e.selCol = 2, 4 + e.hasSelection = true + + lines := e.logicalLines() + lines = e.deleteSelection(lines) + e.commitLines(lines) + + assert.Equal(t, "line three", e.GetText()) +} + +func TestDeleteSelection_ReversedAnchor(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "hello world" + e.mu.Unlock() + e.selAnchorLine, e.selAnchorCol = 0, 11 + e.selLine, e.selCol = 0, 6 + e.hasSelection = true + + lines := e.logicalLines() + lines = e.deleteSelection(lines) + e.commitLines(lines) + + assert.Equal(t, "hello ", e.GetText()) +} + +func TestDeleteSelection_NoSelection(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "hello" + e.mu.Unlock() + e.hasSelection = false + + lines := e.logicalLines() + lines = e.deleteSelection(lines) + e.commitLines(lines) + + assert.Equal(t, "hello", e.GetText()) +} + +// ── pushUndo / undo ────────────────────────────────────────────────────────── + +func TestPushUndo_StoresCursorPosition(t *testing.T) { + e := newTVE() + e.pushUndo("hello", 0, 3) + + require.Len(t, e.undoStack, 1) + assert.Equal(t, "hello", e.undoStack[0].raw) + assert.Equal(t, 0, e.undoStack[0].cursorLine) + assert.Equal(t, 3, e.undoStack[0].cursorCol) +} + +func TestPushUndo_StackLimit(t *testing.T) { + e := newTVE() + for i := 0; i < 110; i++ { + e.pushUndo("x", 0, 0) + } + assert.LessOrEqual(t, len(e.undoStack), 100) +} + +func TestUndo_RestoresTextAndCursor(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "hello" + e.mu.Unlock() + e.cursorLine, e.cursorCol = 0, 5 + + e.pushUndo("hello", 0, 5) + + e.mu.Lock() + e.rawFull = "hello world" + e.visualCacheRaw = "" + e.cursorLine = 0 + e.cursorCol = 11 + e.mu.Unlock() + + e.mu.Lock() + undo := e.undoStack[len(e.undoStack)-1] + e.undoStack = e.undoStack[:len(e.undoStack)-1] + e.rawFull = undo.raw + e.visualCacheRaw = "" + e.cursorLine = undo.cursorLine + e.cursorCol = undo.cursorCol + e.mu.Unlock() + + assert.Equal(t, "hello", e.GetText()) + assert.Equal(t, 0, e.cursorLine) + assert.Equal(t, 5, e.cursorCol) +} + +// ── visual cache ───────────────────────────────────────────────────────────── + +func TestVisualCache_InvalidatedOnCommit(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "hello" + e.mu.Unlock() + + e.mu.Lock() + e.visualCache = buildVisualLines([]string{"hello"}, 80) + e.visualCacheRaw = "hello" + e.visualCacheWidth = 80 + e.mu.Unlock() + + e.commitLines([]string{"world"}) + + e.mu.Lock() + cacheRaw := e.visualCacheRaw + e.mu.Unlock() + + assert.Equal(t, "", cacheRaw, "commitLines should invalidate visual cache") +} + +func TestVisualCache_HitOnSecondCall(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "GET /foo HTTP/1.1\nHost: example.com" + e.visualCacheRaw = "" + e.mu.Unlock() + + // Prime the cache manually without needing a renderer. + cpl := 80 + e.mu.Lock() + built := buildVisualLines(splitRaw(e.rawFull), cpl) + e.visualCache = built + e.visualCacheRaw = e.rawFull + e.visualCacheWidth = cpl + e.mu.Unlock() + + e.mu.Lock() + c1 := e.visualCache + c2 := e.visualCache + e.mu.Unlock() + + require.NotEmpty(t, c1) + assert.Equal(t, &c1[0], &c2[0], "expected same slice on repeated access") +} + +func TestVisualCache_MissAfterMutation(t *testing.T) { + e := newTVE() + e.mu.Lock() + e.rawFull = "hello" + cpl := 80 + e.visualCache = buildVisualLines([]string{"hello"}, cpl) + e.visualCacheRaw = "hello" + e.visualCacheWidth = cpl + e.mu.Unlock() + + e.commitLines([]string{"world"}) + + e.mu.Lock() + cacheRaw := e.visualCacheRaw + e.mu.Unlock() + + assert.Equal(t, "", cacheRaw, "cache should be invalidated after mutation") +} + +// ── commitLines ────────────────────────────────────────────────────────────── + +func TestCommitLines_UpdatesText(t *testing.T) { + e := newTVE() + e.commitLines([]string{"new text"}) + assert.Equal(t, "new text", e.GetText()) +} + +func TestCommitLines_MultiLine(t *testing.T) { + e := newTVE() + e.commitLines([]string{"line one", "line two"}) + assert.Equal(t, "line one\nline two", e.GetText()) +} |
