@inquirer/testing
Version:
Inquirer testing utilities
193 lines (192 loc) • 6.24 kB
JavaScript
import { vi, beforeEach } from 'vitest';
import { Screen } from "./screen.js";
// Global screen instance - exported for tests
const screenInstance = new Screen();
export { screenInstance as screen };
// Reset before each test
beforeEach(() => {
screenInstance.clear();
});
/**
* Wrap a prompt function to use the shared screen I/O.
* Use this in your own `vi.mock()` calls to mock third-party prompts.
*
* @example
* ```ts
* import { wrapPrompt } from '@inquirer/testing/vitest';
*
* vi.mock('@my-company/custom-prompt', async (importOriginal) => {
* const actual = await importOriginal<typeof import('@my-company/custom-prompt')>();
* return { ...actual, default: wrapPrompt(actual.default) };
* });
* ```
*/
export function wrapPrompt(prompt) {
// Unwrap SWC-style module namespace objects where barrel re-exports like
// `export { default as input } from '@inquirer/input'` get transformed into
// `{ default: fn }` instead of the function directly.
const fn = typeof prompt === 'function' ? prompt : prompt.default;
return (config, context) => {
const output = screenInstance.createOutput();
const promise = fn(config, {
...context,
input: screenInstance.input,
output,
});
screenInstance.setActivePromise(promise);
return promise;
};
}
// Mock individual prompt packages (covers `import input from '@inquirer/input'` style).
// All prompt packages are optional peer dependencies, so factories silently skip
// packages that aren't installed in the consumer's project.
vi.mock('@inquirer/input', async (importOriginal) => {
try {
const actual = await importOriginal();
return { ...actual, default: wrapPrompt(actual.default) };
}
catch {
return {};
}
});
vi.mock('@inquirer/select', async (importOriginal) => {
try {
const actual = await importOriginal();
return { ...actual, default: wrapPrompt(actual.default) };
}
catch {
return {};
}
});
vi.mock('@inquirer/confirm', async (importOriginal) => {
try {
const actual = await importOriginal();
return { ...actual, default: wrapPrompt(actual.default) };
}
catch {
return {};
}
});
vi.mock('@inquirer/checkbox', async (importOriginal) => {
try {
const actual = await importOriginal();
return { ...actual, default: wrapPrompt(actual.default) };
}
catch {
return {};
}
});
vi.mock('@inquirer/password', async (importOriginal) => {
try {
const actual = await importOriginal();
return { ...actual, default: wrapPrompt(actual.default) };
}
catch {
return {};
}
});
vi.mock('@inquirer/expand', async (importOriginal) => {
try {
const actual = await importOriginal();
return { ...actual, default: wrapPrompt(actual.default) };
}
catch {
return {};
}
});
vi.mock('@inquirer/rawlist', async (importOriginal) => {
try {
const actual = await importOriginal();
return { ...actual, default: wrapPrompt(actual.default) };
}
catch {
return {};
}
});
vi.mock('@inquirer/number', async (importOriginal) => {
try {
const actual = await importOriginal();
return { ...actual, default: wrapPrompt(actual.default) };
}
catch {
return {};
}
});
vi.mock('@inquirer/search', async (importOriginal) => {
try {
const actual = await importOriginal();
return { ...actual, default: wrapPrompt(actual.default) };
}
catch {
return {};
}
});
vi.mock('@inquirer/editor', async (importOriginal) => {
try {
const actual = await importOriginal();
return { ...actual, default: wrapPrompt(actual.default) };
}
catch {
return {};
}
});
// Mock @inquirer/prompts barrel re-exports (covers `import { input } from '@inquirer/prompts'` style).
// While Vitest's module interception often propagates through ESM re-exports, an explicit mock
// ensures consistent behavior across all environments and bundler configurations.
// Only prompt functions are wrapped; other exports (like Separator) are passed through.
vi.mock('@inquirer/prompts', async (importOriginal) => {
try {
const actual = await importOriginal();
return {
...actual,
input: wrapPrompt(actual.input),
select: wrapPrompt(actual.select),
confirm: wrapPrompt(actual.confirm),
checkbox: wrapPrompt(actual.checkbox),
password: wrapPrompt(actual.password),
expand: wrapPrompt(actual.expand),
rawlist: wrapPrompt(actual.rawlist),
number: wrapPrompt(actual.number),
search: wrapPrompt(actual.search),
editor: wrapPrompt(actual.editor),
};
}
catch {
return {};
}
});
// Mock the external editor to capture typed input instead of spawning a real editor.
// Buffers all screen.type() calls and submits on screen.keypress('enter'), matching
// the interaction pattern of other prompts (type → enter).
vi.mock('@inquirer/external-editor', async (importOriginal) => {
try {
await importOriginal();
}
catch {
return {};
}
return {
editAsync: (_text) => {
let buffer = '';
return new Promise((resolve) => {
const typeSpy = vi
.spyOn(screenInstance, 'type')
.mockImplementation((text) => {
buffer += text;
});
const keypressSpy = vi
.spyOn(screenInstance, 'keypress')
.mockImplementation((key) => {
const name = typeof key === 'string' ? key : key.name;
if (name === 'enter' || name === 'return') {
typeSpy.mockRestore();
keypressSpy.mockRestore();
resolve(buffer);
}
});
});
},
};
});
// Re-export Screen class and KeypressEvent type for advanced use cases
export { Screen } from "./screen.js";