Skip to content

Commit 17342d2

Browse files
43081jdreyfus92
andauthored
fix: export spinner type and add tests (#265)
Co-authored-by: Paul Valladares <[email protected]>
1 parent 041e13c commit 17342d2

File tree

7 files changed

+298
-18
lines changed

7 files changed

+298
-18
lines changed

.changeset/dirty-papayas-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clack/prompts": patch
3+
---
4+
5+
Exposes a new `SpinnerResult` type to describe the return type of `spinner`

packages/core/src/utils/index.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { stdin, stdout } from 'node:process';
22
import type { Key } from 'node:readline';
33
import * as readline from 'node:readline';
4-
import type { Readable } from 'node:stream';
4+
import type { Readable, Writable } from 'node:stream';
5+
import { ReadStream } from 'node:tty';
56
import { cursor } from 'sisteransi';
67
import { isActionKey } from './settings.js';
78

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

26+
export interface BlockOptions {
27+
input?: Readable;
28+
output?: Writable;
29+
overwrite?: boolean;
30+
hideCursor?: boolean;
31+
}
32+
2533
export function block({
2634
input = stdin,
2735
output = stdout,
2836
overwrite = true,
2937
hideCursor = true,
30-
} = {}) {
38+
}: BlockOptions = {}) {
3139
const rl = readline.createInterface({
3240
input,
3341
output,
3442
prompt: '',
3543
tabSize: 1,
3644
});
3745
readline.emitKeypressEvents(input, rl);
38-
if (input.isTTY) input.setRawMode(true);
46+
47+
if (input instanceof ReadStream && input.isTTY) {
48+
input.setRawMode(true);
49+
}
3950

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

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

6780
// @ts-expect-error fix for https://github.com/nodejs/node/issues/31762#issuecomment-1441223907
6881
rl.terminal = false;

packages/prompts/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,16 @@
4646
"packageManager": "[email protected]",
4747
"scripts": {
4848
"build": "unbuild",
49-
"prepack": "pnpm build"
49+
"prepack": "pnpm build",
50+
"test": "vitest run"
5051
},
5152
"dependencies": {
5253
"@clack/core": "workspace:*",
5354
"picocolors": "^1.0.0",
5455
"sisteransi": "^1.0.5"
5556
},
5657
"devDependencies": {
57-
"is-unicode-supported": "^1.3.0"
58+
"is-unicode-supported": "^1.3.0",
59+
"vitest": "^1.6.0"
5860
}
5961
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`spinner > message > sets message for next frame 1`] = `
4+
[
5+
"[?25l",
6+
"│
7+
",
8+
"◒ ",
9+
"",
10+
"",
11+
"◐ foo",
12+
]
13+
`;
14+
15+
exports[`spinner > start > renders frames at interval 1`] = `
16+
[
17+
"[?25l",
18+
"│
19+
",
20+
"◒ ",
21+
"",
22+
"",
23+
"◐ ",
24+
"",
25+
"",
26+
"◓ ",
27+
"",
28+
"",
29+
"◑ ",
30+
]
31+
`;
32+
33+
exports[`spinner > start > renders message 1`] = `
34+
[
35+
"[?25l",
36+
"│
37+
",
38+
"◒ foo",
39+
]
40+
`;
41+
42+
exports[`spinner > start > renders timer when indicator is "timer" 1`] = `
43+
[
44+
"[?25l",
45+
"│
46+
",
47+
"◒ [0s]",
48+
]
49+
`;
50+
51+
exports[`spinner > stop > renders cancel symbol if code = 1 1`] = `
52+
[
53+
"[?25l",
54+
"│
55+
",
56+
"◒ ",
57+
"",
58+
"",
59+
"■
60+
",
61+
"[?25h",
62+
]
63+
`;
64+
65+
exports[`spinner > stop > renders error symbol if code > 1 1`] = `
66+
[
67+
"[?25l",
68+
"│
69+
",
70+
"◒ ",
71+
"",
72+
"",
73+
"▲
74+
",
75+
"[?25h",
76+
]
77+
`;
78+
79+
exports[`spinner > stop > renders message 1`] = `
80+
[
81+
"[?25l",
82+
"│
83+
",
84+
"◒ ",
85+
"",
86+
"",
87+
"◇ foo
88+
",
89+
"[?25h",
90+
]
91+
`;
92+
93+
exports[`spinner > stop > renders submit symbol and stops spinner 1`] = `
94+
[
95+
"[?25l",
96+
"│
97+
",
98+
"◒ ",
99+
"",
100+
"",
101+
"◇
102+
",
103+
"[?25h",
104+
]
105+
`;

