// 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'; function fakeStorage() { const m = new Map(); 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(), }; } describe('store IDE', () => { beforeEach(() => { (globalThis as unknown as { localStorage: unknown }).localStorage = fakeStorage(); setActivePinia(createPinia()); }); it('openFile ouvre un onglet, fixe actif + contexte', () => { const ide = useIdeStore(); ide.openFile('r1', '/wt/a', 'src/x.ts'); expect(ide.editorTabs).toHaveLength(1); expect(ide.activeTabId).toBe(tabId('r1', '/wt/a', 'src/x.ts')); expect(ide.activeContext).toEqual({ repoId: 'r1', wtPath: '/wt/a' }); expect(ide.activeTab?.file).toBe('src/x.ts'); }); it('openFile est idempotent (pas de doublon), refocalise l onglet existant', () => { const ide = useIdeStore(); ide.openFile('r1', '/wt/a', 'x.ts'); ide.openFile('r1', '/wt/b', 'y.ts'); ide.openFile('r1', '/wt/a', 'x.ts'); // ré-ouverture expect(ide.editorTabs).toHaveLength(2); expect(ide.activeTabId).toBe(tabId('r1', '/wt/a', 'x.ts')); }); it('closeTab recalcule l onglet actif sur le voisin', () => { const ide = useIdeStore(); ide.openFile('r', '/w', 'a'); ide.openFile('r', '/w', 'b'); ide.openFile('r', '/w', 'c'); // actif = c ide.setActiveTab(tabId('r', '/w', 'b')); // actif = b (index 1) ide.closeTab(tabId('r', '/w', 'b')); expect(ide.editorTabs).toHaveLength(2); // le voisin qui a glissé à l index 1 est c expect(ide.activeTabId).toBe(tabId('r', '/w', 'c')); }); it('closeTab sur le dernier onglet vide l actif', () => { const ide = useIdeStore(); ide.openFile('r', '/w', 'a'); ide.closeTab(tabId('r', '/w', 'a')); expect(ide.editorTabs).toHaveLength(0); expect(ide.activeTabId).toBeNull(); }); it('closeTabsForWorktree retire tous les onglets d un worktree', () => { const ide = useIdeStore(); ide.openFile('r', '/w1', 'a'); ide.openFile('r', '/w1', 'b'); ide.openFile('r', '/w2', 'c'); ide.closeTabsForWorktree('r', '/w1'); expect(ide.editorTabs.map((t) => t.file)).toEqual(['c']); }); it('moveTab réordonne', () => { const ide = useIdeStore(); ide.openFile('r', '/w', 'a'); ide.openFile('r', '/w', 'b'); ide.openFile('r', '/w', 'c'); ide.moveTab(0, 2); // a -> fin expect(ide.editorTabs.map((t) => t.file)).toEqual(['b', 'c', 'a']); }); it('setTabDirty alimente hasUnsaved / isDirty', () => { const ide = useIdeStore(); ide.openFile('r', '/w', 'a'); const id = tabId('r', '/w', 'a'); expect(ide.hasUnsaved).toBe(false); ide.setTabDirty(id, true); expect(ide.isDirty(id)).toBe(true); expect(ide.hasUnsaved).toBe(true); ide.setTabDirty(id, false); expect(ide.hasUnsaved).toBe(false); }); it('dock : openTerminal ajoute + focus + ouvre le dock ; sans doublon', () => { const ide = useIdeStore(); ide.openTerminal('s1'); ide.openTerminal('s2'); ide.openTerminal('s1'); // déjà présent expect(ide.dockSessionIds).toEqual(['s1', 's2']); expect(ide.activeDockSessionId).toBe('s1'); expect(ide.bottomVisible).toBe(true); }); it('dock : closeTerminal recalcule l actif et referme si vide', () => { const ide = useIdeStore(); ide.openTerminal('s1'); ide.openTerminal('s2'); ide.focusTerminal('s2'); ide.closeTerminal('s2'); expect(ide.dockSessionIds).toEqual(['s1']); expect(ide.activeDockSessionId).toBe('s1'); ide.closeTerminal('s1'); expect(ide.dockSessionIds).toEqual([]); expect(ide.activeDockSessionId).toBeNull(); expect(ide.bottomVisible).toBe(false); }); it('toggleActivity : re-clic sur la vue active replie le panneau', () => { const ide = useIdeStore(); expect(ide.activeActivity).toBe('explorer'); expect(ide.leftVisible).toBe(true); ide.toggleActivity('explorer'); // même vue, ouverte -> repli expect(ide.leftVisible).toBe(false); ide.toggleActivity('git'); // autre vue -> ouvre expect(ide.activeActivity).toBe('git'); expect(ide.leftVisible).toBe(true); }); it('pruneDeadResources retire onglets/terminaux morts ; dirty gardé pour les conservés, nettoyé pour les retirés', () => { const ide = useIdeStore(); ide.openFile('r', '/w1', 'a'); // worktree connu -> gardé ide.openFile('r', '/w2', 'b'); // worktree inconnu -> retiré ide.setTabDirty(tabId('r', '/w1', 'a'), true); ide.setTabDirty(tabId('r', '/w2', 'b'), true); ide.openTerminal('sLive'); ide.openTerminal('sDead'); ide.pruneDeadResources(new Set(['sLive']), new Set([wtKey('r', '/w1')])); expect(ide.editorTabs.map((t) => t.wtPath)).toEqual(['/w1']); expect(ide.dockSessionIds).toEqual(['sLive']); // sDead retiré expect(ide.isDirty(tabId('r', '/w1', 'a'))).toBe(true); // onglet gardé : dirty conservé expect(ide.isDirty(tabId('r', '/w2', 'b'))).toBe(false); // onglet retiré : dirty nettoyé expect(ide.hasUnsaved).toBe(true); // il reste l'onglet /w1 modifié }); });