diff options
| author | Ayesha Shafique <[email protected]> | 2025-08-04 00:53:24 +0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-08-03 19:53:24 +0000 |
| commit | 072d8ba2899f2601dad6d4b0333fdcb80555a7dd (patch) | |
| tree | a8333f75184889929b844c115c5fb93555abdf62 /packages/cli/src/ui/components/InputPrompt.test.tsx | |
| parent | 03ed37d0dc2b5e2077b53073517abaab3d24d9c2 (diff) | |
feat: Add reverse search capability for shell commands (#4793)
Diffstat (limited to 'packages/cli/src/ui/components/InputPrompt.test.tsx')
| -rw-r--r-- | packages/cli/src/ui/components/InputPrompt.test.tsx | 282 |
1 files changed, 195 insertions, 87 deletions
diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index e0d967da..6b7bc7ce 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -19,7 +19,10 @@ import { useShellHistory, UseShellHistoryReturn, } from '../hooks/useShellHistory.js'; -import { useCompletion, UseCompletionReturn } from '../hooks/useCompletion.js'; +import { + useSlashCompletion, + UseSlashCompletionReturn, +} from '../hooks/useSlashCompletion.js'; import { useInputHistory, UseInputHistoryReturn, @@ -28,7 +31,7 @@ import * as clipboardUtils from '../utils/clipboardUtils.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; vi.mock('../hooks/useShellHistory.js'); -vi.mock('../hooks/useCompletion.js'); +vi.mock('../hooks/useSlashCompletion.js'); vi.mock('../hooks/useInputHistory.js'); vi.mock('../utils/clipboardUtils.js'); @@ -83,13 +86,13 @@ const mockSlashCommands: SlashCommand[] = [ describe('InputPrompt', () => { let props: InputPromptProps; let mockShellHistory: UseShellHistoryReturn; - let mockCompletion: UseCompletionReturn; + let mockSlashCompletion: UseSlashCompletionReturn; let mockInputHistory: UseInputHistoryReturn; let mockBuffer: TextBuffer; let mockCommandContext: CommandContext; const mockedUseShellHistory = vi.mocked(useShellHistory); - const mockedUseCompletion = vi.mocked(useCompletion); + const mockedUseSlashCompletion = vi.mocked(useSlashCompletion); const mockedUseInputHistory = vi.mocked(useInputHistory); beforeEach(() => { @@ -115,7 +118,9 @@ describe('InputPrompt', () => { visualScrollRow: 0, handleInput: vi.fn(), move: vi.fn(), - moveToOffset: vi.fn(), + moveToOffset: (offset: number) => { + mockBuffer.cursor = [0, offset]; + }, killLineRight: vi.fn(), killLineLeft: vi.fn(), openInExternalEditor: vi.fn(), @@ -133,6 +138,7 @@ describe('InputPrompt', () => { } as unknown as TextBuffer; mockShellHistory = { + history: [], addCommandToHistory: vi.fn(), getPreviousCommand: vi.fn().mockReturnValue(null), getNextCommand: vi.fn().mockReturnValue(null), @@ -140,7 +146,7 @@ describe('InputPrompt', () => { }; mockedUseShellHistory.mockReturnValue(mockShellHistory); - mockCompletion = { + mockSlashCompletion = { suggestions: [], activeSuggestionIndex: -1, isLoadingSuggestions: false, @@ -154,7 +160,7 @@ describe('InputPrompt', () => { setShowSuggestions: vi.fn(), handleAutocomplete: vi.fn(), }; - mockedUseCompletion.mockReturnValue(mockCompletion); + mockedUseSlashCompletion.mockReturnValue(mockSlashCompletion); mockInputHistory = { navigateUp: vi.fn(), @@ -265,8 +271,8 @@ describe('InputPrompt', () => { }); it('should call completion.navigateUp for both up arrow and Ctrl+P when suggestions are showing', async () => { - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [ { label: 'memory', value: 'memory' }, @@ -285,15 +291,15 @@ describe('InputPrompt', () => { stdin.write('\u0010'); // Ctrl+P await wait(); - expect(mockCompletion.navigateUp).toHaveBeenCalledTimes(2); - expect(mockCompletion.navigateDown).not.toHaveBeenCalled(); + expect(mockSlashCompletion.navigateUp).toHaveBeenCalledTimes(2); + expect(mockSlashCompletion.navigateDown).not.toHaveBeenCalled(); unmount(); }); it('should call completion.navigateDown for both down arrow and Ctrl+N when suggestions are showing', async () => { - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [ { label: 'memory', value: 'memory' }, @@ -311,15 +317,15 @@ describe('InputPrompt', () => { stdin.write('\u000E'); // Ctrl+N await wait(); - expect(mockCompletion.navigateDown).toHaveBeenCalledTimes(2); - expect(mockCompletion.navigateUp).not.toHaveBeenCalled(); + expect(mockSlashCompletion.navigateDown).toHaveBeenCalledTimes(2); + expect(mockSlashCompletion.navigateUp).not.toHaveBeenCalled(); unmount(); }); it('should NOT call completion navigation when suggestions are not showing', async () => { - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: false, }); props.buffer.setText('some text'); @@ -336,8 +342,8 @@ describe('InputPrompt', () => { stdin.write('\u000E'); // Ctrl+N await wait(); - expect(mockCompletion.navigateUp).not.toHaveBeenCalled(); - expect(mockCompletion.navigateDown).not.toHaveBeenCalled(); + expect(mockSlashCompletion.navigateUp).not.toHaveBeenCalled(); + expect(mockSlashCompletion.navigateDown).not.toHaveBeenCalled(); unmount(); }); @@ -466,8 +472,8 @@ describe('InputPrompt', () => { it('should complete a partial parent command', async () => { // SCENARIO: /mem -> Tab - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'memory', value: 'memory', description: '...' }], activeSuggestionIndex: 0, @@ -480,14 +486,14 @@ describe('InputPrompt', () => { stdin.write('\t'); // Press Tab await wait(); - expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0); unmount(); }); it('should append a sub-command when the parent command is already complete', async () => { // SCENARIO: /memory -> Tab (to accept 'add') - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [ { label: 'show', value: 'show' }, @@ -503,14 +509,14 @@ describe('InputPrompt', () => { stdin.write('\t'); // Press Tab await wait(); - expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(1); + expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(1); unmount(); }); it('should handle the "backspace" edge case correctly', async () => { // SCENARIO: /memory -> Backspace -> /memory -> Tab (to accept 'show') - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [ { label: 'show', value: 'show' }, @@ -528,14 +534,14 @@ describe('InputPrompt', () => { await wait(); // It should NOT become '/show'. It should correctly become '/memory show'. - expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0); unmount(); }); it('should complete a partial argument for a command', async () => { // SCENARIO: /chat resume fi- -> Tab - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'fix-foo', value: 'fix-foo' }], activeSuggestionIndex: 0, @@ -548,13 +554,13 @@ describe('InputPrompt', () => { stdin.write('\t'); // Press Tab await wait(); - expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0); unmount(); }); it('should autocomplete on Enter when suggestions are active, without submitting', async () => { - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'memory', value: 'memory' }], activeSuggestionIndex: 0, @@ -568,7 +574,7 @@ describe('InputPrompt', () => { await wait(); // The app should autocomplete the text, NOT submit. - expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0); expect(props.onSubmit).not.toHaveBeenCalled(); unmount(); @@ -584,8 +590,8 @@ describe('InputPrompt', () => { }, ]; - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'help', value: 'help' }], activeSuggestionIndex: 0, @@ -598,7 +604,7 @@ describe('InputPrompt', () => { stdin.write('\t'); // Press Tab for autocomplete await wait(); - expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0); unmount(); }); @@ -616,8 +622,8 @@ describe('InputPrompt', () => { }); it('should submit directly on Enter when isPerfectMatch is true', async () => { - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: false, isPerfectMatch: true, }); @@ -634,8 +640,8 @@ describe('InputPrompt', () => { }); it('should submit directly on Enter when a complete leaf command is typed', async () => { - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: false, isPerfectMatch: false, // Added explicit isPerfectMatch false }); @@ -652,8 +658,8 @@ describe('InputPrompt', () => { }); it('should autocomplete an @-path on Enter without submitting', async () => { - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'index.ts', value: 'index.ts' }], activeSuggestionIndex: 0, @@ -666,7 +672,7 @@ describe('InputPrompt', () => { stdin.write('\r'); await wait(); - expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0); expect(props.onSubmit).not.toHaveBeenCalled(); unmount(); }); @@ -698,7 +704,7 @@ describe('InputPrompt', () => { await wait(); expect(props.buffer.setText).toHaveBeenCalledWith(''); - expect(mockCompletion.resetCompletionState).toHaveBeenCalled(); + expect(mockSlashCompletion.resetCompletionState).toHaveBeenCalled(); expect(props.onSubmit).not.toHaveBeenCalled(); unmount(); }); @@ -722,8 +728,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['@src/components']; mockBuffer.cursor = [0, 15]; - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'Button.tsx', value: 'Button.tsx' }], }); @@ -732,12 +738,13 @@ describe('InputPrompt', () => { await wait(); // Verify useCompletion was called with correct signature - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -749,8 +756,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['/memory']; mockBuffer.cursor = [0, 7]; - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'show', value: 'show' }], }); @@ -758,12 +765,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -775,8 +783,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['@src/file.ts hello']; mockBuffer.cursor = [0, 18]; - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: false, suggestions: [], }); @@ -784,12 +792,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -801,8 +810,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['/memory add']; mockBuffer.cursor = [0, 11]; - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: false, suggestions: [], }); @@ -810,12 +819,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -827,8 +837,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['hello world']; mockBuffer.cursor = [0, 5]; - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: false, suggestions: [], }); @@ -836,12 +846,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -853,8 +864,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['first line', '/memory']; mockBuffer.cursor = [1, 7]; - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: false, suggestions: [], }); @@ -863,12 +874,13 @@ describe('InputPrompt', () => { await wait(); // Verify useCompletion was called with the buffer - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -880,8 +892,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['/memory']; mockBuffer.cursor = [0, 7]; - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'show', value: 'show' }], }); @@ -889,12 +901,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -907,8 +920,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['@src/file๐.txt']; mockBuffer.cursor = [0, 14]; // After the emoji character - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'file๐.txt', value: 'file๐.txt' }], }); @@ -916,12 +929,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -934,8 +948,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['@src/file๐.txt hello']; mockBuffer.cursor = [0, 20]; // After the space - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: false, suggestions: [], }); @@ -943,12 +957,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -961,8 +976,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['@src/my\\ file.txt']; mockBuffer.cursor = [0, 16]; // After the escaped space and filename - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'my file.txt', value: 'my file.txt' }], }); @@ -970,12 +985,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -988,8 +1004,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['@path/my\\ file.txt hello']; mockBuffer.cursor = [0, 24]; // After "hello" - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: false, suggestions: [], }); @@ -997,12 +1013,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -1015,8 +1032,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['@docs/my\\ long\\ file\\ name.md']; mockBuffer.cursor = [0, 29]; // At the end - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [ { label: 'my long file name.md', value: 'my long file name.md' }, @@ -1026,12 +1043,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -1044,8 +1062,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['/memory\\ test']; mockBuffer.cursor = [0, 13]; // At the end - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [{ label: 'test-command', value: 'test-command' }], }); @@ -1053,12 +1071,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -1071,8 +1090,8 @@ describe('InputPrompt', () => { mockBuffer.lines = ['@' + path.join('files', 'emoji\\ ๐\\ test.txt')]; mockBuffer.cursor = [0, 25]; // After the escaped space and emoji - mockedUseCompletion.mockReturnValue({ - ...mockCompletion, + mockedUseSlashCompletion.mockReturnValue({ + ...mockSlashCompletion, showSuggestions: true, suggestions: [ { label: 'emoji ๐ test.txt', value: 'emoji ๐ test.txt' }, @@ -1082,12 +1101,13 @@ describe('InputPrompt', () => { const { unmount } = render(<InputPrompt {...props} />); await wait(); - expect(mockedUseCompletion).toHaveBeenCalledWith( + expect(mockedUseSlashCompletion).toHaveBeenCalledWith( mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, + false, expect.any(Object), ); @@ -1169,4 +1189,92 @@ describe('InputPrompt', () => { unmount(); }); }); + + describe('reverse search', () => { + beforeEach(async () => { + props.shellModeActive = true; + + vi.mocked(useShellHistory).mockReturnValue({ + history: ['echo hello', 'echo world', 'ls'], + getPreviousCommand: vi.fn(), + getNextCommand: vi.fn(), + addCommandToHistory: vi.fn(), + resetHistoryPosition: vi.fn(), + }); + }); + + it('invokes reverse search on Ctrl+R', async () => { + const { stdin, stdout, unmount } = render(<InputPrompt {...props} />); + await wait(); + + stdin.write('\x12'); + await wait(); + + const frame = stdout.lastFrame(); + expect(frame).toContain('(r:)'); + expect(frame).toContain('echo hello'); + expect(frame).toContain('echo world'); + expect(frame).toContain('ls'); + + unmount(); + }); + + it('resets reverse search state on Escape', async () => { + const { stdin, stdout, unmount } = render(<InputPrompt {...props} />); + await wait(); + + stdin.write('\x12'); + await wait(); + stdin.write('\x1B'); + await wait(); + + const frame = stdout.lastFrame(); + expect(frame).not.toContain('(r:)'); + expect(frame).not.toContain('echo hello'); + + unmount(); + }); + + it('completes the highlighted entry on Tab and exits reverse-search', async () => { + const { stdin, stdout, unmount } = render(<InputPrompt {...props} />); + stdin.write('\x12'); + await wait(); + stdin.write('\t'); + await wait(); + + expect(stdout.lastFrame()).not.toContain('(r:)'); + expect(props.buffer.setText).toHaveBeenCalledWith('echo hello'); + unmount(); + }); + + it('submits the highlighted entry on Enter and exits reverse-search', async () => { + const { stdin, stdout, unmount } = render(<InputPrompt {...props} />); + stdin.write('\x12'); + await wait(); + expect(stdout.lastFrame()).toContain('(r:)'); + stdin.write('\r'); + await wait(); + + expect(stdout.lastFrame()).not.toContain('(r:)'); + expect(props.onSubmit).toHaveBeenCalledWith('echo hello'); + unmount(); + }); + + it('text and cursor position should be restored after reverse search', async () => { + props.buffer.setText('initial text'); + props.buffer.cursor = [0, 3]; + const { stdin, stdout, unmount } = render(<InputPrompt {...props} />); + stdin.write('\x12'); + await wait(); + expect(stdout.lastFrame()).toContain('(r:)'); + stdin.write('\x1B'); + await wait(); + + expect(stdout.lastFrame()).not.toContain('(r:)'); + expect(props.buffer.text).toBe('initial text'); + expect(props.buffer.cursor).toEqual([0, 3]); + + unmount(); + }); + }); }); |
