Fix InputPrompt.test.tsx to be windows compatible (#4736)
This commit is contained in:
parent
2e28bb90a0
commit
e9e2f55144
|
@ -8,11 +8,22 @@ import { render } from 'ink-testing-library';
|
||||||
import { InputPrompt, InputPromptProps } from './InputPrompt.js';
|
import { InputPrompt, InputPromptProps } from './InputPrompt.js';
|
||||||
import type { TextBuffer } from './shared/text-buffer.js';
|
import type { TextBuffer } from './shared/text-buffer.js';
|
||||||
import { Config } from '@google/gemini-cli-core';
|
import { Config } from '@google/gemini-cli-core';
|
||||||
import { CommandContext, SlashCommand } from '../commands/types.js';
|
import * as path from 'path';
|
||||||
|
import {
|
||||||
|
CommandContext,
|
||||||
|
SlashCommand,
|
||||||
|
CommandKind,
|
||||||
|
} from '../commands/types.js';
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { useShellHistory } from '../hooks/useShellHistory.js';
|
import {
|
||||||
import { useCompletion } from '../hooks/useCompletion.js';
|
useShellHistory,
|
||||||
import { useInputHistory } from '../hooks/useInputHistory.js';
|
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 * as clipboardUtils from '../utils/clipboardUtils.js';
|
||||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||||
|
|
||||||
|
@ -21,28 +32,47 @@ vi.mock('../hooks/useCompletion.js');
|
||||||
vi.mock('../hooks/useInputHistory.js');
|
vi.mock('../hooks/useInputHistory.js');
|
||||||
vi.mock('../utils/clipboardUtils.js');
|
vi.mock('../utils/clipboardUtils.js');
|
||||||
|
|
||||||
type MockedUseShellHistory = ReturnType<typeof useShellHistory>;
|
|
||||||
type MockedUseCompletion = ReturnType<typeof useCompletion>;
|
|
||||||
type MockedUseInputHistory = ReturnType<typeof useInputHistory>;
|
|
||||||
|
|
||||||
const mockSlashCommands: SlashCommand[] = [
|
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',
|
name: 'memory',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
description: 'Manage memory',
|
description: 'Manage memory',
|
||||||
subCommands: [
|
subCommands: [
|
||||||
{ name: 'show', description: 'Show memory', action: vi.fn() },
|
{
|
||||||
{ name: 'add', description: 'Add to memory', action: vi.fn() },
|
name: 'show',
|
||||||
{ name: 'refresh', description: 'Refresh memory', action: vi.fn() },
|
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',
|
name: 'chat',
|
||||||
description: 'Manage chats',
|
description: 'Manage chats',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
subCommands: [
|
subCommands: [
|
||||||
{
|
{
|
||||||
name: 'resume',
|
name: 'resume',
|
||||||
description: 'Resume a chat',
|
description: 'Resume a chat',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
action: vi.fn(),
|
action: vi.fn(),
|
||||||
completion: async () => ['fix-foo', 'fix-bar'],
|
completion: async () => ['fix-foo', 'fix-bar'],
|
||||||
},
|
},
|
||||||
|
@ -52,9 +82,9 @@ const mockSlashCommands: SlashCommand[] = [
|
||||||
|
|
||||||
describe('InputPrompt', () => {
|
describe('InputPrompt', () => {
|
||||||
let props: InputPromptProps;
|
let props: InputPromptProps;
|
||||||
let mockShellHistory: MockedUseShellHistory;
|
let mockShellHistory: UseShellHistoryReturn;
|
||||||
let mockCompletion: MockedUseCompletion;
|
let mockCompletion: UseCompletionReturn;
|
||||||
let mockInputHistory: MockedUseInputHistory;
|
let mockInputHistory: UseInputHistoryReturn;
|
||||||
let mockBuffer: TextBuffer;
|
let mockBuffer: TextBuffer;
|
||||||
let mockCommandContext: CommandContext;
|
let mockCommandContext: CommandContext;
|
||||||
|
|
||||||
|
@ -112,7 +142,7 @@ describe('InputPrompt', () => {
|
||||||
resetCompletionState: vi.fn(),
|
resetCompletionState: vi.fn(),
|
||||||
setActiveSuggestionIndex: vi.fn(),
|
setActiveSuggestionIndex: vi.fn(),
|
||||||
setShowSuggestions: vi.fn(),
|
setShowSuggestions: vi.fn(),
|
||||||
};
|
} as unknown as UseCompletionReturn;
|
||||||
mockedUseCompletion.mockReturnValue(mockCompletion);
|
mockedUseCompletion.mockReturnValue(mockCompletion);
|
||||||
|
|
||||||
mockInputHistory = {
|
mockInputHistory = {
|
||||||
|
@ -128,10 +158,10 @@ describe('InputPrompt', () => {
|
||||||
userMessages: [],
|
userMessages: [],
|
||||||
onClearScreen: vi.fn(),
|
onClearScreen: vi.fn(),
|
||||||
config: {
|
config: {
|
||||||
getProjectRoot: () => '/test/project',
|
getProjectRoot: () => path.join('test', 'project'),
|
||||||
getTargetDir: () => '/test/project/src',
|
getTargetDir: () => path.join('test', 'project', 'src'),
|
||||||
} as unknown as Config,
|
} as unknown as Config,
|
||||||
slashCommands: [],
|
slashCommands: mockSlashCommands,
|
||||||
commandContext: mockCommandContext,
|
commandContext: mockCommandContext,
|
||||||
shellModeActive: false,
|
shellModeActive: false,
|
||||||
setShellModeActive: vi.fn(),
|
setShellModeActive: vi.fn(),
|
||||||
|
@ -139,8 +169,6 @@ describe('InputPrompt', () => {
|
||||||
suggestionsWidth: 80,
|
suggestionsWidth: 80,
|
||||||
focus: true,
|
focus: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
props.slashCommands = mockSlashCommands;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
|
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
@ -362,10 +390,13 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should insert image path at cursor position with proper spacing', async () => {
|
it('should insert image path at cursor position with proper spacing', async () => {
|
||||||
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
|
const imagePath = path.join(
|
||||||
vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(
|
'test',
|
||||||
'/test/.gemini-clipboard/clipboard-456.png',
|
'.gemini-clipboard',
|
||||||
|
'clipboard-456.png',
|
||||||
);
|
);
|
||||||
|
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
|
||||||
|
vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(imagePath);
|
||||||
|
|
||||||
// Set initial text and cursor position
|
// Set initial text and cursor position
|
||||||
mockBuffer.text = 'Hello world';
|
mockBuffer.text = 'Hello world';
|
||||||
|
@ -387,9 +418,9 @@ describe('InputPrompt', () => {
|
||||||
.calls[0];
|
.calls[0];
|
||||||
expect(actualCall[0]).toBe(5); // start offset
|
expect(actualCall[0]).toBe(5); // start offset
|
||||||
expect(actualCall[1]).toBe(5); // end offset
|
expect(actualCall[1]).toBe(5); // end offset
|
||||||
expect(actualCall[2]).toMatch(
|
expect(actualCall[2]).toBe(
|
||||||
/@.*\.gemini-clipboard\/clipboard-456\.png/,
|
' @' + path.relative(path.join('test', 'project', 'src'), imagePath),
|
||||||
); // flexible path match
|
);
|
||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -529,12 +560,14 @@ describe('InputPrompt', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should complete a command based on its altNames', async () => {
|
it('should complete a command based on its altNames', async () => {
|
||||||
// Add a command with an altNames to our mock for this test
|
props.slashCommands = [
|
||||||
props.slashCommands.push({
|
{
|
||||||
name: 'help',
|
name: 'help',
|
||||||
altNames: ['?'],
|
altNames: ['?'],
|
||||||
description: '...',
|
kind: CommandKind.BUILT_IN,
|
||||||
} as SlashCommand);
|
description: '...',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
mockedUseCompletion.mockReturnValue({
|
mockedUseCompletion.mockReturnValue({
|
||||||
...mockCompletion,
|
...mockCompletion,
|
||||||
|
@ -667,7 +700,7 @@ describe('InputPrompt', () => {
|
||||||
// Verify useCompletion was called with true (should show completion)
|
// Verify useCompletion was called with true (should show completion)
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'@src/components',
|
'@src/components',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
true, // shouldShowCompletion should be true
|
true, // shouldShowCompletion should be true
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -693,7 +726,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'/memory',
|
'/memory',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
true, // shouldShowCompletion should be true
|
true, // shouldShowCompletion should be true
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -719,7 +752,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'@src/file.ts hello',
|
'@src/file.ts hello',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
false, // shouldShowCompletion should be false
|
false, // shouldShowCompletion should be false
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -745,7 +778,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'/memory add',
|
'/memory add',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
false, // shouldShowCompletion should be false
|
false, // shouldShowCompletion should be false
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -771,7 +804,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'hello world',
|
'hello world',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
false, // shouldShowCompletion should be false
|
false, // shouldShowCompletion should be false
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -797,7 +830,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'first line\n/memory',
|
'first line\n/memory',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
false, // shouldShowCompletion should be false (isSlashCommand returns false because text doesn't start with /)
|
false, // shouldShowCompletion should be false (isSlashCommand returns false because text doesn't start with /)
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -823,7 +856,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'/memory',
|
'/memory',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
true, // shouldShowCompletion should be true (isSlashCommand returns true AND cursor is after / without space)
|
true, // shouldShowCompletion should be true (isSlashCommand returns true AND cursor is after / without space)
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -850,7 +883,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'@src/file👍.txt',
|
'@src/file👍.txt',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
true, // shouldShowCompletion should be true
|
true, // shouldShowCompletion should be true
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -877,7 +910,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'@src/file👍.txt hello',
|
'@src/file👍.txt hello',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
false, // shouldShowCompletion should be false
|
false, // shouldShowCompletion should be false
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -904,7 +937,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'@src/my\\ file.txt',
|
'@src/my\\ file.txt',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
true, // shouldShowCompletion should be true
|
true, // shouldShowCompletion should be true
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -931,7 +964,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'@path/my\\ file.txt hello',
|
'@path/my\\ file.txt hello',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
false, // shouldShowCompletion should be false
|
false, // shouldShowCompletion should be false
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -960,7 +993,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'@docs/my\\ long\\ file\\ name.md',
|
'@docs/my\\ long\\ file\\ name.md',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
true, // shouldShowCompletion should be true
|
true, // shouldShowCompletion should be true
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -987,7 +1020,7 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'/memory\\ test',
|
'/memory\\ test',
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
true, // shouldShowCompletion should be true
|
true, // shouldShowCompletion should be true
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
@ -999,8 +1032,8 @@ describe('InputPrompt', () => {
|
||||||
|
|
||||||
it('should handle Unicode characters with escaped spaces', async () => {
|
it('should handle Unicode characters with escaped spaces', async () => {
|
||||||
// Test combining Unicode and escaped spaces
|
// Test combining Unicode and escaped spaces
|
||||||
mockBuffer.text = '@files/emoji\\ 👍\\ test.txt';
|
mockBuffer.text = '@' + path.join('files', 'emoji\\ 👍\\ test.txt');
|
||||||
mockBuffer.lines = ['@files/emoji\\ 👍\\ test.txt'];
|
mockBuffer.lines = ['@' + path.join('files', 'emoji\\ 👍\\ test.txt')];
|
||||||
mockBuffer.cursor = [0, 25]; // After the escaped space and emoji
|
mockBuffer.cursor = [0, 25]; // After the escaped space and emoji
|
||||||
|
|
||||||
mockedUseCompletion.mockReturnValue({
|
mockedUseCompletion.mockReturnValue({
|
||||||
|
@ -1015,8 +1048,8 @@ describe('InputPrompt', () => {
|
||||||
await wait();
|
await wait();
|
||||||
|
|
||||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||||
'@files/emoji\\ 👍\\ test.txt',
|
'@' + path.join('files', 'emoji\\ 👍\\ test.txt'),
|
||||||
'/test/project/src',
|
path.join('test', 'project', 'src'),
|
||||||
true, // shouldShowCompletion should be true
|
true, // shouldShowCompletion should be true
|
||||||
mockSlashCommands,
|
mockSlashCommands,
|
||||||
mockCommandContext,
|
mockCommandContext,
|
||||||
|
|
|
@ -14,7 +14,7 @@ interface UseInputHistoryProps {
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseInputHistoryReturn {
|
export interface UseInputHistoryReturn {
|
||||||
handleSubmit: (value: string) => void;
|
handleSubmit: (value: string) => void;
|
||||||
navigateUp: () => boolean;
|
navigateUp: () => boolean;
|
||||||
navigateDown: () => boolean;
|
navigateDown: () => boolean;
|
||||||
|
|
|
@ -12,6 +12,13 @@ import { isNodeError, getProjectTempDir } from '@google/gemini-cli-core';
|
||||||
const HISTORY_FILE = 'shell_history';
|
const HISTORY_FILE = 'shell_history';
|
||||||
const MAX_HISTORY_LENGTH = 100;
|
const MAX_HISTORY_LENGTH = 100;
|
||||||
|
|
||||||
|
export interface UseShellHistoryReturn {
|
||||||
|
addCommandToHistory: (command: string) => void;
|
||||||
|
getPreviousCommand: () => string | null;
|
||||||
|
getNextCommand: () => string | null;
|
||||||
|
resetHistoryPosition: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
async function getHistoryFilePath(projectRoot: string): Promise<string> {
|
async function getHistoryFilePath(projectRoot: string): Promise<string> {
|
||||||
const historyDir = getProjectTempDir(projectRoot);
|
const historyDir = getProjectTempDir(projectRoot);
|
||||||
return path.join(historyDir, HISTORY_FILE);
|
return path.join(historyDir, HISTORY_FILE);
|
||||||
|
@ -42,7 +49,7 @@ async function writeHistoryFile(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useShellHistory(projectRoot: string) {
|
export function useShellHistory(projectRoot: string): UseShellHistoryReturn {
|
||||||
const [history, setHistory] = useState<string[]>([]);
|
const [history, setHistory] = useState<string[]>([]);
|
||||||
const [historyIndex, setHistoryIndex] = useState(-1);
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
||||||
const [historyFilePath, setHistoryFilePath] = useState<string | null>(null);
|
const [historyFilePath, setHistoryFilePath] = useState<string | null>(null);
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "nodenext",
|
||||||
"target": "es2022",
|
"target": "es2022",
|
||||||
"types": ["node", "vitest/globals"]
|
"types": ["node", "vitest/globals"],
|
||||||
|
"jsx": "react-jsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue