fix(web): les fichiers s'ouvrent enfin, et le travail git par projet au centre
Deux défauts d'usage de la zone centrale de l'IDE. 1. Ouvrir un fichier affichait une page vide. Le conteneur de l'unique instance Monaco vivait sous le `v-if="activeTab"` : monté sans onglet actif (premier usage, ou après avoir tout fermé), `onMounted` sortait en silence sur `!host.value`, `ready` restait faux pour toujours, et les deux watchers d'affichage étaient gardés par lui. Le conteneur est désormais toujours monté (v-show, jamais v-if : sous un v-if, fermer le dernier onglet détruisait le DOM et l'éditeur gardait une référence sur un noeud détaché) et l'éditeur est créé paresseusement depuis `showTab`, devenu l'unique entonnoir d'affichage. `useMonaco` ne mémoïse plus un échec, l'attente et l'erreur sont visibles, et la relecture disque ne se tait plus. Sur mobile, ouvrir un fichier amène enfin la zone centrale au premier plan et la valeur morte `mobilePanel = 'sidebar'` disparaît (migrée). Trouvé par l'E2E : seul l'`editor.worker` était fourni, donc ouvrir un `.ts` lançait le service TypeScript contre un worker qui n'en connaissait pas les méthodes, et jetait un rejet non géré à chaque ouverture. Les workers de langage sont fournis (chunks à la demande), diagnostics sémantiques coupés (sans tsconfig ni node_modules, ils inventeraient des erreurs). 2. Le statut git était à l'étroit et mono-projet. La zone centrale porte un mode « Fichiers / Changements » (bascule à icônes dans la barre d'onglets, donc accessible même sans onglet ouvert). La vue Changements montre un bloc par worktree pour TOUS les projets ouverts : fichiers indexés et non indexés, diff déplié sur place, commit, push, fetch, pull et historique par bloc. Le panneau Git de la barre latérale devient l'index (compteurs déjà diffusés par WS, donc zéro requête) et le badge de la barre d'activité, codé à 0, compte enfin les worktrees à traiter. Au passage : `canPush` était faux dans les deux sens (actif sans rien à pousser, bloqué en étant seulement en retard), l'amend d'un message seul est accepté comme le serveur le permettait déjà, le rebase est proposé dès qu'on est en retard, une confirmation d'abandon ne survit plus au rechargement de la liste, et `commit`/`fetch`/`push` émettent enfin `worktree_changes` (le compteur « n non poussés » restait périmé juste après un push). Vérifié : 528 tests, acceptations P7/P9/P14/P15, `verify-ui` (4 captures nouvelles) et un `verify-editor` qui prouve par clics réels l'ouverture d'un fichier sans onglet persisté, la fermeture/réouverture, la bascule mobile et le chunk éditeur absent. Ce dernier échoue bien si l'on remet le conteneur sous un v-if.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
// Règles d'affichage de la vue Changements et de l'index Git. Fonctions pures, testées sans DOM.
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { RepoSummary, WorktreeGitStatus, WorktreeSummary } from '@arboretum/shared';
|
||||
import {
|
||||
dirtyFileCount,
|
||||
groupWorktreesByRepo,
|
||||
hasPendingWork,
|
||||
pendingWorktreeCount,
|
||||
sortForIndex,
|
||||
} from '../src/lib/changes-model';
|
||||
|
||||
const git = (over: Partial<WorktreeGitStatus> = {}): WorktreeGitStatus => ({
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
dirtyCount: 0,
|
||||
upstream: 'origin/main',
|
||||
...over,
|
||||
});
|
||||
|
||||
const wt = (path: string, over: Partial<WorktreeSummary> = {}): WorktreeSummary => ({
|
||||
repoId: 'r1',
|
||||
path,
|
||||
branch: path.split('/').pop() ?? null,
|
||||
head: 'abc1234',
|
||||
detached: false,
|
||||
locked: false,
|
||||
prunable: false,
|
||||
isMain: false,
|
||||
git: git(),
|
||||
sessions: [],
|
||||
...over,
|
||||
});
|
||||
|
||||
const repo = (id: string, label = id): RepoSummary =>
|
||||
({ id, path: `/repos/${id}`, label, hidden: false, valid: true }) as unknown as RepoSummary;
|
||||
|
||||
describe('hasPendingWork', () => {
|
||||
it('un worktree propre et à jour n a rien à traiter', () => {
|
||||
expect(hasPendingWork(git())).toBe(false);
|
||||
});
|
||||
|
||||
it('détecte chaque forme de travail en attente', () => {
|
||||
expect(hasPendingWork(git({ dirtyCount: 2 }))).toBe(true);
|
||||
expect(hasPendingWork(git({ stagedCount: 1 }))).toBe(true);
|
||||
expect(hasPendingWork(git({ unstagedCount: 1 }))).toBe(true);
|
||||
expect(hasPendingWork(git({ conflictCount: 1 }))).toBe(true);
|
||||
expect(hasPendingWork(git({ ahead: 3 }))).toBe(true); // rien à committer mais à pousser
|
||||
});
|
||||
|
||||
it('être en retard ne demande rien de notre côté', () => {
|
||||
expect(hasPendingWork(git({ behind: 5 }))).toBe(false);
|
||||
});
|
||||
|
||||
it('les compteurs fins étant optionnels dans le protocole, dirtyCount reste le filet', () => {
|
||||
// Cas d'un daemon plus ancien : ni stagedCount ni unstagedCount, seulement dirtyCount.
|
||||
expect(hasPendingWork({ ahead: 0, behind: 0, dirtyCount: 4, upstream: null })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dirtyFileCount', () => {
|
||||
it('prend le maximum entre dirtyCount et la somme des compteurs fins', () => {
|
||||
expect(dirtyFileCount(git({ dirtyCount: 3 }))).toBe(3);
|
||||
expect(dirtyFileCount(git({ dirtyCount: 3, stagedCount: 2, unstagedCount: 2 }))).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupWorktreesByRepo', () => {
|
||||
const repos = [repo('r1', 'alpha'), repo('r2', 'beta')];
|
||||
const byRepo: Record<string, WorktreeSummary[]> = {
|
||||
r1: [
|
||||
wt('/wt/main', { isMain: true, git: git({ dirtyCount: 2, unstagedCount: 2 }) }),
|
||||
wt('/wt/feature'),
|
||||
],
|
||||
r2: [wt('/wt/clean', { repoId: 'r2' })],
|
||||
};
|
||||
const forRepo = (id: string): WorktreeSummary[] => byRepo[id] ?? [];
|
||||
|
||||
it('ne garde que ce qui a du travail, et masque un dépôt sans worktree retenu', () => {
|
||||
const groups = groupWorktreesByRepo(repos, forRepo, { showClean: false, active: null });
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(groups[0]?.repo.id).toBe('r1');
|
||||
expect(groups[0]?.worktrees.map((w) => w.path)).toEqual(['/wt/main']);
|
||||
expect(groups[0]?.dirtyFiles).toBe(2);
|
||||
});
|
||||
|
||||
it('showClean inclut tout', () => {
|
||||
const groups = groupWorktreesByRepo(repos, forRepo, { showClean: true, active: null });
|
||||
expect(groups.map((g) => g.repo.id)).toEqual(['r1', 'r2']);
|
||||
expect(groups[0]?.worktrees).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('le worktree actif reste affiché même propre (la vue ne se vide pas après un commit)', () => {
|
||||
const groups = groupWorktreesByRepo(repos, forRepo, {
|
||||
showClean: false,
|
||||
active: { repoId: 'r2', wtPath: '/wt/clean' },
|
||||
});
|
||||
expect(groups.map((g) => g.repo.id)).toEqual(['r1', 'r2']);
|
||||
expect(groups[1]?.worktrees.map((w) => w.path)).toEqual(['/wt/clean']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortForIndex', () => {
|
||||
it('ce qui demande une action d abord, puis le principal, puis le chemin', () => {
|
||||
const list = [
|
||||
wt('/b/clean'),
|
||||
wt('/a/main', { isMain: true }),
|
||||
wt('/c/dirty', { git: git({ dirtyCount: 1 }) }),
|
||||
];
|
||||
expect(sortForIndex(list).map((w) => w.path)).toEqual(['/c/dirty', '/a/main', '/b/clean']);
|
||||
});
|
||||
|
||||
it('ne mute pas la liste reçue', () => {
|
||||
const list = [wt('/b'), wt('/a')];
|
||||
sortForIndex(list);
|
||||
expect(list.map((w) => w.path)).toEqual(['/b', '/a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pendingWorktreeCount', () => {
|
||||
it('compte les worktrees à traiter, tous dépôts visibles confondus', () => {
|
||||
const forRepo = (id: string): WorktreeSummary[] =>
|
||||
id === 'r1'
|
||||
? [wt('/a', { git: git({ dirtyCount: 1 }) }), wt('/b')]
|
||||
: [wt('/c', { repoId: 'r2', git: git({ ahead: 2 }) })];
|
||||
expect(pendingWorktreeCount([repo('r1'), repo('r2')], forRepo)).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
// Cache des listes de fichiers modifiés : invalidation par jeton, plafond de chargements simultanés,
|
||||
// et erreur qui reste visible au lieu d'être avalée (le défaut de l'ancien panneau Git).
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import type { FileChange, WorktreeChangesResponse } from '@arboretum/shared';
|
||||
|
||||
interface Deferred {
|
||||
resolve: (res: WorktreeChangesResponse) => void;
|
||||
reject: (err: Error) => void;
|
||||
}
|
||||
|
||||
let calls: string[] = [];
|
||||
let inFlight = 0;
|
||||
let maxInFlight = 0;
|
||||
let deferreds: Deferred[] = [];
|
||||
/** Quand true, chaque appel reste en vol jusqu'à résolution explicite (test du plafond). */
|
||||
let manual = false;
|
||||
let failNext = false;
|
||||
|
||||
vi.mock('../src/lib/git-api', () => ({
|
||||
gitApi: {
|
||||
changes: (repoId: string, wt: string): Promise<WorktreeChangesResponse> => {
|
||||
calls.push(`${repoId}\0${wt}`);
|
||||
inFlight++;
|
||||
maxInFlight = Math.max(maxInFlight, inFlight);
|
||||
const settle = <T,>(p: Promise<T>): Promise<T> =>
|
||||
p.finally(() => {
|
||||
inFlight--;
|
||||
});
|
||||
if (manual) {
|
||||
return settle(
|
||||
new Promise<WorktreeChangesResponse>((resolve, reject) => {
|
||||
deferreds.push({ resolve, reject });
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (failNext) {
|
||||
failNext = false;
|
||||
return settle(Promise.reject(new Error('boom')));
|
||||
}
|
||||
return settle(Promise.resolve({ repoId, path: wt, changes: [], truncated: false }));
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const { useChangesStore, MAX_CONCURRENT_LOADS } = await import('../src/stores/changes');
|
||||
const { useWorktreesStore } = await import('../src/stores/worktrees');
|
||||
|
||||
function fakeStorage() {
|
||||
const m = new Map<string, string>();
|
||||
return {
|
||||
getItem: (k: string) => (m.has(k) ? (m.get(k) as string) : null),
|
||||
setItem: (k: string, v: string) => void m.set(k, v),
|
||||
removeItem: (k: string) => void m.delete(k),
|
||||
clear: () => m.clear(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Laisse la file se dépiler (les chargements sont enchaînés par des microtâches). */
|
||||
const settle = async (): Promise<void> => {
|
||||
for (let i = 0; i < 20; i++) await Promise.resolve();
|
||||
};
|
||||
|
||||
describe('store changes', () => {
|
||||
beforeEach(() => {
|
||||
calls = [];
|
||||
deferreds = [];
|
||||
inFlight = 0;
|
||||
maxInFlight = 0;
|
||||
manual = false;
|
||||
failNext = false;
|
||||
(globalThis as unknown as { localStorage: unknown }).localStorage = fakeStorage();
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it('ne recharge pas une entrée fraîche, recharge dès que le jeton bouge', async () => {
|
||||
const changes = useChangesStore();
|
||||
const worktrees = useWorktreesStore();
|
||||
|
||||
changes.request('r1', '/wt/a');
|
||||
await settle();
|
||||
expect(calls).toHaveLength(1);
|
||||
|
||||
changes.request('r1', '/wt/a'); // rien n'a bougé
|
||||
await settle();
|
||||
expect(calls).toHaveLength(1);
|
||||
|
||||
worktrees.noteChange('r1', '/wt/a'); // le disque a bougé
|
||||
changes.request('r1', '/wt/a');
|
||||
await settle();
|
||||
expect(calls).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('invalidate force le rechargement sans jeton', async () => {
|
||||
const changes = useChangesStore();
|
||||
changes.request('r1', '/wt/a');
|
||||
await settle();
|
||||
changes.invalidate('r1', '/wt/a');
|
||||
await settle();
|
||||
expect(calls).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('une erreur reste visible et la liste précédente est conservée', async () => {
|
||||
const changes = useChangesStore();
|
||||
const seeded: FileChange[] = [
|
||||
{
|
||||
path: 'src/x.ts',
|
||||
indexStatus: '.',
|
||||
worktreeStatus: 'M',
|
||||
staged: false,
|
||||
unstaged: true,
|
||||
untracked: false,
|
||||
conflicted: false,
|
||||
insertions: 1,
|
||||
deletions: 0,
|
||||
binary: false,
|
||||
},
|
||||
];
|
||||
// premier chargement réussi, puis échec
|
||||
changes.request('r1', '/wt/a');
|
||||
await settle();
|
||||
changes.entries['r1\0/wt/a'] = { ...changes.entries['r1\0/wt/a']!, changes: seeded };
|
||||
|
||||
failNext = true;
|
||||
changes.invalidate('r1', '/wt/a');
|
||||
await settle();
|
||||
|
||||
const entry = changes.entry('r1', '/wt/a');
|
||||
expect(entry?.error).toBe('boom');
|
||||
expect(entry?.changes).toEqual(seeded); // pas d'écran qui se vide
|
||||
expect(entry?.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('ne dépasse jamais le plafond de chargements simultanés', async () => {
|
||||
manual = true;
|
||||
const changes = useChangesStore();
|
||||
for (let i = 0; i < 10; i++) changes.request('r1', `/wt/${i}`);
|
||||
await settle();
|
||||
expect(maxInFlight).toBe(MAX_CONCURRENT_LOADS);
|
||||
expect(calls).toHaveLength(MAX_CONCURRENT_LOADS);
|
||||
|
||||
// on libère tout : la file se dépile jusqu'au bout
|
||||
while (deferreds.length > 0) {
|
||||
const d = deferreds.shift();
|
||||
d?.resolve({ repoId: 'r1', path: '/wt/x', changes: [], truncated: false });
|
||||
await settle();
|
||||
}
|
||||
expect(calls).toHaveLength(10);
|
||||
expect(maxInFlight).toBe(MAX_CONCURRENT_LOADS);
|
||||
});
|
||||
|
||||
it('forget libère, invalidateAll repérime, reset vide', async () => {
|
||||
const changes = useChangesStore();
|
||||
changes.request('r1', '/wt/a');
|
||||
changes.request('r1', '/wt/b');
|
||||
await settle();
|
||||
expect(Object.keys(changes.entries)).toHaveLength(2);
|
||||
|
||||
changes.forget('r1', '/wt/a');
|
||||
expect(changes.entry('r1', '/wt/a')).toBeNull();
|
||||
|
||||
expect(changes.isStale('r1', '/wt/b')).toBe(false);
|
||||
changes.invalidateAll();
|
||||
expect(changes.isStale('r1', '/wt/b')).toBe(true);
|
||||
|
||||
changes.reset();
|
||||
expect(Object.keys(changes.entries)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
// Store IDE : onglets (ouverture/dedup/fermeture), dock terminaux, activité, réconciliation.
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useIdeStore, tabId, wtKey } from '../src/stores/ide';
|
||||
import { MOBILE_PANELS, parseWtKey, useIdeStore, tabId, wtKey } from '../src/stores/ide';
|
||||
|
||||
function fakeStorage() {
|
||||
const m = new Map<string, string>();
|
||||
@@ -139,4 +139,77 @@ describe('store IDE', () => {
|
||||
expect(ide.isDirty(tabId('r', '/w2', 'b'))).toBe(false); // onglet retiré : dirty nettoyé
|
||||
expect(ide.hasUnsaved).toBe(true); // il reste l'onglet /w1 modifié
|
||||
});
|
||||
|
||||
// --- zone centrale : mode fichiers / changements, et cohérence du layout mobile ---
|
||||
|
||||
it('setCenterMode amène le bon panneau au premier plan sur mobile', () => {
|
||||
const ide = useIdeStore();
|
||||
ide.setCenterMode('changes');
|
||||
expect(ide.centerMode).toBe('changes');
|
||||
expect(ide.mobilePanel).toBe('changes');
|
||||
ide.setCenterMode('files');
|
||||
expect(ide.mobilePanel).toBe('editor');
|
||||
});
|
||||
|
||||
it('ouvrir un fichier ou un onglet ramène la zone centrale aux fichiers', () => {
|
||||
const ide = useIdeStore();
|
||||
ide.setCenterMode('changes');
|
||||
ide.openFile('r1', '/wt/a', 'src/x.ts');
|
||||
expect(ide.centerMode).toBe('files');
|
||||
expect(ide.mobilePanel).toBe('editor');
|
||||
|
||||
ide.setCenterMode('changes');
|
||||
ide.setActiveTab(tabId('r1', '/wt/a', 'src/x.ts'));
|
||||
expect(ide.centerMode).toBe('files');
|
||||
});
|
||||
|
||||
it('openChanges rend le worktree actif, déplie son bloc et bascule la vue', () => {
|
||||
const ide = useIdeStore();
|
||||
ide.openChanges('r1', '/wt/a');
|
||||
expect(ide.activeContext).toEqual({ repoId: 'r1', wtPath: '/wt/a' });
|
||||
expect(ide.changesExpanded).toEqual([wtKey('r1', '/wt/a')]);
|
||||
expect(ide.centerMode).toBe('changes');
|
||||
ide.openChanges('r1', '/wt/a'); // idempotent
|
||||
expect(ide.changesExpanded).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('toggleChangesWt bascule et réassigne le tableau (persistedRef non profond)', () => {
|
||||
const ide = useIdeStore();
|
||||
const before = ide.changesExpanded;
|
||||
ide.toggleChangesWt('k1');
|
||||
expect(ide.changesExpanded).toEqual(['k1']);
|
||||
expect(ide.changesExpanded).not.toBe(before); // nouvelle référence, donc persistée
|
||||
ide.toggleChangesWt('k1');
|
||||
expect(ide.changesExpanded).toEqual([]);
|
||||
});
|
||||
|
||||
it('setActivity pose un panneau mobile que le layout sait rendre', () => {
|
||||
const ide = useIdeStore();
|
||||
for (const view of ['explorer', 'sessions', 'groups'] as const) {
|
||||
ide.setActivity(view);
|
||||
expect(ide.mobilePanel).toBe(view);
|
||||
expect(MOBILE_PANELS).toContain(ide.mobilePanel);
|
||||
}
|
||||
// 'git' est l'index de la sidebar desktop : sur mobile, c'est la vue Changements qui le remplace.
|
||||
ide.setActivity('git');
|
||||
expect(ide.mobilePanel).toBe('changes');
|
||||
expect(ide.centerMode).toBe('changes');
|
||||
expect(MOBILE_PANELS).toContain(ide.mobilePanel);
|
||||
});
|
||||
|
||||
it('un mobilePanel persisté obsolète est ramené à une valeur gérée', () => {
|
||||
localStorage.setItem('arb.ide.mobilePanel', JSON.stringify('sidebar')); // persistedRef stocke du JSON
|
||||
setActivePinia(createPinia());
|
||||
expect(useIdeStore().mobilePanel).toBe('explorer');
|
||||
|
||||
localStorage.setItem('arb.ide.mobilePanel', JSON.stringify('git'));
|
||||
setActivePinia(createPinia());
|
||||
expect(useIdeStore().mobilePanel).toBe('changes');
|
||||
});
|
||||
|
||||
it('parseWtKey est l’inverse de wtKey', () => {
|
||||
expect(parseWtKey(wtKey('r1', '/wt/mon projet'))).toEqual({ repoId: 'r1', wtPath: '/wt/mon projet' });
|
||||
expect(parseWtKey('sans-separateur')).toBeNull();
|
||||
expect(parseWtKey('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Contrat de loadMonaco : le SUCCÈS est partagé, l'ÉCHEC est réessayable. Un chunk manquant ne doit
|
||||
// pas condamner l'éditeur pour toute la session (c'était le cas : la promesse rejetée restait
|
||||
// mémoïsée, donc la zone d'édition restait vide et muette jusqu'au rechargement de la page).
|
||||
// Le cache vit au niveau du module : ce fichier garde donc UN seul test, ordonné.
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type * as Monaco from 'monaco-editor';
|
||||
import { loadMonaco } from '../src/composables/useMonaco';
|
||||
|
||||
const fake = { editor: {} } as unknown as typeof Monaco;
|
||||
|
||||
describe('loadMonaco', () => {
|
||||
it('ne mémoïse pas un échec, mémoïse un succès', async () => {
|
||||
let failed = 0;
|
||||
const failing = (): Promise<typeof Monaco> => {
|
||||
failed++;
|
||||
return Promise.reject(new Error('chunk absent'));
|
||||
};
|
||||
|
||||
await expect(loadMonaco(failing)).rejects.toThrow('chunk absent');
|
||||
await expect(loadMonaco(failing)).rejects.toThrow('chunk absent');
|
||||
expect(failed).toBe(2); // l'import est réellement retenté
|
||||
|
||||
let ok = 0;
|
||||
const working = (): Promise<typeof Monaco> => {
|
||||
ok++;
|
||||
return Promise.resolve(fake);
|
||||
};
|
||||
await expect(loadMonaco(working)).resolves.toBe(fake);
|
||||
await expect(loadMonaco(working)).resolves.toBe(fake);
|
||||
expect(ok).toBe(1); // succès partagé : un seul import
|
||||
});
|
||||
});
|
||||
@@ -117,6 +117,36 @@ describe('useWatchedWorktrees', () => {
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('observe un bloc déplié de la vue Changements, sans dépôt déplié dans l’explorateur', async () => {
|
||||
const ide = useIdeStore();
|
||||
const worktrees = useWorktreesStore();
|
||||
worktrees.worktrees = [wt('r1', '/wt/a'), wt('r2', '/wt/b')];
|
||||
const scope = effectScope();
|
||||
scope.run(() => useWatchedWorktrees());
|
||||
expect(watched).toEqual([]);
|
||||
|
||||
ide.toggleChangesWt('r2\0/wt/b');
|
||||
await nextTick();
|
||||
expect(watched).toEqual(['r2\0/wt/b']);
|
||||
|
||||
ide.toggleChangesWt('r2\0/wt/b');
|
||||
await nextTick();
|
||||
expect(released).toEqual(['r2\0/wt/b']);
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('ignore une clé de bloc dont le worktree n’existe plus', async () => {
|
||||
const ide = useIdeStore();
|
||||
const worktrees = useWorktreesStore();
|
||||
worktrees.worktrees = [wt('r1', '/wt/a')];
|
||||
ide.toggleChangesWt('r9\0/wt/disparu');
|
||||
const scope = effectScope();
|
||||
scope.run(() => useWatchedWorktrees());
|
||||
await nextTick();
|
||||
expect(watched).toEqual([]);
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('incrémente le jeton d’invalidation du store à chaque worktree_changes', () => {
|
||||
const ide = useIdeStore();
|
||||
const worktrees = useWorktreesStore();
|
||||
|
||||
Reference in New Issue
Block a user