summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components/shared/text-buffer.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/components/shared/text-buffer.test.ts')
-rw-r--r--packages/cli/src/ui/components/shared/text-buffer.test.ts192
1 files changed, 171 insertions, 21 deletions
diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts
index 5ea52ba4..7f180dae 100644
--- a/packages/cli/src/ui/components/shared/text-buffer.test.ts
+++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts
@@ -11,8 +11,150 @@ import {
Viewport,
TextBuffer,
offsetToLogicalPos,
+ textBufferReducer,
+ TextBufferState,
+ TextBufferAction,
} from './text-buffer.js';
+const initialState: TextBufferState = {
+ lines: [''],
+ cursorRow: 0,
+ cursorCol: 0,
+ preferredCol: null,
+ undoStack: [],
+ redoStack: [],
+ clipboard: null,
+ selectionAnchor: null,
+};
+
+describe('textBufferReducer', () => {
+ it('should return the initial state if state is undefined', () => {
+ const action = { type: 'unknown_action' } as unknown as TextBufferAction;
+ const state = textBufferReducer(initialState, action);
+ expect(state).toEqual(initialState);
+ });
+
+ describe('set_text action', () => {
+ it('should set new text and move cursor to the end', () => {
+ const action: TextBufferAction = {
+ type: 'set_text',
+ payload: 'hello\nworld',
+ };
+ const state = textBufferReducer(initialState, action);
+ expect(state.lines).toEqual(['hello', 'world']);
+ expect(state.cursorRow).toBe(1);
+ expect(state.cursorCol).toBe(5);
+ expect(state.undoStack.length).toBe(1);
+ });
+
+ it('should not create an undo snapshot if pushToUndo is false', () => {
+ const action: TextBufferAction = {
+ type: 'set_text',
+ payload: 'no undo',
+ pushToUndo: false,
+ };
+ const state = textBufferReducer(initialState, action);
+ expect(state.lines).toEqual(['no undo']);
+ expect(state.undoStack.length).toBe(0);
+ });
+ });
+
+ describe('insert action', () => {
+ it('should insert a character', () => {
+ const action: TextBufferAction = { type: 'insert', payload: 'a' };
+ const state = textBufferReducer(initialState, action);
+ expect(state.lines).toEqual(['a']);
+ expect(state.cursorCol).toBe(1);
+ });
+
+ it('should insert a newline', () => {
+ const stateWithText = { ...initialState, lines: ['hello'] };
+ const action: TextBufferAction = { type: 'insert', payload: '\n' };
+ const state = textBufferReducer(stateWithText, action);
+ expect(state.lines).toEqual(['', 'hello']);
+ expect(state.cursorRow).toBe(1);
+ expect(state.cursorCol).toBe(0);
+ });
+ });
+
+ describe('backspace action', () => {
+ it('should remove a character', () => {
+ const stateWithText: TextBufferState = {
+ ...initialState,
+ lines: ['a'],
+ cursorRow: 0,
+ cursorCol: 1,
+ };
+ const action: TextBufferAction = { type: 'backspace' };
+ const state = textBufferReducer(stateWithText, action);
+ expect(state.lines).toEqual(['']);
+ expect(state.cursorCol).toBe(0);
+ });
+
+ it('should join lines if at the beginning of a line', () => {
+ const stateWithText: TextBufferState = {
+ ...initialState,
+ lines: ['hello', 'world'],
+ cursorRow: 1,
+ cursorCol: 0,
+ };
+ const action: TextBufferAction = { type: 'backspace' };
+ const state = textBufferReducer(stateWithText, action);
+ expect(state.lines).toEqual(['helloworld']);
+ expect(state.cursorRow).toBe(0);
+ expect(state.cursorCol).toBe(5);
+ });
+ });
+
+ describe('undo/redo actions', () => {
+ it('should undo and redo a change', () => {
+ // 1. Insert text
+ const insertAction: TextBufferAction = {
+ type: 'insert',
+ payload: 'test',
+ };
+ const stateAfterInsert = textBufferReducer(initialState, insertAction);
+ expect(stateAfterInsert.lines).toEqual(['test']);
+ expect(stateAfterInsert.undoStack.length).toBe(1);
+
+ // 2. Undo
+ const undoAction: TextBufferAction = { type: 'undo' };
+ const stateAfterUndo = textBufferReducer(stateAfterInsert, undoAction);
+ expect(stateAfterUndo.lines).toEqual(['']);
+ expect(stateAfterUndo.undoStack.length).toBe(0);
+ expect(stateAfterUndo.redoStack.length).toBe(1);
+
+ // 3. Redo
+ const redoAction: TextBufferAction = { type: 'redo' };
+ const stateAfterRedo = textBufferReducer(stateAfterUndo, redoAction);
+ expect(stateAfterRedo.lines).toEqual(['test']);
+ expect(stateAfterRedo.undoStack.length).toBe(1);
+ expect(stateAfterRedo.redoStack.length).toBe(0);
+ });
+ });
+
+ describe('create_undo_snapshot action', () => {
+ it('should create a snapshot without changing state', () => {
+ const stateWithText: TextBufferState = {
+ ...initialState,
+ lines: ['hello'],
+ cursorRow: 0,
+ cursorCol: 5,
+ };
+ const action: TextBufferAction = { type: 'create_undo_snapshot' };
+ const state = textBufferReducer(stateWithText, action);
+
+ expect(state.lines).toEqual(['hello']);
+ expect(state.cursorRow).toBe(0);
+ expect(state.cursorCol).toBe(5);
+ expect(state.undoStack.length).toBe(1);
+ expect(state.undoStack[0].lines).toEqual(['hello']);
+ expect(state.undoStack[0].cursorRow).toBe(0);
+ expect(state.undoStack[0].cursorCol).toBe(5);
+ });
+ });
+});
+
// Helper to get the state from the hook
const getBufferState = (result: { current: TextBuffer }) => ({
text: result.current.text,
@@ -644,11 +786,27 @@ describe('useTextBuffer', () => {
expect(getBufferState(result).cursor).toEqual([0, 5]);
act(() => {
- result.current.applyOperations([
- { type: 'backspace' },
- { type: 'backspace' },
- { type: 'backspace' },
- ]);
+ result.current.handleInput({
+ name: 'backspace',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ sequence: '\x7f',
+ });
+ result.current.handleInput({
+ name: 'backspace',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ sequence: '\x7f',
+ });
+ result.current.handleInput({
+ name: 'backspace',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ sequence: '\x7f',
+ });
});
expect(getBufferState(result).text).toBe('ab');
expect(getBufferState(result).cursor).toEqual([0, 2]);
@@ -666,9 +824,7 @@ describe('useTextBuffer', () => {
expect(getBufferState(result).cursor).toEqual([0, 5]);
act(() => {
- result.current.applyOperations([
- { type: 'insert', payload: '\x7f\x7f\x7f' },
- ]);
+ result.current.insert('\x7f\x7f\x7f');
});
expect(getBufferState(result).text).toBe('ab');
expect(getBufferState(result).cursor).toEqual([0, 2]);
@@ -686,9 +842,7 @@ describe('useTextBuffer', () => {
expect(getBufferState(result).cursor).toEqual([0, 5]);
act(() => {
- result.current.applyOperations([
- { type: 'insert', payload: '\x7fI\x7f\x7fNEW' },
- ]);
+ result.current.insert('\x7fI\x7f\x7fNEW');
});
expect(getBufferState(result).text).toBe('abcNEW');
expect(getBufferState(result).cursor).toEqual([0, 6]);
@@ -774,11 +928,9 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
// Simulate pasting the long text multiple times
act(() => {
- result.current.applyOperations([
- { type: 'insert', payload: longText },
- { type: 'insert', payload: longText },
- { type: 'insert', payload: longText },
- ]);
+ result.current.insert(longText);
+ result.current.insert(longText);
+ result.current.insert(longText);
});
const state = getBufferState(result);
@@ -909,17 +1061,15 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
isValidPath: () => false,
}),
);
- let success = true;
act(() => {
- success = result.current.replaceRange(0, 5, 0, 3, 'fail'); // startCol > endCol in same line
+ result.current.replaceRange(0, 5, 0, 3, 'fail'); // startCol > endCol in same line
});
- expect(success).toBe(false);
+
expect(getBufferState(result).text).toBe('test');
act(() => {
- success = result.current.replaceRange(1, 0, 0, 0, 'fail'); // startRow > endRow
+ result.current.replaceRange(1, 0, 0, 0, 'fail'); // startRow > endRow
});
- expect(success).toBe(false);
expect(getBufferState(result).text).toBe('test');
});