UNPKG

@inquirer/testing

Version:
193 lines (192 loc) 6.24 kB
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";