Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
---

feat: 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": "pnpm@8.6.12",
"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