Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 222 additions & 0 deletions packages/inquirer/inquirer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,228 @@ describe('inquirer.prompt(...)', () => {
]);
expect(answers).toEqual({ q: 'foo' });
});

it('should display skipped question when `when` returns { ask: false, display: true }', async () => {
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'll be cleaner here to reuse the BufferedStream from packages/testing/src/index.ts to collect the writes.


const answers = await inquirer.prompt([
{
type: 'stub',
name: 'q1',
message: 'Question 1',
},
{
type: 'stub',
name: 'q2',
message: 'Question 2',
when() {
return { ask: false, display: true };
},
},
]);

const output = writeSpy.mock.calls.map(([text]) => text).join('');
expect(output).toContain('Question 2');
expect(answers).toEqual({ q1: 'bar' });
writeSpy.mockRestore();
});

it('should skip question entirely when `when` returns { ask: false, display: false }', async () => {
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);

const answers = await inquirer.prompt([
{
type: 'stub',
name: 'q1',
message: 'Question 1',
},
{
type: 'stub',
name: 'q2',
message: 'Question 2',
when() {
return { ask: false, display: false };
},
},
{
type: 'stub',
name: 'q3',
message: 'Question 3',
},
]);

const output = writeSpy.mock.calls.map(([text]) => text).join('');
expect(output).not.toContain('Question 2');
expect(answers).toEqual({ q1: 'bar', q3: 'bar' });
writeSpy.mockRestore();
});

it('should display skipped confirm question with default true', async () => {
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
const answers = await inquirer.prompt([
{ type: 'stub', name: 'q1', message: 'Question 1' },
{
type: 'confirm',
name: 'confirmQ',
message: 'Are you sure?',
default: true,
when() {
return { ask: false, display: true };
},
},
]);

const output = writeSpy.mock.calls.map(([text]) => text).join('');
expect(output).toContain('Are you sure?');
expect(output).toContain('Yes');
expect(answers).toEqual({ q1: 'bar' });
writeSpy.mockRestore();
});

it('should display skipped select question', async () => {
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
const answers = await inquirer.prompt([
{ type: 'stub', name: 'q1', message: 'Question 1' },
{
type: 'select',
name: 'fruit',
message: 'Select fruit',
choices: [
{ name: 'Apple', value: 'a' },
{ name: 'Banana', value: 'b' },
],
default: 'b',
when() {
return { ask: false, display: true };
},
},
]);

const output = writeSpy.mock.calls.map(([text]) => text).join('');
expect(output).toContain('Select fruit');
expect(output).toContain('Banana');
expect(answers).toEqual({ q1: 'bar' });
writeSpy.mockRestore();
});

it('should display skipped checkbox question', async () => {
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);

const answers = await inquirer.prompt([
{ type: 'stub', name: 'q1', message: 'Question 1' },
{
type: 'checkbox',
name: 'foods',
message: 'Pick foods',
choices: [
{ name: 'Pizza', value: 'p' },
{ name: 'Burger', value: 'b' },
],
default: ['p', 'b'],
when() {
return { ask: false, display: true };
},
},
]);

const output = writeSpy.mock.calls.map(([text]) => text).join('');
expect(output).toContain('Pick foods');
expect(output).toContain('Pizza, Burger');
expect(answers).toEqual({ q1: 'bar' });
writeSpy.mockRestore();
});

it('should display skipped password question', async () => {
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);

const answers = await inquirer.prompt([
{ type: 'stub', name: 'q1', message: 'Question 1' },
{
type: 'password',
name: 'pwd',
message: 'Enter password',
default: 'secret',
when() {
return { ask: false, display: true };
},
},
]);

const output = writeSpy.mock.calls.map(([text]) => text).join('');
expect(output).toContain('Enter password');
expect(output).toContain('[PASSWORD SET]');
expect(answers).toEqual({ q1: 'bar' });
writeSpy.mockRestore();
});

it('should display skipped editor question', async () => {
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);

const answers = await inquirer.prompt([
{ type: 'stub', name: 'q1', message: 'Question 1' },
{
type: 'editor',
name: 'ed',
message: 'Write content',
default: 'notes...',
when() {
return { ask: false, display: true };
},
},
]);

const output = writeSpy.mock.calls.map(([text]) => text).join('');
expect(output).toContain('Write content');
expect(output).toContain('[Default Content]');
expect(answers).toEqual({ q1: 'bar' });
writeSpy.mockRestore();
});

