[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

test: add tests for `text` prompt (#266)

authored by

James Garbutt and committed by
GitHub
(Apr 4, 2025, 2:45 PM +0100) e6ff090d 2ad8000e

+389 -23
+248 -16
packages/prompts/src/__snapshots__/index.test.ts.snap
··· 1 1 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 2 3 - exports[`isCI = false > spinner > message > sets message for next frame 1`] = ` 3 + exports[`prompts (isCI = false) > spinner > message > sets message for next frame 1`] = ` 4 4 [ 5 5 "[?25l", 6 6 "│ ··· 12 12 ] 13 13 `; 14 14 15 - exports[`isCI = false > spinner > start > renders frames at interval 1`] = ` 15 + exports[`prompts (isCI = false) > spinner > start > renders frames at interval 1`] = ` 16 16 [ 17 17 "[?25l", 18 18 "│ ··· 30 30 ] 31 31 `; 32 32 33 - exports[`isCI = false > spinner > start > renders message 1`] = ` 33 + exports[`prompts (isCI = false) > spinner > start > renders message 1`] = ` 34 34 [ 35 35 "[?25l", 36 36 "│ ··· 39 39 ] 40 40 `; 41 41 42 - exports[`isCI = false > spinner > start > renders timer when indicator is "timer" 1`] = ` 42 + exports[`prompts (isCI = false) > spinner > start > renders timer when indicator is "timer" 1`] = ` 43 43 [ 44 44 "[?25l", 45 45 "│ ··· 48 48 ] 49 49 `; 50 50 51 - exports[`isCI = false > spinner > stop > renders cancel symbol if code = 1 1`] = ` 51 + exports[`prompts (isCI = false) > spinner > stop > renders cancel symbol if code = 1 1`] = ` 52 52 [ 53 53 "[?25l", 54 54 "│ ··· 62 62 ] 63 63 `; 64 64 65 - exports[`isCI = false > spinner > stop > renders error symbol if code > 1 1`] = ` 65 + exports[`prompts (isCI = false) > spinner > stop > renders error symbol if code > 1 1`] = ` 66 66 [ 67 67 "[?25l", 68 68 "│ ··· 76 76 ] 77 77 `; 78 78 79 - exports[`isCI = false > spinner > stop > renders message 1`] = ` 79 + exports[`prompts (isCI = false) > spinner > stop > renders message 1`] = ` 80 80 [ 81 81 "[?25l", 82 82 "│ ··· 90 90 ] 91 91 `; 92 92 93 - exports[`isCI = false > spinner > stop > renders submit symbol and stops spinner 1`] = ` 93 + exports[`prompts (isCI = false) > spinner > stop > renders submit symbol and stops spinner 1`] = ` 94 94 [ 95 95 "[?25l", 96 96 "│ ··· 104 104 ] 105 105 `; 106 106 107 - exports[`isCI = true > spinner > message > sets message for next frame 1`] = ` 107 + exports[`prompts (isCI = false) > text > can cancel 1`] = ` 108 + [ 109 + "[?25l", 110 + "│ 111 + ◆ foo 112 + │ _ 113 + └ 114 + ", 115 + "", 116 + "", 117 + "", 118 + "■ foo 119 + │", 120 + " 121 + ", 122 + "[?25h", 123 + ] 124 + `; 125 + 126 + exports[`prompts (isCI = false) > text > renders cancelled value if one set 1`] = ` 127 + [ 128 + "[?25l", 129 + "│ 130 + ◆ foo 131 + │ _ 132 + └ 133 + ", 134 + "", 135 + "", 136 + "", 137 + "│ x█", 138 + "", 139 + "", 140 + "", 141 + "", 142 + "│ xy█", 143 + "", 144 + "", 145 + "", 146 + "", 147 + "■ foo 148 + │ xy 149 + │", 150 + " 151 + ", 152 + "[?25h", 153 + ] 154 + `; 155 + 156 + exports[`prompts (isCI = false) > text > renders message 1`] = ` 157 + [ 158 + "[?25l", 159 + "│ 160 + ◆ foo 161 + │ _ 162 + └ 163 + ", 164 + "", 165 + "", 166 + "", 167 + "◇ foo 168 + │ undefined", 169 + " 170 + ", 171 + "[?25h", 172 + ] 173 + `; 174 + 175 + exports[`prompts (isCI = false) > text > renders placeholder if set 1`] = ` 176 + [ 177 + "[?25l", 178 + "│ 179 + ◆ foo 180 + │ bar 181 + └ 182 + ", 183 + "", 184 + "", 185 + "", 186 + "◇ foo 187 + │ bar", 188 + " 189 + ", 190 + "[?25h", 191 + ] 192 + `; 193 + 194 + exports[`prompts (isCI = false) > text > renders submitted value 1`] = ` 195 + [ 196 + "[?25l", 197 + "│ 198 + ◆ foo 199 + │ _ 200 + └ 201 + ", 202 + "", 203 + "", 204 + "", 205 + "│ x█", 206 + "", 207 + "", 208 + "", 209 + "", 210 + "│ xy█", 211 + "", 212 + "", 213 + "", 214 + "", 215 + "◇ foo 216 + │ xy", 217 + " 218 + ", 219 + "[?25h", 220 + ] 221 + `; 222 + 223 + exports[`prompts (isCI = true) > spinner > message > sets message for next frame 1`] = ` 108 224 [ 109 225 "[?25l", 110 226 "│ ··· 118 234 ] 119 235 `; 120 236 121 - exports[`isCI = true > spinner > start > renders frames at interval 1`] = ` 237 + exports[`prompts (isCI = true) > spinner > start > renders frames at interval 1`] = ` 122 238 [ 123 239 "[?25l", 124 240 "│ ··· 127 243 ] 128 244 `; 129 245 130 - exports[`isCI = true > spinner > start > renders message 1`] = ` 246 + exports[`prompts (isCI = true) > spinner > start > renders message 1`] = ` 131 247 [ 132 248 "[?25l", 133 249 "│ ··· 136 252 ] 137 253 `; 138 254 139 - exports[`isCI = true > spinner > start > renders timer when indicator is "timer" 1`] = ` 255 + exports[`prompts (isCI = true) > spinner > start > renders timer when indicator is "timer" 1`] = ` 140 256 [ 141 257 "[?25l", 142 258 "│ ··· 145 261 ] 146 262 `; 147 263 148 - exports[`isCI = true > spinner > stop > renders cancel symbol if code = 1 1`] = ` 264 + exports[`prompts (isCI = true) > spinner > stop > renders cancel symbol if code = 1 1`] = ` 149 265 [ 150 266 "[?25l", 151 267 "│ ··· 161 277 ] 162 278 `; 163 279 164 - exports[`isCI = true > spinner > stop > renders error symbol if code > 1 1`] = ` 280 + exports[`prompts (isCI = true) > spinner > stop > renders error symbol if code > 1 1`] = ` 165 281 [ 166 282 "[?25l", 167 283 "│ ··· 177 293 ] 178 294 `; 179 295 180 - exports[`isCI = true > spinner > stop > renders message 1`] = ` 296 + exports[`prompts (isCI = true) > spinner > stop > renders message 1`] = ` 181 297 [ 182 298 "[?25l", 183 299 "│ ··· 193 309 ] 194 310 `; 195 311 196 - exports[`isCI = true > spinner > stop > renders submit symbol and stops spinner 1`] = ` 312 + exports[`prompts (isCI = true) > spinner > stop > renders submit symbol and stops spinner 1`] = ` 197 313 [ 198 314 "[?25l", 199 315 "│ ··· 208 324 "[?25h", 209 325 ] 210 326 `; 327 + 328 + exports[`prompts (isCI = true) > text > can cancel 1`] = ` 329 + [ 330 + "[?25l", 331 + "│ 332 + ◆ foo 333 + │ _ 334 + └ 335 + ", 336 + "", 337 + "", 338 + "", 339 + "■ foo 340 + │", 341 + " 342 + ", 343 + "[?25h", 344 + ] 345 + `; 346 + 347 + exports[`prompts (isCI = true) > text > renders cancelled value if one set 1`] = ` 348 + [ 349 + "[?25l", 350 + "│ 351 + ◆ foo 352 + │ _ 353 + └ 354 + ", 355 + "", 356 + "", 357 + "", 358 + "│ x█", 359 + "", 360 + "", 361 + "", 362 + "", 363 + "│ xy█", 364 + "", 365 + "", 366 + "", 367 + "", 368 + "■ foo 369 + │ xy 370 + │", 371 + " 372 + ", 373 + "[?25h", 374 + ] 375 + `; 376 + 377 + exports[`prompts (isCI = true) > text > renders message 1`] = ` 378 + [ 379 + "[?25l", 380 + "│ 381 + ◆ foo 382 + │ _ 383 + └ 384 + ", 385 + "", 386 + "", 387 + "", 388 + "◇ foo 389 + │ undefined", 390 + " 391 + ", 392 + "[?25h", 393 + ] 394 + `; 395 + 396 + exports[`prompts (isCI = true) > text > renders placeholder if set 1`] = ` 397 + [ 398 + "[?25l", 399 + "│ 400 + ◆ foo 401 + │ bar 402 + └ 403 + ", 404 + "", 405 + "", 406 + "", 407 + "◇ foo 408 + │ bar", 409 + " 410 + ", 411 + "[?25h", 412 + ] 413 + `; 414 + 415 + exports[`prompts (isCI = true) > text > renders submitted value 1`] = ` 416 + [ 417 + "[?25l", 418 + "│ 419 + ◆ foo 420 + │ _ 421 + └ 422 + ", 423 + "", 424 + "", 425 + "", 426 + "│ x█", 427 + "", 428 + "", 429 + "", 430 + "", 431 + "│ xy█", 432 + "", 433 + "", 434 + "", 435 + "", 436 + "◇ foo 437 + │ xy", 438 + " 439 + ", 440 + "[?25h", 441 + ] 442 + `;
+136 -6
packages/prompts/src/index.test.ts
··· 1 - import { Writable } from 'node:stream'; 1 + import { Readable, Writable } from 'node:stream'; 2 2 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 3 3 import * as prompts from './index.js'; 4 4 ··· 16 16 } 17 17 } 18 18 19 - describe.each(['true', 'false'])('isCI = %s', (isCI) => { 19 + class MockReadable extends Readable { 20 + protected _buffer: unknown[] | null = []; 21 + 22 + _read() { 23 + if (this._buffer === null) { 24 + this.push(null); 25 + return; 26 + } 27 + 28 + for (const val of this._buffer) { 29 + this.push(val); 30 + } 31 + 32 + this._buffer = []; 33 + } 34 + 35 + pushValue(val: unknown): void { 36 + this._buffer?.push(val); 37 + } 38 + 39 + close(): void { 40 + this._buffer = null; 41 + } 42 + } 43 + 44 + describe.each(['true', 'false'])('prompts (isCI = %s)', (isCI) => { 20 45 let originalCI: string | undefined; 46 + let output: MockWritable; 47 + let input: MockReadable; 21 48 22 49 beforeAll(() => { 23 50 originalCI = process.env.CI; ··· 28 55 process.env.CI = originalCI; 29 56 }); 30 57 31 - describe('spinner', () => { 32 - let output: MockWritable; 58 + beforeEach(() => { 59 + output = new MockWritable(); 60 + input = new MockReadable(); 61 + }); 33 62 63 + afterEach(() => { 64 + vi.restoreAllMocks(); 65 + }); 66 + 67 + describe('spinner', () => { 34 68 beforeEach(() => { 35 69 vi.useFakeTimers(); 36 - output = new MockWritable(); 37 70 }); 38 71 39 72 afterEach(() => { 40 - vi.restoreAllMocks(); 41 73 vi.useRealTimers(); 42 74 }); 43 75 ··· 150 182 151 183 expect(output.buffer).toMatchSnapshot(); 152 184 }); 185 + }); 186 + }); 187 + 188 + describe('text', () => { 189 + test('renders message', async () => { 190 + const result = prompts.text({ 191 + message: 'foo', 192 + input, 193 + output, 194 + }); 195 + 196 + input.emit('keypress', '', { name: 'return' }); 197 + 198 + await result; 199 + 200 + expect(output.buffer).toMatchSnapshot(); 201 + }); 202 + 203 + test('renders placeholder if set', async () => { 204 + const result = prompts.text({ 205 + message: 'foo', 206 + placeholder: 'bar', 207 + input, 208 + output, 209 + }); 210 + 211 + input.emit('keypress', '', { name: 'return' }); 212 + 213 + await result; 214 + 215 + expect(output.buffer).toMatchSnapshot(); 216 + // TODO (43081j): uncomment this when #263 is fixed 217 + // expect(value).toBe('bar'); 218 + }); 219 + 220 + test('<tab> applies placeholder', async () => { 221 + const result = prompts.text({ 222 + message: 'foo', 223 + placeholder: 'bar', 224 + input, 225 + output, 226 + }); 227 + 228 + input.emit('keypress', '\t', { name: 'tab' }); 229 + input.emit('keypress', '', { name: 'return' }); 230 + 231 + const value = await result; 232 + 233 + expect(value).toBe('bar'); 234 + }); 235 + 236 + test('can cancel', async () => { 237 + const result = prompts.text({ 238 + message: 'foo', 239 + input, 240 + output, 241 + }); 242 + 243 + input.emit('keypress', 'escape', { name: 'escape' }); 244 + 245 + const value = await result; 246 + 247 + expect(prompts.isCancel(value)).toBe(true); 248 + expect(output.buffer).toMatchSnapshot(); 249 + }); 250 + 251 + test('renders cancelled value if one set', async () => { 252 + const result = prompts.text({ 253 + message: 'foo', 254 + input, 255 + output, 256 + }); 257 + 258 + input.emit('keypress', 'x', { name: 'x' }); 259 + input.emit('keypress', 'y', { name: 'y' }); 260 + input.emit('keypress', '', { name: 'escape' }); 261 + 262 + const value = await result; 263 + 264 + expect(prompts.isCancel(value)).toBe(true); 265 + expect(output.buffer).toMatchSnapshot(); 266 + }); 267 + 268 + test('renders submitted value', async () => { 269 + const result = prompts.text({ 270 + message: 'foo', 271 + input, 272 + output, 273 + }); 274 + 275 + input.emit('keypress', 'x', { name: 'x' }); 276 + input.emit('keypress', 'y', { name: 'y' }); 277 + input.emit('keypress', '', { name: 'return' }); 278 + 279 + const value = await result; 280 + 281 + expect(value).toBe('xy'); 282 + expect(output.buffer).toMatchSnapshot(); 153 283 }); 154 284 }); 155 285 });
+5 -1
packages/prompts/src/index.ts
··· 1 - import type { Writable } from 'node:stream'; 1 + import type { Readable, Writable } from 'node:stream'; 2 2 import { stripVTControlCharacters as strip } from 'node:util'; 3 3 import { 4 4 ConfirmPrompt, ··· 104 104 defaultValue?: string; 105 105 initialValue?: string; 106 106 validate?: (value: string) => string | Error | undefined; 107 + input?: Readable; 108 + output?: Writable; 107 109 } 108 110 export const text = (opts: TextOptions) => { 109 111 return new TextPrompt({ ··· 111 113 placeholder: opts.placeholder, 112 114 defaultValue: opts.defaultValue, 113 115 initialValue: opts.initialValue, 116 + output: opts.output, 117 + input: opts.input, 114 118 render() { 115 119 const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 116 120 const placeholder = opts.placeholder