mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
refactor(cli): consolidate repetitive tests in InputPrompt using it.each (#12524)
This commit is contained in:
@@ -559,94 +559,52 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should complete a partial parent command', async () => {
|
||||
// SCENARIO: /mem -> Tab
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
it.each([
|
||||
{
|
||||
name: 'should complete a partial parent command',
|
||||
bufferText: '/mem',
|
||||
suggestions: [{ label: 'memory', value: 'memory', description: '...' }],
|
||||
activeSuggestionIndex: 0,
|
||||
});
|
||||
props.buffer.setText('/mem');
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should append a sub-command when the parent command is already complete', async () => {
|
||||
// SCENARIO: /memory -> Tab (to accept 'add')
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
activeIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'should append a sub-command when parent command is complete',
|
||||
bufferText: '/memory ',
|
||||
suggestions: [
|
||||
{ label: 'show', value: 'show' },
|
||||
{ label: 'add', value: 'add' },
|
||||
],
|
||||
activeSuggestionIndex: 1, // 'add' is highlighted
|
||||
});
|
||||
props.buffer.setText('/memory ');
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(1),
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle the "backspace" edge case correctly', async () => {
|
||||
// SCENARIO: /memory -> Backspace -> /memory -> Tab (to accept 'show')
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
activeIndex: 1,
|
||||
},
|
||||
{
|
||||
name: 'should handle the backspace edge case correctly',
|
||||
bufferText: '/memory',
|
||||
suggestions: [
|
||||
{ label: 'show', value: 'show' },
|
||||
{ label: 'add', value: 'add' },
|
||||
],
|
||||
activeSuggestionIndex: 0, // 'show' is highlighted
|
||||
});
|
||||
// The user has backspaced, so the query is now just '/memory'
|
||||
props.buffer.setText('/memory');
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
});
|
||||
await waitFor(() =>
|
||||
// It should NOT become '/show'. It should correctly become '/memory show'.
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should complete a partial argument for a command', async () => {
|
||||
// SCENARIO: /chat resume fi- -> Tab
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
activeIndex: 0,
|
||||
},
|
||||
{
|
||||
name: 'should complete a partial argument for a command',
|
||||
bufferText: '/chat resume fi-',
|
||||
suggestions: [{ label: 'fix-foo', value: 'fix-foo' }],
|
||||
activeSuggestionIndex: 0,
|
||||
activeIndex: 0,
|
||||
},
|
||||
])('$name', async ({ bufferText, suggestions, activeIndex }) => {
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions,
|
||||
activeSuggestionIndex: activeIndex,
|
||||
});
|
||||
props.buffer.setText('/chat resume fi-');
|
||||
|
||||
props.buffer.setText(bufferText);
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Press Tab
|
||||
});
|
||||
await act(async () => stdin.write('\t'));
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(
|
||||
activeIndex,
|
||||
),
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
@@ -937,51 +895,36 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
describe('vim mode', () => {
|
||||
it('should not call buffer.handleInput when vim mode is enabled and vim handles the input', async () => {
|
||||
props.vimHandleInput = vi.fn().mockReturnValue(true); // Mock that vim handled it.
|
||||
it.each([
|
||||
{
|
||||
name: 'should not call buffer.handleInput when vim handles input',
|
||||
vimHandled: true,
|
||||
expectBufferHandleInput: false,
|
||||
},
|
||||
{
|
||||
name: 'should call buffer.handleInput when vim does not handle input',
|
||||
vimHandled: false,
|
||||
expectBufferHandleInput: true,
|
||||
},
|
||||
{
|
||||
name: 'should call handleInput when vim mode is disabled',
|
||||
vimHandled: false,
|
||||
expectBufferHandleInput: true,
|
||||
},
|
||||
])('$name', async ({ vimHandled, expectBufferHandleInput }) => {
|
||||
props.vimHandleInput = vi.fn().mockReturnValue(vimHandled);
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('i');
|
||||
});
|
||||
await act(async () => stdin.write('i'));
|
||||
await waitFor(() => {
|
||||
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.vimHandleInput = vi.fn().mockReturnValue(false); // Mock that vim did NOT handle it.
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('i');
|
||||
});
|
||||
await waitFor(() => {
|
||||
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 } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('i');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(props.vimHandleInput).toHaveBeenCalled();
|
||||
expect(mockBuffer.handleInput).toHaveBeenCalled();
|
||||
if (expectBufferHandleInput) {
|
||||
expect(mockBuffer.handleInput).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(mockBuffer.handleInput).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
@@ -2226,8 +2169,6 @@ describe('InputPrompt', () => {
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`);
|
||||
// This snapshot is good to make sure there was an input prompt but does
|
||||
// not show the inverted cursor because snapshots do not show colors.
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
unmount();
|
||||
|
||||
@@ -97,61 +97,75 @@ describe('fileUtils', () => {
|
||||
});
|
||||
|
||||
describe('isWithinRoot', () => {
|
||||
const root = path.resolve('/project/root');
|
||||
const defaultRoot = path.resolve('/project/root');
|
||||
|
||||
it('should return true for paths directly within the root', () => {
|
||||
expect(isWithinRoot(path.join(root, 'file.txt'), root)).toBe(true);
|
||||
expect(isWithinRoot(path.join(root, 'subdir', 'file.txt'), root)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true for the root path itself', () => {
|
||||
expect(isWithinRoot(root, root)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for paths outside the root', () => {
|
||||
expect(
|
||||
isWithinRoot(path.resolve('/project/other', 'file.txt'), root),
|
||||
).toBe(false);
|
||||
expect(isWithinRoot(path.resolve('/unrelated', 'file.txt'), root)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for paths that only partially match the root prefix', () => {
|
||||
expect(
|
||||
isWithinRoot(
|
||||
path.resolve('/project/root-but-actually-different'),
|
||||
root,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle paths with trailing slashes correctly', () => {
|
||||
expect(isWithinRoot(path.join(root, 'file.txt') + path.sep, root)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isWithinRoot(root + path.sep, root)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle different path separators (POSIX vs Windows)', () => {
|
||||
const posixRoot = '/project/root';
|
||||
const posixPathInside = '/project/root/file.txt';
|
||||
const posixPathOutside = '/project/other/file.txt';
|
||||
expect(isWithinRoot(posixPathInside, posixRoot)).toBe(true);
|
||||
expect(isWithinRoot(posixPathOutside, posixRoot)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a root path that is a sub-path of the path to check', () => {
|
||||
const pathToCheck = path.resolve('/project/root/sub');
|
||||
const rootSub = path.resolve('/project/root');
|
||||
expect(isWithinRoot(pathToCheck, rootSub)).toBe(true);
|
||||
|
||||
const pathToCheckSuper = path.resolve('/project/root');
|
||||
const rootSuper = path.resolve('/project/root/sub');
|
||||
expect(isWithinRoot(pathToCheckSuper, rootSuper)).toBe(false);
|
||||
});
|
||||
it.each([
|
||||
{
|
||||
name: 'a path directly within the root',
|
||||
path: path.join(defaultRoot, 'file.txt'),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'a path in a subdirectory within the root',
|
||||
path: path.join(defaultRoot, 'subdir', 'file.txt'),
|
||||
expected: true,
|
||||
},
|
||||
{ name: 'the root path itself', path: defaultRoot, expected: true },
|
||||
{
|
||||
name: 'a path with a trailing slash',
|
||||
path: path.join(defaultRoot, 'file.txt') + path.sep,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'the root path with a trailing slash',
|
||||
path: defaultRoot + path.sep,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'a sub-path of the path to check',
|
||||
path: path.resolve('/project/root/sub'),
|
||||
root: path.resolve('/project/root'),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'a path outside the root',
|
||||
path: path.resolve('/project/other', 'file.txt'),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'an unrelated path',
|
||||
path: path.resolve('/unrelated', 'file.txt'),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'a path that only partially matches the root prefix',
|
||||
path: path.resolve('/project/root-but-actually-different'),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'a root path that is a sub-path of the path to check',
|
||||
path: path.resolve('/project/root'),
|
||||
root: path.resolve('/project/root/sub'),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'a POSIX path inside',
|
||||
path: '/project/root/file.txt',
|
||||
root: '/project/root',
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'a POSIX path outside',
|
||||
path: '/project/other/file.txt',
|
||||
root: '/project/root',
|
||||
expected: false,
|
||||
},
|
||||
])(
|
||||
'should return $expected for $name',
|
||||
({ path: testPath, root, expected }) => {
|
||||
expect(isWithinRoot(testPath, root || defaultRoot)).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('fileExists', () => {
|
||||
@@ -610,45 +624,27 @@ describe('fileUtils', () => {
|
||||
expect(await detectFileType('component.tsx')).toBe('text');
|
||||
});
|
||||
|
||||
it('should detect image type by extension (png)', async () => {
|
||||
mockMimeGetType.mockReturnValueOnce('image/png');
|
||||
expect(await detectFileType('file.png')).toBe('image');
|
||||
});
|
||||
|
||||
it('should detect image type by extension (jpeg)', async () => {
|
||||
mockMimeGetType.mockReturnValueOnce('image/jpeg');
|
||||
expect(await detectFileType('file.jpg')).toBe('image');
|
||||
});
|
||||
it.each([
|
||||
{ type: 'image', file: 'file.png', mime: 'image/png' },
|
||||
{ type: 'image', file: 'file.jpg', mime: 'image/jpeg' },
|
||||
{ type: 'pdf', file: 'file.pdf', mime: 'application/pdf' },
|
||||
{ type: 'audio', file: 'song.mp3', mime: 'audio/mpeg' },
|
||||
{ type: 'video', file: 'movie.mp4', mime: 'video/mp4' },
|
||||
{ type: 'binary', file: 'archive.zip', mime: 'application/zip' },
|
||||
{ type: 'binary', file: 'app.exe', mime: 'application/octet-stream' },
|
||||
])(
|
||||
'should detect $type type for $file by extension',
|
||||
async ({ file, mime, type }) => {
|
||||
mockMimeGetType.mockReturnValueOnce(mime);
|
||||
expect(await detectFileType(file)).toBe(type);
|
||||
},
|
||||
);
|
||||
|
||||
it('should detect svg type by extension', async () => {
|
||||
expect(await detectFileType('image.svg')).toBe('svg');
|
||||
expect(await detectFileType('image.icon.svg')).toBe('svg');
|
||||
});
|
||||
|
||||
it('should detect pdf type by extension', async () => {
|
||||
mockMimeGetType.mockReturnValueOnce('application/pdf');
|
||||
expect(await detectFileType('file.pdf')).toBe('pdf');
|
||||
});
|
||||
|
||||
it('should detect audio type by extension', async () => {
|
||||
mockMimeGetType.mockReturnValueOnce('audio/mpeg');
|
||||
expect(await detectFileType('song.mp3')).toBe('audio');
|
||||
});
|
||||
|
||||
it('should detect video type by extension', async () => {
|
||||
mockMimeGetType.mockReturnValueOnce('video/mp4');
|
||||
expect(await detectFileType('movie.mp4')).toBe('video');
|
||||
});
|
||||
|
||||
it('should detect known binary extensions as binary (e.g. .zip)', async () => {
|
||||
mockMimeGetType.mockReturnValueOnce('application/zip');
|
||||
expect(await detectFileType('archive.zip')).toBe('binary');
|
||||
});
|
||||
it('should detect known binary extensions as binary (e.g. .exe)', async () => {
|
||||
mockMimeGetType.mockReturnValueOnce('application/octet-stream'); // Common for .exe
|
||||
expect(await detectFileType('app.exe')).toBe('binary');
|
||||
});
|
||||
|
||||
it('should use isBinaryFile for unknown extensions and detect as binary', async () => {
|
||||
mockMimeGetType.mockReturnValueOnce(false); // Unknown mime type
|
||||
// Create a file that isBinaryFile will identify as binary
|
||||
|
||||
Reference in New Issue
Block a user