Skip to content

Commit c0273d6

Browse files
committed
test: add disabled select tests
1 parent d9f86eb commit c0273d6

File tree

9 files changed

+269
-11
lines changed

9 files changed

+269
-11
lines changed

packages/core/src/utils/cursor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function findCursor<T extends { disabled?: boolean }>(
88
const clampedCursor = newCursor < 0 ? maxCursor : newCursor > maxCursor ? 0 : newCursor;
99
const newOption = options[clampedCursor];
1010
if (newOption.disabled) {
11-
return findCursor(clampedCursor, (delta < 0 ? -1 : 1), options);
11+
return findCursor(clampedCursor, delta < 0 ? -1 : 1, options);
1212
}
1313
return clampedCursor;
1414
}

packages/core/test/prompts/multi-select.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,84 @@ describe('MultiSelectPrompt', () => {
130130
input.emit('keypress', 'i', { name: 'i' });
131131
expect(instance.value).toEqual(['foo']);
132132
});
133+
134+
test('disabled options are skipped', () => {
135+
const instance = new MultiSelectPrompt({
136+
input,
137+
output,
138+
render: () => 'foo',
139+
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
140+
});
141+
instance.prompt();
142+
143+
expect(instance.cursor).to.equal(0);
144+
input.emit('keypress', 'down', { name: 'down' });
145+
expect(instance.cursor).to.equal(2);
146+
input.emit('keypress', 'up', { name: 'up' });
147+
expect(instance.cursor).to.equal(0);
148+
});
149+
150+
test('initial cursorAt on disabled option', () => {
151+
const instance = new MultiSelectPrompt({
152+
input,
153+
output,
154+
render: () => 'foo',
155+
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
156+
cursorAt: 'bar',
157+
});
158+
instance.prompt();
159+
160+
expect(instance.cursor).to.equal(2);
161+
});
162+
});
163+
164+
describe('toggleAll', () => {
165+
test('selects all enabled options', () => {
166+
const instance = new MultiSelectPrompt({
167+
input,
168+
output,
169+
render: () => 'foo',
170+
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
171+
});
172+
instance.prompt();
173+
174+
input.emit('keypress', 'a', { name: 'a' });
175+
expect(instance.value).toEqual(['foo', 'baz']);
176+
});
177+
178+
test('unselects all enabled options if all selected', () => {
179+
const instance = new MultiSelectPrompt({
180+
input,
181+
output,
182+
render: () => 'foo',
183+
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
184+
initialValues: ['foo', 'baz'],
185+
});
186+
instance.prompt();
187+
188+
input.emit('keypress', 'a', { name: 'a' });
189+
expect(instance.value).toEqual([]);
190+
});
191+
});
192+
193+
describe('toggleInvert', () => {
194+
test('inverts selection of enabled options', () => {
195+
const instance = new MultiSelectPrompt({
196+
input,
197+
output,
198+
render: () => 'foo',
199+
options: [
200+
{ value: 'foo' },
201+
{ value: 'bar', disabled: true },
202+
{ value: 'baz' },
203+
{ value: 'qux' },
204+
],
205+
initialValues: ['foo', 'baz'],
206+
});
207+
instance.prompt();
208+
209+
input.emit('keypress', 'i', { name: 'i' });
210+
expect(instance.value).toEqual(['qux']);
211+
});
133212
});
134213
});

packages/core/test/prompts/select.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,43 @@ describe('SelectPrompt', () => {
100100
instance.prompt();
101101
expect(instance.cursor).to.equal(1);
102102
});
103+
104+
test('cursor skips disabled options (down)', () => {
105+
const instance = new SelectPrompt({
106+
input,
107+
output,
108+
render: () => 'foo',
109+
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
110+
});
111+
instance.prompt();
112+
expect(instance.cursor).to.equal(0);
113+
input.emit('keypress', 'down', { name: 'down' });
114+
expect(instance.cursor).to.equal(2);
115+
});
116+
117+
test('cursor skips disabled options (up)', () => {
118+
const instance = new SelectPrompt({
119+
input,
120+
output,
121+
render: () => 'foo',
122+
initialValue: 'baz',
123+
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
124+
});
125+
instance.prompt();
126+
expect(instance.cursor).to.equal(2);
127+
input.emit('keypress', 'up', { name: 'up' });
128+
expect(instance.cursor).to.equal(0);
129+
});
130+
131+
test('cursor skips initial disabled option', () => {
132+
const instance = new SelectPrompt({
133+
input,
134+
output,
135+
render: () => 'foo',
136+
options: [{ value: 'foo', disabled: true }, { value: 'bar' }, { value: 'baz' }],
137+
});
138+
instance.prompt();
139+
expect(instance.cursor).to.equal(1);
140+
});
103141
});
104142
});

