[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.

Merge branch 'main' into wip-component

James Garbutt (Nov 18, 2025, 10:57 AM UTC) 30394801 d6ececf3

+1048 -60
+5
.changeset/busy-baths-work.md
··· 1 + --- 2 + "@clack/prompts": patch 3 + --- 4 + 5 + Fixes rendering of multi-line messages and options in select prompt.
+5
.changeset/fine-swans-retire.md
··· 1 + --- 2 + "@clack/prompts": patch 3 + --- 4 + 5 + Add support for wrapped messages in multi line prompts
+5
.changeset/little-ghosts-retire.md
··· 1 + --- 2 + "@clack/core": patch 3 + --- 4 + 5 + Support short terminal windows when re-rendering by accounting for off-screen lines
+27
.changeset/odd-bikes-nail.md
··· 1 + --- 2 + "@clack/prompts": minor 3 + --- 4 + 5 + Updates the API for stopping spinners and progress bars to be clearer 6 + 7 + Previously, both the spinner and progress bar components used a single `stop` method that accepted a code to indicate success, cancellation, or error. This update separates these into distinct methods: `stop()`, `cancel()`, and `error()`: 8 + 9 + ```diff 10 + const spinner = prompts.spinner(); 11 + spinner.start(); 12 + 13 + // Cancelling a spinner 14 + - spinner.stop(undefined, 1); 15 + + spinner.cancel(); 16 + 17 + // Stopping with an error 18 + - spinner.stop(undefined, 2); 19 + + spinner.error(); 20 + ``` 21 + 22 + As before, you can pass a message to each method to customize the output displayed: 23 + 24 + ```js 25 + spinner.cancel("Operation cancelled by user"); 26 + progressBar.error("An error occurred during processing"); 27 + ```
+5
.changeset/plenty-snakes-ring.md
··· 1 + --- 2 + "@clack/prompts": patch 3 + --- 4 + 5 + Fixes wrapping of cancelled and success messages of select prompt
+5 -6
packages/core/src/component.ts
··· 30 30 const fromLineCount = fromLines.length; 31 31 const toLineCount = toLines.length; 32 32 33 - output.write(cursor.hide); 34 - output.write(cursor.left); 35 - 36 33 if (fromLineCount > 0) { 34 + output.write(cursor.left); 37 35 output.write(cursor.up(fromLineCount)); 38 36 } 39 37 ··· 44 42 if (fromLine === toLine) { 45 43 output.write(cursor.down(1)); 46 44 } else { 47 - output.write(erase.line); 45 + if (fromLine !== undefined) { 46 + output.write(erase.line); 47 + } 48 48 if (toLine !== undefined) { 49 49 output.write(`${toLine}\n`); 50 50 } else { ··· 56 56 if (fromLineCount > toLineCount) { 57 57 output.write(erase.down()); 58 58 } 59 - 60 - output.write(cursor.show); 61 59 } 62 60 63 61 function renderToOutput( ··· 158 156 159 157 onMount(): void { 160 158 super.onMount(); 159 + this.#output.write(cursor.hide); 161 160 this.#output.on('resize', this.#onResize); 162 161 } 163 162
+1 -1
packages/core/src/index.ts
··· 9 9 export { default as SelectKeyPrompt } from './prompts/select-key.js'; 10 10 export { default as TextPrompt } from './prompts/text.js'; 11 11 export type { ClackState as State } from './types.js'; 12 - export { block, getColumns, getRows, isCancel } from './utils/index.js'; 12 + export { block, getColumns, getRows, isCancel, wrapTextWithPrefix } from './utils/index.js'; 13 13 export type { ClackSettings } from './utils/settings.js'; 14 14 export { settings, updateSettings } from './utils/settings.js';
+11 -3
packages/core/src/prompts/prompt.ts
··· 4 4 import { Component, RenderHost } from '../component.js'; 5 5 import type { ClackEvents, ClackState } from '../types.js'; 6 6 import type { Action } from '../utils/index.js'; 7 - import { CANCEL_SYMBOL, isActionKey, setRawMode, settings } from '../utils/index.js'; 7 + import { 8 + CANCEL_SYMBOL, 9 + diffLines, 10 + getRows, 11 + isActionKey, 12 + setRawMode, 13 + settings, 14 + } from '../utils/index.js'; 8 15 9 16 export interface PromptOptions<TValue, Self extends Prompt<TValue>> { 10 17 render(this: Omit<Self, 'prompt'>): string | undefined; ··· 252 259 this.rl = undefined; 253 260 this.emit(`${this.state}`, this.value); 254 261 this.unsubscribe(); 262 + this.#renderHost.onUnmount(); 255 263 } 256 264 257 265 requestUpdate() { 258 - super.requestUpdate(); 259 - 260 266 if (this.state === 'initial') { 261 267 this.state = 'active'; 262 268 } 269 + 270 + super.requestUpdate(); 263 271 } 264 272 265 273 render() {
+21
packages/core/src/utils/index.ts
··· 3 3 import * as readline from 'node:readline'; 4 4 import type { Readable, Writable } from 'node:stream'; 5 5 import { ReadStream } from 'node:tty'; 6 + import { wrapAnsi } from 'fast-wrap-ansi'; 6 7 import { cursor } from 'sisteransi'; 7 8 import { isActionKey } from './settings.js'; 8 9 ··· 96 97 } 97 98 return 20; 98 99 }; 100 + 101 + export function wrapTextWithPrefix( 102 + output: Writable | undefined, 103 + text: string, 104 + prefix: string, 105 + startPrefix: string = prefix 106 + ): string { 107 + const columns = getColumns(output ?? stdout); 108 + const wrapped = wrapAnsi(text, columns - prefix.length, { 109 + hard: true, 110 + trim: false, 111 + }); 112 + const lines = wrapped 113 + .split('\n') 114 + .map((line, index) => { 115 + return `${index === 0 ? startPrefix : prefix}${line}`; 116 + }) 117 + .join('\n'); 118 + return lines; 119 + }
+8 -2
packages/core/src/utils/string.ts
··· 3 3 4 4 const aLines = a.split('\n'); 5 5 const bLines = b.split('\n'); 6 + const numLines = Math.max(aLines.length, bLines.length); 6 7 const diff: number[] = []; 7 8 8 - for (let i = 0; i < Math.max(aLines.length, bLines.length); i++) { 9 + for (let i = 0; i < numLines; i++) { 9 10 if (aLines[i] !== bLines[i]) diff.push(i); 10 11 } 11 12 12 - return diff; 13 + return { 14 + lines: diff, 15 + numLinesBefore: aLines.length, 16 + numLinesAfter: bLines.length, 17 + numLines, 18 + }; 13 19 }
+14
packages/prompts/src/common.ts
··· 53 53 } 54 54 }; 55 55 56 + export const symbolBar = (state: State) => { 57 + switch (state) { 58 + case 'initial': 59 + case 'active': 60 + return color.cyan(S_BAR); 61 + case 'cancel': 62 + return color.red(S_BAR); 63 + case 'error': 64 + return color.yellow(S_BAR); 65 + case 'submit': 66 + return color.green(S_BAR); 67 + } 68 + }; 69 + 56 70 export interface CommonOptions { 57 71 input?: Readable; 58 72 output?: Writable;
+34 -13
packages/prompts/src/multi-select.ts
··· 1 - import { MultiSelectPrompt } from '@clack/core'; 1 + import { MultiSelectPrompt, wrapTextWithPrefix } from '@clack/core'; 2 2 import color from 'picocolors'; 3 3 import { 4 4 type CommonOptions, ··· 8 8 S_CHECKBOX_INACTIVE, 9 9 S_CHECKBOX_SELECTED, 10 10 symbol, 11 + symbolBar, 11 12 } from './common.js'; 12 13 import { limitOptions } from './limit-options.js'; 13 14 import type { Option } from './select.js'; ··· 20 21 required?: boolean; 21 22 cursorAt?: Value; 22 23 } 24 + const computeLabel = (label: string, format: (text: string) => string) => { 25 + return label 26 + .split('\n') 27 + .map((line) => format(line)) 28 + .join('\n'); 29 + }; 30 + 23 31 export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => { 24 32 const opt = ( 25 33 option: Option<Value>, ··· 34 42 ) => { 35 43 const label = option.label ?? String(option.value); 36 44 if (state === 'disabled') { 37 - return `${color.gray(S_CHECKBOX_INACTIVE)} ${color.gray(label)}${ 45 + return `${color.gray(S_CHECKBOX_INACTIVE)} ${computeLabel(label, color.gray)}${ 38 46 option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : '' 39 47 }`; 40 48 } ··· 44 52 }`; 45 53 } 46 54 if (state === 'selected') { 47 - return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}${ 55 + return `${color.green(S_CHECKBOX_SELECTED)} ${computeLabel(label, color.dim)}${ 48 56 option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 49 57 }`; 50 58 } 51 59 if (state === 'cancelled') { 52 - return `${color.strikethrough(color.dim(label))}`; 60 + return `${computeLabel(label, (text) => color.strikethrough(color.dim(text)))}`; 53 61 } 54 62 if (state === 'active-selected') { 55 63 return `${color.green(S_CHECKBOX_SELECTED)} ${label}${ ··· 57 65 }`; 58 66 } 59 67 if (state === 'submitted') { 60 - return `${color.dim(label)}`; 68 + return `${computeLabel(label, color.dim)}`; 61 69 } 62 - return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; 70 + return `${color.dim(S_CHECKBOX_INACTIVE)} ${computeLabel(label, color.dim)}`; 63 71 }; 64 72 const required = opts.required ?? true; 65 73 ··· 82 90 )}`; 83 91 }, 84 92 render() { 85 - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 93 + const wrappedMessage = wrapTextWithPrefix( 94 + opts.output, 95 + opts.message, 96 + `${symbolBar(this.state)} `, 97 + `${symbol(this.state)} ` 98 + ); 99 + const title = `${color.gray(S_BAR)}\n${wrappedMessage}\n`; 86 100 const value = this.value ?? []; 87 101 88 102 const styleOption = (option: Option<Value>, active: boolean) => { ··· 101 115 102 116 switch (this.state) { 103 117 case 'submit': { 104 - return `${title}${color.gray(S_BAR)} ${ 118 + const submitText = 105 119 this.options 106 120 .filter(({ value: optionValue }) => value.includes(optionValue)) 107 121 .map((option) => opt(option, 'submitted')) 108 - .join(color.dim(', ')) || color.dim('none') 109 - }`; 122 + .join(color.dim(', ')) || color.dim('none'); 123 + const wrappedSubmitText = wrapTextWithPrefix( 124 + opts.output, 125 + submitText, 126 + `${color.gray(S_BAR)} ` 127 + ); 128 + return `${title}${wrappedSubmitText}`; 110 129 } 111 130 case 'cancel': { 112 131 const label = this.options 113 132 .filter(({ value: optionValue }) => value.includes(optionValue)) 114 133 .map((option) => opt(option, 'cancelled')) 115 134 .join(color.dim(', ')); 116 - return `${title}${color.gray(S_BAR)}${ 117 - label.trim() ? ` ${label}\n${color.gray(S_BAR)}` : '' 118 - }`; 135 + if (label.trim() === '') { 136 + return `${title}${color.gray(S_BAR)}`; 137 + } 138 + const wrappedLabel = wrapTextWithPrefix(opts.output, label, `${color.gray(S_BAR)} `); 139 + return `${title}${wrappedLabel}\n${color.gray(S_BAR)}`; 119 140 } 120 141 case 'error': { 121 142 const prefix = `${color.yellow(S_BAR)} `;
+2
packages/prompts/src/progress-bar.ts
··· 63 63 return { 64 64 start, 65 65 stop: spin.stop, 66 + cancel: spin.cancel, 67 + error: spin.error, 66 68 advance, 67 69 isCancelled: spin.isCancelled, 68 70 message: (msg: string) => advance(0, msg),
+43 -13
packages/prompts/src/select.ts
··· 1 - import { SelectPrompt } from '@clack/core'; 1 + import { SelectPrompt, wrapTextWithPrefix } from '@clack/core'; 2 2 import color from 'picocolors'; 3 3 import { 4 4 type CommonOptions, ··· 7 7 S_RADIO_ACTIVE, 8 8 S_RADIO_INACTIVE, 9 9 symbol, 10 + symbolBar, 10 11 } from './common.js'; 11 12 import { limitOptions } from './limit-options.js'; 12 13 ··· 71 72 maxItems?: number; 72 73 } 73 74 75 + const computeLabel = (label: string, format: (text: string) => string) => { 76 + if (!label.includes('\n')) { 77 + return format(label); 78 + } 79 + return label 80 + .split('\n') 81 + .map((line) => format(line)) 82 + .join('\n'); 83 + }; 84 + 74 85 export const select = <Value>(opts: SelectOptions<Value>) => { 75 86 const opt = ( 76 87 option: Option<Value>, ··· 79 90 const label = option.label ?? String(option.value); 80 91 switch (state) { 81 92 case 'disabled': 82 - return `${color.gray(S_RADIO_INACTIVE)} ${color.gray(label)}${ 93 + return `${color.gray(S_RADIO_INACTIVE)} ${computeLabel(label, color.gray)}${ 83 94 option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : '' 84 95 }`; 85 96 case 'selected': 86 - return `${color.dim(label)}`; 97 + return `${computeLabel(label, color.dim)}`; 87 98 case 'active': 88 99 return `${color.green(S_RADIO_ACTIVE)} ${label}${ 89 100 option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 90 101 }`; 91 102 case 'cancelled': 92 - return `${color.strikethrough(color.dim(label))}`; 103 + return `${computeLabel(label, (str) => color.strikethrough(color.dim(str)))}`; 93 104 default: 94 - return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; 105 + return `${color.dim(S_RADIO_INACTIVE)} ${computeLabel(label, color.dim)}`; 95 106 } 96 107 }; 97 108 ··· 102 113 output: opts.output, 103 114 initialValue: opts.initialValue, 104 115 render() { 105 - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 116 + const titlePrefix = `${symbol(this.state)} `; 117 + const titlePrefixBar = `${symbolBar(this.state)} `; 118 + const messageLines = wrapTextWithPrefix( 119 + opts.output, 120 + opts.message, 121 + titlePrefixBar, 122 + titlePrefix 123 + ); 124 + const title = `${color.gray(S_BAR)}\n${messageLines}\n`; 106 125 107 126 switch (this.state) { 108 - case 'submit': 109 - return `${title}${color.gray(S_BAR)} ${opt(this.options[this.cursor], 'selected')}`; 110 - case 'cancel': 111 - return `${title}${color.gray(S_BAR)} ${opt( 112 - this.options[this.cursor], 113 - 'cancelled' 114 - )}\n${color.gray(S_BAR)}`; 127 + case 'submit': { 128 + const submitPrefix = `${color.gray(S_BAR)} `; 129 + const wrappedLines = wrapTextWithPrefix( 130 + opts.output, 131 + opt(this.options[this.cursor], 'selected'), 132 + submitPrefix 133 + ); 134 + return `${title}${wrappedLines}`; 135 + } 136 + case 'cancel': { 137 + const cancelPrefix = `${color.gray(S_BAR)} `; 138 + const wrappedLines = wrapTextWithPrefix( 139 + opts.output, 140 + opt(this.options[this.cursor], 'cancelled'), 141 + cancelPrefix 142 + ); 143 + return `${title}${wrappedLines}\n${color.gray(S_BAR)}`; 144 + } 115 145 default: { 116 146 const prefix = `${color.cyan(S_BAR)} `; 117 147 return `${title}${prefix}${limitOptions({
+11 -3
packages/prompts/src/spinner.ts
··· 24 24 25 25 export interface SpinnerResult { 26 26 start(msg?: string): void; 27 - stop(msg?: string, code?: number): void; 27 + stop(msg?: string): void; 28 + cancel(msg?: string): void; 29 + error(msg?: string): void; 28 30 message(msg?: string): void; 29 31 readonly isCancelled: boolean; 30 32 } ··· 61 63 : (cancelMessage ?? settings.messages.cancel); 62 64 isCancelled = code === 1; 63 65 if (isSpinnerActive) { 64 - stop(msg, code); 66 + _stop(msg, code); 65 67 if (isCancelled && typeof onCancel === 'function') { 66 68 onCancel(); 67 69 } ··· 163 165 }, delay); 164 166 }; 165 167 166 - const stop = (msg = '', code = 0): void => { 168 + const _stop = (msg = '', code = 0): void => { 167 169 if (!isSpinnerActive) return; 168 170 isSpinnerActive = false; 169 171 clearInterval(loop); ··· 184 186 unblock(); 185 187 }; 186 188 189 + const stop = (msg = ''): void => _stop(msg, 0); 190 + const cancel = (msg = ''): void => _stop(msg, 1); 191 + const error = (msg = ''): void => _stop(msg, 2); 192 + 187 193 const message = (msg = ''): void => { 188 194 _message = removeTrailingDots(msg ?? _message); 189 195 }; ··· 192 198 start, 193 199 stop, 194 200 message, 201 + cancel, 202 + error, 195 203 get isCancelled() { 196 204 return isCancelled; 197 205 },
+2 -3
packages/prompts/test/__snapshots__/autocomplete.test.ts.snap
··· 61 61 │ ↑/↓ to select • Enter: confirm • Type: to search 62 62 └", 63 63 "<cursor.backward count=999><cursor.up count=10>", 64 - "<cursor.down count=1>", 65 64 "<erase.down>", 66 65 "◇ Select an option 67 66 │ Line 0 ··· 136 135 │ ↑/↓ to select • Enter: confirm • Type: to search 137 136 └", 138 137 "<cursor.backward count=999><cursor.up count=7>", 139 - "<cursor.down count=1>", 140 138 "<erase.down>", 141 - "◇ Select an option 139 + "│ 140 + ◇ Select an option 142 141 │ Option 2", 143 142 " 144 143 ",
+196
packages/prompts/test/__snapshots__/multi-select.test.ts.snap
··· 636 636 ] 637 637 `; 638 638 639 + exports[`multiselect (isCI = false) > wraps cancelled state with long options 1`] = ` 640 + [ 641 + "<cursor.hide>", 642 + "│ 643 + ◆ foo 644 + │ ◻ Option 0 Option 0 Option 645 + │ 0 Option 0 Option 0 Option 646 + │ 0 Option 0 Option 0 Option 647 + │ 0 Option 0 648 + │ ◻ Option 1 Option 1 Option  649 + │ 1 Option 1 Option 1 Option  650 + │ 1 Option 1 Option 1 Option  651 + │ 1 Option 1 652 + └ 653 + ", 654 + "<cursor.backward count=999><cursor.up count=11>", 655 + "<cursor.down count=2>", 656 + "<erase.line><cursor.left count=1>", 657 + "│ ◼ Option 0 Option 0 Option ", 658 + "<cursor.down count=9>", 659 + "<cursor.backward count=999><cursor.up count=11>", 660 + "<cursor.down count=1>", 661 + "<erase.down>", 662 + "■ foo 663 + │ Option 0 Option 0 Option 0  664 + │ Option 0 Option 0 Option 0  665 + │ Option 0 Option 0 Option 0  666 + │ Option 0 667 + │", 668 + " 669 + ", 670 + "<cursor.show>", 671 + ] 672 + `; 673 + 674 + exports[`multiselect (isCI = false) > wraps long messages 1`] = ` 675 + [ 676 + "<cursor.hide>", 677 + "│ 678 + ◆ foo foo foo foo foo foo foo 679 + │ foo foo foo foo foo foo 680 + │ foo foo foo foo foo foo foo 681 + │ ◻ opt0 682 + │ ◻ opt1 683 + └ 684 + ", 685 + "<cursor.backward count=999><cursor.up count=7>", 686 + "<cursor.down count=4>", 687 + "<erase.line><cursor.left count=1>", 688 + "│ ◼ opt0", 689 + "<cursor.down count=3>", 690 + "<cursor.backward count=999><cursor.up count=7>", 691 + "<cursor.down count=1>", 692 + "<erase.down>", 693 + "◇ foo foo foo foo foo foo foo 694 + │ foo foo foo foo foo foo 695 + │ foo foo foo foo foo foo foo 696 + │ opt0", 697 + " 698 + ", 699 + "<cursor.show>", 700 + ] 701 + `; 702 + 703 + exports[`multiselect (isCI = false) > wraps success state with long options 1`] = ` 704 + [ 705 + "<cursor.hide>", 706 + "│ 707 + ◆ foo 708 + │ ◻ Option 0 Option 0 Option 709 + │ 0 Option 0 Option 0 Option 710 + │ 0 Option 0 Option 0 Option 711 + │ 0 Option 0 712 + │ ◻ Option 1 Option 1 Option  713 + │ 1 Option 1 Option 1 Option  714 + │ 1 Option 1 Option 1 Option  715 + │ 1 Option 1 716 + └ 717 + ", 718 + "<cursor.backward count=999><cursor.up count=11>", 719 + "<cursor.down count=2>", 720 + "<erase.line><cursor.left count=1>", 721 + "│ ◼ Option 0 Option 0 Option ", 722 + "<cursor.down count=9>", 723 + "<cursor.backward count=999><cursor.up count=11>", 724 + "<cursor.down count=1>", 725 + "<erase.down>", 726 + "◇ foo 727 + │ Option 0 Option 0 Option 0  728 + │ Option 0 Option 0 Option 0  729 + │ Option 0 Option 0 Option 0  730 + │ Option 0", 731 + " 732 + ", 733 + "<cursor.show>", 734 + ] 735 + `; 736 + 639 737 exports[`multiselect (isCI = true) > can be aborted by a signal 1`] = ` 640 738 [ 641 739 "<cursor.hide>", ··· 1271 1369 "<cursor.show>", 1272 1370 ] 1273 1371 `; 1372 + 1373 + exports[`multiselect (isCI = true) > wraps cancelled state with long options 1`] = ` 1374 + [ 1375 + "<cursor.hide>", 1376 + "│ 1377 + ◆ foo 1378 + │ ◻ Option 0 Option 0 Option 1379 + │ 0 Option 0 Option 0 Option 1380 + │ 0 Option 0 Option 0 Option 1381 + │ 0 Option 0 1382 + │ ◻ Option 1 Option 1 Option  1383 + │ 1 Option 1 Option 1 Option  1384 + │ 1 Option 1 Option 1 Option  1385 + │ 1 Option 1 1386 + └ 1387 + ", 1388 + "<cursor.backward count=999><cursor.up count=11>", 1389 + "<cursor.down count=2>", 1390 + "<erase.line><cursor.left count=1>", 1391 + "│ ◼ Option 0 Option 0 Option ", 1392 + "<cursor.down count=9>", 1393 + "<cursor.backward count=999><cursor.up count=11>", 1394 + "<cursor.down count=1>", 1395 + "<erase.down>", 1396 + "■ foo 1397 + │ Option 0 Option 0 Option 0  1398 + │ Option 0 Option 0 Option 0  1399 + │ Option 0 Option 0 Option 0  1400 + │ Option 0 1401 + │", 1402 + " 1403 + ", 1404 + "<cursor.show>", 1405 + ] 1406 + `; 1407 + 1408 + exports[`multiselect (isCI = true) > wraps long messages 1`] = ` 1409 + [ 1410 + "<cursor.hide>", 1411 + "│ 1412 + ◆ foo foo foo foo foo foo foo 1413 + │ foo foo foo foo foo foo 1414 + │ foo foo foo foo foo foo foo 1415 + │ ◻ opt0 1416 + │ ◻ opt1 1417 + └ 1418 + ", 1419 + "<cursor.backward count=999><cursor.up count=7>", 1420 + "<cursor.down count=4>", 1421 + "<erase.line><cursor.left count=1>", 1422 + "│ ◼ opt0", 1423 + "<cursor.down count=3>", 1424 + "<cursor.backward count=999><cursor.up count=7>", 1425 + "<cursor.down count=1>", 1426 + "<erase.down>", 1427 + "◇ foo foo foo foo foo foo foo 1428 + │ foo foo foo foo foo foo 1429 + │ foo foo foo foo foo foo foo 1430 + │ opt0", 1431 + " 1432 + ", 1433 + "<cursor.show>", 1434 + ] 1435 + `; 1436 + 1437 + exports[`multiselect (isCI = true) > wraps success state with long options 1`] = ` 1438 + [ 1439 + "<cursor.hide>", 1440 + "│ 1441 + ◆ foo 1442 + │ ◻ Option 0 Option 0 Option 1443 + │ 0 Option 0 Option 0 Option 1444 + │ 0 Option 0 Option 0 Option 1445 + │ 0 Option 0 1446 + │ ◻ Option 1 Option 1 Option  1447 + │ 1 Option 1 Option 1 Option  1448 + │ 1 Option 1 Option 1 Option  1449 + │ 1 Option 1 1450 + └ 1451 + ", 1452 + "<cursor.backward count=999><cursor.up count=11>", 1453 + "<cursor.down count=2>", 1454 + "<erase.line><cursor.left count=1>", 1455 + "│ ◼ Option 0 Option 0 Option ", 1456 + "<cursor.down count=9>", 1457 + "<cursor.backward count=999><cursor.up count=11>", 1458 + "<cursor.down count=1>", 1459 + "<erase.down>", 1460 + "◇ foo 1461 + │ Option 0 Option 0 Option 0  1462 + │ Option 0 Option 0 Option 0  1463 + │ Option 0 Option 0 Option 0  1464 + │ Option 0", 1465 + " 1466 + ", 1467 + "<cursor.show>", 1468 + ] 1469 + `;
+64 -4
packages/prompts/test/__snapshots__/progress-bar.test.ts.snap
··· 125 125 ] 126 126 `; 127 127 128 - exports[`prompts - progress (isCI = false) > stop > renders cancel symbol if code = 1 1`] = ` 128 + exports[`prompts - progress (isCI = false) > stop > renders cancel symbol when calling cancel() 1`] = ` 129 129 [ 130 130 "<cursor.hide>", 131 131 "│ ··· 139 139 ] 140 140 `; 141 141 142 - exports[`prompts - progress (isCI = false) > stop > renders error symbol if code > 1 1`] = ` 142 + exports[`prompts - progress (isCI = false) > stop > renders error symbol when calling error() 1`] = ` 143 143 [ 144 144 "<cursor.hide>", 145 145 "│ ··· 162 162 "<cursor.left count=1>", 163 163 "<erase.down>", 164 164 "◇ foo 165 + ", 166 + "<cursor.show>", 167 + ] 168 + `; 169 + 170 + exports[`prompts - progress (isCI = false) > stop > renders message when cancelling 1`] = ` 171 + [ 172 + "<cursor.hide>", 173 + "│ 174 + ", 175 + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", 176 + "<cursor.left count=1>", 177 + "<erase.down>", 178 + "■ cancelled :-( 179 + ", 180 + "<cursor.show>", 181 + ] 182 + `; 183 + 184 + exports[`prompts - progress (isCI = false) > stop > renders message when erroring 1`] = ` 185 + [ 186 + "<cursor.hide>", 187 + "│ 188 + ", 189 + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", 190 + "<cursor.left count=1>", 191 + "<erase.down>", 192 + "▲ FATAL ERROR! 165 193 ", 166 194 "<cursor.show>", 167 195 ] ··· 382 410 ] 383 411 `; 384 412 385 - exports[`prompts - progress (isCI = true) > stop > renders cancel symbol if code = 1 1`] = ` 413 + exports[`prompts - progress (isCI = true) > stop > renders cancel symbol when calling cancel() 1`] = ` 386 414 [ 387 415 "<cursor.hide>", 388 416 "│ ··· 398 426 ] 399 427 `; 400 428 401 - exports[`prompts - progress (isCI = true) > stop > renders error symbol if code > 1 1`] = ` 429 + exports[`prompts - progress (isCI = true) > stop > renders error symbol when calling error() 1`] = ` 402 430 [ 403 431 "<cursor.hide>", 404 432 "│ ··· 425 453 "<cursor.left count=1>", 426 454 "<erase.down>", 427 455 "◇ foo 456 + ", 457 + "<cursor.show>", 458 + ] 459 + `; 460 + 461 + exports[`prompts - progress (isCI = true) > stop > renders message when cancelling 1`] = ` 462 + [ 463 + "<cursor.hide>", 464 + "│ 465 + ", 466 + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", 467 + " 468 + ", 469 + "<cursor.left count=1>", 470 + "<erase.down>", 471 + "■ cancelled :-( 472 + ", 473 + "<cursor.show>", 474 + ] 475 + `; 476 + 477 + exports[`prompts - progress (isCI = true) > stop > renders message when erroring 1`] = ` 478 + [ 479 + "<cursor.hide>", 480 + "│ 481 + ", 482 + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", 483 + " 484 + ", 485 + "<cursor.left count=1>", 486 + "<erase.down>", 487 + "▲ FATAL ERROR! 428 488 ", 429 489 "<cursor.show>", 430 490 ]
+296
packages/prompts/test/__snapshots__/select.test.ts.snap
··· 63 63 ] 64 64 `; 65 65 66 + exports[`select (isCI = false) > handles mixed size re-renders 1`] = ` 67 + [ 68 + "<cursor.hide>", 69 + "│ 70 + ◆ Whatever 71 + │ ● Long Option 72 + │ Long Option 73 + │ Long Option 74 + │ Long Option 75 + │ Long Option 76 + │ Long Option 77 + │ Long Option 78 + │ Long Option 79 + │ ... 80 + └ 81 + ", 82 + "<cursor.backward count=999><cursor.up count=12>", 83 + "<erase.down>", 84 + "│ 85 + ◆ Whatever 86 + │ ... 87 + │ ○ Option 0 88 + │ ○ Option 1 89 + │ ○ Option 2 90 + │ ● Option 3 91 + └ 92 + ", 93 + "<cursor.backward count=999><cursor.up count=8>", 94 + "<cursor.down count=1>", 95 + "<erase.down>", 96 + "◇ Whatever 97 + │ Option 3", 98 + " 99 + ", 100 + "<cursor.show>", 101 + ] 102 + `; 103 + 66 104 exports[`select (isCI = false) > renders disabled options 1`] = ` 67 105 [ 68 106 "<cursor.hide>", ··· 71 109 │ ○ Option 0 72 110 │ ● Option 1 73 111 │ ○ Option 2 (Hint 2) 112 + └ 113 + ", 114 + "<cursor.backward count=999><cursor.up count=6>", 115 + "<cursor.down count=1>", 116 + "<erase.down>", 117 + "◇ foo 118 + │ Option 1", 119 + " 120 + ", 121 + "<cursor.show>", 122 + ] 123 + `; 124 + 125 + exports[`select (isCI = false) > renders multi-line option labels 1`] = ` 126 + [ 127 + "<cursor.hide>", 128 + "│ 129 + ◆ foo 130 + │ ● Option 0 131 + │ with multiple lines 132 + │ ○ Option 1 133 + └ 134 + ", 135 + "<cursor.backward count=999><cursor.up count=6>", 136 + "<cursor.down count=2>", 137 + "<erase.down>", 138 + "│ ○ Option 0 139 + │ with multiple lines 140 + │ ● Option 1 74 141 └ 75 142 ", 76 143 "<cursor.backward count=999><cursor.up count=6>", ··· 172 239 "<erase.down>", 173 240 "◇ foo 174 241 │ opt0", 242 + " 243 + ", 244 + "<cursor.show>", 245 + ] 246 + `; 247 + 248 + exports[`select (isCI = false) > wraps long cancelled message 1`] = ` 249 + [ 250 + "<cursor.hide>", 251 + "│ 252 + ◆ foo 253 + │ ● foo foo foo foo foo foo 254 + │ foo foo foo foo foo foo foo 255 + │ foo foo foo foo foo foo 256 + │ foo foo foo foo foo foo foo 257 + │ foo foo foo foo 258 + │ ○ Option 1 259 + └ 260 + ", 261 + "<cursor.backward count=999><cursor.up count=9>", 262 + "<cursor.down count=1>", 263 + "<erase.down>", 264 + "■ foo 265 + │ foo foo foo foo foo foo foo 266 + │  foo foo foo foo foo foo  267 + │ foo foo foo foo foo foo foo 268 + │  foo foo foo foo foo foo  269 + │ foo foo foo foo 270 + │", 271 + " 272 + ", 273 + "<cursor.show>", 274 + ] 275 + `; 276 + 277 + exports[`select (isCI = false) > wraps long messages 1`] = ` 278 + [ 279 + "<cursor.hide>", 280 + "│ 281 + ◆ foo foo foo foo foo foo foo 282 + │ foo foo foo foo foo foo 283 + │ foo foo foo foo foo foo foo 284 + │ ● opt0 285 + │ ○ opt1 286 + └ 287 + ", 288 + "<cursor.backward count=999><cursor.up count=7>", 289 + "<cursor.down count=1>", 290 + "<erase.down>", 291 + "◇ foo foo foo foo foo foo foo 292 + │ foo foo foo foo foo foo 293 + │ foo foo foo foo foo foo foo 294 + │ opt0", 295 + " 296 + ", 297 + "<cursor.show>", 298 + ] 299 + `; 300 + 301 + exports[`select (isCI = false) > wraps long results 1`] = ` 302 + [ 303 + "<cursor.hide>", 304 + "│ 305 + ◆ foo 306 + │ ● foo foo foo foo foo foo 307 + │ foo foo foo foo foo foo foo 308 + │ foo foo foo foo foo foo 309 + │ foo foo foo foo foo foo foo 310 + │ foo foo foo foo 311 + │ ○ Option 1 312 + └ 313 + ", 314 + "<cursor.backward count=999><cursor.up count=9>", 315 + "<cursor.down count=1>", 316 + "<erase.down>", 317 + "◇ foo 318 + │ foo foo foo foo foo foo foo 319 + │  foo foo foo foo foo foo  320 + │ foo foo foo foo foo foo foo 321 + │  foo foo foo foo foo foo  322 + │ foo foo foo foo", 175 323 " 176 324 ", 177 325 "<cursor.show>", ··· 241 389 ] 242 390 `; 243 391 392 + exports[`select (isCI = true) > handles mixed size re-renders 1`] = ` 393 + [ 394 + "<cursor.hide>", 395 + "│ 396 + ◆ Whatever 397 + │ ● Long Option 398 + │ Long Option 399 + │ Long Option 400 + │ Long Option 401 + │ Long Option 402 + │ Long Option 403 + │ Long Option 404 + │ Long Option 405 + │ ... 406 + └ 407 + ", 408 + "<cursor.backward count=999><cursor.up count=12>", 409 + "<erase.down>", 410 + "│ 411 + ◆ Whatever 412 + │ ... 413 + │ ○ Option 0 414 + │ ○ Option 1 415 + │ ○ Option 2 416 + │ ● Option 3 417 + └ 418 + ", 419 + "<cursor.backward count=999><cursor.up count=8>", 420 + "<cursor.down count=1>", 421 + "<erase.down>", 422 + "◇ Whatever 423 + │ Option 3", 424 + " 425 + ", 426 + "<cursor.show>", 427 + ] 428 + `; 429 + 244 430 exports[`select (isCI = true) > renders disabled options 1`] = ` 245 431 [ 246 432 "<cursor.hide>", ··· 249 435 │ ○ Option 0 250 436 │ ● Option 1 251 437 │ ○ Option 2 (Hint 2) 438 + └ 439 + ", 440 + "<cursor.backward count=999><cursor.up count=6>", 441 + "<cursor.down count=1>", 442 + "<erase.down>", 443 + "◇ foo 444 + │ Option 1", 445 + " 446 + ", 447 + "<cursor.show>", 448 + ] 449 + `; 450 + 451 + exports[`select (isCI = true) > renders multi-line option labels 1`] = ` 452 + [ 453 + "<cursor.hide>", 454 + "│ 455 + ◆ foo 456 + │ ● Option 0 457 + │ with multiple lines 458 + │ ○ Option 1 459 + └ 460 + ", 461 + "<cursor.backward count=999><cursor.up count=6>", 462 + "<cursor.down count=2>", 463 + "<erase.down>", 464 + "│ ○ Option 0 465 + │ with multiple lines 466 + │ ● Option 1 252 467 └ 253 468 ", 254 469 "<cursor.backward count=999><cursor.up count=6>", ··· 355 570 "<cursor.show>", 356 571 ] 357 572 `; 573 + 574 + exports[`select (isCI = true) > wraps long cancelled message 1`] = ` 575 + [ 576 + "<cursor.hide>", 577 + "│ 578 + ◆ foo 579 + │ ● foo foo foo foo foo foo 580 + │ foo foo foo foo foo foo foo 581 + │ foo foo foo foo foo foo 582 + │ foo foo foo foo foo foo foo 583 + │ foo foo foo foo 584 + │ ○ Option 1 585 + └ 586 + ", 587 + "<cursor.backward count=999><cursor.up count=9>", 588 + "<cursor.down count=1>", 589 + "<erase.down>", 590 + "■ foo 591 + │ foo foo foo foo foo foo foo 592 + │  foo foo foo foo foo foo  593 + │ foo foo foo foo foo foo foo 594 + │  foo foo foo foo foo foo  595 + │ foo foo foo foo 596 + │", 597 + " 598 + ", 599 + "<cursor.show>", 600 + ] 601 + `; 602 + 603 + exports[`select (isCI = true) > wraps long messages 1`] = ` 604 + [ 605 + "<cursor.hide>", 606 + "│ 607 + ◆ foo foo foo foo foo foo foo 608 + │ foo foo foo foo foo foo 609 + │ foo foo foo foo foo foo foo 610 + │ ● opt0 611 + │ ○ opt1 612 + └ 613 + ", 614 + "<cursor.backward count=999><cursor.up count=7>", 615 + "<cursor.down count=1>", 616 + "<erase.down>", 617 + "◇ foo foo foo foo foo foo foo 618 + │ foo foo foo foo foo foo 619 + │ foo foo foo foo foo foo foo 620 + │ opt0", 621 + " 622 + ", 623 + "<cursor.show>", 624 + ] 625 + `; 626 + 627 + exports[`select (isCI = true) > wraps long results 1`] = ` 628 + [ 629 + "<cursor.hide>", 630 + "│ 631 + ◆ foo 632 + │ ● foo foo foo foo foo foo 633 + │ foo foo foo foo foo foo foo 634 + │ foo foo foo foo foo foo 635 + │ foo foo foo foo foo foo foo 636 + │ foo foo foo foo 637 + │ ○ Option 1 638 + └ 639 + ", 640 + "<cursor.backward count=999><cursor.up count=9>", 641 + "<cursor.down count=1>", 642 + "<erase.down>", 643 + "◇ foo 644 + │ foo foo foo foo foo foo foo 645 + │  foo foo foo foo foo foo  646 + │ foo foo foo foo foo foo foo 647 + │  foo foo foo foo foo foo  648 + │ foo foo foo foo", 649 + " 650 + ", 651 + "<cursor.show>", 652 + ] 653 + `;
+64 -4
packages/prompts/test/__snapshots__/spinner.test.ts.snap
··· 461 461 ] 462 462 `; 463 463 464 - exports[`spinner (isCI = false) > stop > renders cancel symbol if code = 1 1`] = ` 464 + exports[`spinner (isCI = false) > stop > renders cancel symbol when calling cancel() 1`] = ` 465 465 [ 466 466 "<cursor.hide>", 467 467 "│ ··· 475 475 ] 476 476 `; 477 477 478 - exports[`spinner (isCI = false) > stop > renders error symbol if code > 1 1`] = ` 478 + exports[`spinner (isCI = false) > stop > renders error symbol when calling error() 1`] = ` 479 479 [ 480 480 "<cursor.hide>", 481 481 "│ ··· 498 498 "<cursor.left count=1>", 499 499 "<erase.down>", 500 500 "◇ foo 501 + ", 502 + "<cursor.show>", 503 + ] 504 + `; 505 + 506 + exports[`spinner (isCI = false) > stop > renders message when cancelling 1`] = ` 507 + [ 508 + "<cursor.hide>", 509 + "│ 510 + ", 511 + "◒ ", 512 + "<cursor.left count=1>", 513 + "<erase.down>", 514 + "■ too dizzy — spinning cancelled 515 + ", 516 + "<cursor.show>", 517 + ] 518 + `; 519 + 520 + exports[`spinner (isCI = false) > stop > renders message when erroring 1`] = ` 521 + [ 522 + "<cursor.hide>", 523 + "│ 524 + ", 525 + "◒ ", 526 + "<cursor.left count=1>", 527 + "<erase.down>", 528 + "▲ error: spun too fast! 501 529 ", 502 530 "<cursor.show>", 503 531 ] ··· 789 817 ] 790 818 `; 791 819 792 - exports[`spinner (isCI = true) > stop > renders cancel symbol if code = 1 1`] = ` 820 + exports[`spinner (isCI = true) > stop > renders cancel symbol when calling cancel() 1`] = ` 793 821 [ 794 822 "<cursor.hide>", 795 823 "│ ··· 805 833 ] 806 834 `; 807 835 808 - exports[`spinner (isCI = true) > stop > renders error symbol if code > 1 1`] = ` 836 + exports[`spinner (isCI = true) > stop > renders error symbol when calling error() 1`] = ` 809 837 [ 810 838 "<cursor.hide>", 811 839 "│ ··· 832 860 "<cursor.left count=1>", 833 861 "<erase.down>", 834 862 "◇ foo 863 + ", 864 + "<cursor.show>", 865 + ] 866 + `; 867 + 868 + exports[`spinner (isCI = true) > stop > renders message when cancelling 1`] = ` 869 + [ 870 + "<cursor.hide>", 871 + "│ 872 + ", 873 + "◒ ...", 874 + " 875 + ", 876 + "<cursor.left count=1>", 877 + "<erase.down>", 878 + "■ too dizzy — spinning cancelled 879 + ", 880 + "<cursor.show>", 881 + ] 882 + `; 883 + 884 + exports[`spinner (isCI = true) > stop > renders message when erroring 1`] = ` 885 + [ 886 + "<cursor.hide>", 887 + "│ 888 + ", 889 + "◒ ...", 890 + " 891 + ", 892 + "<cursor.left count=1>", 893 + "<erase.down>", 894 + "▲ error: spun too fast! 835 895 ", 836 896 "<cursor.show>", 837 897 ]
+63
packages/prompts/test/multi-select.test.ts
··· 336 336 expect(value).toEqual(['opt1']); 337 337 expect(output.buffer).toMatchSnapshot(); 338 338 }); 339 + 340 + test('wraps long messages', async () => { 341 + output.columns = 40; 342 + 343 + const result = prompts.multiselect({ 344 + message: 'foo '.repeat(20).trim(), 345 + options: [{ value: 'opt0' }, { value: 'opt1' }], 346 + input, 347 + output, 348 + }); 349 + 350 + input.emit('keypress', '', { name: 'space' }); 351 + input.emit('keypress', '', { name: 'return' }); 352 + 353 + const value = await result; 354 + 355 + expect(value).toEqual(['opt0']); 356 + expect(output.buffer).toMatchSnapshot(); 357 + }); 358 + 359 + test('wraps cancelled state with long options', async () => { 360 + output.columns = 40; 361 + 362 + const result = prompts.multiselect({ 363 + message: 'foo', 364 + options: [ 365 + { value: 'opt0', label: 'Option 0 '.repeat(10).trim() }, 366 + { value: 'opt1', label: 'Option 1 '.repeat(10).trim() }, 367 + ], 368 + input, 369 + output, 370 + }); 371 + 372 + input.emit('keypress', '', { name: 'space' }); 373 + input.emit('keypress', 'escape', { name: 'escape' }); 374 + 375 + const value = await result; 376 + 377 + expect(prompts.isCancel(value)).toBe(true); 378 + expect(output.buffer).toMatchSnapshot(); 379 + }); 380 + 381 + test('wraps success state with long options', async () => { 382 + output.columns = 40; 383 + 384 + const result = prompts.multiselect({ 385 + message: 'foo', 386 + options: [ 387 + { value: 'opt0', label: 'Option 0 '.repeat(10).trim() }, 388 + { value: 'opt1', label: 'Option 1 '.repeat(10).trim() }, 389 + ], 390 + input, 391 + output, 392 + }); 393 + 394 + input.emit('keypress', '', { name: 'space' }); 395 + input.emit('keypress', '', { name: 'return' }); 396 + 397 + const value = await result; 398 + 399 + expect(value).toEqual(['opt0']); 400 + expect(output.buffer).toMatchSnapshot(); 401 + }); 339 402 });
+28 -4
packages/prompts/test/progress-bar.test.ts
··· 87 87 expect(output.buffer).toMatchSnapshot(); 88 88 }); 89 89 90 - test('renders cancel symbol if code = 1', () => { 90 + test('renders cancel symbol when calling cancel()', () => { 91 91 const result = prompts.progress({ output }); 92 92 93 93 result.start(); 94 94 95 95 vi.advanceTimersByTime(80); 96 96 97 - result.stop('', 1); 97 + result.cancel(); 98 98 99 99 expect(output.buffer).toMatchSnapshot(); 100 100 }); 101 101 102 - test('renders error symbol if code > 1', () => { 102 + test('renders error symbol when calling error()', () => { 103 103 const result = prompts.progress({ output }); 104 104 105 105 result.start(); 106 106 107 107 vi.advanceTimersByTime(80); 108 108 109 - result.stop('', 2); 109 + result.error(); 110 110 111 111 expect(output.buffer).toMatchSnapshot(); 112 112 }); ··· 131 131 vi.advanceTimersByTime(80); 132 132 133 133 result.stop('foo.'); 134 + 135 + expect(output.buffer).toMatchSnapshot(); 136 + }); 137 + 138 + test('renders message when cancelling', () => { 139 + const result = prompts.progress({ output }); 140 + 141 + result.start(); 142 + 143 + vi.advanceTimersByTime(80); 144 + 145 + result.cancel('cancelled :-('); 146 + 147 + expect(output.buffer).toMatchSnapshot(); 148 + }); 149 + 150 + test('renders message when erroring', () => { 151 + const result = prompts.progress({ output }); 152 + 153 + result.start(); 154 + 155 + vi.advanceTimersByTime(80); 156 + 157 + result.error('FATAL ERROR!'); 134 158 135 159 expect(output.buffer).toMatchSnapshot(); 136 160 });
+110
packages/prompts/test/select.test.ts
··· 165 165 expect(value).toBe('opt1'); 166 166 expect(output.buffer).toMatchSnapshot(); 167 167 }); 168 + 169 + test('wraps long results', async () => { 170 + output.columns = 40; 171 + 172 + const result = prompts.select({ 173 + message: 'foo', 174 + options: [ 175 + { 176 + value: 'opt0', 177 + label: 'foo '.repeat(30).trim(), 178 + }, 179 + { value: 'opt1', label: 'Option 1' }, 180 + ], 181 + input, 182 + output, 183 + }); 184 + 185 + input.emit('keypress', '', { name: 'return' }); 186 + 187 + await result; 188 + 189 + expect(output.buffer).toMatchSnapshot(); 190 + }); 191 + 192 + test('wraps long cancelled message', async () => { 193 + output.columns = 40; 194 + 195 + const result = prompts.select({ 196 + message: 'foo', 197 + options: [ 198 + { 199 + value: 'opt0', 200 + label: 'foo '.repeat(30).trim(), 201 + }, 202 + { value: 'opt1', label: 'Option 1' }, 203 + ], 204 + input, 205 + output, 206 + }); 207 + 208 + input.emit('keypress', 'escape', { name: 'escape' }); 209 + 210 + await result; 211 + 212 + expect(output.buffer).toMatchSnapshot(); 213 + }); 214 + 215 + test('wraps long messages', async () => { 216 + output.columns = 40; 217 + 218 + const result = prompts.select({ 219 + message: 'foo '.repeat(20).trim(), 220 + options: [{ value: 'opt0' }, { value: 'opt1' }], 221 + input, 222 + output, 223 + }); 224 + 225 + input.emit('keypress', '', { name: 'return' }); 226 + 227 + const value = await result; 228 + 229 + expect(value).toEqual('opt0'); 230 + expect(output.buffer).toMatchSnapshot(); 231 + }); 232 + 233 + test('renders multi-line option labels', async () => { 234 + const result = prompts.select({ 235 + message: 'foo', 236 + options: [ 237 + { value: 'opt0', label: 'Option 0\nwith multiple lines' }, 238 + { value: 'opt1', label: 'Option 1' }, 239 + ], 240 + input, 241 + output, 242 + }); 243 + 244 + input.emit('keypress', '', { name: 'down' }); 245 + input.emit('keypress', '', { name: 'return' }); 246 + 247 + await result; 248 + 249 + expect(output.buffer).toMatchSnapshot(); 250 + }); 251 + 252 + test('handles mixed size re-renders', async () => { 253 + output.rows = 10; 254 + 255 + const result = prompts.select({ 256 + message: 'Whatever', 257 + options: [ 258 + { 259 + value: 'longopt', 260 + label: Array.from({ length: 8 }, () => 'Long Option').join('\n'), 261 + }, 262 + ...Array.from({ length: 4 }, (_, i) => ({ 263 + value: `opt${i}`, 264 + label: `Option ${i}`, 265 + })), 266 + ], 267 + input, 268 + output, 269 + }); 270 + 271 + input.emit('keypress', '', { name: 'up' }); 272 + input.emit('keypress', '', { name: 'return' }); 273 + 274 + await result; 275 + 276 + expect(output.buffer).toMatchSnapshot(); 277 + }); 168 278 });
+28 -4
packages/prompts/test/spinner.test.ts
··· 117 117 expect(output.buffer).toMatchSnapshot(); 118 118 }); 119 119 120 - test('renders cancel symbol if code = 1', () => { 120 + test('renders cancel symbol when calling cancel()', () => { 121 121 const result = prompts.spinner({ output }); 122 122 123 123 result.start(); 124 124 125 125 vi.advanceTimersByTime(80); 126 126 127 - result.stop('', 1); 127 + result.cancel(); 128 128 129 129 expect(output.buffer).toMatchSnapshot(); 130 130 }); 131 131 132 - test('renders error symbol if code > 1', () => { 132 + test('renders error symbol when calling error()', () => { 133 133 const result = prompts.spinner({ output }); 134 134 135 135 result.start(); 136 136 137 137 vi.advanceTimersByTime(80); 138 138 139 - result.stop('', 2); 139 + result.error(); 140 140 141 141 expect(output.buffer).toMatchSnapshot(); 142 142 }); ··· 161 161 vi.advanceTimersByTime(80); 162 162 163 163 result.stop('foo.'); 164 + 165 + expect(output.buffer).toMatchSnapshot(); 166 + }); 167 + 168 + test('renders message when cancelling', () => { 169 + const result = prompts.spinner({ output }); 170 + 171 + result.start(); 172 + 173 + vi.advanceTimersByTime(80); 174 + 175 + result.cancel('too dizzy — spinning cancelled'); 176 + 177 + expect(output.buffer).toMatchSnapshot(); 178 + }); 179 + 180 + test('renders message when erroring', () => { 181 + const result = prompts.spinner({ output }); 182 + 183 + result.start(); 184 + 185 + vi.advanceTimersByTime(80); 186 + 187 + result.error('error: spun too fast!'); 164 188 165 189 expect(output.buffer).toMatchSnapshot(); 166 190 });