import { describe, it, expect, afterEach, vi } from 'vitest'; import { clipboardIntent, isMacPlatform, readClipboard, writeClipboard, type ClipboardKey, } from '../src/lib/terminal-clipboard'; function key(k: string, mods: Partial = {}): ClipboardKey { return { key: k, ctrlKey: false, shiftKey: false, metaKey: false, altKey: false, ...mods }; } describe('clipboardIntent (Linux / Windows)', () => { it('copie sur Ctrl+Shift+C quand il y a une sélection', () => { expect(clipboardIntent(key('C', { ctrlKey: true, shiftKey: true }), true, false)).toBe('copy'); }); it('laisse passer Ctrl+Shift+C sans sélection (rien à copier)', () => { expect(clipboardIntent(key('C', { ctrlKey: true, shiftKey: true }), false, false)).toBeNull(); }); it('colle sur Ctrl+Shift+V, sélection ou pas', () => { expect(clipboardIntent(key('V', { ctrlKey: true, shiftKey: true }), false, false)).toBe('paste'); expect(clipboardIntent(key('v', { ctrlKey: true, shiftKey: true }), true, false)).toBe('paste'); }); it("n'intercepte JAMAIS Ctrl+C seul : c'est SIGINT", () => { expect(clipboardIntent(key('c', { ctrlKey: true }), true, false)).toBeNull(); expect(clipboardIntent(key('c', { ctrlKey: true }), false, false)).toBeNull(); }); it('laisse passer Ctrl+V seul (le collage natif d’xterm suffit)', () => { expect(clipboardIntent(key('v', { ctrlKey: true }), false, false)).toBeNull(); }); it('ignore les combinaisons avec Alt ou Meta', () => { expect(clipboardIntent(key('c', { ctrlKey: true, shiftKey: true, altKey: true }), true, false)).toBeNull(); expect(clipboardIntent(key('c', { ctrlKey: true, shiftKey: true, metaKey: true }), true, false)).toBeNull(); }); it('supporte Ctrl+Insert / Shift+Insert', () => { expect(clipboardIntent(key('Insert', { ctrlKey: true }), true, false)).toBe('copy'); expect(clipboardIntent(key('Insert', { ctrlKey: true }), false, false)).toBeNull(); expect(clipboardIntent(key('Insert', { shiftKey: true }), false, false)).toBe('paste'); expect(clipboardIntent(key('Insert', { ctrlKey: true, shiftKey: true }), true, false)).toBeNull(); }); }); describe('clipboardIntent (macOS)', () => { it('copie sur Cmd+C avec sélection, colle sur Cmd+V', () => { expect(clipboardIntent(key('c', { metaKey: true }), true, true)).toBe('copy'); expect(clipboardIntent(key('c', { metaKey: true }), false, true)).toBeNull(); expect(clipboardIntent(key('v', { metaKey: true }), false, true)).toBe('paste'); }); it('garde Ctrl+C en SIGINT sur macOS aussi', () => { expect(clipboardIntent(key('c', { ctrlKey: true }), true, true)).toBeNull(); }); it('ignore Ctrl+Shift+C sur macOS (réservé Cmd)', () => { expect(clipboardIntent(key('c', { ctrlKey: true, shiftKey: true }), true, true)).toBeNull(); }); }); // Le pont de l'app de bureau doit primer : dans Electron, navigator.clipboard rejette en // NotAllowedError (lecture ET écriture), ce qui rendait la copie impossible depuis un terminal. describe('writeClipboard / readClipboard', () => { // `globalThis.navigator` est un getter en Node : seul stubGlobal sait le remplacer proprement. const stub = (name: string, value: unknown): void => vi.stubGlobal(name, value); afterEach(() => { vi.unstubAllGlobals(); }); it('écrit par le pont desktop quand il est présent', async () => { const writeText = vi.fn().mockResolvedValue(true); stub('arboretumDesktop', { clipboard: { writeText } }); stub('navigator', { clipboard: { writeText: vi.fn().mockRejectedValue(new Error('NotAllowedError')) } }); expect(await writeClipboard('bonjour')).toBe(true); expect(writeText).toHaveBeenCalledWith('bonjour'); }); it('retombe sur navigator.clipboard si le pont échoue', async () => { stub('arboretumDesktop', { clipboard: { writeText: vi.fn().mockRejectedValue(new Error('ipc down')) } }); const navWrite = vi.fn().mockResolvedValue(undefined); stub('navigator', { clipboard: { writeText: navWrite } }); expect(await writeClipboard('secours')).toBe(true); expect(navWrite).toHaveBeenCalledWith('secours'); }); it('ne tente rien pour un texte vide', async () => { const writeText = vi.fn(); stub('arboretumDesktop', { clipboard: { writeText } }); expect(await writeClipboard('')).toBe(false); expect(writeText).not.toHaveBeenCalled(); }); it('lit par le pont desktop, sinon par navigator, sinon null', async () => { stub('arboretumDesktop', { clipboard: { readText: vi.fn().mockResolvedValue('du pont') } }); expect(await readClipboard()).toBe('du pont'); stub('arboretumDesktop', undefined); stub('navigator', { clipboard: { readText: vi.fn().mockResolvedValue('du navigateur') } }); expect(await readClipboard()).toBe('du navigateur'); stub('navigator', { clipboard: { readText: vi.fn().mockRejectedValue(new Error('NotAllowedError')) } }); expect(await readClipboard()).toBeNull(); }); }); describe('isMacPlatform', () => { it('reconnaît macOS et iPadOS, pas Linux ni Windows', () => { expect(isMacPlatform({ platform: 'MacIntel', userAgent: 'Mozilla/5.0 (Macintosh)' })).toBe(true); expect(isMacPlatform({ platform: 'iPhone', userAgent: '' })).toBe(true); expect(isMacPlatform({ platform: 'Linux x86_64', userAgent: 'Mozilla/5.0 (X11; Linux)' })).toBe(false); expect(isMacPlatform({ platform: 'Win32', userAgent: 'Mozilla/5.0 (Windows NT 10.0)' })).toBe(false); }); });