summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components/InputPrompt.test.tsx
diff options
context:
space:
mode:
authorAyesha Shafique <[email protected]>2025-08-04 00:53:24 +0500
committerGitHub <[email protected]>2025-08-03 19:53:24 +0000
commit072d8ba2899f2601dad6d4b0333fdcb80555a7dd (patch)
treea8333f75184889929b844c115c5fb93555abdf62 /packages/cli/src/ui/components/InputPrompt.test.tsx
parent03ed37d0dc2b5e2077b53073517abaab3d24d9c2 (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.tsx282
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();
+ });
+ });
});