release: git-arboretum 3.5.0 (cache SPA, copier/coller terminal, fichiers des groupes), desktop 0.2.1
CI / No em/en dashes (push) Successful in 3s
CI / Build & test (Node 24) (push) Successful in 10m50s
CI / Build & test (Node 22) (push) Successful in 11m2s
Release / Publish to Gitea npm registry (push) Successful in 10m56s
Desktop Release / Build Linux (AppImage + deb) (push) Successful in 13m21s
Desktop Release / Publish floating desktop-latest release (push) Successful in 11s
CI / Pack & boot smoke (Node 22) (push) Successful in 9m53s
Desktop Release / Build Windows (NSIS + portable) (push) Canceled after 0s
CI / No em/en dashes (push) Successful in 3s
CI / Build & test (Node 24) (push) Successful in 10m50s
CI / Build & test (Node 22) (push) Successful in 11m2s
Release / Publish to Gitea npm registry (push) Successful in 10m56s
Desktop Release / Build Linux (AppImage + deb) (push) Successful in 13m21s
Desktop Release / Publish floating desktop-latest release (push) Successful in 11s
CI / Pack & boot smoke (Node 22) (push) Successful in 9m53s
Desktop Release / Build Windows (NSIS + portable) (push) Canceled after 0s
Cause racine de l'ecran noir apres mise a jour : l'etag faible de @fastify/static derive de taille+mtime, et npm pack fige le mtime de tout le tarball a une date constante (1985-10-26). Deux index.html de versions differentes mais de meme taille partageaient donc le meme etag : le client recevait un 304, gardait son index perime, et demandait des /assets/<hash> disparus ; le fallback SPA repondait index.html en text/html pour ces modules, le navigateur refusait le script, rien ne peignait. - fix(server): index.html et tous les fichiers non haches servis en no-store, validation conditionnelle desactivee (etag/lastModified) pour qu'un client bloque sur un index perime se repare seul ; /assets/ (noms haches) passent en immutable un an - feat(web): copier/coller dans le terminal xterm, dont la selection n'est pas une selection DOM : Ctrl+Maj+C / Ctrl+Maj+V, Cmd+C / Cmd+V sur macOS, Ctrl+Inser / Maj+Inser, plus interception de l'evenement DOM copy pour que le Copier natif fonctionne. Ctrl+C reste SIGINT - fix(web): script anti-FOUC sorti dans /theme-boot.js, la CSP script-src 'self' du daemon refusait de l'executer inline (le theme n'etait donc pose qu'au montage de la SPA) - feat(web): les worktrees d'un groupe se deplient sur leur arborescence de fichiers dans le panneau Groupes (meme composant et meme etat d'expansion que l'Explorateur), et un worktree ainsi deplie est desormais surveille en temps reel - fix: octet nul litteral remplace par \0 dans trois sources (stores/ide.ts, GitPanel.vue, vscode/repos-tree.ts) : git et grep les traitaient comme binaires, leurs diffs etaient illisibles en revue et la garde CI lint-dashes (git grep -I) les sautait en silence - test: cacheControlFor et clipboardIntent en tests purs, verify-clipboard.mjs (E2E Chromium CDP : copie, collage et SIGINT prouves par le presse-papier reel et par le systeme de fichiers), capture groups-dark-desktop ajoutee a verify-ui.mjs
This commit is contained in:
+4
-21
@@ -5,27 +5,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="theme-color" content="#09090b" />
|
||||
<!-- Anti-FOUC : applique la préférence de thème (arb.theme) avant le premier paint,
|
||||
sinon un utilisateur en thème clair verrait un flash sombre au rechargement.
|
||||
Doit rester inline/synchrone (pas de module async). Synchronisé avec lib/theme.ts. -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var raw = localStorage.getItem('arb.theme');
|
||||
var mode = raw ? JSON.parse(raw) : 'dark';
|
||||
var dark = mode === 'dark' || (mode === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
var theme = dark ? 'dark' : 'light';
|
||||
var bg = dark ? '#09090b' : '#fafafa';
|
||||
var el = document.documentElement;
|
||||
el.dataset.theme = theme;
|
||||
el.style.backgroundColor = bg;
|
||||
var cs = document.querySelector('meta[name=color-scheme]');
|
||||
if (cs) cs.setAttribute('content', theme);
|
||||
var tc = document.querySelector('meta[name=theme-color]');
|
||||
if (tc) tc.setAttribute('content', bg);
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<!-- Anti-FOUC : pose le thème (arb.theme) avant le premier paint. Externalisé dans
|
||||
public/theme-boot.js car la CSP du daemon impose `script-src 'self'` et refusait ce
|
||||
script quand il était inline. Doit rester synchrone (ni defer ni module). -->
|
||||
<script src="/theme-boot.js"></script>
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Anti-FOUC : applique la préférence de thème (arb.theme) avant le premier paint, sinon un
|
||||
// utilisateur en thème clair verrait un flash sombre au rechargement.
|
||||
//
|
||||
// Pourquoi un fichier séparé plutôt qu'un <script> inline dans index.html : la CSP du daemon
|
||||
// impose `script-src 'self'` (cf. SECURITY_HEADERS dans server/src/app.ts), qui refuse tout
|
||||
// script inline. Inline, ce code ne s'exécutait pas du tout et le thème n'était posé qu'au
|
||||
// montage de la SPA. Chargé en <script src> synchrone dans le <head>, il garde le même timing
|
||||
// (avant le premier paint) sans exiger 'unsafe-inline' ni un hash à regénérer à chaque édition.
|
||||
//
|
||||
// Doit rester synchrone (pas de module, pas de defer). Synchronisé avec lib/theme.ts.
|
||||
(function () {
|
||||
try {
|
||||
var raw = localStorage.getItem('arb.theme');
|
||||
var mode = raw ? JSON.parse(raw) : 'dark';
|
||||
var dark = mode === 'dark' || (mode === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
var theme = dark ? 'dark' : 'light';
|
||||
var bg = dark ? '#09090b' : '#fafafa';
|
||||
var el = document.documentElement;
|
||||
el.dataset.theme = theme;
|
||||
el.style.backgroundColor = bg;
|
||||
var cs = document.querySelector('meta[name=color-scheme]');
|
||||
if (cs) cs.setAttribute('content', theme);
|
||||
var tc = document.querySelector('meta[name=theme-color]');
|
||||
if (tc) tc.setAttribute('content', bg);
|
||||
} catch (e) {}
|
||||
})();
|
||||
@@ -27,6 +27,7 @@ import '@xterm/xterm/css/xterm.css';
|
||||
import { wsClient, type Attachment } from '../lib/ws-client';
|
||||
import { terminalTheme, TERMINAL_FONT_FAMILY } from '../lib/terminal-theme';
|
||||
import { resolvedTheme } from '../lib/theme';
|
||||
import { clipboardIntent, isMacPlatform } from '../lib/terminal-clipboard';
|
||||
|
||||
const props = withDefaults(defineProps<{ sessionId: string; mode?: 'interactive' | 'observer' }>(), {
|
||||
mode: 'interactive',
|
||||
@@ -42,6 +43,7 @@ let attachment: Attachment | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let intersectionObserver: IntersectionObserver | null = null;
|
||||
let onVisible: (() => void) | null = null;
|
||||
let onDomCopy: ((e: ClipboardEvent) => void) | null = null;
|
||||
let stopThemeWatch: (() => void) | null = null;
|
||||
let disposed = false;
|
||||
|
||||
@@ -145,6 +147,43 @@ onMounted(async () => {
|
||||
requestAnimationFrame(refit); // recale la taille une fois le layout fullbleed stabilisé
|
||||
|
||||
activeTerm.onData((data) => attachment?.sendStdin(data));
|
||||
|
||||
// Copier / coller. Deux voies complémentaires (cf. lib/terminal-clipboard.ts) :
|
||||
// 1. raccourcis dédiés (Ctrl+Shift+C/V, Cmd+C/V, Ctrl/Shift+Insert) interceptés AVANT le PTY ;
|
||||
// 2. l'événement DOM `copy`, seul moyen de rattraper le « Copier » natif (menu Édition d'Electron,
|
||||
// menu contextuel du navigateur) : il se déclenche sans rien copier puisque la sélection xterm
|
||||
// est invisible au DOM, on y injecte donc nous-mêmes le texte sélectionné.
|
||||
const copySelection = async (): Promise<void> => {
|
||||
const text = activeTerm.getSelection();
|
||||
if (!text) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
/* presse-papier refusé (contexte non sécurisé) : le menu Édition natif reste disponible */
|
||||
}
|
||||
};
|
||||
const pasteClipboard = async (): Promise<void> => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text) attachment?.sendStdin(text);
|
||||
} catch {
|
||||
/* lecture refusée : xterm reçoit de toute façon les collages natifs via son textarea */
|
||||
}
|
||||
};
|
||||
activeTerm.attachCustomKeyEventHandler((e) => {
|
||||
if (e.type !== 'keydown') return true;
|
||||
const intent = clipboardIntent(e, activeTerm.hasSelection(), isMacPlatform());
|
||||
if (!intent) return true;
|
||||
void (intent === 'copy' ? copySelection() : pasteClipboard());
|
||||
return false; // ne pas transmettre la frappe au PTY
|
||||
});
|
||||
onDomCopy = (e: ClipboardEvent): void => {
|
||||
const text = activeTerm.getSelection();
|
||||
if (!text || !container.value?.contains(document.activeElement)) return;
|
||||
e.clipboardData?.setData('text/plain', text);
|
||||
e.preventDefault();
|
||||
};
|
||||
document.addEventListener('copy', onDomCopy);
|
||||
resizeObserver = new ResizeObserver(refit);
|
||||
resizeObserver.observe(container.value);
|
||||
// Re-révélation RÉELLE du conteneur : cellule de grille démontée/remontée (maximize↔grille), scroll
|
||||
@@ -176,6 +215,7 @@ onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect();
|
||||
intersectionObserver?.disconnect();
|
||||
if (onVisible) document.removeEventListener('visibilitychange', onVisible);
|
||||
if (onDomCopy) document.removeEventListener('copy', onDomCopy);
|
||||
attachment?.detach();
|
||||
term?.dispose();
|
||||
});
|
||||
|
||||
Binary file not shown.
@@ -61,21 +61,36 @@
|
||||
<FolderGit2 :size="11" class="shrink-0 text-fg-subtle" />
|
||||
<span class="min-w-0 truncate" :title="repo.path">{{ repo.label }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-for="wt in worktrees.worktreesForRepo(repo.id)"
|
||||
:key="wt.path"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-1 rounded py-0.5 pr-2 pl-5 text-left text-[11px] hover:bg-surface-2/60"
|
||||
:class="isActiveWt(wt) ? 'bg-surface-2 text-fg' : 'text-fg-muted'"
|
||||
@click="reveal(wt)"
|
||||
>
|
||||
<component :is="wt.isMain ? Home : GitBranch" :size="11" class="shrink-0 text-fg-subtle" />
|
||||
<span class="min-w-0 truncate font-mono" :title="wt.branch ?? wt.head">{{ wt.branch ?? wt.head.slice(0, 7) }}</span>
|
||||
<span v-if="sessionCount(wt) > 0" class="shrink-0 text-fg-subtle" :title="t('groups.sessionCount', sessionCount(wt))">
|
||||
<SquareTerminal :size="11" />
|
||||
</span>
|
||||
<GitStatusBadge :git="wt.git" :branch="wt.branch" dense class="ml-auto shrink-0 pl-1" />
|
||||
</button>
|
||||
<div v-for="wt in worktrees.worktreesForRepo(repo.id)" :key="wt.path">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-1 rounded py-0.5 pr-2 pl-3 text-left text-[11px] hover:bg-surface-2/60"
|
||||
:class="isActiveWt(wt) ? 'bg-surface-2 text-fg' : 'text-fg-muted'"
|
||||
@click="onWtClick(wt)"
|
||||
>
|
||||
<component :is="isWtExpanded(wt) ? ChevronDown : ChevronRight" :size="11" class="shrink-0 text-fg-subtle" />
|
||||
<component :is="wt.isMain ? Home : GitBranch" :size="11" class="shrink-0 text-fg-subtle" />
|
||||
<span class="min-w-0 truncate font-mono" :title="wt.branch ?? wt.head">{{ wt.branch ?? wt.head.slice(0, 7) }}</span>
|
||||
<span v-if="sessionCount(wt) > 0" class="shrink-0 text-fg-subtle" :title="t('groups.sessionCount', sessionCount(wt))">
|
||||
<SquareTerminal :size="11" />
|
||||
</span>
|
||||
<GitStatusBadge :git="wt.git" :branch="wt.branch" dense class="ml-auto shrink-0 pl-1" />
|
||||
</button>
|
||||
|
||||
<!-- Arborescence de fichiers du worktree, exactement comme dans l'Explorateur (même
|
||||
composant, même état d'expansion partagé) : on travaille dans un groupe sans avoir
|
||||
à repasser par l'autre panneau pour ouvrir un fichier. -->
|
||||
<div v-if="isWtExpanded(wt)" class="pl-3">
|
||||
<FileTree
|
||||
:wt="wt.path"
|
||||
:active="activeFileFor(wt)"
|
||||
embedded
|
||||
:depth="0"
|
||||
:version="worktrees.changeVersion(wt.repoId, wt.path)"
|
||||
@open="(rel) => ide.openFile(wt.repoId, wt.path, rel)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- sessions du groupe (une session de groupe couvre plusieurs dépôts : elle n'appartient à
|
||||
@@ -136,6 +151,7 @@ import { useToastsStore } from '../../stores/toasts';
|
||||
import { useContextMenu, type ContextMenuItem } from '../../composables/useContextMenu';
|
||||
import { sessionLabel } from '../../lib/session-label';
|
||||
import GitStatusBadge from '../workspace/GitStatusBadge.vue';
|
||||
import FileTree from '../workspace/FileTree.vue';
|
||||
import SkeletonRow from '../ui/SkeletonRow.vue';
|
||||
import SessionStateBadge from '../SessionStateBadge.vue';
|
||||
import GroupSessionModal from '../GroupSessionModal.vue';
|
||||
@@ -183,9 +199,18 @@ function sessionTitle(s: SessionSummary): string {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** Rend le worktree actif et visible dans l'explorateur (déplie son dépôt). */
|
||||
function reveal(wt: WorktreeSummary): void {
|
||||
ide.revealWorktree(wt.repoId, wt.path);
|
||||
const isWtExpanded = (wt: WorktreeSummary): boolean => ide.expandedWtPaths.includes(wt.path);
|
||||
|
||||
/** Fichier ouvert appartenant à CE worktree, pour surligner la bonne ligne de l'arbre. */
|
||||
function activeFileFor(wt: WorktreeSummary): string | null {
|
||||
const tab = ide.activeTab;
|
||||
return tab && tab.repoId === wt.repoId && tab.wtPath === wt.path ? tab.file : null;
|
||||
}
|
||||
|
||||
/** Même geste que dans l'Explorateur : le worktree devient actif et son arbre se déplie. */
|
||||
function onWtClick(wt: WorktreeSummary): void {
|
||||
ide.setActiveWorktree(wt.repoId, wt.path);
|
||||
ide.toggleWt(wt.path);
|
||||
}
|
||||
|
||||
function openNewGroup(): void {
|
||||
|
||||
@@ -43,6 +43,13 @@ export function useWatchedWorktrees(): void {
|
||||
// Le worktree actif d'abord : il doit survivre au plafond.
|
||||
const ctx = ide.activeContext;
|
||||
if (ctx) add(ctx.repoId, ctx.wtPath);
|
||||
// Worktrees dont l'arbre de fichiers est déplié : ils peuvent l'être depuis le panneau Groupes,
|
||||
// sans que leur dépôt soit déplié dans l'Explorateur. Sans eux, cet arbre-là ne recevrait aucun
|
||||
// `worktree_changes` et resterait figé sur son premier chargement.
|
||||
for (const path of ide.expandedWtPaths) {
|
||||
const wt = worktrees.worktrees.find((w) => w.path === path);
|
||||
if (wt) add(wt.repoId, wt.path);
|
||||
}
|
||||
for (const repoId of ide.expandedRepoIds) {
|
||||
for (const wt of worktrees.worktreesForRepo(repoId)) add(wt.repoId, wt.path);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copier / coller dans le terminal xterm.
|
||||
//
|
||||
// Pourquoi ce module existe : la sélection d'xterm n'est PAS une sélection DOM (elle vit dans son
|
||||
// propre renderer). Le « Copier » natif (rôle Electron `copy`, menu contextuel du navigateur,
|
||||
// document.execCommand) ne voit donc rien à copier, et l'utilisateur n'a AUCUN moyen de récupérer
|
||||
// du texte affiché par Claude. Par ailleurs Ctrl+C doit rester SIGINT dans un terminal : on ne peut
|
||||
// pas le détourner vers la copie, d'où les raccourcis dédiés ci-dessous.
|
||||
//
|
||||
// Conventions retenues (celles de gnome-terminal / Windows Terminal / VS Code) :
|
||||
// - Linux, Windows : Ctrl+Shift+C copie, Ctrl+Shift+V colle ; Ctrl+Insert / Shift+Insert aussi.
|
||||
// - macOS : Cmd+C copie, Cmd+V colle (Ctrl+C y reste SIGINT comme ailleurs).
|
||||
|
||||
export type ClipboardIntent = 'copy' | 'paste' | null;
|
||||
|
||||
export interface ClipboardKey {
|
||||
key: string;
|
||||
ctrlKey: boolean;
|
||||
shiftKey: boolean;
|
||||
metaKey: boolean;
|
||||
altKey: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traduit une frappe en intention de presse-papier, ou `null` si la touche doit partir au PTY.
|
||||
* `hasSelection` évite de manger un raccourci de copie quand il n'y a rien à copier.
|
||||
*/
|
||||
export function clipboardIntent(e: ClipboardKey, hasSelection: boolean, isMac: boolean): ClipboardIntent {
|
||||
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
||||
|
||||
// Insert : convention historique des terminaux, indépendante de la plateforme.
|
||||
if (key === 'Insert') {
|
||||
if (e.ctrlKey && !e.shiftKey && !e.altKey) return hasSelection ? 'copy' : null;
|
||||
if (e.shiftKey && !e.ctrlKey && !e.altKey) return 'paste';
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMac) {
|
||||
// Cmd sans Ctrl/Alt. Cmd+C sans sélection ne doit rien intercepter.
|
||||
if (!e.metaKey || e.ctrlKey || e.altKey) return null;
|
||||
if (key === 'c') return hasSelection ? 'copy' : null;
|
||||
if (key === 'v') return 'paste';
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ctrl+Shift, sans Alt ni Meta : Ctrl+C seul reste réservé à SIGINT.
|
||||
if (!e.ctrlKey || !e.shiftKey || e.altKey || e.metaKey) return null;
|
||||
if (key === 'c') return hasSelection ? 'copy' : null;
|
||||
if (key === 'v') return 'paste';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Vraie plateforme Apple, y compris les iPad qui annoncent « MacIntel ». */
|
||||
export function isMacPlatform(nav: { platform?: string; userAgent?: string } = navigator): boolean {
|
||||
const p = `${nav.platform ?? ''} ${nav.userAgent ?? ''}`;
|
||||
return /Mac|iPhone|iPad|iPod/i.test(p);
|
||||
}
|
||||
Binary file not shown.
@@ -133,6 +133,10 @@ const en: HelpSection[] = [
|
||||
title: 'Observe vs interact',
|
||||
body: 'Every client can write. Open a session as observer to follow it read-only, which never slows the session down, even on a poor connection.',
|
||||
},
|
||||
{
|
||||
title: 'Copy & paste',
|
||||
body: 'Select with the mouse, then Ctrl+Shift+C to copy (Cmd+C on macOS); Ctrl+Shift+V or Shift+Insert pastes. Ctrl+C is left alone on purpose: in a terminal it interrupts the running command.',
|
||||
},
|
||||
{
|
||||
title: 'Fine-grained state',
|
||||
body: 'Claude sessions report busy, waiting or idle, read from the terminal screen. Waiting means a dialog is blocking: answer it from the sidebar without opening the terminal.',
|
||||
@@ -368,6 +372,10 @@ const fr: HelpSection[] = [
|
||||
title: 'Observer ou interagir',
|
||||
body: 'Tout client peut écrire. Ouvrez une session en observateur pour la suivre en lecture seule : cela ne ralentit jamais la session, même sur une connexion médiocre.',
|
||||
},
|
||||
{
|
||||
title: 'Copier & coller',
|
||||
body: 'Sélectionnez à la souris puis Ctrl+Maj+C pour copier (Cmd+C sur macOS) ; Ctrl+Maj+V ou Maj+Inser colle. Ctrl+C reste volontairement intact : dans un terminal, il interrompt la commande en cours.',
|
||||
},
|
||||
{
|
||||
title: 'États fins',
|
||||
body: 'Les sessions Claude remontent occupée, en attente ou disponible, lus à l’écran du terminal. « En attente » signifie qu’un dialogue bloque : répondez-y depuis la barre latérale, sans ouvrir le terminal.',
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { clipboardIntent, isMacPlatform, 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();
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user