Files
arboretum/packages/web/test/terminal-clipboard.test.ts
T
johanleroy 114fbc8ba0
CI / No em/en dashes (push) Successful in 3s
CI / Build & test (Node 24) (push) Successful in 10m48s
Release / Publish to Gitea npm registry (push) Successful in 10m52s
CI / Build & test (Node 22) (push) Successful in 11m6s
Desktop Release / Build Linux (AppImage + deb) (push) Successful in 13m38s
Desktop Release / Publish floating desktop-latest release (push) Successful in 11s
CI / Pack & boot smoke (Node 22) (push) Successful in 9m56s
Desktop Release / Build Windows (NSIS + portable) (push) Canceled after 0s
fix(desktop, web): pont presse-papier Electron, sans quoi copier ne faisait rien dans l'app
git-arboretum 3.5.1, desktop 0.2.2.

Le copier/coller livre en 3.5.0 ne fonctionnait que dans un navigateur : verifie dans l'app packagee,
`navigator.clipboard` y rejette en NotAllowedError A LA LECTURE COMME A L'ECRITURE. La copie echouait
donc silencieusement dans le contexte meme ou le probleme avait ete signale.

- feat(desktop): `arboretumDesktop.clipboard` expose par le preload, relaye en IPC vers le module
  `clipboard` d'Electron (lecture + ecriture, ecriture bornee a 1 M de caracteres). Le module
  `clipboard` n'etant pas accessible a un preload sandboxe, le passage par le main est obligatoire.
  Constantes de canaux isolees dans src/shared/ipc.ts pour que le bundle du preload ne tire pas
  `ipcMain` avec lui.
- fix(web): acces au presse-papier en cascade, du plus fiable au plus degrade : pont de l'app de
  bureau, puis `navigator.clipboard`, puis `document.execCommand('copy')` pour l'ecriture. Ce dernier
  recours couvre aussi l'acces LAN en clair (http://<ip>:7317), ou l'API Clipboard est indisponible.
- test: quatre cas de plus sur l'ordre de la cascade et le retour a null quand tout est refuse
  (501 tests). Pont IPC verifie dans un vrai Electron : writeText/readText repondent OK, alors que
  `navigator.clipboard` y refuse. Le chemin complet frappe -> copie -> collage -> SIGINT reste
  couvert par verify-clipboard.mjs (Chromium CDP, ALL GREEN).
2026-08-04 21:11:46 +02:00

119 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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> = {}): 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);
});
});