Fix InputPrompt.test.tsx to be windows compatible (#4736)

This commit is contained in:
Tommaso Sciortino 2025-07-23 15:49:09 -07:00 committed by GitHub
parent 2e28bb90a0
commit e9e2f55144
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 94 additions and 53 deletions

View File

@ -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,

View File

@ -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;

View File

@ -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);

View File

@ -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"
} }
} }