Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
150 changes: 89 additions & 61 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.
Outdated
...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);

const result = await clipboardHasImage();
expect(result).toBe(false);
expect(mockHasFormat).toHaveBeenCalledWith('image');
});
// Mock execSync to return successfully (wl-paste found)
mockExecSync.mockReturnValue(Buffer.from('/usr/bin/wl-paste'));

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.
Outdated
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,5 @@ 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);
});
});
});
Loading
Loading