summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components/InputPrompt.test.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/components/InputPrompt.test.tsx')
-rw-r--r--packages/cli/src/ui/components/InputPrompt.test.tsx126
1 files changed, 123 insertions, 3 deletions
diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx
index 6f3f996d..ad7a3985 100644
--- a/packages/cli/src/ui/components/InputPrompt.test.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.test.tsx
@@ -13,11 +13,13 @@ import { vi } from 'vitest';
import { useShellHistory } from '../hooks/useShellHistory.js';
import { useCompletion } from '../hooks/useCompletion.js';
import { useInputHistory } from '../hooks/useInputHistory.js';
+import * as clipboardUtils from '../utils/clipboardUtils.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
vi.mock('../hooks/useShellHistory.js');
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>;
@@ -76,6 +78,7 @@ describe('InputPrompt', () => {
mockBuffer.viewportVisualLines = [newText];
mockBuffer.allVisualLines = [newText];
}),
+ replaceRangeByOffset: vi.fn(),
viewportVisualLines: [''],
allVisualLines: [''],
visualCursor: [0, 0],
@@ -87,7 +90,6 @@ describe('InputPrompt', () => {
killLineLeft: vi.fn(),
openInExternalEditor: vi.fn(),
newline: vi.fn(),
- replaceRangeByOffset: vi.fn(),
} as unknown as TextBuffer;
mockShellHistory = {
@@ -218,6 +220,126 @@ describe('InputPrompt', () => {
unmount();
});
+ describe('clipboard image paste', () => {
+ beforeEach(() => {
+ vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
+ vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(null);
+ vi.mocked(clipboardUtils.cleanupOldClipboardImages).mockResolvedValue(
+ undefined,
+ );
+ });
+
+ it('should handle Ctrl+V when clipboard has an image', async () => {
+ vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
+ vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(
+ '/test/.gemini-clipboard/clipboard-123.png',
+ );
+
+ const { stdin, unmount } = render(<InputPrompt {...props} />);
+ await wait();
+
+ // Send Ctrl+V
+ stdin.write('\x16'); // Ctrl+V
+ await wait();
+
+ expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
+ expect(clipboardUtils.saveClipboardImage).toHaveBeenCalledWith(
+ props.config.getTargetDir(),
+ );
+ expect(clipboardUtils.cleanupOldClipboardImages).toHaveBeenCalledWith(
+ props.config.getTargetDir(),
+ );
+ expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should not insert anything when clipboard has no image', async () => {
+ vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
+
+ const { stdin, unmount } = render(<InputPrompt {...props} />);
+ await wait();
+
+ stdin.write('\x16'); // Ctrl+V
+ await wait();
+
+ expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
+ expect(clipboardUtils.saveClipboardImage).not.toHaveBeenCalled();
+ expect(mockBuffer.setText).not.toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should handle image save failure gracefully', async () => {
+ vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
+ vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(null);
+
+ const { stdin, unmount } = render(<InputPrompt {...props} />);
+ await wait();
+
+ stdin.write('\x16'); // Ctrl+V
+ await wait();
+
+ expect(clipboardUtils.saveClipboardImage).toHaveBeenCalled();
+ expect(mockBuffer.setText).not.toHaveBeenCalled();
+ unmount();
+ });
+
+ 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',
+ );
+
+ // Set initial text and cursor position
+ mockBuffer.text = 'Hello world';
+ mockBuffer.cursor = [0, 5]; // Cursor after "Hello"
+ mockBuffer.lines = ['Hello world'];
+ mockBuffer.replaceRangeByOffset = vi.fn();
+
+ const { stdin, unmount } = render(<InputPrompt {...props} />);
+ await wait();
+
+ stdin.write('\x16'); // Ctrl+V
+ await wait();
+
+ // Should insert at cursor position with spaces
+ expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
+
+ // Get the actual call to see what path was used
+ const actualCall = vi.mocked(mockBuffer.replaceRangeByOffset).mock
+ .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
+ unmount();
+ });
+
+ it('should handle errors during clipboard operations', async () => {
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+ vi.mocked(clipboardUtils.clipboardHasImage).mockRejectedValue(
+ new Error('Clipboard error'),
+ );
+
+ const { stdin, unmount } = render(<InputPrompt {...props} />);
+ await wait();
+
+ stdin.write('\x16'); // Ctrl+V
+ await wait();
+
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Error handling clipboard image:',
+ expect.any(Error),
+ );
+ expect(mockBuffer.setText).not.toHaveBeenCalled();
+
+ consoleErrorSpy.mockRestore();
+ unmount();
+ });
+ });
+
it('should complete a partial parent command and add a space', async () => {
// SCENARIO: /mem -> Tab
mockedUseCompletion.mockReturnValue({
@@ -355,8 +477,6 @@ describe('InputPrompt', () => {
unmount();
});
- // ADD this test for defensive coverage
-
it('should not submit on Enter when the buffer is empty or only contains whitespace', async () => {
props.buffer.setText(' '); // Set buffer to whitespace