This commit is contained in:
seb
2026-09-25 06:26:59 +02:00
parent b7ef321e05
commit 2efd9bda39
7 changed files with 383 additions and 19 deletions

115
test/ui.test.js Normal file
View File

@@ -0,0 +1,115 @@
'use strict';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createElement as h } from 'react';
import { PassThrough } from 'node:stream';
import { render, renderToString } from 'ink';
import { App, getHistoryWindow } from '../src/ui/app.js';
import { createStore } from '../src/ui/store.js';
const stripAnsi = (value) => value.replace(/\x1b\[[0-9;]*m/g, '');
const historyLines = (output) => stripAnsi(output)
.split('\n')
.filter((line) => /^F\d+$/.test(line));
test('history stays bottom-anchored and scrolls through a long transcript', () => {
const store = createStore();
store.setMeta({ model: 'test-model', workspace: '/test' });
store.prompt({ type: 'main', message: 'You' });
for (let i = 1; i <= 1000; i++) store.log(`F${i}`);
const live = renderToString(
h(App, { store, sizeOverride: { columns: 80, rows: 24 } }),
{ columns: 80 },
);
const liveLines = historyLines(live);
assert.equal(liveLines.at(-1), 'F1000', 'live view ends at the current tail');
assert.equal(liveLines.length, 14, 'window is sized to the available pane');
store.scrollBy(10);
const scrolled = renderToString(
h(App, { store, sizeOverride: { columns: 80, rows: 24 } }),
{ columns: 80 },
);
const scrolledLines = historyLines(scrolled);
assert.equal(scrolledLines.at(-1), 'F990', 'scrolling moves the window toward older lines');
assert.ok(!scrolledLines.includes('F1000'), 'scrolled view hides the live tail');
});
test('App translates SGR wheel reports to history scroll', async () => {
const stdin = new PassThrough();
stdin.isTTY = true;
stdin.setRawMode = () => {};
stdin.setEncoding = () => {};
stdin.ref = () => stdin;
stdin.unref = () => stdin;
const stdout = new PassThrough();
stdout.isTTY = true;
stdout.columns = 80;
stdout.rows = 24;
const store = createStore();
store.prompt({ type: 'main', message: 'You' });
for (let i = 1; i <= 30; i++) store.log(`wheel line ${i}`);
const instance = render(h(App, { store }), {
stdin,
stdout,
alternateScreen: false,
exitOnCtrlC: false,
patchConsole: false,
});
try {
await new Promise((resolve) => setTimeout(resolve, 50));
stdin.push('\x1b[<64;88;20M');
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(store.getState().scroll, 3);
} finally {
instance.unmount();
await instance.waitUntilExit();
}
});
test('history window helper bounds the mounted line count', () => {
const entries = Array.from({ length: 200 }, (_, i) => ({
id: i,
text: `line ${i}`,
}));
const window = getHistoryWindow(entries, 0, 10);
assert.equal(window.lines.length, 10);
assert.equal(window.lines.at(-1).text, 'line 199');
assert.equal(window.start, 190);
assert.equal(window.end, 200);
});
test('runExternally returns the callback result through Ink suspension', async () => {
const store = createStore();
store._suspend = async (callback) => {
await callback();
};
let mouseTrackingRestores = 0;
store._setMouseTracking = (on) => {
if (on) mouseTrackingRestores++;
};
assert.equal(await store.runExternally(() => 'saved editor text'), 'saved editor text');
assert.equal(mouseTrackingRestores, 1, 'mouse tracking is restored after suspension');
});
test('finalizing reasoning or submitting preserves the history offset', async () => {
const store = createStore();
for (let i = 1; i <= 10; i++) store.log(`line ${i}`);
store.scrollBy(3);
store.setReasoningDraft('checking the files');
store.finalizeReasoning();
assert.equal(store.getState().scroll, 3, 'finalizing does not jump to the tail');
const prompt = store.prompt({ type: 'main', message: 'You' });
store.resolve('next question', '› You next question');
assert.equal(store.getState().scroll, 3, 'submitting does not jump to the tail');
await prompt;
});