it('should display skipped input question', async () => {
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);

const answers = await inquirer.prompt([
{ type: 'stub', name: 'q1', message: 'Question 1' },
{
type: 'input',
name: 'name',
message: 'Enter name',
default: 'John',
when() {
return { ask: false, display: true };
},
},
]);

const output = writeSpy.mock.calls.map(([text]) => text).join('');
expect(output).toContain('Enter name');
expect(output).toContain('John');
expect(answers).toEqual({ q1: 'bar' });
writeSpy.mockRestore();
});

it('should display skipped number question', async () => {
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);

const answers = await inquirer.prompt([
{ type: 'stub', name: 'q1', message: 'Question 1' },
{
type: 'number',
name: 'age',
message: 'Enter age',
default: 25,
when() {
return { ask: false, display: true };
},
},
]);

const output = writeSpy.mock.calls.map(([text]) => text).join('');
expect(output).toContain('Enter age');
expect(output).toContain('25');
expect(answers).toEqual({ q1: 'bar' });
writeSpy.mockRestore();
});
});

describe('Prefilling answers', () => {
Expand Down
9 changes: 7 additions & 2 deletions packages/inquirer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ export interface QuestionMap {
type KeyValueOrAsyncGetterFunction<T, k extends string, A extends Answers> =
T extends Record<string, any> ? T[k] | AsyncGetterFunction<T[k], A> : never;

export type CustomWhenResult = {
display: boolean;
ask: boolean;
};

export type Question<A extends Answers = Answers, Type extends string = string> = {
type: Type;
name: string;
Expand All @@ -119,7 +124,7 @@ export type Question<A extends Answers = Answers, Type extends string = string>
choices?: any;
filter?: (answer: any, answers: NoInfer<Partial<A>>) => any;
askAnswered?: boolean;
when?: boolean | AsyncGetterFunction<boolean, A>;
when?: boolean | CustomWhenResult | AsyncGetterFunction<boolean | CustomWhenResult, A>;
};

type QuestionWithGetters<
Expand All @@ -131,7 +136,7 @@ type QuestionWithGetters<
{
type: Type;
askAnswered?: boolean;
when?: boolean | AsyncGetterFunction<boolean, A>;
when?: boolean | AsyncGetterFunction<boolean | CustomWhenResult, A>;
filter?(input: any, answers: NoInfer<A>): any;
message: KeyValueOrAsyncGetterFunction<Q, 'message', A>;
default?: KeyValueOrAsyncGetterFunction<Q, 'default', A>;
Expand Down
36 changes: 24 additions & 12 deletions packages/inquirer/src/ui/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
from,
of,
concatMap,
filter,
reduce,
isObservable,
Observable,
Expand All @@ -23,7 +22,9 @@ import type {
AsyncGetterFunction,
PromptSession,
StreamOptions,
CustomWhenResult,
} from '../types.ts';
import SkippedRenderer from './skipped-renderer.ts';

export const _ = {
set: (obj: Record<string, unknown>, path: string = '', value: unknown): void => {
Expand Down Expand Up @@ -230,16 +231,18 @@ export default class PromptsRunner<A extends Answers> {
concatMap((question) =>
of(question).pipe(
concatMap((question) =>
from(
this.shouldRun(question).then((shouldRun: boolean | void) => {
if (shouldRun) {
return question;
from(this.shouldRun(question)).pipe(
concatMap(({ display, ask }) => {
if (ask) {
return defer(() => from(this.fetchAnswer(question)));
}
return;
if (display) {
this.displaySkippedQuestion(question);
}
return EMPTY;
}),
).pipe(filter((val) => val != null)),
),
),
concatMap((question) => defer(() => from(this.fetchAnswer(question)))),
),
),
);
Expand Down Expand Up @@ -391,27 +394,36 @@ export default class PromptsRunner<A extends Answers> {
});
};

private displaySkippedQuestion(question: Question<any>) {
const output = this.opt.output || process.stdout;
const renderer = SkippedRenderer[question.type] || SkippedRenderer.default;
output.write(`${renderer(question)}\n`);
}

/**
* Close the interface and cleanup listeners
*/
close = () => {
this.abortController.abort();
};

private shouldRun = async (question: Question<A>): Promise<boolean> => {
private shouldRun = async (question: Question<A>): Promise<CustomWhenResult> => {
if (
question.askAnswered !== true &&
_.get(this.answers, question.name) !== undefined
) {
return false;
return { display: false, ask: false };
}

const { when } = question;
if (typeof when === 'function') {
const shouldRun = await runAsync(when)(this.answers);
return Boolean(shouldRun);
return typeof shouldRun === 'object'
? shouldRun
: { display: Boolean(shouldRun), ask: Boolean(shouldRun) };
}

return when !== false;
const ask = when !== false;
return { display: ask, ask };
};
}
Loading