diff --git a/packages/prompts/src/__snapshots__/index.test.ts.snap b/packages/prompts/src/__snapshots__/index.test.ts.snap index b692a1be..53b3d447 100644 --- a/packages/prompts/src/__snapshots__/index.test.ts.snap +++ b/packages/prompts/src/__snapshots__/index.test.ts.snap @@ -149,6 +149,475 @@ exports[`prompts (isCI = false) > confirm > right arrow moves to next choice 1`] ] `; +exports[`prompts (isCI = false) > multiselect > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "■ foo +│", + " +", + "[?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 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 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 > 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) > select > can cancel 1`] = ` [ "[?25l", @@ -755,6 +1224,475 @@ exports[`prompts (isCI = true) > confirm > right arrow moves to next choice 1`] ] `; +exports[`prompts (isCI = true) > multiselect > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "■ foo +│", + " +", + "[?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 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 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 > 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) > select > can cancel 1`] = ` [ "[?25l", diff --git a/packages/prompts/src/index.test.ts b/packages/prompts/src/index.test.ts index bbcc1e43..39221225 100644 --- a/packages/prompts/src/index.test.ts +++ b/packages/prompts/src/index.test.ts @@ -556,4 +556,198 @@ describe.each(['true', 'false'])('prompts (isCI = %s)', (isCI) => { 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(); + }); + }); }); diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index cdaac77b..62d74808 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -459,6 +459,7 @@ export const multiselect = (opts: MultiSelectOptions) => { ) .join('\n'); return `${title + color.yellow(S_BAR)} ${limitOptions({ + output: opts.output, options: this.options, cursor: this.cursor, maxItems: opts.maxItems, @@ -467,6 +468,7 @@ export const multiselect = (opts: MultiSelectOptions) => { } default: { return `${title}${color.cyan(S_BAR)} ${limitOptions({ + output: opts.output, options: this.options, cursor: this.cursor, maxItems: opts.maxItems,