packages/prompts/src/multi-select.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
3434
) => {
3535
const label = option.label ?? String(option.value);
3636
if (state === 'disabled') {
37-
return `${color.black(S_CHECKBOX_SELECTED)} ${color.dim(label)}${
38-
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
37+
return `${color.gray(S_CHECKBOX_INACTIVE)} ${color.gray(label)}${
38+
option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : ''
3939
}`;
4040
}
4141
if (state === 'active') {
@@ -118,28 +118,32 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
118118
}`;
119119
}
120120
case 'error': {
121+
const prefix = `${color.yellow(S_BAR)} `;
121122
const footer = this.error
122123
.split('\n')
123124
.map((ln, i) =>
124125
i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}`
125126
)
126127
.join('\n');
127-
return `${title + color.yellow(S_BAR)} ${limitOptions({
128+
return `${title}${prefix}${limitOptions({
128129
output: opts.output,
129130
options: this.options,
130131
cursor: this.cursor,
131132
maxItems: opts.maxItems,
133+
columnPadding: prefix.length,
132134
style: styleOption,
133-
}).join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`;
135+
}).join(`\n${prefix}`)}\n${footer}\n`;
134136
}
135137
default: {
136-
return `${title}${color.cyan(S_BAR)} ${limitOptions({
138+
const prefix = `${color.cyan(S_BAR)} `;
139+
return `${title}${prefix}${limitOptions({
137140
output: opts.output,
138141
options: this.options,
139142
cursor: this.cursor,
140143
maxItems: opts.maxItems,
144+
columnPadding: prefix.length,
141145
style: styleOption,
142-
}).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
146+
}).join(`\n${prefix}`)}\n${color.cyan(S_BAR_END)}\n`;
143147
}
144148
}
145149
},

packages/prompts/src/select.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
7979
const label = option.label ?? String(option.value);
8080
switch (state) {
8181
case 'disabled':
82-
return `${color.black(S_RADIO_ACTIVE)} ${color.dim(label)}${
83-
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
82+
return `${color.gray(S_RADIO_INACTIVE)} ${color.gray(label)}${
83+
option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : ''
8484
}`;
8585
case 'selected':
8686
return `${color.dim(label)}`;
@@ -113,14 +113,16 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
113113
'cancelled'
114114
)}\n${color.gray(S_BAR)}`;
115115
default: {
116-
return `${title}${color.cyan(S_BAR)} ${limitOptions({
116+
const prefix = `${color.cyan(S_BAR)} `;
117+
return `${title}${prefix}${limitOptions({
117118
output: opts.output,
118119
cursor: this.cursor,
119120
options: this.options,
120121
maxItems: opts.maxItems,
122+
columnPadding: prefix.length,
121123
style: (item, active) =>
122124
opt(item, item.disabled ? 'disabled' : active ? 'active' : 'inactive'),
123-
}).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
125+
}).join(`\n${prefix}`)}\n${color.cyan(S_BAR_END)}\n`;
124126
}
125127
}
126128
},

packages/prompts/test/__snapshots__/multi-select.test.ts.snap

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,32 @@ exports[`multiselect (isCI = false) > maxItems renders a sliding window 1`] = `
240240
]
241241
`;
242242
243+
exports[`multiselect (isCI = false) > renders disabled options 1`] = `
244+
[
245+
"<cursor.hide>",
246+
"│
247+
◆ foo
248+
│ ◻ opt0
249+
│ ◻ opt1
250+
│ ◻ opt2 (Hint 2)
251+
└
252+
",
253+
"<cursor.backward count=999><cursor.up count=6>",
254+
"<cursor.down count=3>",
255+
"<erase.line><cursor.left count=1>",
256+
"│ ◼ opt1",
257+
"<cursor.down count=3>",
258+
"<cursor.backward count=999><cursor.up count=6>",
259+
"<cursor.down count=1>",
260+
"<erase.down>",
261+
"◇ foo
262+
│ opt1",
263+
"
264+
",
265+
"<cursor.show>",
266+
]
267+
`;
268+
243269
exports[`multiselect (isCI = false) > renders message 1`] = `
244270
[
245271
"<cursor.hide>",
@@ -850,6 +876,32 @@ exports[`multiselect (isCI = true) > maxItems renders a sliding window 1`] = `
850876
]
851877
`;
852878
879+
exports[`multiselect (isCI = true) > renders disabled options 1`] = `
880+
[
881+
"<cursor.hide>",
882+
"│
883+
◆ foo
884+
│ ◻ opt0
885+
│ ◻ opt1
886+
│ ◻ opt2 (Hint 2)
887+
└
888+
",
889+
"<cursor.backward count=999><cursor.up count=6>",
890+
"<cursor.down count=3>",
891+
"<erase.line><cursor.left count=1>",
892+
"│ ◼ opt1",
893+
"<cursor.down count=3>",
894+
"<cursor.backward count=999><cursor.up count=6>",
895+
"<cursor.down count=1>",
896+
"<erase.down>",
897+
"◇ foo
898+
│ opt1",
899+
"
900+
",
901+
"<cursor.show>",
902+
]
903+
`;
904+
853905
exports[`multiselect (isCI = true) > renders message 1`] = `
854906
[
855907
"<cursor.hide>",

packages/prompts/test/__snapshots__/select.test.ts.snap

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,27 @@ exports[`select (isCI = false) > down arrow selects next option 1`] = `
6363
]
6464
`;
6565
66+
exports[`select (isCI = false) > renders disabled options 1`] = `
67+
[
68+
"<cursor.hide>",
69+
"│
70+
◆ foo
71+
│ ○ Option 0
72+
│ ● Option 1
73+
│ ○ Option 2 (Hint 2)
74+
└
75+
",
76+
"<cursor.backward count=999><cursor.up count=6>",
77+
"<cursor.down count=1>",
78+
"<erase.down>",
79+
"◇ foo
80+
│ Option 1",
81+
"
82+
",
83+
"<cursor.show>",
84+
]
85+
`;
86+
6687
exports[`select (isCI = false) > renders option hints 1`] = `
6788
[
6889
"<cursor.hide>",
@@ -220,6 +241,27 @@ exports[`select (isCI = true) > down arrow selects next option 1`] = `
220241
]
221242
`;
222243
244+
exports[`select (isCI = true) > renders disabled options 1`] = `
245+
[
246+
"<cursor.hide>",
247+
"│
248+
◆ foo
249+
│ ○ Option 0
250+
│ ● Option 1
251+
│ ○ Option 2 (Hint 2)
252+
└
253+
",
254+
"<cursor.backward count=999><cursor.up count=6>",
255+
"<cursor.down count=1>",
256+
"<erase.down>",
257+
"◇ foo
258+
│ Option 1",
259+
"
260+
",
261+
"<cursor.show>",
262+
]
263+
`;
264+
223265
exports[`select (isCI = true) > renders option hints 1`] = `
224266
[
225267
"<cursor.hide>",

packages/prompts/test/multi-select.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,25 @@ describe.each(['true', 'false'])('multiselect (isCI = %s)', (isCI) => {
315315
expect(prompts.isCancel(value)).toBe(true);
316316
expect(output.buffer).toMatchSnapshot();
317317
});
318+
319+
test('renders disabled options', async () => {
320+
const result = prompts.multiselect({
321+
message: 'foo',
322+
options: [
323+
{ value: 'opt0', disabled: true },
324+
{ value: 'opt1' },
325+
{ value: 'opt2', disabled: true, hint: 'Hint 2' },
326+
],
327+
input,
328+
output,
329+
});
330+
331+
input.emit('keypress', '', { name: 'space' });
332+
input.emit('keypress', '', { name: 'return' });
333+
334+
const value = await result;
335+
336+
expect(value).toEqual(['opt1']);
337+
expect(output.buffer).toMatchSnapshot();
338+
});
318339
});

packages/prompts/test/select.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,24 @@ describe.each(['true', 'false'])('select (isCI = %s)', (isCI) => {
145145
expect(prompts.isCancel(value)).toBe(true);
146146
expect(output.buffer).toMatchSnapshot();
147147
});
148+
149+
test('renders disabled options', async () => {
150+
const result = prompts.select({
151+
message: 'foo',
152+
options: [
153+
{ value: 'opt0', label: 'Option 0', disabled: true },
154+
{ value: 'opt1', label: 'Option 1' },
155+
{ value: 'opt2', label: 'Option 2', disabled: true, hint: 'Hint 2' },
156+
],
157+
input,
158+
output,
159+
});
160+
161+
input.emit('keypress', '', { name: 'return' });
162+
163+
const value = await result;
164+
165+
expect(value).toBe('opt1');
166+
expect(output.buffer).toMatchSnapshot();
167+
});
148168
});

0 commit comments

Comments
 (0)