116 lines
4.0 KiB
JavaScript
116 lines
4.0 KiB
JavaScript
'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;
|
||
});
|