Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

170 changes: 111 additions & 59 deletions packages/cli/src/ui/utils/clipboardUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,120 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
clipboardHasImage,
saveClipboardImage,
cleanupOldClipboardImages,
resetLinuxClipboardTool,
} from './clipboardUtils.js';
import { EventEmitter } from 'node:events';

// Mock ClipboardManager
const mockHasFormat = vi.fn();
const mockGetImageData = vi.fn();
// Use vi.hoisted to define mock functions before vi.mock is hoisted
const { mockSpawn, mockExecSync } = vi.hoisted(() => ({
mockSpawn: vi.fn(),
mockExecSync: vi.fn(),
}));

// Mock @teddyzhu/clipboard
vi.mock('@teddyzhu/clipboard', () => ({
default: {
ClipboardManager: vi.fn().mockImplementation(() => ({
hasFormat: mockHasFormat,
getImageData: mockGetImageData,
hasFormat: vi.fn().mockReturnValue(false),
getImageData: vi.fn().mockReturnValue({ data: null }),
})),
},
ClipboardManager: vi.fn().mockImplementation(() => ({
hasFormat: mockHasFormat,
getImageData: mockGetImageData,
hasFormat: vi.fn().mockReturnValue(false),
getImageData: vi.fn().mockReturnValue({ data: null }),
})),
}));

// Mock node:child_process
vi.mock('node:child_process', () => ({
default: {
spawn: mockSpawn,
execSync: mockExecSync,
exec: vi.fn(),
execFile: vi.fn(),
},
spawn: mockSpawn,
execSync: mockExecSync,
exec: vi.fn(),
execFile: vi.fn(),
}));

/**
* Create a mock child process that emits stdout data and close event.
*/
function createMockChild(stdoutData: string, exitCode: number = 0) {
const stdout = new EventEmitter();
const child = new EventEmitter() as EventEmitter & {
stdout: EventEmitter;
kill: ReturnType<typeof vi.fn>;
killed: boolean;
};
child.stdout = stdout;
child.kill = vi.fn();
child.killed = false;

// Emit data asynchronously
process.nextTick(() => {
stdout.emit('data', Buffer.from(stdoutData));
child.emit('close', exitCode);
});

return child;
}

describe('clipboardUtils', () => {
beforeEach(() => {
vi.clearAllMocks();
resetLinuxClipboardTool();

// Stub process.platform to 'linux'
vi.stubGlobal('process', {
Comment thread
CNCSMonster marked this conversation as resolved.
...process,
platform: 'linux',
env: {
...process.env,
WAYLAND_DISPLAY: 'wayland-0',
XDG_SESSION_TYPE: undefined,
},
});
});

afterEach(() => {
vi.unstubAllGlobals();
});
Comment thread
CNCSMonster marked this conversation as resolved.

describe('clipboardHasImage', () => {
it('should return true when clipboard contains image', async () => {
mockHasFormat.mockReturnValue(true);
// Mock execSync to return successfully (wl-paste found)
mockExecSync.mockReturnValue(Buffer.from('/usr/bin/wl-paste'));

const mockChild = createMockChild('image/png\nimage/bmp\n', 0);
mockSpawn.mockReturnValue(mockChild);

const result = await clipboardHasImage();
expect(result).toBe(true);
expect(mockHasFormat).toHaveBeenCalledWith('image');
});

it('should return false when clipboard does not contain image', async () => {
mockHasFormat.mockReturnValue(false);
// Mock execSync to return successfully (wl-paste found)
mockExecSync.mockReturnValue(Buffer.from('/usr/bin/wl-paste'));

const result = await clipboardHasImage();
expect(result).toBe(false);
expect(mockHasFormat).toHaveBeenCalledWith('image');
});

it('should return false on error', async () => {
mockHasFormat.mockImplementation(() => {
throw new Error('Clipboard error');
});
const mockChild = createMockChild('text/plain\n', 0);
mockSpawn.mockReturnValue(mockChild);

const result = await clipboardHasImage();
expect(result).toBe(false);
});

it('should return false and not throw when error occurs in DEBUG mode', async () => {
const originalEnv = process.env;
vi.stubGlobal('process', {
...process,
env: { ...originalEnv, DEBUG: '1' },
});

mockHasFormat.mockImplementation(() => {
throw new Error('Test error');
it('should return false when wl-paste is not found', async () => {
// Mock execSync to throw (wl-paste not found)
mockExecSync.mockImplementation(() => {
throw new Error('command not found');
});

const result = await clipboardHasImage();
Expand All @@ -76,39 +126,23 @@ describe('clipboardUtils', () => {
});

describe('saveClipboardImage', () => {
Comment thread
CNCSMonster marked this conversation as resolved.
it('should return null when clipboard has no image', async () => {
mockHasFormat.mockReturnValue(false);

const result = await saveClipboardImage('/tmp/test');
expect(result).toBe(null);
});

it('should return null when image data buffer is null', async () => {
mockHasFormat.mockReturnValue(true);
mockGetImageData.mockReturnValue({ data: null });

const result = await saveClipboardImage('/tmp/test');
expect(result).toBe(null);
});

it('should handle errors gracefully and return null', async () => {
mockHasFormat.mockImplementation(() => {
throw new Error('Clipboard error');
it('should return null when no clipboard tool is available', async () => {
// Mock execSync to throw (wl-paste not found)
mockExecSync.mockImplementation(() => {
throw new Error('command not found');
});

const result = await saveClipboardImage('/tmp/test');
expect(result).toBe(null);
});

it('should return null and not throw when error occurs in DEBUG mode', async () => {
const originalEnv = process.env;
vi.stubGlobal('process', {
...process,
env: { ...originalEnv, DEBUG: '1' },
});
it('should return null on spawn error', async () => {
// Mock execSync to return successfully (wl-paste found)
mockExecSync.mockReturnValue(Buffer.from('/usr/bin/wl-paste'));

mockHasFormat.mockImplementation(() => {
throw new Error('Test error');
// Mock spawn to throw an error
mockSpawn.mockImplementation(() => {
throw new Error('spawn error');
});

const result = await saveClipboardImage('/tmp/test');
Expand All @@ -126,11 +160,29 @@ describe('clipboardUtils', () => {
it('should complete without errors on valid directory', async () => {
await expect(cleanupOldClipboardImages('.')).resolves.not.toThrow();
});
});

it('should use clipboard directory consistently with saveClipboardImage', () => {
// This test verifies that both functions use the same directory structure
// The implementation uses 'clipboard' subdirectory for both functions
expect(true).toBe(true);
describe('macOS/Windows fallback', () => {
it('should return false on non-linux platform when @teddyzhu/clipboard fails', async () => {
vi.stubGlobal('process', {
...process,
platform: 'darwin',
});

// @teddyzhu/clipboard mock returns false by default
const result = await clipboardHasImage();
expect(result).toBe(false);
});

it('should return null on non-linux platform when saving fails', async () => {
vi.stubGlobal('process', {
...process,
platform: 'win32',
});

// @teddyzhu/clipboard mock returns false by default
const result = await saveClipboardImage('/tmp/test');
expect(result).toBe(null);
});
});
});
Loading
Loading