refactor(cli): consolidate repetitive tests in InputPrompt using it.each (#12524)

This commit is contained in:
Jainam M
2025-11-06 23:11:50 +05:30
committed by GitHub
parent 9ba1cd0336
commit c585470a71
2 changed files with 139 additions and 202 deletions

View File

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

View File

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