packages/prompts/src/index.test.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { Writable } from 'node:stream';
2+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
3+
import * as prompts from './index.js';
4+
5+
// TODO (43081j): move this into a util?
6+
class MockWritable extends Writable {
7+
public buffer: string[] = [];
8+
9+
_write(
10+
chunk: any,
11+
_encoding: BufferEncoding,
12+
callback: (error?: Error | null | undefined) => void
13+
): void {
14+
this.buffer.push(chunk.toString());
15+
callback();
16+
}
17+
}
18+
19+
describe('spinner', () => {
20+
let output: MockWritable;
21+
22+
beforeEach(() => {
23+
vi.useFakeTimers();
24+
output = new MockWritable();
25+
});
26+
27+
afterEach(() => {
28+
vi.restoreAllMocks();
29+
vi.useRealTimers();
30+
});
31+
32+
test('returns spinner API', () => {
33+
const api = prompts.spinner({ output });
34+
35+
expect(api.stop).toBeTypeOf('function');
36+
expect(api.start).toBeTypeOf('function');
37+
expect(api.message).toBeTypeOf('function');
38+
});
39+
40+
describe('start', () => {
41+
test('renders frames at interval', () => {
42+
const result = prompts.spinner({ output });
43+
44+
result.start();
45+
46+
// there are 4 frames
47+
for (let i = 0; i < 4; i++) {
48+
vi.advanceTimersByTime(80);
49+
}
50+
51+
expect(output.buffer).toMatchSnapshot();
52+
});
53+
54+
test('renders message', () => {
55+
const result = prompts.spinner({ output });
56+
57+
result.start('foo');
58+
59+
vi.advanceTimersByTime(80);
60+
61+
expect(output.buffer).toMatchSnapshot();
62+
});
63+
64+
test('renders timer when indicator is "timer"', () => {
65+
const result = prompts.spinner({ output, indicator: 'timer' });
66+
67+
result.start();
68+
69+
vi.advanceTimersByTime(80);
70+
71+
expect(output.buffer).toMatchSnapshot();
72+
});
73+
});
74+
75+
describe('stop', () => {
76+
test('renders submit symbol and stops spinner', () => {
77+
const result = prompts.spinner({ output });
78+
79+
result.start();
80+
81+
vi.advanceTimersByTime(80);
82+
83+
result.stop();
84+
85+
vi.advanceTimersByTime(80);
86+
87+
expect(output.buffer).toMatchSnapshot();
88+
});
89+
90+
test('renders cancel symbol if code = 1', () => {
91+
const result = prompts.spinner({ output });
92+
93+
result.start();
94+
95+
vi.advanceTimersByTime(80);
96+
97+
result.stop('', 1);
98+
99+
expect(output.buffer).toMatchSnapshot();
100+
});
101+
102+
test('renders error symbol if code > 1', () => {
103+
const result = prompts.spinner({ output });
104+
105+
result.start();
106+
107+
vi.advanceTimersByTime(80);
108+
109+
result.stop('', 2);
110+
111+
expect(output.buffer).toMatchSnapshot();
112+
});
113+
114+
test('renders message', () => {
115+
const result = prompts.spinner({ output });
116+
117+
result.start();
118+
119+
vi.advanceTimersByTime(80);
120+
121+
result.stop('foo');
122+
123+
expect(output.buffer).toMatchSnapshot();
124+
});
125+
});
126+
127+
describe('message', () => {
128+
test('sets message for next frame', () => {
129+
const result = prompts.spinner({ output });
130+
131+
result.start();
132+
133+
vi.advanceTimersByTime(80);
134+
135+
result.message('foo');
136+
137+
vi.advanceTimersByTime(80);
138+
139+
expect(output.buffer).toMatchSnapshot();
140+
});
141+
});
142+
});

0 commit comments

Comments
 (0)