terminal plugin, card edit history, bump script
Terminal: in-app command line with 22 commands (Shift+T toggle).
Navigation, CRUD, search, view toggles, tab completion, history.
Card history: card_history table tracks field changes on every PATCH.
GET /api/cards/{id}/history endpoint. Terminal `history` command.
bump.sh: standalone version bump for config.php, electron/package.json,
website footer. dist.sh no longer bumps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b9bd294c90
commit
2169c1de1b
11 files changed
+707
-1
No files matched your search
@@ -0,0 +1,192 @@
|
||||
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__');
|
||||
await sleep(400);
|
||||
const out = termOutput();
|
||||
expect(out).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');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user