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
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).
119 lines
5.4 KiB
TypeScript
119 lines
5.4 KiB
TypeScript
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);
|
||
});
|
||
});
|