diff --git a/.changeset/afraid-socks-deny.md b/.changeset/afraid-socks-deny.md new file mode 100644 index 0000000..877fe63 --- /dev/null +++ b/.changeset/afraid-socks-deny.md @@ -0,0 +1,5 @@ +--- +"@clack/prompts": patch +--- + +Fixes multiselect only shows hints on the first item in the options list. Now correctly shows hints for all selected options with hint property. diff --git a/packages/prompts/src/__snapshots__/index.test.ts.snap b/packages/prompts/src/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000..d38c1f8 --- /dev/null +++ b/packages/prompts/src/__snapshots__/index.test.ts.snap @@ -0,0 +1,3550 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`prompts (isCI = false) > confirm > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Yes / ○ No +└ +", + "", + "", + "", + "■ foo +│ No +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > confirm > can set initialValue 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ○ Yes / ● No +└ +", + "", + "", + "", + "◇ foo +│ No", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > confirm > left arrow moves to previous choice 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Yes / ○ No +└ +", + "", + "", + "", + "│ ○ Yes / ● No", + "", + "", + "", + "", + "│ ● Yes / ○ No", + "", + "", + "", + "", + "◇ foo +│ Yes", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > confirm > renders custom active choice 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● bleep / ○ No +└ +", + "", + "", + "", + "◇ foo +│ bleep", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > confirm > renders custom inactive choice 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Yes / ○ bleep +└ +", + "", + "", + "", + "◇ foo +│ Yes", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > confirm > renders message with choices 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Yes / ○ No +└ +", + "", + "", + "", + "◇ foo +│ Yes", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > confirm > right arrow moves to next choice 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Yes / ○ No +└ +", + "", + "", + "", + "│ ○ Yes / ● No", + "", + "", + "", + "", + "◇ foo +│ No", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > can deselect an option 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ │ ◼ group1value0", + "", + "", + "", + "", + "│ │ ◼ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ ◼ group1 +│ │ ◼ group1value0 +│ └ ◼ group1value1 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◼ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > can select a group 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ ◼ group1 +│ │ ◼ group1value0 +│ └ ◼ group1value1 +└ +", + "", + "", + "", + "◇ foo +│ group1value0, group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > can select a group by selecting all members 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ │ ◼ group1value0", + "", + "", + "", + "", + "│ │ ◼ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ ◼ group1 +│ │ ◼ group1value0 +│ └ ◼ group1value1 +└ +", + "", + "", + "", + "◇ foo +│ group1value0, group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > can select multiple options 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ │ ◻ group1value1 +│ └ ◻ group1value2 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◻ group1value0 +│ │ ◻ group1value1 +│ └ ◻ group1value2 +└ +", + "", + "", + "", + "│ │ ◼ group1value0", + "", + "", + "", + "", + "│ │ ◼ group1value0 +│ │ ◻ group1value1 +│ └ ◻ group1value2 +└ +", + "", + "", + "", + "│ │ ◼ group1value1", + "", + "", + "", + "", + "◇ foo +│ group1value0, group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > can submit empty selection when require = false 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "◇ foo +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > cursorAt sets initial selection 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ └ ◼ group1value1", + "", + "", + "", + "", + "◇ foo +│ group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > initial values can be set 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◼ group1value1 +└ +", + "", + "", + "", + "◇ foo +│ group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > renders error when nothing selected 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "▲ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ Please select at least one option. +Press  space  to select,  enter  to submit +", + "", + "", + "", + "◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ │ ◼ group1value0", + "", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > renders message with options 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ │ ◼ group1value0", + "", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > selectableGroups = false > cannot select groups 1`] = ` +[ + "[?25l", + "│ +◆ foo +│  group1 +│  ◻ group1value0 +│  ◻ group1value1 +└ +", + "", + "", + "", + "│  ◼ group1value0", + "", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > selectableGroups = false > selecting all members of group does not select group 1`] = ` +[ + "[?25l", + "│ +◆ foo +│  group1 +│  ◻ group1value0 +│  ◻ group1value1 +└ +", + "", + "", + "", + "│  ◼ group1value0", + "", + "", + "", + "", + "│  ◼ group1value0 +│  ◻ group1value1 +└ +", + "", + "", + "", + "│  ◼ group1value1", + "", + "", + "", + "", + "◇ foo +│ group1value0, group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > values can be non-primitive 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ value0 +│ └ ◻ value1 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◻ value0 +│ └ ◻ value1 +└ +", + "", + "", + "", + "│ │ ◼ value0", + "", + "", + "", + "", + "◇ foo +│ value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "■ foo +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > can render option hints 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 (Hint 0) +│ ◻ opt1 +└ +", + "", + "", + "", + "│ ◼ opt0 (Hint 0)", + "", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > can set cursorAt to preselect an option 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "│ ◼ opt1", + "", + "", + "", + "", + "◇ foo +│ opt1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > can set custom labels 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ Option 0 +│ ◻ Option 1 +└ +", + "", + "", + "", + "│ ◼ Option 0", + "", + "", + "", + "", + "◇ foo +│ Option 0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > can set initial values 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◼ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > can submit without selection when required = false 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "◇ foo +│ none", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > maxItems renders a sliding window 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ... +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ◻ opt5 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt3 +│ ◻ opt4 +│ ◻ opt5 +│ ◻ opt6 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt4 +│ ◻ opt5 +│ ◻ opt6 +│ ◻ opt7 +│ ... +└ +", + "", + "", + "", + "│ ◼ opt6", + "", + "", + "", + "", + "◇ foo +│ opt6", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > renders message 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "│ ◼ opt0", + "", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > renders multiple cancelled values 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt0", + "", + "", + "", + "", + "│ ◼ opt0 +│ ◻ opt1 +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt1", + "", + "", + "", + "", + "■ foo +│ opt0, opt1 +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > renders multiple selected options 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt0", + "", + "", + "", + "", + "│ ◼ opt0 +│ ◻ opt1 +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt1", + "", + "", + "", + "", + "│ ◼ opt1 +│ ◻ opt2 +└ +", + "", + "", + "", + "◇ foo +│ opt0, opt1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > renders validation errors 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "▲ foo +│ ◻ opt0 +│ ◻ opt1 +└ Please select at least one option. +Press  space  to select,  enter  to submit +", + "", + "", + "", + "◆ foo +│ ◼ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > shows hints for all selected options 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◼ opt0 (Hint 0) +│ ◼ opt1 (Hint 1) +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt0 (Hint 0) +│ ◼ opt1 (Hint 1) +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt1 (Hint 1) +│ ◻ opt2 (Hint 2) +└ +", + "", + "", + "", + "◇ foo +│ opt0, opt1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > sliding window loops downwards 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ... +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ◻ opt5 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt3 +│ ◻ opt4 +│ ◻ opt5 +│ ◻ opt6 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt4 +│ ◻ opt5 +│ ◻ opt6 +│ ◻ opt7 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt5 +│ ◻ opt6 +│ ◻ opt7 +│ ◻ opt8 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt6 +│ ◻ opt7 +│ ◻ opt8 +│ ◻ opt9 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt7 +│ ◻ opt8 +│ ◻ opt9 +│ ◻ opt10 +│ ◻ opt11 +└ +", + "", + "", + "", + "│ ◻ opt9 +│ ◻ opt10 +│ ◻ opt11 +└ +", + "", + "", + "", + "│ ◻ opt10 +│ ◻ opt11 +└ +", + "", + "", + "", + "│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◼ opt0", + "", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > multiselect > sliding window loops upwards 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ... +│ ◻ opt7 +│ ◻ opt8 +│ ◻ opt9 +│ ◻ opt10 +│ ◻ opt11 +└ +", + "", + "", + "", + "│ ◼ opt11", + "", + "", + "", + "", + "◇ foo +│ opt11", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > password > renders and clears validation errors 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ ▪_", + "", + "", + "", + "", + "▲ foo +│ ▪ +└ Password must be at least 2 characters +", + "", + "", + "", + "◆ foo +│ ▪▪_ +└ +", + "", + "", + "", + "◇ foo +│ ▪▪", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > password > renders cancelled value 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ ▪_", + "", + "", + "", + "", + "■ foo +│ ▪ +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > password > renders custom mask 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ *_", + "", + "", + "", + "", + "│ **_", + "", + "", + "", + "", + "◇ foo +│ **", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > password > renders masked value 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ ▪_", + "", + "", + "", + "", + "│ ▪▪_", + "", + "", + "", + "", + "◇ foo +│ ▪▪", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > password > renders message 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "◇ foo +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > select > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "■ foo +│ opt0 +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > select > down arrow selects next option 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "│ ○ opt0 +│ ● opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > select > renders option hints 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● opt0 (Hint 0) +│ ○ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > select > renders option labels 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Option 0 +│ ○ Option 1 +└ +", + "", + "", + "", + "◇ foo +│ Option 0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > select > renders options and message 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > select > up arrow selects previous option 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "│ ○ opt0 +│ ● opt1 +└ +", + "", + "", + "", + "│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > spinner > message > sets message for next frame 1`] = ` +[ + "[?25l", + "│ +", + "◒ ", + "", + "", + "◐ foo", +] +`; + +exports[`prompts (isCI = false) > spinner > start > renders frames at interval 1`] = ` +[ + "[?25l", + "│ +", + "◒ ", + "", + "", + "◐ ", + "", + "", + "◓ ", + "", + "", + "◑ ", +] +`; + +exports[`prompts (isCI = false) > spinner > start > renders message 1`] = ` +[ + "[?25l", + "│ +", + "◒ foo", +] +`; + +exports[`prompts (isCI = false) > spinner > start > renders timer when indicator is "timer" 1`] = ` +[ + "[?25l", + "│ +", + "◒ [0s]", +] +`; + +exports[`prompts (isCI = false) > spinner > stop > renders cancel symbol if code = 1 1`] = ` +[ + "[?25l", + "│ +", + "◒ ", + "", + "", + "■ +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > spinner > stop > renders error symbol if code > 1 1`] = ` +[ + "[?25l", + "│ +", + "◒ ", + "", + "", + "▲ +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > spinner > stop > renders message 1`] = ` +[ + "[?25l", + "│ +", + "◒ ", + "", + "", + "◇ foo +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > spinner > stop > renders submit symbol and stops spinner 1`] = ` +[ + "[?25l", + "│ +", + "◒ ", + "", + "", + "◇ +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "■ foo +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > defaultValue sets the value but does not render 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "◇ foo +│ bar", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > renders cancelled value if one set 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "│ xy█", + "", + "", + "", + "", + "■ foo +│ xy +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > renders message 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "◇ foo +│ undefined", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > renders placeholder if set 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ bar +└ +", + "", + "", + "", + "◇ foo +│ bar", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > renders submitted value 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "│ xy█", + "", + "", + "", + "", + "◇ foo +│ xy", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > validation errors render and clear (using Error) 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "▲ foo +│ x█ +└ should be xy +", + "", + "", + "", + "◆ foo +│ xy█ +└ +", + "", + "", + "", + "◇ foo +│ xy", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > validation errors render and clear 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "▲ foo +│ x█ +└ should be xy +", + "", + "", + "", + "◆ foo +│ xy█ +└ +", + "", + "", + "", + "◇ foo +│ xy", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > confirm > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Yes / ○ No +└ +", + "", + "", + "", + "■ foo +│ No +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > confirm > can set initialValue 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ○ Yes / ● No +└ +", + "", + "", + "", + "◇ foo +│ No", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > confirm > left arrow moves to previous choice 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Yes / ○ No +└ +", + "", + "", + "", + "│ ○ Yes / ● No", + "", + "", + "", + "", + "│ ● Yes / ○ No", + "", + "", + "", + "", + "◇ foo +│ Yes", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > confirm > renders custom active choice 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● bleep / ○ No +└ +", + "", + "", + "", + "◇ foo +│ bleep", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > confirm > renders custom inactive choice 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Yes / ○ bleep +└ +", + "", + "", + "", + "◇ foo +│ Yes", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > confirm > renders message with choices 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Yes / ○ No +└ +", + "", + "", + "", + "◇ foo +│ Yes", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > confirm > right arrow moves to next choice 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Yes / ○ No +└ +", + "", + "", + "", + "│ ○ Yes / ● No", + "", + "", + "", + "", + "◇ foo +│ No", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > can deselect an option 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ │ ◼ group1value0", + "", + "", + "", + "", + "│ │ ◼ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ ◼ group1 +│ │ ◼ group1value0 +│ └ ◼ group1value1 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◼ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > can select a group 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ ◼ group1 +│ │ ◼ group1value0 +│ └ ◼ group1value1 +└ +", + "", + "", + "", + "◇ foo +│ group1value0, group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > can select a group by selecting all members 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ │ ◼ group1value0", + "", + "", + "", + "", + "│ │ ◼ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ ◼ group1 +│ │ ◼ group1value0 +│ └ ◼ group1value1 +└ +", + "", + "", + "", + "◇ foo +│ group1value0, group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > can select multiple options 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ │ ◻ group1value1 +│ └ ◻ group1value2 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◻ group1value0 +│ │ ◻ group1value1 +│ └ ◻ group1value2 +└ +", + "", + "", + "", + "│ │ ◼ group1value0", + "", + "", + "", + "", + "│ │ ◼ group1value0 +│ │ ◻ group1value1 +│ └ ◻ group1value2 +└ +", + "", + "", + "", + "│ │ ◼ group1value1", + "", + "", + "", + "", + "◇ foo +│ group1value0, group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > can submit empty selection when require = false 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "◇ foo +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > cursorAt sets initial selection 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ └ ◼ group1value1", + "", + "", + "", + "", + "◇ foo +│ group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > initial values can be set 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◼ group1value1 +└ +", + "", + "", + "", + "◇ foo +│ group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > renders error when nothing selected 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "▲ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ Please select at least one option. +Press  space  to select,  enter  to submit +", + "", + "", + "", + "◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "│ │ ◼ group1value0", + "", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > renders message with options 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ │ ◼ group1value0", + "", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > selectableGroups = false > cannot select groups 1`] = ` +[ + "[?25l", + "│ +◆ foo +│  group1 +│  ◻ group1value0 +│  ◻ group1value1 +└ +", + "", + "", + "", + "│  ◼ group1value0", + "", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > selectableGroups = false > selecting all members of group does not select group 1`] = ` +[ + "[?25l", + "│ +◆ foo +│  group1 +│  ◻ group1value0 +│  ◻ group1value1 +└ +", + "", + "", + "", + "│  ◼ group1value0", + "", + "", + "", + "", + "│  ◼ group1value0 +│  ◻ group1value1 +└ +", + "", + "", + "", + "│  ◼ group1value1", + "", + "", + "", + "", + "◇ foo +│ group1value0, group1value1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > values can be non-primitive 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ value0 +│ └ ◻ value1 +└ +", + "", + "", + "", + "│ ◻ group1 +│ │ ◻ value0 +│ └ ◻ value1 +└ +", + "", + "", + "", + "│ │ ◼ value0", + "", + "", + "", + "", + "◇ foo +│ value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "■ foo +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > can render option hints 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 (Hint 0) +│ ◻ opt1 +└ +", + "", + "", + "", + "│ ◼ opt0 (Hint 0)", + "", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > can set cursorAt to preselect an option 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "│ ◼ opt1", + "", + "", + "", + "", + "◇ foo +│ opt1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > can set custom labels 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ Option 0 +│ ◻ Option 1 +└ +", + "", + "", + "", + "│ ◼ Option 0", + "", + "", + "", + "", + "◇ foo +│ Option 0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > can set initial values 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◼ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > can submit without selection when required = false 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "◇ foo +│ none", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > maxItems renders a sliding window 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ... +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ◻ opt5 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt3 +│ ◻ opt4 +│ ◻ opt5 +│ ◻ opt6 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt4 +│ ◻ opt5 +│ ◻ opt6 +│ ◻ opt7 +│ ... +└ +", + "", + "", + "", + "│ ◼ opt6", + "", + "", + "", + "", + "◇ foo +│ opt6", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > renders message 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "│ ◼ opt0", + "", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > renders multiple cancelled values 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt0", + "", + "", + "", + "", + "│ ◼ opt0 +│ ◻ opt1 +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt1", + "", + "", + "", + "", + "■ foo +│ opt0, opt1 +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > renders multiple selected options 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt0", + "", + "", + "", + "", + "│ ◼ opt0 +│ ◻ opt1 +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt1", + "", + "", + "", + "", + "│ ◼ opt1 +│ ◻ opt2 +└ +", + "", + "", + "", + "◇ foo +│ opt0, opt1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > renders validation errors 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "▲ foo +│ ◻ opt0 +│ ◻ opt1 +└ Please select at least one option. +Press  space  to select,  enter  to submit +", + "", + "", + "", + "◆ foo +│ ◼ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > shows hints for all selected options 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◼ opt0 (Hint 0) +│ ◼ opt1 (Hint 1) +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt0 (Hint 0) +│ ◼ opt1 (Hint 1) +│ ◻ opt2 +└ +", + "", + "", + "", + "│ ◼ opt1 (Hint 1) +│ ◻ opt2 (Hint 2) +└ +", + "", + "", + "", + "◇ foo +│ opt0, opt1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > sliding window loops downwards 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ... +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ◻ opt5 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt3 +│ ◻ opt4 +│ ◻ opt5 +│ ◻ opt6 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt4 +│ ◻ opt5 +│ ◻ opt6 +│ ◻ opt7 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt5 +│ ◻ opt6 +│ ◻ opt7 +│ ◻ opt8 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt6 +│ ◻ opt7 +│ ◻ opt8 +│ ◻ opt9 +│ ... +└ +", + "", + "", + "", + "│ ◻ opt7 +│ ◻ opt8 +│ ◻ opt9 +│ ◻ opt10 +│ ◻ opt11 +└ +", + "", + "", + "", + "│ ◻ opt9 +│ ◻ opt10 +│ ◻ opt11 +└ +", + "", + "", + "", + "│ ◻ opt10 +│ ◻ opt11 +└ +", + "", + "", + "", + "│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ◼ opt0", + "", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > multiselect > sliding window loops upwards 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +│ ◻ opt2 +│ ◻ opt3 +│ ◻ opt4 +│ ... +└ +", + "", + "", + "", + "│ ... +│ ◻ opt7 +│ ◻ opt8 +│ ◻ opt9 +│ ◻ opt10 +│ ◻ opt11 +└ +", + "", + "", + "", + "│ ◼ opt11", + "", + "", + "", + "", + "◇ foo +│ opt11", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > password > renders and clears validation errors 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ ▪_", + "", + "", + "", + "", + "▲ foo +│ ▪ +└ Password must be at least 2 characters +", + "", + "", + "", + "◆ foo +│ ▪▪_ +└ +", + "", + "", + "", + "◇ foo +│ ▪▪", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > password > renders cancelled value 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ ▪_", + "", + "", + "", + "", + "■ foo +│ ▪ +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > password > renders custom mask 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ *_", + "", + "", + "", + "", + "│ **_", + "", + "", + "", + "", + "◇ foo +│ **", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > password > renders masked value 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ ▪_", + "", + "", + "", + "", + "│ ▪▪_", + "", + "", + "", + "", + "◇ foo +│ ▪▪", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > password > renders message 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "◇ foo +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > select > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "■ foo +│ opt0 +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > select > down arrow selects next option 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "│ ○ opt0 +│ ● opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt1", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > select > renders option hints 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● opt0 (Hint 0) +│ ○ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > select > renders option labels 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● Option 0 +│ ○ Option 1 +└ +", + "", + "", + "", + "◇ foo +│ Option 0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > select > renders options and message 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > select > up arrow selects previous option 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "│ ○ opt0 +│ ● opt1 +└ +", + "", + "", + "", + "│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > spinner > message > sets message for next frame 1`] = ` +[ + "[?25l", + "│ +", + "◒ ...", + " +", + "", + "", + "◐ foo...", +] +`; + +exports[`prompts (isCI = true) > spinner > start > renders frames at interval 1`] = ` +[ + "[?25l", + "│ +", + "◒ ...", +] +`; + +exports[`prompts (isCI = true) > spinner > start > renders message 1`] = ` +[ + "[?25l", + "│ +", + "◒ foo...", +] +`; + +exports[`prompts (isCI = true) > spinner > start > renders timer when indicator is "timer" 1`] = ` +[ + "[?25l", + "│ +", + "◒ ...", +] +`; + +exports[`prompts (isCI = true) > spinner > stop > renders cancel symbol if code = 1 1`] = ` +[ + "[?25l", + "│ +", + "◒ ...", + " +", + "", + "", + "■ +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > spinner > stop > renders error symbol if code > 1 1`] = ` +[ + "[?25l", + "│ +", + "◒ ...", + " +", + "", + "", + "▲ +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > spinner > stop > renders message 1`] = ` +[ + "[?25l", + "│ +", + "◒ ...", + " +", + "", + "", + "◇ foo +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > spinner > stop > renders submit symbol and stops spinner 1`] = ` +[ + "[?25l", + "│ +", + "◒ ...", + " +", + "", + "", + "◇ +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "■ foo +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > defaultValue sets the value but does not render 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "◇ foo +│ bar", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > renders cancelled value if one set 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "│ xy█", + "", + "", + "", + "", + "■ foo +│ xy +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > renders message 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "◇ foo +│ undefined", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > renders placeholder if set 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ bar +└ +", + "", + "", + "", + "◇ foo +│ bar", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > renders submitted value 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "│ xy█", + "", + "", + "", + "", + "◇ foo +│ xy", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > validation errors render and clear (using Error) 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "▲ foo +│ x█ +└ should be xy +", + "", + "", + "", + "◆ foo +│ xy█ +└ +", + "", + "", + "", + "◇ foo +│ xy", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > validation errors render and clear 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "▲ foo +│ x█ +└ should be xy +", + "", + "", + "", + "◆ foo +│ xy█ +└ +", + "", + "", + "", + "◇ foo +│ xy", + " +", + "[?25h", +] +`; diff --git a/packages/prompts/src/index.test.ts b/packages/prompts/src/index.test.ts new file mode 100644 index 0000000..4e89f68 --- /dev/null +++ b/packages/prompts/src/index.test.ts @@ -0,0 +1,1208 @@ +import { Readable, Writable } from 'node:stream'; +import { afterAll, afterEach, beforeAll, 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(); + } +} + +class MockReadable extends Readable { + protected _buffer: unknown[] | null = []; + + _read() { + if (this._buffer === null) { + this.push(null); + return; + } + + for (const val of this._buffer) { + this.push(val); + } + + this._buffer = []; + } + + pushValue(val: unknown): void { + this._buffer?.push(val); + } + + close(): void { + this._buffer = null; + } +} + +describe.each(['true', 'false'])('prompts (isCI = %s)', (isCI) => { + let originalCI: string | undefined; + let output: MockWritable; + let input: MockReadable; + + beforeAll(() => { + originalCI = process.env.CI; + process.env.CI = isCI; + }); + + afterAll(() => { + process.env.CI = originalCI; + }); + + beforeEach(() => { + output = new MockWritable(); + input = new MockReadable(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('spinner', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + 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(); + }); + }); + }); + + describe('text', () => { + test('renders message', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders placeholder if set', async () => { + const result = prompts.text({ + message: 'foo', + placeholder: 'bar', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + // TODO (43081j): uncomment this when #263 is fixed + // expect(value).toBe('bar'); + }); + + test(' applies placeholder', async () => { + const result = prompts.text({ + message: 'foo', + placeholder: 'bar', + input, + output, + }); + + input.emit('keypress', '\t', { name: 'tab' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('bar'); + }); + + test('can cancel', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'escape', { name: 'escape' }); + + const value = await result; + + expect(prompts.isCancel(value)).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders cancelled value if one set', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', 'y', { name: 'y' }); + input.emit('keypress', '', { name: 'escape' }); + + const value = await result; + + expect(prompts.isCancel(value)).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders submitted value', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', 'y', { name: 'y' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('xy'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('defaultValue sets the value but does not render', async () => { + const result = prompts.text({ + message: 'foo', + defaultValue: 'bar', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('bar'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('validation errors render and clear', async () => { + const result = prompts.text({ + message: 'foo', + validate: (val) => (val !== 'xy' ? 'should be xy' : undefined), + input, + output, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', '', { name: 'return' }); + input.emit('keypress', 'y', { name: 'y' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('xy'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('validation errors render and clear (using Error)', async () => { + const result = prompts.text({ + message: 'foo', + validate: (val) => (val !== 'xy' ? new Error('should be xy') : undefined), + input, + output, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', '', { name: 'return' }); + input.emit('keypress', 'y', { name: 'y' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('xy'); + expect(output.buffer).toMatchSnapshot(); + }); + }); + + describe('confirm', () => { + test('renders message with choices', async () => { + const result = prompts.confirm({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders custom active choice', async () => { + const result = prompts.confirm({ + message: 'foo', + active: 'bleep', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders custom inactive choice', async () => { + const result = prompts.confirm({ + message: 'foo', + inactive: 'bleep', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('right arrow moves to next choice', async () => { + const result = prompts.confirm({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'right', { name: 'right' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe(false); + expect(output.buffer).toMatchSnapshot(); + }); + + test('left arrow moves to previous choice', async () => { + const result = prompts.confirm({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'right', { name: 'right' }); + input.emit('keypress', 'left', { name: 'left' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can cancel', async () => { + const result = prompts.confirm({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'escape', { name: 'escape' }); + + const value = await result; + + expect(prompts.isCancel(value)).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can set initialValue', async () => { + const result = prompts.confirm({ + message: 'foo', + initialValue: false, + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe(false); + expect(output.buffer).toMatchSnapshot(); + }); + }); + + describe('select', () => { + test('renders options and message', async () => { + const result = prompts.select({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('opt0'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('down arrow selects next option', async () => { + const result = prompts.select({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + input, + output, + }); + + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('opt1'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('up arrow selects previous option', async () => { + const result = prompts.select({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + input, + output, + }); + + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'up' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('opt0'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can cancel', async () => { + const result = prompts.select({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + input, + output, + }); + + input.emit('keypress', 'escape', { name: 'escape' }); + + const value = await result; + + expect(prompts.isCancel(value)).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders option labels', async () => { + const result = prompts.select({ + message: 'foo', + options: [ + { value: 'opt0', label: 'Option 0' }, + { value: 'opt1', label: 'Option 1' }, + ], + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('opt0'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders option hints', async () => { + const result = prompts.select({ + message: 'foo', + options: [ + { value: 'opt0', hint: 'Hint 0' }, + { value: 'opt1', hint: 'Hint 1' }, + ], + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('opt0'); + expect(output.buffer).toMatchSnapshot(); + }); + }); + + describe('multiselect', () => { + test('renders message', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + input, + output, + }); + + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt0']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders multiple selected options', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }, { value: 'opt2' }], + input, + output, + }); + + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt0', 'opt1']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can cancel', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + input, + output, + }); + + input.emit('keypress', 'escape', { name: 'escape' }); + + const value = await result; + + expect(prompts.isCancel(value)).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders validation errors', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + input, + output, + }); + + // try submit with nothing selected + input.emit('keypress', '', { name: 'return' }); + // select and submit + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt0']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can submit without selection when required = false', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + required: false, + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual([]); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can set cursorAt to preselect an option', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + cursorAt: 'opt1', + input, + output, + }); + + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt1']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can set initial values', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + initialValues: ['opt1'], + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt1']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('maxItems renders a sliding window', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [...Array(12).keys()].map((k) => ({ + value: `opt${k}`, + })), + maxItems: 6, + input, + output, + }); + + for (let i = 0; i < 6; i++) { + input.emit('keypress', '', { name: 'down' }); + } + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt6']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('sliding window loops upwards', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [...Array(12).keys()].map((k) => ({ + value: `opt${k}`, + })), + maxItems: 6, + input, + output, + }); + + input.emit('keypress', '', { name: 'up' }); + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt11']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('sliding window loops downwards', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [...Array(12).keys()].map((k) => ({ + value: `opt${k}`, + })), + maxItems: 6, + input, + output, + }); + + for (let i = 0; i < 12; i++) { + input.emit('keypress', '', { name: 'down' }); + } + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt0']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can set custom labels', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [ + { value: 'opt0', label: 'Option 0' }, + { value: 'opt1', label: 'Option 1' }, + ], + input, + output, + }); + + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt0']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can render option hints', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [ + { value: 'opt0', hint: 'Hint 0' }, + { value: 'opt1', hint: 'Hint 1' }, + ], + input, + output, + }); + + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt0']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('shows hints for all selected options', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [ + { value: 'opt0', hint: 'Hint 0' }, + { value: 'opt1', hint: 'Hint 1' }, + { value: 'opt2', hint: 'Hint 2' }, + ], + initialValues: ['opt0', 'opt1'], + input, + output, + }); + + // Check that both selected options show their hints + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['opt0', 'opt1']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders multiple cancelled values', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }, { value: 'opt2' }], + input, + output, + }); + + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'escape' }); + + const value = await result; + + expect(prompts.isCancel(value)).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + }); + + describe('password', () => { + test('renders message', async () => { + const result = prompts.password({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders masked value', async () => { + const result = prompts.password({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', 'y', { name: 'y' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('xy'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders custom mask', async () => { + const result = prompts.password({ + message: 'foo', + mask: '*', + input, + output, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', 'y', { name: 'y' }); + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders and clears validation errors', async () => { + const result = prompts.password({ + message: 'foo', + validate: (value) => { + if (value.length < 2) { + return 'Password must be at least 2 characters'; + } + + return undefined; + }, + input, + output, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', '', { name: 'return' }); + input.emit('keypress', 'y', { name: 'y' }); + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders cancelled value', async () => { + const result = prompts.password({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', '', { name: 'escape' }); + + const value = await result; + + expect(prompts.isCancel(value)).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + }); + + describe('groupMultiselect', () => { + test('renders message with options', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + group2: [{ value: 'group2value0' }], + }, + }); + + // Select the first non-group option + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + + // submit + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['group1value0']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can select multiple options', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }, { value: 'group1value2' }], + }, + }); + + // Select the first non-group option + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + // Select the second non-group option + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + + // submit + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['group1value0', 'group1value1']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can select a group', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + }, + }); + + // Select the group as a whole + input.emit('keypress', '', { name: 'space' }); + + // submit + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['group1value0', 'group1value1']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can select a group by selecting all members', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + }, + }); + + // Select the first group option + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + // Select the second group option + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + + // submit + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['group1value0', 'group1value1']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('can deselect an option', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + }, + }); + + // Select the first group option + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + // Select the second group option + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + // Deselect it + input.emit('keypress', '', { name: 'space' }); + + // submit + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['group1value0']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders error when nothing selected', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + }, + }); + + // try submit + input.emit('keypress', '', { name: 'return' }); + // now select something and submit + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['group1value0']); + expect(output.buffer).toMatchSnapshot(); + }); + + describe('selectableGroups = false', () => { + test('cannot select groups', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + }, + selectableGroups: false, + }); + + // first selectable item should be group's child + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['group1value0']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('selecting all members of group does not select group', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + }, + selectableGroups: false, + }); + + // first selectable item should be group's child + input.emit('keypress', '', { name: 'space' }); + // select second item + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + // submit + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['group1value0', 'group1value1']); + expect(output.buffer).toMatchSnapshot(); + }); + }); + + test('can submit empty selection when require = false', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + }, + required: false, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual([]); + expect(output.buffer).toMatchSnapshot(); + }); + + test('cursorAt sets initial selection', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + }, + cursorAt: 'group1value1', + }); + + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['group1value1']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('initial values can be set', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + }, + initialValues: ['group1value1'], + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual(['group1value1']); + expect(output.buffer).toMatchSnapshot(); + }); + + test('values can be non-primitive', async () => { + const value0 = Symbol(); + const value1 = Symbol(); + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [ + { value: value0, label: 'value0' }, + { value: value1, label: 'value1' }, + ], + }, + }); + + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual([value0]); + expect(output.buffer).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 01cd736..ad3ac64 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -366,7 +366,9 @@ export const multiselect = (opts: MultiSelectOptions) => { }`; } if (state === 'selected') { - return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)} ${ + option.hint ? color.dim(`(${option.hint})`) : '' + }`; } if (state === 'cancelled') { return `${color.strikethrough(color.dim(label))}`; @@ -498,7 +500,9 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => } if (state === 'selected') { const selectedCheckbox = isItem || selectableGroups ? color.green(S_CHECKBOX_SELECTED) : ''; - return `${color.dim(prefix)}${selectedCheckbox} ${color.dim(label)}`; + return `${color.dim(prefix)}${selectedCheckbox} ${color.dim(label)} ${ + option.hint ? color.dim(`(${option.hint})`) : '' + }`; } if (state === 'cancelled') { return `${color.strikethrough(color.dim(label))}`;