import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=4'; import { state } from '../../js/modules/state.js'; function termToggle() { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'T', shiftKey: true, bubbles: true })); } function termExec(cmd) { const input = document.getElementById('hterm-input'); if (!input) return; input.value = cmd; input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); } function termOutput() { return document.getElementById('hterm-output')?.textContent || ''; } async function makeCard(title, body = '') { return fetch('api/cards', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() }, body: JSON.stringify({ title, body, color: '#38bdf8' }), }).then(r => r.json()); } describe('Terminal: Toggle', () => { afterEach(async () => { document.getElementById('hterm')?.classList.add('hidden'); await closeAll(); }); it('panel exists but is hidden', () => { const panel = document.getElementById('hterm'); expect(panel).toExist(); expect(panel.classList.contains('hidden')).toBe(true); }); it('Shift+T opens the terminal', async () => { termToggle(); await sleep(50); expect(document.getElementById('hterm').classList.contains('hidden')).toBe(false); }); it('Shift+T again closes it', async () => { termToggle(); await sleep(50); termToggle(); await sleep(50); expect(document.getElementById('hterm').classList.contains('hidden')).toBe(true); }); it('ESC closes the terminal', async () => { termToggle(); await sleep(50); const input = document.getElementById('hterm-input'); input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); await sleep(50); expect(document.getElementById('hterm').classList.contains('hidden')).toBe(true); }); }); describe('Terminal: Commands', () => { afterEach(async () => { document.getElementById('hterm')?.classList.add('hidden'); document.getElementById('hterm-output').innerHTML = ''; await closeAll(); }); it('help lists commands', async () => { termToggle(); await sleep(50); termExec('help'); await sleep(50); const out = termOutput(); expect(out).toContain('help'); expect(out).toContain('new'); expect(out).toContain('search'); }); it('count shows numbers', async () => { termToggle(); await sleep(50); termExec('count'); await sleep(50); expect(termOutput()).toContain('cards'); expect(termOutput()).toContain('buckets'); }); it('version shows version string', async () => { termToggle(); await sleep(50); termExec('version'); await sleep(50); expect(termOutput()).toContain('v'); }); it('unknown command shows error', async () => { termToggle(); await sleep(50); termExec('xyznonexistent'); await sleep(50); expect(termOutput()).toContain('unknown command'); }); it('tags lists existing tags', async () => { await makeCard('__term_tag_test__', '#alpha #beta'); await window._reloadAll?.(); await sleep(200); termToggle(); await sleep(50); termExec('tags'); await sleep(50); expect(termOutput()).toContain('#alpha'); expect(termOutput()).toContain('#beta'); }); it('find matches cards by title', async () => { await makeCard('__term_find_me__', 'findable body'); await window._reloadAll?.(); await sleep(200); termToggle(); await sleep(50); termExec('find __term_find_me__'); await sleep(50); expect(termOutput()).toContain('__term_find_me__'); }); it('go switches to bucket', async () => { termToggle(); await sleep(50); termExec('go all'); await sleep(100); expect(String(state.activeBucket)).toBe('all'); }); it('new creates a card', async () => { termToggle(); await sleep(50); termExec('new __term_new_card__'); // termExec fires off an async API call + full reload; poll rather // than sleeping a fixed amount so slow machines don't flake. const start = Date.now(); while (!termOutput().includes('created') && Date.now() - start < 3000) { await sleep(50); } expect(termOutput()).toContain('created'); const cards = await fetch('api/cards').then(r => r.json()); expect(cards.some(c => c.title === '__term_new_card__')).toBe(true); }); it('clear empties output', async () => { termToggle(); await sleep(50); termExec('help'); await sleep(50); expect(termOutput().length).toBeGreaterThan(10); termExec('clear'); await sleep(50); expect(document.getElementById('hterm-output').children.length).toBe(0); }); }); describe('Terminal: Tab Completion', () => { afterEach(async () => { document.getElementById('hterm')?.classList.add('hidden'); document.getElementById('hterm-output').innerHTML = ''; await closeAll(); }); it('Tab completes unique command', async () => { termToggle(); await sleep(50); const input = document.getElementById('hterm-input'); input.value = 'vers'; input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })); await sleep(50); expect(input.value).toBe('version '); }); it('Tab shows multiple matches', async () => { termToggle(); await sleep(50); const input = document.getElementById('hterm-input'); input.value = 'c'; input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })); await sleep(50); const out = termOutput(); expect(out).toContain('clear'); expect(out).toContain('count'); expect(out).toContain('calendar'); expect(out).toContain('color'); }); });