mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Merge tag 'v0.1.15' into feature/yiheng/sync-gemini-cli-0.1.15
This commit is contained in:
@@ -8,11 +8,22 @@ import { render } from 'ink-testing-library';
|
||||
import { InputPrompt, InputPromptProps } from './InputPrompt.js';
|
||||
import type { TextBuffer } from './shared/text-buffer.js';
|
||||
import { Config } from '@qwen-code/qwen-code-core';
|
||||
import { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import { vi } from 'vitest';
|
||||
import { useShellHistory } from '../hooks/useShellHistory.js';
|
||||
import { useCompletion } from '../hooks/useCompletion.js';
|
||||
import { useInputHistory } from '../hooks/useInputHistory.js';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
CommandContext,
|
||||
SlashCommand,
|
||||
CommandKind,
|
||||
} from '../commands/types.js';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
useShellHistory,
|
||||
UseShellHistoryReturn,
|
||||
} from '../hooks/useShellHistory.js';
|
||||
import { useCompletion, UseCompletionReturn } from '../hooks/useCompletion.js';
|
||||
import {
|
||||
useInputHistory,
|
||||
UseInputHistoryReturn,
|
||||
} from '../hooks/useInputHistory.js';
|
||||
import * as clipboardUtils from '../utils/clipboardUtils.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
|
||||
@@ -21,28 +32,47 @@ vi.mock('../hooks/useCompletion.js');
|
||||
vi.mock('../hooks/useInputHistory.js');
|
||||
vi.mock('../utils/clipboardUtils.js');
|
||||
|
||||
type MockedUseShellHistory = ReturnType<typeof useShellHistory>;
|
||||
type MockedUseCompletion = ReturnType<typeof useCompletion>;
|
||||
type MockedUseInputHistory = ReturnType<typeof useInputHistory>;
|
||||
|
||||
const mockSlashCommands: SlashCommand[] = [
|
||||
{ name: 'clear', description: 'Clear screen', action: vi.fn() },
|
||||
{
|
||||
name: 'clear',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'Clear screen',
|
||||
action: vi.fn(),
|
||||
},
|
||||
{
|
||||
name: 'memory',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'Manage memory',
|
||||
subCommands: [
|
||||
{ name: 'show', description: 'Show memory', action: vi.fn() },
|
||||
{ name: 'add', description: 'Add to memory', action: vi.fn() },
|
||||
{ name: 'refresh', description: 'Refresh memory', action: vi.fn() },
|
||||
{
|
||||
name: 'show',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'Show memory',
|
||||
action: vi.fn(),
|
||||
},
|
||||
{
|
||||
name: 'add',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'Add to memory',
|
||||
action: vi.fn(),
|
||||
},
|
||||
{
|
||||
name: 'refresh',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'Refresh memory',
|
||||
action: vi.fn(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'chat',
|
||||
description: 'Manage chats',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [
|
||||
{
|
||||
name: 'resume',
|
||||
description: 'Resume a chat',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: vi.fn(),
|
||||
completion: async () => ['fix-foo', 'fix-bar'],
|
||||
},
|
||||
@@ -52,9 +82,9 @@ const mockSlashCommands: SlashCommand[] = [
|
||||
|
||||
describe('InputPrompt', () => {
|
||||
let props: InputPromptProps;
|
||||
let mockShellHistory: MockedUseShellHistory;
|
||||
let mockCompletion: MockedUseCompletion;
|
||||
let mockInputHistory: MockedUseInputHistory;
|
||||
let mockShellHistory: UseShellHistoryReturn;
|
||||
let mockCompletion: UseCompletionReturn;
|
||||
let mockInputHistory: UseInputHistoryReturn;
|
||||
let mockBuffer: TextBuffer;
|
||||
let mockCommandContext: CommandContext;
|
||||
|
||||
@@ -91,6 +121,15 @@ describe('InputPrompt', () => {
|
||||
openInExternalEditor: vi.fn(),
|
||||
newline: vi.fn(),
|
||||
backspace: vi.fn(),
|
||||
preferredCol: null,
|
||||
selectionAnchor: null,
|
||||
insert: vi.fn(),
|
||||
del: vi.fn(),
|
||||
undo: vi.fn(),
|
||||
redo: vi.fn(),
|
||||
replaceRange: vi.fn(),
|
||||
deleteWordLeft: vi.fn(),
|
||||
deleteWordRight: vi.fn(),
|
||||
} as unknown as TextBuffer;
|
||||
|
||||
mockShellHistory = {
|
||||
@@ -107,11 +146,13 @@ describe('InputPrompt', () => {
|
||||
isLoadingSuggestions: false,
|
||||
showSuggestions: false,
|
||||
visibleStartIndex: 0,
|
||||
isPerfectMatch: false,
|
||||
navigateUp: vi.fn(),
|
||||
navigateDown: vi.fn(),
|
||||
resetCompletionState: vi.fn(),
|
||||
setActiveSuggestionIndex: vi.fn(),
|
||||
setShowSuggestions: vi.fn(),
|
||||
handleAutocomplete: vi.fn(),
|
||||
};
|
||||
mockedUseCompletion.mockReturnValue(mockCompletion);
|
||||
|
||||
@@ -128,10 +169,11 @@ describe('InputPrompt', () => {
|
||||
userMessages: [],
|
||||
onClearScreen: vi.fn(),
|
||||
config: {
|
||||
getProjectRoot: () => '/test/project',
|
||||
getTargetDir: () => '/test/project/src',
|
||||
getProjectRoot: () => path.join('test', 'project'),
|
||||
getTargetDir: () => path.join('test', 'project', 'src'),
|
||||
getVimMode: () => false,
|
||||
} as unknown as Config,
|
||||
slashCommands: [],
|
||||
slashCommands: mockSlashCommands,
|
||||
commandContext: mockCommandContext,
|
||||
shellModeActive: false,
|
||||
setShellModeActive: vi.fn(),
|
||||
@@ -139,8 +181,6 @@ describe('InputPrompt', () => {
|
||||
suggestionsWidth: 80,
|
||||
focus: true,
|
||||
};
|
||||
|
||||
props.slashCommands = mockSlashCommands;
|
||||
});
|
||||
|
||||
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
@@ -148,10 +188,10 @@ describe('InputPrompt', () => {
|
||||
it('should call shellHistory.getPreviousCommand on up arrow in shell mode', async () => {
|
||||
props.shellModeActive = true;
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait(100); // Increased wait time for CI environment
|
||||
await wait();
|
||||
|
||||
stdin.write('\u001B[A');
|
||||
await wait(100); // Increased wait time to ensure input is processed
|
||||
await wait();
|
||||
|
||||
expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled();
|
||||
unmount();
|
||||
@@ -160,10 +200,10 @@ describe('InputPrompt', () => {
|
||||
it('should call shellHistory.getNextCommand on down arrow in shell mode', async () => {
|
||||
props.shellModeActive = true;
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait(100); // Increased wait time for CI environment
|
||||
await wait();
|
||||
|
||||
stdin.write('\u001B[B');
|
||||
await wait(100); // Increased wait time to ensure input is processed
|
||||
await wait();
|
||||
|
||||
expect(mockShellHistory.getNextCommand).toHaveBeenCalled();
|
||||
unmount();
|
||||
@@ -175,10 +215,10 @@ describe('InputPrompt', () => {
|
||||
'previous command',
|
||||
);
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait(100); // Increased wait time for CI environment
|
||||
await wait();
|
||||
|
||||
stdin.write('\u001B[A');
|
||||
await wait(100); // Increased wait time to ensure input is processed
|
||||
await wait();
|
||||
|
||||
expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled();
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('previous command');
|
||||
@@ -221,6 +261,83 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should call completion.navigateUp for both up arrow and Ctrl+P when suggestions are showing', async () => {
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [
|
||||
{ label: 'memory', value: 'memory' },
|
||||
{ label: 'memcache', value: 'memcache' },
|
||||
],
|
||||
});
|
||||
|
||||
props.buffer.setText('/mem');
|
||||
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
// Test up arrow
|
||||
stdin.write('\u001B[A'); // Up arrow
|
||||
await wait();
|
||||
|
||||
stdin.write('\u0010'); // Ctrl+P
|
||||
await wait();
|
||||
expect(mockCompletion.navigateUp).toHaveBeenCalledTimes(2);
|
||||
expect(mockCompletion.navigateDown).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should call completion.navigateDown for both down arrow and Ctrl+N when suggestions are showing', async () => {
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [
|
||||
{ label: 'memory', value: 'memory' },
|
||||
{ label: 'memcache', value: 'memcache' },
|
||||
],
|
||||
});
|
||||
props.buffer.setText('/mem');
|
||||
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
// Test down arrow
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
await wait();
|
||||
|
||||
stdin.write('\u000E'); // Ctrl+N
|
||||
await wait();
|
||||
expect(mockCompletion.navigateDown).toHaveBeenCalledTimes(2);
|
||||
expect(mockCompletion.navigateUp).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT call completion navigation when suggestions are not showing', async () => {
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: false,
|
||||
});
|
||||
props.buffer.setText('some text');
|
||||
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
stdin.write('\u001B[A'); // Up arrow
|
||||
await wait();
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
await wait();
|
||||
stdin.write('\u0010'); // Ctrl+P
|
||||
await wait();
|
||||
stdin.write('\u000E'); // Ctrl+N
|
||||
await wait();
|
||||
|
||||
expect(mockCompletion.navigateUp).not.toHaveBeenCalled();
|
||||
expect(mockCompletion.navigateDown).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('clipboard image paste', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
|
||||
@@ -285,10 +402,13 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
it('should insert image path at cursor position with proper spacing', async () => {
|
||||
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
|
||||
vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(
|
||||
'/test/.gemini-clipboard/clipboard-456.png',
|
||||
const imagePath = path.join(
|
||||
'test',
|
||||
'.gemini-clipboard',
|
||||
'clipboard-456.png',
|
||||
);
|
||||
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
|
||||
vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(imagePath);
|
||||
|
||||
// Set initial text and cursor position
|
||||
mockBuffer.text = 'Hello world';
|
||||
@@ -310,9 +430,9 @@ describe('InputPrompt', () => {
|
||||
.calls[0];
|
||||
expect(actualCall[0]).toBe(5); // start offset
|
||||
expect(actualCall[1]).toBe(5); // end offset
|
||||
expect(actualCall[2]).toMatch(
|
||||
/@.*\.gemini-clipboard\/clipboard-456\.png/,
|
||||
); // flexible path match
|
||||
expect(actualCall[2]).toBe(
|
||||
' @' + path.relative(path.join('test', 'project', 'src'), imagePath),
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -341,7 +461,7 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should complete a partial parent command and add a space', async () => {
|
||||
it('should complete a partial parent command', async () => {
|
||||
// SCENARIO: /mem -> Tab
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
@@ -357,12 +477,12 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
await wait();
|
||||
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('/memory ');
|
||||
expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should append a sub-command when the parent command is already complete with a space', async () => {
|
||||
// SCENARIO: /memory -> Tab (to accept 'add')
|
||||
it('should append a sub-command when the parent command is already complete', async () => {
|
||||
// SCENARIO: /memory -> Tab (to accept 'add')
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
@@ -380,13 +500,12 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
await wait();
|
||||
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('/memory add ');
|
||||
expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(1);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle the "backspace" edge case correctly', async () => {
|
||||
// SCENARIO: /memory -> Backspace -> /memory -> Tab (to accept 'show')
|
||||
// This is the critical bug we fixed.
|
||||
// SCENARIO: /memory -> Backspace -> /memory -> Tab (to accept 'show')
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
@@ -405,8 +524,8 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
await wait();
|
||||
|
||||
// It should NOT become '/show '. It should correctly become '/memory show '.
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('/memory show ');
|
||||
// It should NOT become '/show'. It should correctly become '/memory show'.
|
||||
expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -426,7 +545,7 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
await wait();
|
||||
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('/chat resume fix-foo ');
|
||||
expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -446,19 +565,21 @@ describe('InputPrompt', () => {
|
||||
await wait();
|
||||
|
||||
// The app should autocomplete the text, NOT submit.
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('/memory ');
|
||||
expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should complete a command based on its altName', async () => {
|
||||
// Add a command with an altName to our mock for this test
|
||||
props.slashCommands.push({
|
||||
name: 'help',
|
||||
altName: '?',
|
||||
description: '...',
|
||||
});
|
||||
it('should complete a command based on its altNames', async () => {
|
||||
props.slashCommands = [
|
||||
{
|
||||
name: 'help',
|
||||
altNames: ['?'],
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: '...',
|
||||
},
|
||||
];
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
@@ -471,10 +592,10 @@ describe('InputPrompt', () => {
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
stdin.write('\t'); // Press Tab
|
||||
stdin.write('\t'); // Press Tab for autocomplete
|
||||
await wait();
|
||||
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('/help ');
|
||||
expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -491,10 +612,29 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should submit directly on Enter when a complete leaf command is typed', async () => {
|
||||
it('should submit directly on Enter when isPerfectMatch is true', async () => {
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: false,
|
||||
isPerfectMatch: true,
|
||||
});
|
||||
props.buffer.setText('/clear');
|
||||
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
stdin.write('\r');
|
||||
await wait();
|
||||
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('/clear');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should submit directly on Enter when a complete leaf command is typed', async () => {
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: false,
|
||||
isPerfectMatch: false, // Added explicit isPerfectMatch false
|
||||
});
|
||||
props.buffer.setText('/clear');
|
||||
|
||||
@@ -505,7 +645,6 @@ describe('InputPrompt', () => {
|
||||
await wait();
|
||||
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('/clear');
|
||||
expect(props.buffer.setText).not.toHaveBeenCalledWith('/clear ');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -524,13 +663,16 @@ describe('InputPrompt', () => {
|
||||
stdin.write('\r');
|
||||
await wait();
|
||||
|
||||
expect(props.buffer.replaceRangeByOffset).toHaveBeenCalled();
|
||||
expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should add a newline on enter when the line ends with a backslash', async () => {
|
||||
props.buffer.setText('first line\\');
|
||||
// This test simulates multi-line input, not submission
|
||||
mockBuffer.text = 'first line\\';
|
||||
mockBuffer.cursor = [0, 11];
|
||||
mockBuffer.lines = ['first line\\'];
|
||||
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
@@ -543,4 +685,471 @@ describe('InputPrompt', () => {
|
||||
expect(props.buffer.newline).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should clear the buffer on Ctrl+C if it has text', async () => {
|
||||
props.buffer.setText('some text to clear');
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
stdin.write('\x03'); // Ctrl+C character
|
||||
await wait();
|
||||
|
||||
expect(props.buffer.setText).toHaveBeenCalledWith('');
|
||||
expect(mockCompletion.resetCompletionState).toHaveBeenCalled();
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT clear the buffer on Ctrl+C if it is empty', async () => {
|
||||
props.buffer.text = '';
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
stdin.write('\x03'); // Ctrl+C character
|
||||
await wait();
|
||||
|
||||
expect(props.buffer.setText).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('cursor-based completion trigger', () => {
|
||||
it('should trigger completion when cursor is after @ without spaces', async () => {
|
||||
// Set up buffer state
|
||||
mockBuffer.text = '@src/components';
|
||||
mockBuffer.lines = ['@src/components'];
|
||||
mockBuffer.cursor = [0, 15];
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [{ label: 'Button.tsx', value: 'Button.tsx' }],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
// Verify useCompletion was called with correct signature
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should trigger completion when cursor is after / without spaces', async () => {
|
||||
mockBuffer.text = '/memory';
|
||||
mockBuffer.lines = ['/memory'];
|
||||
mockBuffer.cursor = [0, 7];
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [{ label: 'show', value: 'show' }],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT trigger completion when cursor is after space following @', async () => {
|
||||
mockBuffer.text = '@src/file.ts hello';
|
||||
mockBuffer.lines = ['@src/file.ts hello'];
|
||||
mockBuffer.cursor = [0, 18];
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: false,
|
||||
suggestions: [],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT trigger completion when cursor is after space following /', async () => {
|
||||
mockBuffer.text = '/memory add';
|
||||
mockBuffer.lines = ['/memory add'];
|
||||
mockBuffer.cursor = [0, 11];
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: false,
|
||||
suggestions: [],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT trigger completion when cursor is not after @ or /', async () => {
|
||||
mockBuffer.text = 'hello world';
|
||||
mockBuffer.lines = ['hello world'];
|
||||
mockBuffer.cursor = [0, 5];
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: false,
|
||||
suggestions: [],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle multiline text correctly', async () => {
|
||||
mockBuffer.text = 'first line\n/memory';
|
||||
mockBuffer.lines = ['first line', '/memory'];
|
||||
mockBuffer.cursor = [1, 7];
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: false,
|
||||
suggestions: [],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
// Verify useCompletion was called with the buffer
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle single line slash command correctly', async () => {
|
||||
mockBuffer.text = '/memory';
|
||||
mockBuffer.lines = ['/memory'];
|
||||
mockBuffer.cursor = [0, 7];
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [{ label: 'show', value: 'show' }],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle Unicode characters (emojis) correctly in paths', async () => {
|
||||
// Test with emoji in path after @
|
||||
mockBuffer.text = '@src/file👍.txt';
|
||||
mockBuffer.lines = ['@src/file👍.txt'];
|
||||
mockBuffer.cursor = [0, 14]; // After the emoji character
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [{ label: 'file👍.txt', value: 'file👍.txt' }],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle Unicode characters with spaces after them', async () => {
|
||||
// Test with emoji followed by space - should NOT trigger completion
|
||||
mockBuffer.text = '@src/file👍.txt hello';
|
||||
mockBuffer.lines = ['@src/file👍.txt hello'];
|
||||
mockBuffer.cursor = [0, 20]; // After the space
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: false,
|
||||
suggestions: [],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle escaped spaces in paths correctly', async () => {
|
||||
// Test with escaped space in path - should trigger completion
|
||||
mockBuffer.text = '@src/my\\ file.txt';
|
||||
mockBuffer.lines = ['@src/my\\ file.txt'];
|
||||
mockBuffer.cursor = [0, 16]; // After the escaped space and filename
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [{ label: 'my file.txt', value: 'my file.txt' }],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT trigger completion after unescaped space following escaped space', async () => {
|
||||
// Test: @path/my\ file.txt hello (unescaped space after escaped space)
|
||||
mockBuffer.text = '@path/my\\ file.txt hello';
|
||||
mockBuffer.lines = ['@path/my\\ file.txt hello'];
|
||||
mockBuffer.cursor = [0, 24]; // After "hello"
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: false,
|
||||
suggestions: [],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle multiple escaped spaces in paths', async () => {
|
||||
// Test with multiple escaped spaces
|
||||
mockBuffer.text = '@docs/my\\ long\\ file\\ name.md';
|
||||
mockBuffer.lines = ['@docs/my\\ long\\ file\\ name.md'];
|
||||
mockBuffer.cursor = [0, 29]; // At the end
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [
|
||||
{ label: 'my long file name.md', value: 'my long file name.md' },
|
||||
],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle escaped spaces in slash commands', async () => {
|
||||
// Test escaped spaces with slash commands (though less common)
|
||||
mockBuffer.text = '/memory\\ test';
|
||||
mockBuffer.lines = ['/memory\\ test'];
|
||||
mockBuffer.cursor = [0, 13]; // At the end
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [{ label: 'test-command', value: 'test-command' }],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle Unicode characters with escaped spaces', async () => {
|
||||
// Test combining Unicode and escaped spaces
|
||||
mockBuffer.text = '@' + path.join('files', 'emoji\\ 👍\\ test.txt');
|
||||
mockBuffer.lines = ['@' + path.join('files', 'emoji\\ 👍\\ test.txt')];
|
||||
mockBuffer.cursor = [0, 25]; // After the escaped space and emoji
|
||||
|
||||
mockedUseCompletion.mockReturnValue({
|
||||
...mockCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [
|
||||
{ label: 'emoji 👍 test.txt', value: 'emoji 👍 test.txt' },
|
||||
],
|
||||
});
|
||||
|
||||
const { unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('vim mode', () => {
|
||||
it('should not call buffer.handleInput when vim mode is enabled and vim handles the input', async () => {
|
||||
props.vimModeEnabled = true;
|
||||
props.vimHandleInput = vi.fn().mockReturnValue(true); // Mock that vim handled it.
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
stdin.write('i');
|
||||
await wait();
|
||||
|
||||
expect(props.vimHandleInput).toHaveBeenCalled();
|
||||
expect(mockBuffer.handleInput).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should call buffer.handleInput when vim mode is enabled but vim does not handle the input', async () => {
|
||||
props.vimModeEnabled = true;
|
||||
props.vimHandleInput = vi.fn().mockReturnValue(false); // Mock that vim did NOT handle it.
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
stdin.write('i');
|
||||
await wait();
|
||||
|
||||
expect(props.vimHandleInput).toHaveBeenCalled();
|
||||
expect(mockBuffer.handleInput).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should call handleInput when vim mode is disabled', async () => {
|
||||
// Mock vimHandleInput to return false (vim didn't handle the input)
|
||||
props.vimHandleInput = vi.fn().mockReturnValue(false);
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
stdin.write('i');
|
||||
await wait();
|
||||
|
||||
expect(props.vimHandleInput).toHaveBeenCalled();
|
||||
expect(mockBuffer.handleInput).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unfocused paste', () => {
|
||||
it('should handle bracketed paste when not focused', async () => {
|
||||
props.focus = false;
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
stdin.write('\x1B[200~pasted text\x1B[201~');
|
||||
await wait();
|
||||
|
||||
expect(mockBuffer.handleInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
paste: true,
|
||||
sequence: 'pasted text',
|
||||
}),
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should ignore regular keypresses when not focused', async () => {
|
||||
props.focus = false;
|
||||
const { stdin, unmount } = render(<InputPrompt {...props} />);
|
||||
await wait();
|
||||
|
||||
stdin.write('a');
|
||||
await wait();
|
||||
|
||||
expect(mockBuffer.handleInput).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user