Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a3ef752
fix(clipboard): use platform-native tools for image paste on Linux
CNCSMonster May 30, 2026
b3832bc
test: update clipboard tests for platform-native tools
CNCSMonster May 31, 2026
f9121f2
fix: address critical review comments
CNCSMonster May 31, 2026
accb428
fix: address review suggestions for resource leaks and robustness
CNCSMonster May 31, 2026
0c3d2de
fix: address all remaining review comments
CNCSMonster May 31, 2026
9124d74
perf: cache wl-paste --list-types result to avoid redundant calls
CNCSMonster May 31, 2026
7572ac9
fix: address remaining review suggestions
CNCSMonster May 31, 2026
d3270e1
fix: address critical cache invalidation and other review feedback
CNCSMonster Jun 1, 2026
158fcf2
fix: address remaining code review issues
CNCSMonster Jun 1, 2026
010a262
fix: restore getClipboardModule import caching (regression fix)
CNCSMonster Jun 1, 2026
7344381
test: add saveClipboardImage success path and cache behavior tests
CNCSMonster Jun 1, 2026
890d88d
fix: revert execSync to fix WSL2 clipboard detection
CNCSMonster Jun 1, 2026
237073c
fix: address critical file leak and filter issues from review
CNCSMonster Jun 2, 2026
5e803cf
test: add xclip, BMP, error path test coverage; fix weak assertion
CNCSMonster Jun 2, 2026
747adab
fix: remove unused _setupWaylandEnv function that breaks TS build
CNCSMonster Jun 3, 2026
243e1fb
fix: clean up tempFilePath on PIL conversion failure
CNCSMonster Jun 4, 2026
7e43d66
fix: address review feedback on file leaks and test coverage
CNCSMonster Jun 4, 2026
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.

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