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

feat: add `caseSensitive` option to select-key (#443)

authored by

James Garbutt and committed by
GitHub
(Jan 25, 2026, 5:13 PM -0600) 68dbf9bc 55eb280e

+847 -24
+6
.changeset/three-boxes-follow.md
··· 1 + --- 2 + "@clack/prompts": patch 3 + "@clack/core": patch 4 + --- 5 + 6 + select-key: Fixed wrapping and added new `caseSensitive` option
+17 -4
packages/core/src/prompts/select-key.ts
··· 3 3 export interface SelectKeyOptions<T extends { value: string }> 4 4 extends PromptOptions<T['value'], SelectKeyPrompt<T>> { 5 5 options: T[]; 6 + caseSensitive?: boolean; 6 7 } 7 8 export default class SelectKeyPrompt<T extends { value: string }> extends Prompt<T['value']> { 8 9 options: T[]; ··· 12 13 super(opts, false); 13 14 14 15 this.options = opts.options; 15 - const keys = this.options.map(({ value: [initial] }) => initial?.toLowerCase()); 16 + const caseSensitive = opts.caseSensitive === true; 17 + const keys = this.options.map(({ value: [initial] }) => { 18 + return caseSensitive ? initial : initial?.toLowerCase(); 19 + }); 16 20 this.cursor = Math.max(keys.indexOf(opts.initialValue), 0); 17 21 18 - this.on('key', (key) => { 19 - if (!key || !keys.includes(key)) return; 20 - const value = this.options.find(({ value: [initial] }) => initial?.toLowerCase() === key); 22 + this.on('key', (key, keyInfo) => { 23 + if (!key) { 24 + return; 25 + } 26 + const casedKey = caseSensitive && keyInfo.shift ? key.toUpperCase() : key; 27 + if (!keys.includes(casedKey)) { 28 + return; 29 + } 30 + 31 + const value = this.options.find(({ value: [initial] }) => { 32 + return caseSensitive ? initial === casedKey : initial?.toLowerCase() === key; 33 + }); 21 34 if (value) { 22 35 this.value = value.value; 23 36 this.state = 'submit';
+47 -20
packages/prompts/src/select-key.ts
··· 1 - import { SelectKeyPrompt } from '@clack/core'; 1 + import { SelectKeyPrompt, wrapTextWithPrefix } from '@clack/core'; 2 2 import color from 'picocolors'; 3 - import { S_BAR, S_BAR_END, symbol } from './common.js'; 4 - import type { Option, SelectOptions } from './select.js'; 3 + import { type CommonOptions, S_BAR, S_BAR_END, symbol } from './common.js'; 4 + import type { Option } from './select.js'; 5 + 6 + export interface SelectKeyOptions<Value extends string> extends CommonOptions { 7 + message: string; 8 + options: Option<Value>[]; 9 + initialValue?: Value; 10 + caseSensitive?: boolean; 11 + } 5 12 6 - export const selectKey = <Value extends string>(opts: SelectOptions<Value>) => { 13 + export const selectKey = <Value extends string>(opts: SelectKeyOptions<Value>) => { 7 14 const opt = ( 8 15 option: Option<Value>, 9 16 state: 'inactive' | 'active' | 'selected' | 'cancelled' = 'inactive' ··· 16 23 return `${color.strikethrough(color.dim(label))}`; 17 24 } 18 25 if (state === 'active') { 19 - return `${color.bgCyan(color.gray(` ${option.value} `))} ${label} ${ 20 - option.hint ? color.dim(`(${option.hint})`) : '' 26 + return `${color.bgCyan(color.gray(` ${option.value} `))} ${label}${ 27 + option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 21 28 }`; 22 29 } 23 - return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label} ${ 24 - option.hint ? color.dim(`(${option.hint})`) : '' 30 + return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label}${ 31 + option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 25 32 }`; 26 33 }; 27 34 ··· 31 38 input: opts.input, 32 39 output: opts.output, 33 40 initialValue: opts.initialValue, 41 + caseSensitive: opts.caseSensitive, 34 42 render() { 35 43 const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 36 44 37 45 switch (this.state) { 38 - case 'submit': 39 - return `${title}${color.gray(S_BAR)} ${opt( 40 - this.options.find((opt) => opt.value === this.value) ?? opts.options[0], 41 - 'selected' 42 - )}`; 43 - case 'cancel': 44 - return `${title}${color.gray(S_BAR)} ${opt(this.options[0], 'cancelled')}\n${color.gray( 45 - S_BAR 46 - )}`; 46 + case 'submit': { 47 + const submitPrefix = `${color.gray(S_BAR)} `; 48 + const selectedOption = 49 + this.options.find((opt) => opt.value === this.value) ?? opts.options[0]; 50 + const wrapped = wrapTextWithPrefix( 51 + opts.output, 52 + opt(selectedOption, 'selected'), 53 + submitPrefix 54 + ); 55 + return `${title}${wrapped}`; 56 + } 57 + case 'cancel': { 58 + const cancelPrefix = `${color.gray(S_BAR)} `; 59 + const wrapped = wrapTextWithPrefix( 60 + opts.output, 61 + opt(this.options[0], 'cancelled'), 62 + cancelPrefix 63 + ); 64 + return `${title}${wrapped}\n${color.gray(S_BAR)}`; 65 + } 47 66 default: { 48 - return `${title}${color.cyan(S_BAR)} ${this.options 49 - .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')) 50 - .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; 67 + const defaultPrefix = `${color.cyan(S_BAR)} `; 68 + const wrapped = this.options 69 + .map((option, i) => 70 + wrapTextWithPrefix( 71 + opts.output, 72 + opt(option, i === this.cursor ? 'active' : 'inactive'), 73 + defaultPrefix 74 + ) 75 + ) 76 + .join('\n'); 77 + return `${title}${wrapped}\n${color.cyan(S_BAR_END)}\n`; 51 78 } 52 79 } 53 80 },
+541
packages/prompts/test/__snapshots__/select-key.test.ts.snap
··· 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 + 3 + exports[`text (isCI = false) > can cancel by pressing escape 1`] = ` 4 + [ 5 + "<cursor.hide>", 6 + "│ 7 + ◆ foo 8 + │  a  Option A 9 + │  b  Option B 10 + └ 11 + ", 12 + "<cursor.backward count=999><cursor.up count=5>", 13 + "<cursor.down count=1>", 14 + "<erase.down>", 15 + "■ foo 16 + │ Option A 17 + │", 18 + " 19 + ", 20 + "<cursor.show>", 21 + ] 22 + `; 23 + 24 + exports[`text (isCI = false) > caseSensitive: true makes input case-sensitive 1`] = ` 25 + [ 26 + "<cursor.hide>", 27 + "│ 28 + ◆ foo 29 + │  a  Option a 30 + │  A  Option A 31 + │  b  Option B 32 + └ 33 + ", 34 + "<cursor.show>", 35 + "<cursor.backward count=999><cursor.up count=6>", 36 + "<cursor.down count=1>", 37 + "<erase.down>", 38 + "◇ foo 39 + │ Option A", 40 + " 41 + ", 42 + ] 43 + `; 44 + 45 + exports[`text (isCI = false) > caseSensitive: true makes options case-sensitive 1`] = ` 46 + [ 47 + "<cursor.hide>", 48 + "│ 49 + ◆ foo 50 + │  A  Option A 51 + │  b  Option B 52 + └ 53 + ", 54 + "<cursor.backward count=999><cursor.up count=5>", 55 + "<cursor.down count=1>", 56 + "<erase.down>", 57 + "■ foo 58 + │ Option A 59 + │", 60 + " 61 + ", 62 + "<cursor.show>", 63 + ] 64 + `; 65 + 66 + exports[`text (isCI = false) > input is case-insensitive by default 1`] = ` 67 + [ 68 + "<cursor.hide>", 69 + "│ 70 + ◆ foo 71 + │  a  Option A 72 + │  b  Option B 73 + └ 74 + ", 75 + "<cursor.show>", 76 + "<cursor.backward count=999><cursor.up count=5>", 77 + "<cursor.down count=1>", 78 + "<erase.down>", 79 + "◇ foo 80 + │ Option A", 81 + " 82 + ", 83 + ] 84 + `; 85 + 86 + exports[`text (isCI = false) > long cancelled labels are wrapped correctly 1`] = ` 87 + [ 88 + "<cursor.hide>", 89 + "│ 90 + ◆ Select an option: 91 + │  a  This is a somewhat long 92 + │ label This is a somewhat 93 + │ long label This is a 94 + │ somewhat long label This is 95 + │ a somewhat long label This 96 + │ is a somewhat long label 97 + │ This is a somewhat long 98 + │ label This is a somewhat 99 + │ long label This is a 100 + │ somewhat long label This is 101 + │ a somewhat long label This 102 + │ is a somewhat long label 103 + │  b  Short label 104 + └ 105 + ", 106 + "<cursor.backward count=999><cursor.up count=16>", 107 + "<cursor.down count=1>", 108 + "<erase.down>", 109 + "■ Select an option: 110 + │ This is a somewhat long  111 + │ label This is a somewhat  112 + │ long label This is a  113 + │ somewhat long label This is 114 + │  a somewhat long label This 115 + │  is a somewhat long label  116 + │ This is a somewhat long  117 + │ label This is a somewhat  118 + │ long label This is a  119 + │ somewhat long label This is 120 + │  a somewhat long label This 121 + │  is a somewhat long label 122 + │", 123 + " 124 + ", 125 + "<cursor.show>", 126 + ] 127 + `; 128 + 129 + exports[`text (isCI = false) > long option labels are wrapped correctly 1`] = ` 130 + [ 131 + "<cursor.hide>", 132 + "│ 133 + ◆ Select an option: 134 + │  a  This is a somewhat long 135 + │ label This is a somewhat 136 + │ long label This is a 137 + │ somewhat long label This is 138 + │ a somewhat long label This 139 + │ is a somewhat long label 140 + │ This is a somewhat long 141 + │ label This is a somewhat 142 + │ long label This is a 143 + │ somewhat long label This is 144 + │ a somewhat long label This 145 + │ is a somewhat long label 146 + │  b  Short label 147 + └ 148 + ", 149 + "<cursor.show>", 150 + "<cursor.backward count=999><cursor.up count=16>", 151 + "<cursor.down count=1>", 152 + "<erase.down>", 153 + "◇ Select an option: 154 + │ This is a somewhat long  155 + │ label This is a somewhat  156 + │ long label This is a  157 + │ somewhat long label This is 158 + │  a somewhat long label This 159 + │  is a somewhat long label  160 + │ This is a somewhat long  161 + │ label This is a somewhat  162 + │ long label This is a  163 + │ somewhat long label This is 164 + │  a somewhat long label This 165 + │  is a somewhat long label", 166 + " 167 + ", 168 + ] 169 + `; 170 + 171 + exports[`text (isCI = false) > long submitted labels are wrapped correctly 1`] = ` 172 + [ 173 + "<cursor.hide>", 174 + "│ 175 + ◆ Select an option: 176 + │  a  This is a somewhat long 177 + │ label This is a somewhat 178 + │ long label This is a 179 + │ somewhat long label This is 180 + │ a somewhat long label This 181 + │ is a somewhat long label 182 + │ This is a somewhat long 183 + │ label This is a somewhat 184 + │ long label This is a 185 + │ somewhat long label This is 186 + │ a somewhat long label This 187 + │ is a somewhat long label 188 + │  b  Short label 189 + └ 190 + ", 191 + "<cursor.show>", 192 + "<cursor.backward count=999><cursor.up count=16>", 193 + "<cursor.down count=1>", 194 + "<erase.down>", 195 + "◇ Select an option: 196 + │ This is a somewhat long  197 + │ label This is a somewhat  198 + │ long label This is a  199 + │ somewhat long label This is 200 + │  a somewhat long label This 201 + │  is a somewhat long label  202 + │ This is a somewhat long  203 + │ label This is a somewhat  204 + │ long label This is a  205 + │ somewhat long label This is 206 + │  a somewhat long label This 207 + │  is a somewhat long label", 208 + " 209 + ", 210 + ] 211 + `; 212 + 213 + exports[`text (isCI = false) > options are case-insensitive by default 1`] = ` 214 + [ 215 + "<cursor.hide>", 216 + "│ 217 + ◆ foo 218 + │  A  Option A 219 + │  b  Option B 220 + └ 221 + ", 222 + "<cursor.show>", 223 + "<cursor.backward count=999><cursor.up count=5>", 224 + "<cursor.down count=1>", 225 + "<erase.down>", 226 + "◇ foo 227 + │ Option A", 228 + " 229 + ", 230 + ] 231 + `; 232 + 233 + exports[`text (isCI = false) > renders message with options 1`] = ` 234 + [ 235 + "<cursor.hide>", 236 + "│ 237 + ◆ foo 238 + │  a  Option A 239 + │  b  Option B 240 + └ 241 + ", 242 + "<cursor.backward count=999><cursor.up count=5>", 243 + "<cursor.down count=1>", 244 + "<erase.down>", 245 + "◇ foo 246 + │ Option A", 247 + " 248 + ", 249 + "<cursor.show>", 250 + ] 251 + `; 252 + 253 + exports[`text (isCI = false) > selects option by keypress 1`] = ` 254 + [ 255 + "<cursor.hide>", 256 + "│ 257 + ◆ foo 258 + │  a  Option A 259 + │  b  Option B 260 + └ 261 + ", 262 + "<cursor.show>", 263 + "<cursor.backward count=999><cursor.up count=5>", 264 + "<cursor.down count=1>", 265 + "<erase.down>", 266 + "◇ foo 267 + │ Option B", 268 + " 269 + ", 270 + ] 271 + `; 272 + 273 + exports[`text (isCI = true) > can cancel by pressing escape 1`] = ` 274 + [ 275 + "<cursor.hide>", 276 + "│ 277 + ◆ foo 278 + │  a  Option A 279 + │  b  Option B 280 + └ 281 + ", 282 + "<cursor.backward count=999><cursor.up count=5>", 283 + "<cursor.down count=1>", 284 + "<erase.down>", 285 + "■ foo 286 + │ Option A 287 + │", 288 + " 289 + ", 290 + "<cursor.show>", 291 + ] 292 + `; 293 + 294 + exports[`text (isCI = true) > caseSensitive: true makes input case-sensitive 1`] = ` 295 + [ 296 + "<cursor.hide>", 297 + "│ 298 + ◆ foo 299 + │  a  Option a 300 + │  A  Option A 301 + │  b  Option B 302 + └ 303 + ", 304 + "<cursor.show>", 305 + "<cursor.backward count=999><cursor.up count=6>", 306 + "<cursor.down count=1>", 307 + "<erase.down>", 308 + "◇ foo 309 + │ Option A", 310 + " 311 + ", 312 + ] 313 + `; 314 + 315 + exports[`text (isCI = true) > caseSensitive: true makes options case-sensitive 1`] = ` 316 + [ 317 + "<cursor.hide>", 318 + "│ 319 + ◆ foo 320 + │  A  Option A 321 + │  b  Option B 322 + └ 323 + ", 324 + "<cursor.backward count=999><cursor.up count=5>", 325 + "<cursor.down count=1>", 326 + "<erase.down>", 327 + "■ foo 328 + │ Option A 329 + │", 330 + " 331 + ", 332 + "<cursor.show>", 333 + ] 334 + `; 335 + 336 + exports[`text (isCI = true) > input is case-insensitive by default 1`] = ` 337 + [ 338 + "<cursor.hide>", 339 + "│ 340 + ◆ foo 341 + │  a  Option A 342 + │  b  Option B 343 + └ 344 + ", 345 + "<cursor.show>", 346 + "<cursor.backward count=999><cursor.up count=5>", 347 + "<cursor.down count=1>", 348 + "<erase.down>", 349 + "◇ foo 350 + │ Option A", 351 + " 352 + ", 353 + ] 354 + `; 355 + 356 + exports[`text (isCI = true) > long cancelled labels are wrapped correctly 1`] = ` 357 + [ 358 + "<cursor.hide>", 359 + "│ 360 + ◆ Select an option: 361 + │  a  This is a somewhat long 362 + │ label This is a somewhat 363 + │ long label This is a 364 + │ somewhat long label This is 365 + │ a somewhat long label This 366 + │ is a somewhat long label 367 + │ This is a somewhat long 368 + │ label This is a somewhat 369 + │ long label This is a 370 + │ somewhat long label This is 371 + │ a somewhat long label This 372 + │ is a somewhat long label 373 + │  b  Short label 374 + └ 375 + ", 376 + "<cursor.backward count=999><cursor.up count=16>", 377 + "<cursor.down count=1>", 378 + "<erase.down>", 379 + "■ Select an option: 380 + │ This is a somewhat long  381 + │ label This is a somewhat  382 + │ long label This is a  383 + │ somewhat long label This is 384 + │  a somewhat long label This 385 + │  is a somewhat long label  386 + │ This is a somewhat long  387 + │ label This is a somewhat  388 + │ long label This is a  389 + │ somewhat long label This is 390 + │  a somewhat long label This 391 + │  is a somewhat long label 392 + │", 393 + " 394 + ", 395 + "<cursor.show>", 396 + ] 397 + `; 398 + 399 + exports[`text (isCI = true) > long option labels are wrapped correctly 1`] = ` 400 + [ 401 + "<cursor.hide>", 402 + "│ 403 + ◆ Select an option: 404 + │  a  This is a somewhat long 405 + │ label This is a somewhat 406 + │ long label This is a 407 + │ somewhat long label This is 408 + │ a somewhat long label This 409 + │ is a somewhat long label 410 + │ This is a somewhat long 411 + │ label This is a somewhat 412 + │ long label This is a 413 + │ somewhat long label This is 414 + │ a somewhat long label This 415 + │ is a somewhat long label 416 + │  b  Short label 417 + └ 418 + ", 419 + "<cursor.show>", 420 + "<cursor.backward count=999><cursor.up count=16>", 421 + "<cursor.down count=1>", 422 + "<erase.down>", 423 + "◇ Select an option: 424 + │ This is a somewhat long  425 + │ label This is a somewhat  426 + │ long label This is a  427 + │ somewhat long label This is 428 + │  a somewhat long label This 429 + │  is a somewhat long label  430 + │ This is a somewhat long  431 + │ label This is a somewhat  432 + │ long label This is a  433 + │ somewhat long label This is 434 + │  a somewhat long label This 435 + │  is a somewhat long label", 436 + " 437 + ", 438 + ] 439 + `; 440 + 441 + exports[`text (isCI = true) > long submitted labels are wrapped correctly 1`] = ` 442 + [ 443 + "<cursor.hide>", 444 + "│ 445 + ◆ Select an option: 446 + │  a  This is a somewhat long 447 + │ label This is a somewhat 448 + │ long label This is a 449 + │ somewhat long label This is 450 + │ a somewhat long label This 451 + │ is a somewhat long label 452 + │ This is a somewhat long 453 + │ label This is a somewhat 454 + │ long label This is a 455 + │ somewhat long label This is 456 + │ a somewhat long label This 457 + │ is a somewhat long label 458 + │  b  Short label 459 + └ 460 + ", 461 + "<cursor.show>", 462 + "<cursor.backward count=999><cursor.up count=16>", 463 + "<cursor.down count=1>", 464 + "<erase.down>", 465 + "◇ Select an option: 466 + │ This is a somewhat long  467 + │ label This is a somewhat  468 + │ long label This is a  469 + │ somewhat long label This is 470 + │  a somewhat long label This 471 + │  is a somewhat long label  472 + │ This is a somewhat long  473 + │ label This is a somewhat  474 + │ long label This is a  475 + │ somewhat long label This is 476 + │  a somewhat long label This 477 + │  is a somewhat long label", 478 + " 479 + ", 480 + ] 481 + `; 482 + 483 + exports[`text (isCI = true) > options are case-insensitive by default 1`] = ` 484 + [ 485 + "<cursor.hide>", 486 + "│ 487 + ◆ foo 488 + │  A  Option A 489 + │  b  Option B 490 + └ 491 + ", 492 + "<cursor.show>", 493 + "<cursor.backward count=999><cursor.up count=5>", 494 + "<cursor.down count=1>", 495 + "<erase.down>", 496 + "◇ foo 497 + │ Option A", 498 + " 499 + ", 500 + ] 501 + `; 502 + 503 + exports[`text (isCI = true) > renders message with options 1`] = ` 504 + [ 505 + "<cursor.hide>", 506 + "│ 507 + ◆ foo 508 + │  a  Option A 509 + │  b  Option B 510 + └ 511 + ", 512 + "<cursor.backward count=999><cursor.up count=5>", 513 + "<cursor.down count=1>", 514 + "<erase.down>", 515 + "◇ foo 516 + │ Option A", 517 + " 518 + ", 519 + "<cursor.show>", 520 + ] 521 + `; 522 + 523 + exports[`text (isCI = true) > selects option by keypress 1`] = ` 524 + [ 525 + "<cursor.hide>", 526 + "│ 527 + ◆ foo 528 + │  a  Option A 529 + │  b  Option B 530 + └ 531 + ", 532 + "<cursor.show>", 533 + "<cursor.backward count=999><cursor.up count=5>", 534 + "<cursor.down count=1>", 535 + "<erase.down>", 536 + "◇ foo 537 + │ Option B", 538 + " 539 + ", 540 + ] 541 + `;
+236
packages/prompts/test/select-key.test.ts
··· 1 + import { updateSettings } from '@clack/core'; 2 + import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 3 + import * as prompts from '../src/index.js'; 4 + import { MockReadable, MockWritable } from './test-utils.js'; 5 + 6 + describe.each(['true', 'false'])('text (isCI = %s)', (isCI) => { 7 + let originalCI: string | undefined; 8 + let output: MockWritable; 9 + let input: MockReadable; 10 + 11 + beforeAll(() => { 12 + originalCI = process.env.CI; 13 + process.env.CI = isCI; 14 + }); 15 + 16 + afterAll(() => { 17 + process.env.CI = originalCI; 18 + }); 19 + 20 + beforeEach(() => { 21 + output = new MockWritable(); 22 + input = new MockReadable(); 23 + }); 24 + 25 + afterEach(() => { 26 + vi.restoreAllMocks(); 27 + updateSettings({ withGuide: true }); 28 + }); 29 + 30 + test('renders message with options', async () => { 31 + const result = prompts.selectKey({ 32 + message: 'foo', 33 + options: [ 34 + { label: 'Option A', value: 'a' }, 35 + { label: 'Option B', value: 'b' }, 36 + ], 37 + input, 38 + output, 39 + }); 40 + 41 + input.emit('keypress', '', { name: 'return' }); 42 + 43 + const value = await result; 44 + 45 + expect(output.buffer).toMatchSnapshot(); 46 + expect(value).toBe(undefined); 47 + }); 48 + 49 + test('selects option by keypress', async () => { 50 + const result = prompts.selectKey({ 51 + message: 'foo', 52 + options: [ 53 + { label: 'Option A', value: 'a' }, 54 + { label: 'Option B', value: 'b' }, 55 + ], 56 + input, 57 + output, 58 + }); 59 + 60 + input.emit('keypress', 'b', { name: 'b' }); 61 + 62 + const value = await result; 63 + 64 + expect(output.buffer).toMatchSnapshot(); 65 + expect(value).toBe('b'); 66 + }); 67 + 68 + test('can cancel by pressing escape', async () => { 69 + const result = prompts.selectKey({ 70 + message: 'foo', 71 + options: [ 72 + { label: 'Option A', value: 'a' }, 73 + { label: 'Option B', value: 'b' }, 74 + ], 75 + input, 76 + output, 77 + }); 78 + 79 + input.emit('keypress', 'escape', { name: 'escape' }); 80 + 81 + const value = await result; 82 + 83 + expect(output.buffer).toMatchSnapshot(); 84 + expect(prompts.isCancel(value)).toBe(true); 85 + }); 86 + 87 + test('options are case-insensitive by default', async () => { 88 + const result = prompts.selectKey({ 89 + message: 'foo', 90 + options: [ 91 + { label: 'Option A', value: 'A' }, 92 + { label: 'Option B', value: 'b' }, 93 + ], 94 + input, 95 + output, 96 + }); 97 + 98 + input.emit('keypress', 'a', { name: 'a' }); 99 + 100 + const value = await result; 101 + 102 + expect(output.buffer).toMatchSnapshot(); 103 + expect(value).toBe('A'); 104 + }); 105 + 106 + test('input is case-insensitive by default', async () => { 107 + const result = prompts.selectKey({ 108 + message: 'foo', 109 + options: [ 110 + { label: 'Option A', value: 'a' }, 111 + { label: 'Option B', value: 'b' }, 112 + ], 113 + input, 114 + output, 115 + }); 116 + 117 + input.emit('keypress', 'a', { name: 'a', shift: true }); 118 + 119 + const value = await result; 120 + 121 + expect(output.buffer).toMatchSnapshot(); 122 + expect(value).toBe('a'); 123 + }); 124 + 125 + test('caseSensitive: true makes options case-sensitive', async () => { 126 + const result = prompts.selectKey({ 127 + message: 'foo', 128 + options: [ 129 + { label: 'Option A', value: 'A' }, 130 + { label: 'Option B', value: 'b' }, 131 + ], 132 + caseSensitive: true, 133 + input, 134 + output, 135 + }); 136 + 137 + input.emit('keypress', 'a', { name: 'a' }); 138 + input.emit('keypress', '', { name: 'escape' }); 139 + 140 + const value = await result; 141 + 142 + expect(output.buffer).toMatchSnapshot(); 143 + expect(prompts.isCancel(value)).toBe(true); 144 + }); 145 + 146 + test('caseSensitive: true makes input case-sensitive', async () => { 147 + const result = prompts.selectKey({ 148 + message: 'foo', 149 + options: [ 150 + { label: 'Option a', value: 'a' }, 151 + { label: 'Option A', value: 'A' }, 152 + { label: 'Option B', value: 'b' }, 153 + ], 154 + caseSensitive: true, 155 + input, 156 + output, 157 + }); 158 + 159 + input.emit('keypress', 'a', { name: 'a', shift: true }); 160 + 161 + const value = await result; 162 + 163 + expect(output.buffer).toMatchSnapshot(); 164 + expect(value).toBe('A'); 165 + }); 166 + 167 + test('long option labels are wrapped correctly', async () => { 168 + output.columns = 40; 169 + 170 + const result = prompts.selectKey({ 171 + message: 'Select an option:', 172 + options: [ 173 + { 174 + label: 'This is a somewhat long label '.repeat(10).trimEnd(), 175 + value: 'a', 176 + }, 177 + { label: 'Short label', value: 'b' }, 178 + ], 179 + input, 180 + output, 181 + }); 182 + 183 + input.emit('keypress', 'a', { name: 'a' }); 184 + 185 + const value = await result; 186 + 187 + expect(output.buffer).toMatchSnapshot(); 188 + expect(value).toBe('a'); 189 + }); 190 + 191 + test('long cancelled labels are wrapped correctly', async () => { 192 + output.columns = 40; 193 + 194 + const result = prompts.selectKey({ 195 + message: 'Select an option:', 196 + options: [ 197 + { 198 + label: 'This is a somewhat long label '.repeat(10).trimEnd(), 199 + value: 'a', 200 + }, 201 + { label: 'Short label', value: 'b' }, 202 + ], 203 + input, 204 + output, 205 + }); 206 + 207 + input.emit('keypress', '', { name: 'escape' }); 208 + 209 + await result; 210 + 211 + expect(output.buffer).toMatchSnapshot(); 212 + }); 213 + 214 + test('long submitted labels are wrapped correctly', async () => { 215 + output.columns = 40; 216 + 217 + const result = prompts.selectKey({ 218 + message: 'Select an option:', 219 + options: [ 220 + { 221 + label: 'This is a somewhat long label '.repeat(10).trimEnd(), 222 + value: 'a', 223 + }, 224 + { label: 'Short label', value: 'b' }, 225 + ], 226 + input, 227 + output, 228 + }); 229 + 230 + input.emit('keypress', 'a', { name: 'a' }); 231 + 232 + await result; 233 + 234 + expect(output.buffer).toMatchSnapshot(); 235 + }); 236 + });