diff --git a/packages/cli/src/ui/utils/clipboardUtils.test.ts b/packages/cli/src/ui/utils/clipboardUtils.test.ts index 82cb46cc1..c7197c5cf 100644 --- a/packages/cli/src/ui/utils/clipboardUtils.test.ts +++ b/packages/cli/src/ui/utils/clipboardUtils.test.ts @@ -5,19 +5,22 @@ */ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { execCommand } from '@qwen-code/qwen-code-core'; import { clipboardHasImage, saveClipboardImage, cleanupOldClipboardImages, } from './clipboardUtils.js'; -// Mock execCommand -vi.mock('@qwen-code/qwen-code-core', () => ({ - execCommand: vi.fn(), -})); +// Mock ClipboardManager +const mockHasFormat = vi.fn(); +const mockGetImageData = vi.fn(); -const mockExecCommand = vi.mocked(execCommand); +vi.mock('@teddyzhu/clipboard', () => ({ + ClipboardManager: vi.fn().mockImplementation(() => ({ + hasFormat: mockHasFormat, + getImageData: mockGetImageData, + })), +})); describe('clipboardUtils', () => { beforeEach(() => { @@ -25,319 +28,99 @@ describe('clipboardUtils', () => { }); describe('clipboardHasImage', () => { - describe('macOS platform', () => { - beforeEach(() => { - vi.stubGlobal('process', { - ...process, - platform: 'darwin', - env: process.env, - }); - }); + it('should return true when clipboard contains image', async () => { + mockHasFormat.mockReturnValue(true); - it('should return true when clipboard contains PNG image', async () => { - mockExecCommand.mockResolvedValue({ - stdout: '«class PNGf»', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(true); - expect(mockExecCommand).toHaveBeenCalledWith( - 'osascript', - ['-e', 'clipboard info'], - { timeout: 1500 }, - ); - }); - - it('should return true when clipboard contains JPEG image', async () => { - mockExecCommand.mockResolvedValue({ - stdout: '«class JPEG»', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(true); - }); - - it('should return true when clipboard contains WebP image', async () => { - mockExecCommand.mockResolvedValue({ - stdout: '«class WEBP»', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(true); - }); - - it('should return true when clipboard contains HEIC image', async () => { - mockExecCommand.mockResolvedValue({ - stdout: 'public.heic', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(true); - }); - - it('should return true when clipboard contains BMP image', async () => { - mockExecCommand.mockResolvedValue({ - stdout: '«class BMPf»', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(true); - }); - - it('should return false when clipboard contains text', async () => { - mockExecCommand.mockResolvedValue({ - stdout: '«class utf8»', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(false); - }); - - it('should return false on error', async () => { - mockExecCommand.mockRejectedValue(new Error('Command failed')); - - const result = await clipboardHasImage(); - expect(result).toBe(false); - }); + const result = await clipboardHasImage(); + expect(result).toBe(true); + expect(mockHasFormat).toHaveBeenCalledWith('image'); }); - describe('Windows platform', () => { - beforeEach(() => { - vi.stubGlobal('process', { - ...process, - platform: 'win32', - env: process.env, - }); - }); + it('should return false when clipboard does not contain image', async () => { + mockHasFormat.mockReturnValue(false); - it('should return true when clipboard contains image', async () => { - mockExecCommand.mockResolvedValue({ - stdout: 'true', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(true); - expect(mockExecCommand).toHaveBeenCalledWith( - 'powershell.exe', - expect.arrayContaining([ - '-command', - expect.stringContaining('Get-Clipboard'), - ]), - ); - }); - - it('should return false when clipboard does not contain image', async () => { - mockExecCommand.mockResolvedValue({ - stdout: 'false', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(false); - }); - - it('should return false when all PowerShell hosts fail', async () => { - mockExecCommand - .mockRejectedValueOnce(new Error('PowerShell not found')) - .mockRejectedValueOnce(new Error('PowerShell not found')) - .mockRejectedValueOnce(new Error('PowerShell not found')); - - const result = await clipboardHasImage(); - expect(result).toBe(false); - expect(mockExecCommand).toHaveBeenCalledTimes(3); - }); + const result = await clipboardHasImage(); + expect(result).toBe(false); + expect(mockHasFormat).toHaveBeenCalledWith('image'); }); - describe('Linux platform', () => { - beforeEach(() => { - vi.stubGlobal('process', { - ...process, - platform: 'linux', - env: process.env, - }); + it('should return false on error', async () => { + mockHasFormat.mockImplementation(() => { + throw new Error('Clipboard error'); }); - it('should return true when xclip has PNG image', async () => { - // First call: which xclip (success) - // Second call: xclip get PNG (has content) - mockExecCommand - .mockResolvedValueOnce({ - stdout: '/usr/bin/xclip', - stderr: '', - code: 0, - }) - .mockResolvedValueOnce({ stdout: 'image-data', stderr: '', code: 0 }); + const result = await clipboardHasImage(); + expect(result).toBe(false); + }); - const result = await clipboardHasImage(); - expect(result).toBe(true); + it('should log errors in DEBUG mode', async () => { + const originalEnv = process.env; + vi.stubGlobal('process', { + ...process, + env: { ...originalEnv, DEBUG: '1' }, }); - it('should try multiple formats with xclip', async () => { - // which xclip succeeds - mockExecCommand.mockResolvedValueOnce({ - stdout: '/usr/bin/xclip', - stderr: '', - code: 0, - }); - // PNG fails - mockExecCommand.mockRejectedValueOnce(new Error('No PNG')); - // JPEG succeeds - mockExecCommand.mockResolvedValueOnce({ - stdout: 'jpeg-data', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(true); + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + mockHasFormat.mockImplementation(() => { + throw new Error('Test error'); }); - it('should fallback to xsel when xclip not available', async () => { - // which xclip fails - mockExecCommand.mockRejectedValueOnce(new Error('xclip not found')); - // which xsel succeeds - mockExecCommand.mockResolvedValueOnce({ - stdout: '/usr/bin/xsel', - stderr: '', - code: 0, - }); - // xsel -b -t returns image MIME types - mockExecCommand.mockResolvedValueOnce({ - stdout: 'text/plain\nimage/png\ntext/html', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(true); - }); - - it('should fallback to wl-paste when xclip and xsel not available', async () => { - // which xclip fails - mockExecCommand.mockRejectedValueOnce(new Error('xclip not found')); - // which xsel fails - mockExecCommand.mockRejectedValueOnce(new Error('xsel not found')); - // which wl-paste succeeds - mockExecCommand.mockResolvedValueOnce({ - stdout: '/usr/bin/wl-paste', - stderr: '', - code: 0, - }); - // wl-paste --list-types returns image MIME type - mockExecCommand.mockResolvedValueOnce({ - stdout: 'text/plain\nimage/png\ntext/html', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(true); - }); - - it('should return false when no clipboard tool available', async () => { - // All tools fail - mockExecCommand.mockRejectedValue(new Error('Not found')); - - const result = await clipboardHasImage(); - expect(result).toBe(false); - }); - - it('should return false when xsel has no image types', async () => { - // which xclip fails - mockExecCommand.mockRejectedValueOnce(new Error('xclip not found')); - // which xsel succeeds - mockExecCommand.mockResolvedValueOnce({ - stdout: '/usr/bin/xsel', - stderr: '', - code: 0, - }); - // xsel -b -t returns only text types - mockExecCommand.mockResolvedValueOnce({ - stdout: 'text/plain\ntext/html', - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(false); - }); + await clipboardHasImage(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Error checking clipboard for image:', + expect.any(Error), + ); + consoleErrorSpy.mockRestore(); }); }); describe('saveClipboardImage', () => { - const testTempDir = '/tmp/test-clipboard'; + it('should return null when clipboard has no image', async () => { + mockHasFormat.mockReturnValue(false); - it('should create clipboard directory when saving image', async () => { - vi.stubGlobal('process', { - ...process, - platform: 'darwin', - env: process.env, - }); + const result = await saveClipboardImage('/tmp/test'); + expect(result).toBe(null); + }); - // Mock all execCommand calls to fail (no image in clipboard) - mockExecCommand.mockRejectedValue(new Error('No image')); + it('should return null when image data buffer is null', async () => { + mockHasFormat.mockReturnValue(true); + mockGetImageData.mockReturnValue({ data: null }); - const result = await saveClipboardImage(testTempDir); - // Should return null when no image available + const result = await saveClipboardImage('/tmp/test'); expect(result).toBe(null); }); it('should handle errors gracefully and return null', async () => { - const result = await saveClipboardImage( - '/invalid/path/that/does/not/exist', - ); + mockHasFormat.mockImplementation(() => { + throw new Error('Clipboard error'); + }); + + const result = await saveClipboardImage('/tmp/test'); expect(result).toBe(null); }); - it('should support macOS platform', async () => { + it('should log errors in DEBUG mode', async () => { + const originalEnv = process.env; vi.stubGlobal('process', { ...process, - platform: 'darwin', - env: process.env, + env: { ...originalEnv, DEBUG: '1' }, }); - mockExecCommand.mockRejectedValue(new Error('No image')); - const result = await saveClipboardImage(); - expect(result === null || typeof result === 'string').toBe(true); - }); - - it('should support Windows platform', async () => { - vi.stubGlobal('process', { - ...process, - platform: 'win32', - env: process.env, + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + mockHasFormat.mockImplementation(() => { + throw new Error('Test error'); }); - mockExecCommand.mockRejectedValue(new Error('No image')); - const result = await saveClipboardImage(); - expect(result === null || typeof result === 'string').toBe(true); - }); - - it('should support Linux platform', async () => { - vi.stubGlobal('process', { - ...process, - platform: 'linux', - env: process.env, - }); - - mockExecCommand.mockRejectedValue(new Error('No image')); - const result = await saveClipboardImage(); - expect(result === null || typeof result === 'string').toBe(true); + await saveClipboardImage('/tmp/test'); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Error saving clipboard image:', + expect.any(Error), + ); + consoleErrorSpy.mockRestore(); }); }); @@ -358,84 +141,4 @@ describe('clipboardUtils', () => { expect(true).toBe(true); }); }); - - describe('multi-format support', () => { - beforeEach(() => { - vi.stubGlobal('process', { - ...process, - platform: 'darwin', - env: process.env, - }); - }); - - const formats = [ - { name: 'PNG', pattern: '«class PNGf»' }, - { name: 'JPEG', pattern: '«class JPEG»' }, - { name: 'WebP', pattern: '«class WEBP»' }, - { name: 'HEIC', pattern: '«class heic»' }, - { name: 'HEIF', pattern: 'public.heif' }, - { name: 'TIFF', pattern: '«class TIFF»' }, - { name: 'GIF', pattern: '«class GIFf»' }, - { name: 'BMP', pattern: '«class BMPf»' }, - ]; - - formats.forEach(({ name, pattern }) => { - it(`should detect ${name} format on macOS`, async () => { - mockExecCommand.mockResolvedValue({ - stdout: pattern, - stderr: '', - code: 0, - }); - - const result = await clipboardHasImage(); - expect(result).toBe(true); - }); - }); - }); - - describe('error handling with DEBUG mode', () => { - const originalEnv = process.env; - - describe('clipboardHasImage', () => { - beforeEach(() => { - vi.stubGlobal('process', { - ...process, - platform: 'darwin', - env: { ...originalEnv, DEBUG: '1' }, - }); - }); - - it('should log errors in DEBUG mode', async () => { - const consoleErrorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - mockExecCommand.mockRejectedValue(new Error('Test error')); - - await clipboardHasImage(); - expect(consoleErrorSpy).toHaveBeenCalled(); - consoleErrorSpy.mockRestore(); - }); - }); - - describe('saveClipboardImage on Windows', () => { - beforeEach(() => { - vi.stubGlobal('process', { - ...process, - platform: 'win32', - env: { ...originalEnv, DEBUG: '1' }, - }); - }); - - it('should log errors in DEBUG mode', async () => { - const consoleErrorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - mockExecCommand.mockRejectedValue(new Error('Test error')); - - await saveClipboardImage('/invalid/path'); - expect(consoleErrorSpy).toHaveBeenCalled(); - consoleErrorSpy.mockRestore(); - }); - }); - }); });