feat(terminal): add optional live timers and spinners to terminal tasks

This commit is contained in:
2026-05-13 20:18:52 +00:00
parent e2eb4eb040
commit 1ae31e36bc
8 changed files with 228 additions and 15 deletions
+70
View File
@@ -2,6 +2,10 @@ import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as smartcli from '../ts/index.js';
const delay = async (millisecondsArg: number) => {
await new Promise((resolve) => setTimeout(resolve, millisecondsArg));
};
class TestWritable implements smartcli.ISmartcliWritable {
public chunks: string[] = [];
public isTTY: boolean;
@@ -109,6 +113,29 @@ tap.test('should collapse failed terminal tasks into permanent output', async ()
expect(terminal.getTasks()).toHaveLength(0);
});
tap.test('should prefix every non-interactive multiline message with the task name', async () => {
const stream = new TestWritable(false);
const terminal = new smartcli.SmartcliTerminal({
stream,
interactive: false,
colors: false,
nonInteractiveThrottleMs: 0,
});
const updateTask = terminal.task('multiline update');
updateTask.update('line one\nline two');
updateTask.complete('done');
const failedTask = terminal.task('multiline failure');
failedTask.fail('first failure line\nsecond failure line');
const output = stream.toString();
expect(output).toInclude('update multiline update: line one');
expect(output).toInclude('update multiline update: line two');
expect(output).toInclude('fail multiline failure in');
expect(output).toInclude('fail multiline failure: second failure line');
});
tap.test('should set task progress', async () => {
const stream = new TestWritable(true);
const terminal = new smartcli.SmartcliTerminal({
@@ -127,6 +154,49 @@ tap.test('should set task progress', async () => {
terminal.clear();
});
tap.test('should render an optional second-precision timer', async () => {
const stream = new TestWritable(true);
const terminal = new smartcli.SmartcliTerminal({
stream,
interactive: true,
colors: false,
symbols: 'ascii',
cleanup: false,
});
const task = terminal.task('timed task', { rows: 2, showTimer: true });
expect(task.getTimerText()).toEqual('0s');
expect(task.renderPlainRows(80)[0]).toInclude('0s');
task.complete('timed complete');
expect(stream.toString()).toInclude('timed complete');
});
tap.test('should render an optional spinner', async () => {
const stream = new TestWritable(true);
const terminal = new smartcli.SmartcliTerminal({
stream,
interactive: true,
colors: false,
symbols: 'ascii',
cleanup: false,
});
const task = terminal.task('spinner task', {
rows: 2,
showSpinner: true,
spinnerFrames: ['a', 'b'],
spinnerIntervalMs: 20,
});
await delay(90);
const output = stream.toString();
expect(output).toInclude('spinner task');
expect(output.includes('a spinner task') || output.includes('b spinner task')).toBeTrue();
expect(task.getLiveRenderIntervalMs()).toEqual(20);
task.complete('spinner complete');
});
tap.test('should auto-complete task.run', async () => {
const stream = new TestWritable(false);
const terminal = new smartcli.SmartcliTerminal({