Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-papayas-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clack/prompts": patch
---

Exposes a new `SpinnerResult` type to describe the return type of `spinner`
21 changes: 17 additions & 4 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { stdin, stdout } from 'node:process';
import type { Key } from 'node:readline';
import * as readline from 'node:readline';
import type { Readable } from 'node:stream';
import type { Readable, Writable } from 'node:stream';
import { ReadStream } from 'node:tty';
import { cursor } from 'sisteransi';
import { isActionKey } from './settings.js';

Expand All @@ -22,20 +23,30 @@ export function setRawMode(input: Readable, value: boolean) {
if (i.isTTY) i.setRawMode(value);
}

export interface BlockOptions {
input?: Readable;
output?: Writable;
overwrite?: boolean;
hideCursor?: boolean;
}

export function block({
input = stdin,
output = stdout,
overwrite = true,
hideCursor = true,
} = {}) {
}: BlockOptions = {}) {
const rl = readline.createInterface({
input,
output,
prompt: '',
tabSize: 1,
});
readline.emitKeypressEvents(input, rl);
if (input.isTTY) input.setRawMode(true);

if (input instanceof ReadStream && input.isTTY) {
input.setRawMode(true);
}

const clear = (data: Buffer, { name, sequence }: Key) => {
const str = String(data);
Expand All @@ -62,7 +73,9 @@ export function block({
if (hideCursor) output.write(cursor.show);

// Prevent Windows specific issues: https://github.com/bombshell-dev/clack/issues/176
if (input.isTTY && !isWindows) input.setRawMode(false);
if (input instanceof ReadStream && input.isTTY && !isWindows) {
input.setRawMode(false);
}

// @ts-expect-error fix for https://github.com/nodejs/node/issues/31762#issuecomment-1441223907
rl.terminal = false;
Expand Down
6 changes: 4 additions & 2 deletions packages/prompts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,16 @@
"packageManager": "[email protected]",
"scripts": {
"build": "unbuild",
"prepack": "pnpm build"
"prepack": "pnpm build",
"test": "vitest run"
},
"dependencies": {
"@clack/core": "workspace:*",
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
},
"devDependencies": {
"is-unicode-supported": "^1.3.0"
"is-unicode-supported": "^1.3.0",
"vitest": "^1.6.0"
}
}
105 changes: 105 additions & 0 deletions packages/prompts/src/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`spinner > message > sets message for next frame 1`] = `
[
"[?25l",
"│
",
"◒ ",
"",
"",
"◐ foo",
]
`;

exports[`spinner > start > renders frames at interval 1`] = `
[
"[?25l",
"│
",
"◒ ",
"",
"",
"◐ ",
"",
"",
"◓ ",
"",
"",
"◑ ",
]
`;

exports[`spinner > start > renders message 1`] = `
[
"[?25l",
"│
",
"◒ foo",
]
`;

exports[`spinner > start > renders timer when indicator is "timer" 1`] = `
[
"[?25l",
"│
",
"◒ [0s]",
]
`;

exports[`spinner > stop > renders cancel symbol if code = 1 1`] = `
[
"[?25l",
"│
",
"◒ ",
"",
"",
"■
",
"[?25h",
]
`;

exports[`spinner > stop > renders error symbol if code > 1 1`] = `
[
"[?25l",
"│
",
"◒ ",
"",
"",
"▲
",
"[?25h",
]
`;

exports[`spinner > stop > renders message 1`] = `
[
"[?25l",
"│
",
"◒ ",
"",
"",
"◇ foo
",
"[?25h",
]
`;

exports[`spinner > stop > renders submit symbol and stops spinner 1`] = `
[
"[?25l",
"│
",
"◒ ",
"",
"",
"◇
",
"[?25h",
]
`;
142 changes: 142 additions & 0 deletions packages/prompts/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Writable } from 'node:stream';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import * as prompts from './index.js';

// TODO (43081j): move this into a util?
class MockWritable extends Writable {
public buffer: string[] = [];

_write(
chunk: any,
_encoding: BufferEncoding,
callback: (error?: Error | null | undefined) => void
): void {
this.buffer.push(chunk.toString());
callback();
}
}

describe('spinner', () => {
let output: MockWritable;

beforeEach(() => {
vi.useFakeTimers();
output = new MockWritable();
});

afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});

test('returns spinner API', () => {
const api = prompts.spinner({ output });

expect(api.stop).toBeTypeOf('function');
expect(api.start).toBeTypeOf('function');
expect(api.message).toBeTypeOf('function');
});

describe('start', () => {
test('renders frames at interval', () => {
const result = prompts.spinner({ output });

result.start();

// there are 4 frames
for (let i = 0; i < 4; i++) {
vi.advanceTimersByTime(80);
}

expect(output.buffer).toMatchSnapshot();
});

test('renders message', () => {
const result = prompts.spinner({ output });

result.start('foo');

vi.advanceTimersByTime(80);

expect(output.buffer).toMatchSnapshot();
});

test('renders timer when indicator is "timer"', () => {
const result = prompts.spinner({ output, indicator: 'timer' });

result.start();

vi.advanceTimersByTime(80);

expect(output.buffer).toMatchSnapshot();
});
});

describe('stop', () => {
test('renders submit symbol and stops spinner', () => {
const result = prompts.spinner({ output });

result.start();

vi.advanceTimersByTime(80);

result.stop();

vi.advanceTimersByTime(80);

expect(output.buffer).toMatchSnapshot();
});

test('renders cancel symbol if code = 1', () => {
const result = prompts.spinner({ output });

result.start();

vi.advanceTimersByTime(80);

result.stop('', 1);

expect(output.buffer).toMatchSnapshot();
});

test('renders error symbol if code > 1', () => {
const result = prompts.spinner({ output });

result.start();

vi.advanceTimersByTime(80);

result.stop('', 2);

expect(output.buffer).toMatchSnapshot();
});

test('renders message', () => {
const result = prompts.spinner({ output });

result.start();

vi.advanceTimersByTime(80);

result.stop('foo');

expect(output.buffer).toMatchSnapshot();
});
});

describe('message', () => {
test('sets message for next frame', () => {
const result = prompts.spinner({ output });

result.start();

vi.advanceTimersByTime(80);

result.message('foo');

vi.advanceTimersByTime(80);

expect(output.buffer).toMatchSnapshot();
});
});
});
Loading