@inquirer/testing 3.3.1 → 3.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ import { Stream } from 'node:stream';
2
+ export declare class BufferedStream extends Stream.Writable {
3
+ #private;
4
+ columns: number;
5
+ get writeCount(): number;
6
+ _write(chunk: Buffer, _encoding: string, callback: () => void): void;
7
+ getLastChunk({ raw }?: {
8
+ raw?: boolean;
9
+ }): string;
10
+ getFullOutput(): string;
11
+ clear(): void;
12
+ }
@@ -0,0 +1,43 @@
1
+ import { Stream } from 'node:stream';
2
+ import { stripVTControlCharacters } from 'node:util';
3
+ export class BufferedStream extends Stream.Writable {
4
+ // Expose a large column width so cli-width (used by @inquirer/core's breakLines)
5
+ // doesn't hard-wrap output at 80 columns. This prevents artificial line breaks
6
+ // that would break assertions like toContain() in tests.
7
+ columns = 10_000;
8
+ #fullOutput = '';
9
+ #chunks = [];
10
+ #rawChunks = [];
11
+ #writeCount = 0;
12
+ get writeCount() {
13
+ return this.#writeCount;
14
+ }
15
+ _write(chunk, _encoding, callback) {
16
+ const str = chunk.toString();
17
+ this.#fullOutput += str;
18
+ // Keep track of every chunk sent through.
19
+ this.#rawChunks.push(str);
20
+ // Stripping the ANSI codes here because Inquirer will push commands ANSI (like cursor move.)
21
+ // This is probably fine since we don't care about those for testing; but this could become
22
+ // an issue if we ever want to test for those.
23
+ if (stripVTControlCharacters(str).trim().length > 0) {
24
+ this.#chunks.push(str);
25
+ this.#writeCount++;
26
+ this.emit('render');
27
+ }
28
+ callback();
29
+ }
30
+ getLastChunk({ raw } = {}) {
31
+ const chunks = raw ? this.#rawChunks : this.#chunks;
32
+ const lastChunk = chunks.at(-1);
33
+ return lastChunk ?? '';
34
+ }
35
+ getFullOutput() {
36
+ return this.#fullOutput;
37
+ }
38
+ clear() {
39
+ this.#fullOutput = '';
40
+ this.#chunks = [];
41
+ this.#rawChunks = [];
42
+ }
43
+ }
@@ -0,0 +1,24 @@
1
+ import MuteStream from 'mute-stream';
2
+ import type { Prompt, Context } from '@inquirer/type';
3
+ type RenderOptions = Omit<Context, 'input' | 'output'>;
4
+ export declare function render<Value, const Config>(prompt: Prompt<Value, Config>, config: Config, options?: RenderOptions): Promise<{
5
+ answer: Promise<Value>;
6
+ input: MuteStream;
7
+ events: {
8
+ keypress: (key: string | {
9
+ name?: string;
10
+ ctrl?: boolean;
11
+ meta?: boolean;
12
+ shift?: boolean;
13
+ }) => void;
14
+ type: (text: string) => void;
15
+ };
16
+ getScreen: ({ raw }?: {
17
+ raw?: boolean;
18
+ }) => string;
19
+ getFullOutput: ({ raw }?: {
20
+ raw?: boolean;
21
+ }) => Promise<string>;
22
+ nextRender: () => Promise<void>;
23
+ }>;
24
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ import { stripVTControlCharacters } from 'node:util';
2
+ import MuteStream from 'mute-stream';
3
+ import { BufferedStream } from "./buffered-stream.js";
4
+ import { interpretTerminalOutput } from "./terminal.js";
5
+ export async function render(prompt, config, options) {
6
+ const input = new MuteStream();
7
+ input.unmute();
8
+ const output = new BufferedStream();
9
+ const firstRender = new Promise((resolve) => output.once('render', resolve));
10
+ const answer = prompt(config, { ...options, input, output });
11
+ // The first render is synchronous. If our BufferedStream received a write, we're ready.
12
+ if (output.writeCount === 0) {
13
+ // Our BufferedStream didn't receive a write yet. This happens when the prompt
14
+ // errored before rendering. Race against the answer promise to handle that case.
15
+ await Promise.race([firstRender, answer.catch(() => { })]);
16
+ }
17
+ const events = {
18
+ keypress(key) {
19
+ if (typeof key === 'string') {
20
+ input.emit('keypress', null, { name: key });
21
+ }
22
+ else {
23
+ input.emit('keypress', null, key);
24
+ }
25
+ },
26
+ type(text) {
27
+ input.write(text);
28
+ for (const char of text) {
29
+ input.emit('keypress', null, { name: char });
30
+ }
31
+ },
32
+ };
33
+ let rendersConsumed = output.writeCount;
34
+ function nextRender() {
35
+ const waitForRender = output.writeCount > rendersConsumed
36
+ ? Promise.resolve()
37
+ : new Promise((resolve) => output.once('render', resolve));
38
+ // After detecting a render, let the event loop settle so that multiple
39
+ // synchronous-ish renders (e.g. validation: loading → error) are coalesced.
40
+ return waitForRender.then(() => new Promise((resolve) => {
41
+ setImmediate(() => {
42
+ rendersConsumed = output.writeCount;
43
+ resolve();
44
+ });
45
+ }));
46
+ }
47
+ return {
48
+ answer,
49
+ input,
50
+ events,
51
+ getScreen: ({ raw } = {}) => {
52
+ const lastScreen = output.getLastChunk({ raw: Boolean(raw) });
53
+ return raw ? lastScreen : stripVTControlCharacters(lastScreen).trim();
54
+ },
55
+ getFullOutput: async ({ raw } = {}) => {
56
+ const fullOutput = output.getFullOutput();
57
+ if (raw)
58
+ return fullOutput;
59
+ return interpretTerminalOutput(fullOutput);
60
+ },
61
+ nextRender,
62
+ };
63
+ }
package/dist/jest.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { Prompt } from '@inquirer/type';
2
+ import { Screen } from './screen.ts';
3
+ declare const screenInstance: Screen;
4
+ export { screenInstance as screen };
5
+ /**
6
+ * Wrap a prompt function to use the shared screen I/O.
7
+ * Use this in your own `jest.mock()` calls to mock third-party prompts.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * jest.mock('@my-company/custom-prompt', () => {
12
+ * const { wrapPrompt } = jest.requireActual('@inquirer/testing/jest');
13
+ * const actual = jest.requireActual('@my-company/custom-prompt');
14
+ * return { ...actual, default: wrapPrompt(actual.default) };
15
+ * });
16
+ * ```
17
+ */
18
+ export declare function wrapPrompt<Value, Config>(prompt: Prompt<Value, Config> | {
19
+ default: Prompt<Value, Config>;
20
+ }): Prompt<Value, Config>;
21
+ export { Screen, type KeypressEvent } from './screen.js';
package/dist/jest.js ADDED
@@ -0,0 +1,198 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access */
2
+ // Note: Jest's requireActual returns `any` by design, unlike Vitest's typed importOriginal
3
+ import { Screen } from "./screen.js";
4
+ // Global screen instance - exported for tests
5
+ const screenInstance = new Screen();
6
+ export { screenInstance as screen };
7
+ // Reset before each test (Jest's beforeEach)
8
+ beforeEach(() => {
9
+ screenInstance.clear();
10
+ });
11
+ /**
12
+ * Wrap a prompt function to use the shared screen I/O.
13
+ * Use this in your own `jest.mock()` calls to mock third-party prompts.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * jest.mock('@my-company/custom-prompt', () => {
18
+ * const { wrapPrompt } = jest.requireActual('@inquirer/testing/jest');
19
+ * const actual = jest.requireActual('@my-company/custom-prompt');
20
+ * return { ...actual, default: wrapPrompt(actual.default) };
21
+ * });
22
+ * ```
23
+ */
24
+ export function wrapPrompt(prompt) {
25
+ // Unwrap SWC-style module namespace objects where barrel re-exports like
26
+ // `export { default as input } from '@inquirer/input'` get transformed into
27
+ // `{ default: fn }` instead of the function directly.
28
+ const fn = typeof prompt === 'function' ? prompt : prompt.default;
29
+ return (config, context) => {
30
+ const output = screenInstance.createOutput();
31
+ const promise = fn(config, {
32
+ ...context,
33
+ input: screenInstance.input,
34
+ output,
35
+ });
36
+ screenInstance.setActivePromise(promise);
37
+ return promise;
38
+ };
39
+ }
40
+ // Prompt names used by the @inquirer/prompts barrel mock to wrap only prompt exports.
41
+ const promptNames = [
42
+ 'input',
43
+ 'select',
44
+ 'confirm',
45
+ 'checkbox',
46
+ 'password',
47
+ 'expand',
48
+ 'rawlist',
49
+ 'number',
50
+ 'search',
51
+ 'editor',
52
+ ];
53
+ // Mock individual prompt packages (covers `import input from '@inquirer/input'` style).
54
+ // All prompt packages are optional peer dependencies, so factories silently skip
55
+ // packages that aren't installed in the consumer's project.
56
+ jest.mock('@inquirer/input', () => {
57
+ try {
58
+ const actual = jest.requireActual('@inquirer/input');
59
+ return { ...actual, default: wrapPrompt(actual.default) };
60
+ }
61
+ catch {
62
+ return {};
63
+ }
64
+ });
65
+ jest.mock('@inquirer/select', () => {
66
+ try {
67
+ const actual = jest.requireActual('@inquirer/select');
68
+ return { ...actual, default: wrapPrompt(actual.default) };
69
+ }
70
+ catch {
71
+ return {};
72
+ }
73
+ });
74
+ jest.mock('@inquirer/confirm', () => {
75
+ try {
76
+ const actual = jest.requireActual('@inquirer/confirm');
77
+ return { ...actual, default: wrapPrompt(actual.default) };
78
+ }
79
+ catch {
80
+ return {};
81
+ }
82
+ });
83
+ jest.mock('@inquirer/checkbox', () => {
84
+ try {
85
+ const actual = jest.requireActual('@inquirer/checkbox');
86
+ return { ...actual, default: wrapPrompt(actual.default) };
87
+ }
88
+ catch {
89
+ return {};
90
+ }
91
+ });
92
+ jest.mock('@inquirer/password', () => {
93
+ try {
94
+ const actual = jest.requireActual('@inquirer/password');
95
+ return { ...actual, default: wrapPrompt(actual.default) };
96
+ }
97
+ catch {
98
+ return {};
99
+ }
100
+ });
101
+ jest.mock('@inquirer/expand', () => {
102
+ try {
103
+ const actual = jest.requireActual('@inquirer/expand');
104
+ return { ...actual, default: wrapPrompt(actual.default) };
105
+ }
106
+ catch {
107
+ return {};
108
+ }
109
+ });
110
+ jest.mock('@inquirer/rawlist', () => {
111
+ try {
112
+ const actual = jest.requireActual('@inquirer/rawlist');
113
+ return { ...actual, default: wrapPrompt(actual.default) };
114
+ }
115
+ catch {
116
+ return {};
117
+ }
118
+ });
119
+ jest.mock('@inquirer/number', () => {
120
+ try {
121
+ const actual = jest.requireActual('@inquirer/number');
122
+ return { ...actual, default: wrapPrompt(actual.default) };
123
+ }
124
+ catch {
125
+ return {};
126
+ }
127
+ });
128
+ jest.mock('@inquirer/search', () => {
129
+ try {
130
+ const actual = jest.requireActual('@inquirer/search');
131
+ return { ...actual, default: wrapPrompt(actual.default) };
132
+ }
133
+ catch {
134
+ return {};
135
+ }
136
+ });
137
+ jest.mock('@inquirer/editor', () => {
138
+ try {
139
+ const actual = jest.requireActual('@inquirer/editor');
140
+ return { ...actual, default: wrapPrompt(actual.default) };
141
+ }
142
+ catch {
143
+ return {};
144
+ }
145
+ });
146
+ // Mock @inquirer/prompts barrel re-exports (covers `import { input } from '@inquirer/prompts'` style).
147
+ // Jest's module mock for individual packages doesn't propagate through barrel re-exports.
148
+ // Only prompt functions are wrapped; other exports (like Separator) are passed through.
149
+ jest.mock('@inquirer/prompts', () => {
150
+ try {
151
+ const actual = jest.requireActual('@inquirer/prompts');
152
+ const wrapped = { ...actual };
153
+ for (const name of promptNames) {
154
+ if (name in actual) {
155
+ wrapped[name] = wrapPrompt(actual[name]);
156
+ }
157
+ }
158
+ return wrapped;
159
+ }
160
+ catch {
161
+ return {};
162
+ }
163
+ });
164
+ // Mock the external editor to capture typed input instead of spawning a real editor.
165
+ // Buffers all screen.type() calls and submits on screen.keypress('enter'), matching
166
+ // the interaction pattern of other prompts (type → enter).
167
+ jest.mock('@inquirer/external-editor', () => {
168
+ try {
169
+ jest.requireActual('@inquirer/external-editor');
170
+ }
171
+ catch {
172
+ return {};
173
+ }
174
+ return {
175
+ editAsync: (_text) => {
176
+ let buffer = '';
177
+ return new Promise((resolve) => {
178
+ const typeSpy = jest
179
+ .spyOn(screenInstance, 'type')
180
+ .mockImplementation((text) => {
181
+ buffer += text;
182
+ });
183
+ const keypressSpy = jest
184
+ .spyOn(screenInstance, 'keypress')
185
+ .mockImplementation((key) => {
186
+ const name = typeof key === 'string' ? key : key.name;
187
+ if (name === 'enter' || name === 'return') {
188
+ typeSpy.mockRestore();
189
+ keypressSpy.mockRestore();
190
+ resolve(buffer);
191
+ }
192
+ });
193
+ });
194
+ },
195
+ };
196
+ });
197
+ // Re-export Screen class and KeypressEvent type for advanced use cases
198
+ export { Screen } from './screen.js';
@@ -0,0 +1,35 @@
1
+ import MuteStream from 'mute-stream';
2
+ import { BufferedStream } from './buffered-stream.ts';
3
+ export type KeypressEvent = {
4
+ name?: string;
5
+ ctrl?: boolean;
6
+ meta?: boolean;
7
+ shift?: boolean;
8
+ };
9
+ export declare class Screen {
10
+ #private;
11
+ constructor();
12
+ get input(): MuteStream;
13
+ createOutput(): BufferedStream;
14
+ setActivePromise(promise: Promise<unknown>): void;
15
+ /**
16
+ * Wait for the next screen update.
17
+ *
18
+ * Handles re-renders within the same prompt (e.g., validation errors,
19
+ * async updates) and prompt transitions in multi-prompt flows
20
+ * (automatically waits for the next prompt).
21
+ *
22
+ * Note: The initial prompt render is available immediately via getScreen()
23
+ * — no next() call is needed before reading it.
24
+ */
25
+ next(): Promise<void>;
26
+ getScreen({ raw }?: {
27
+ raw?: boolean;
28
+ }): string;
29
+ getFullOutput({ raw }?: {
30
+ raw?: boolean;
31
+ }): Promise<string>;
32
+ keypress(key: string | KeypressEvent): void;
33
+ type(text: string): void;
34
+ clear(): void;
35
+ }
package/dist/screen.js ADDED
@@ -0,0 +1,150 @@
1
+ import { stripVTControlCharacters } from 'node:util';
2
+ import MuteStream from 'mute-stream';
3
+ import { BufferedStream } from "./buffered-stream.js";
4
+ import { interpretTerminalOutput } from "./terminal.js";
5
+ export class Screen {
6
+ #input;
7
+ #outputs = [];
8
+ #currentOutput = null;
9
+ #activePromise = null;
10
+ #rendersConsumed = 0;
11
+ #renderResolve = null;
12
+ constructor() {
13
+ this.#input = new MuteStream();
14
+ this.#input.unmute();
15
+ }
16
+ get input() {
17
+ return this.#input;
18
+ }
19
+ createOutput() {
20
+ const output = new BufferedStream();
21
+ this.#outputs.push(output);
22
+ this.#currentOutput = output;
23
+ this.#rendersConsumed = 0;
24
+ // Forward render events for cross-output listening
25
+ output.on('render', () => {
26
+ if (this.#renderResolve) {
27
+ this.#renderResolve();
28
+ }
29
+ });
30
+ return output;
31
+ }
32
+ setActivePromise(promise) {
33
+ this.#activePromise = promise;
34
+ // Auto-consume the initial render. Since createPrompt renders synchronously,
35
+ // getScreen() works immediately after starting a prompt — no next() needed.
36
+ this.#rendersConsumed = this.#currentOutput?.writeCount ?? 0;
37
+ }
38
+ /**
39
+ * Wait for the next screen update.
40
+ *
41
+ * Handles re-renders within the same prompt (e.g., validation errors,
42
+ * async updates) and prompt transitions in multi-prompt flows
43
+ * (automatically waits for the next prompt).
44
+ *
45
+ * Note: The initial prompt render is available immediately via getScreen()
46
+ * — no next() call is needed before reading it.
47
+ */
48
+ async next() {
49
+ if (this.#activePromise) {
50
+ const currentPromise = this.#activePromise;
51
+ // Consume any renders that happened synchronously (e.g., loading state).
52
+ // We want to wait for the next meaningful state change, not an intermediate render.
53
+ this.#rendersConsumed = this.#currentOutput?.writeCount ?? 0;
54
+ // Race: a future render (validation error, async update) vs the promise settling
55
+ // (prompt completed, possibly synchronously before any new render arrives).
56
+ const renderPromise = this.#waitForNextRender();
57
+ const settlePromise = currentPromise.then(() => 'settled', () => 'settled');
58
+ const result = await Promise.race([
59
+ renderPromise.then(() => 'render'),
60
+ settlePromise,
61
+ ]);
62
+ if (result === 'settled') {
63
+ if (this.#activePromise !== currentPromise) {
64
+ // New prompt already started — its render was caught by the race's renderPromise.
65
+ this.#rendersConsumed = this.#currentOutput?.writeCount ?? 0;
66
+ }
67
+ else {
68
+ // Prompt settled but no new prompt yet. Wait for the next prompt's first render.
69
+ await this.#waitForNextRender();
70
+ }
71
+ }
72
+ else {
73
+ // Got a render. Check if the prompt also completed (making this a "done" render).
74
+ // Microtasks (promise settlement) always drain before macrotasks (setImmediate),
75
+ // so if the prompt resolved, its .then wins the race.
76
+ const settled = await Promise.race([
77
+ currentPromise.then(() => true, () => true),
78
+ new Promise((resolve) => setImmediate(() => resolve(false))),
79
+ ]);
80
+ if (settled) {
81
+ if (this.#activePromise !== currentPromise) {
82
+ // New prompt already started — its render was caught by the race.
83
+ this.#rendersConsumed = this.#currentOutput?.writeCount ?? 0;
84
+ }
85
+ else {
86
+ // Prompt completed but no new prompt yet. Wait for it.
87
+ await this.#waitForNextRender();
88
+ }
89
+ }
90
+ }
91
+ }
92
+ else {
93
+ // No active promise — wait for a render (e.g., prompt hasn't started yet)
94
+ await this.#waitForNextRender();
95
+ }
96
+ }
97
+ async #waitForNextRender() {
98
+ const writeCount = this.#currentOutput?.writeCount ?? 0;
99
+ if (writeCount > this.#rendersConsumed) {
100
+ this.#rendersConsumed = writeCount;
101
+ return;
102
+ }
103
+ await new Promise((resolve) => {
104
+ const handler = () => {
105
+ this.#renderResolve = null;
106
+ resolve();
107
+ };
108
+ // Listen on current output (for re-renders within same prompt)
109
+ this.#currentOutput?.once('render', handler);
110
+ // Also set up cross-output listener (for prompt transitions and initial render)
111
+ this.#renderResolve = handler;
112
+ });
113
+ this.#rendersConsumed = this.#currentOutput?.writeCount ?? 0;
114
+ }
115
+ getScreen({ raw } = {}) {
116
+ const lastScreen = this.#currentOutput?.getLastChunk({ raw: Boolean(raw) }) ?? '';
117
+ return raw ? lastScreen : stripVTControlCharacters(lastScreen).trim();
118
+ }
119
+ async getFullOutput({ raw } = {}) {
120
+ const output = this.#outputs.map((o) => o.getFullOutput()).join('');
121
+ if (raw)
122
+ return output;
123
+ return interpretTerminalOutput(output);
124
+ }
125
+ keypress(key) {
126
+ if (typeof key === 'string') {
127
+ this.#input.emit('keypress', null, { name: key });
128
+ }
129
+ else {
130
+ this.#input.emit('keypress', null, key);
131
+ }
132
+ }
133
+ type(text) {
134
+ this.#input.write(text);
135
+ for (const char of text) {
136
+ this.#input.emit('keypress', null, { name: char });
137
+ }
138
+ }
139
+ clear() {
140
+ // Recreate input stream to ensure clean state
141
+ this.#input = new MuteStream();
142
+ this.#input.unmute();
143
+ // Reset output tracking
144
+ this.#outputs = [];
145
+ this.#currentOutput = null;
146
+ this.#activePromise = null;
147
+ this.#rendersConsumed = 0;
148
+ this.#renderResolve = null;
149
+ }
150
+ }
@@ -0,0 +1 @@
1
+ export declare function interpretTerminalOutput(rawOutput: string, cols?: number, rows?: number): Promise<string>;
@@ -0,0 +1,20 @@
1
+ import xterm from '@xterm/headless';
2
+ export async function interpretTerminalOutput(rawOutput, cols = 10_000, rows = 4000) {
3
+ const term = new xterm.Terminal({
4
+ cols,
5
+ rows,
6
+ allowProposedApi: true,
7
+ convertEol: true,
8
+ });
9
+ await new Promise((resolve) => term.write(rawOutput, resolve));
10
+ const lines = [];
11
+ for (let i = 0; i < term.rows; i++) {
12
+ lines.push(term.buffer.active.getLine(i)?.translateToString(true) ?? '');
13
+ }
14
+ term.dispose();
15
+ // Trim trailing empty lines
16
+ while (lines.length > 0 && lines.at(-1) === '') {
17
+ lines.pop();
18
+ }
19
+ return lines.join('\n');
20
+ }
@@ -0,0 +1,22 @@
1
+ import type { Prompt } from '@inquirer/type';
2
+ import { Screen } from './screen.ts';
3
+ declare const screenInstance: Screen;
4
+ export { screenInstance as screen };
5
+ /**
6
+ * Wrap a prompt function to use the shared screen I/O.
7
+ * Use this in your own `vi.mock()` calls to mock third-party prompts.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { wrapPrompt } from '@inquirer/testing/vitest';
12
+ *
13
+ * vi.mock('@my-company/custom-prompt', async (importOriginal) => {
14
+ * const actual = await importOriginal<typeof import('@my-company/custom-prompt')>();
15
+ * return { ...actual, default: wrapPrompt(actual.default) };
16
+ * });
17
+ * ```
18
+ */
19
+ export declare function wrapPrompt<Value, Config>(prompt: Prompt<Value, Config> | {
20
+ default: Prompt<Value, Config>;
21
+ }): Prompt<Value, Config>;
22
+ export { Screen, type KeypressEvent } from './screen.js';
package/dist/vitest.js ADDED
@@ -0,0 +1,192 @@
1
+ import { vi, beforeEach } from 'vitest';
2
+ import { Screen } from "./screen.js";
3
+ // Global screen instance - exported for tests
4
+ const screenInstance = new Screen();
5
+ export { screenInstance as screen };
6
+ // Reset before each test
7
+ beforeEach(() => {
8
+ screenInstance.clear();
9
+ });
10
+ /**
11
+ * Wrap a prompt function to use the shared screen I/O.
12
+ * Use this in your own `vi.mock()` calls to mock third-party prompts.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { wrapPrompt } from '@inquirer/testing/vitest';
17
+ *
18
+ * vi.mock('@my-company/custom-prompt', async (importOriginal) => {
19
+ * const actual = await importOriginal<typeof import('@my-company/custom-prompt')>();
20
+ * return { ...actual, default: wrapPrompt(actual.default) };
21
+ * });
22
+ * ```
23
+ */
24
+ export function wrapPrompt(prompt) {
25
+ // Unwrap SWC-style module namespace objects where barrel re-exports like
26
+ // `export { default as input } from '@inquirer/input'` get transformed into
27
+ // `{ default: fn }` instead of the function directly.
28
+ const fn = typeof prompt === 'function' ? prompt : prompt.default;
29
+ return (config, context) => {
30
+ const output = screenInstance.createOutput();
31
+ const promise = fn(config, {
32
+ ...context,
33
+ input: screenInstance.input,
34
+ output,
35
+ });
36
+ screenInstance.setActivePromise(promise);
37
+ return promise;
38
+ };
39
+ }
40
+ // Mock individual prompt packages (covers `import input from '@inquirer/input'` style).
41
+ // All prompt packages are optional peer dependencies, so factories silently skip
42
+ // packages that aren't installed in the consumer's project.
43
+ vi.mock('@inquirer/input', async (importOriginal) => {
44
+ try {
45
+ const actual = await importOriginal();
46
+ return { ...actual, default: wrapPrompt(actual.default) };
47
+ }
48
+ catch {
49
+ return {};
50
+ }
51
+ });
52
+ vi.mock('@inquirer/select', async (importOriginal) => {
53
+ try {
54
+ const actual = await importOriginal();
55
+ return { ...actual, default: wrapPrompt(actual.default) };
56
+ }
57
+ catch {
58
+ return {};
59
+ }
60
+ });
61
+ vi.mock('@inquirer/confirm', async (importOriginal) => {
62
+ try {
63
+ const actual = await importOriginal();
64
+ return { ...actual, default: wrapPrompt(actual.default) };
65
+ }
66
+ catch {
67
+ return {};
68
+ }
69
+ });
70
+ vi.mock('@inquirer/checkbox', async (importOriginal) => {
71
+ try {
72
+ const actual = await importOriginal();
73
+ return { ...actual, default: wrapPrompt(actual.default) };
74
+ }
75
+ catch {
76
+ return {};
77
+ }
78
+ });
79
+ vi.mock('@inquirer/password', async (importOriginal) => {
80
+ try {
81
+ const actual = await importOriginal();
82
+ return { ...actual, default: wrapPrompt(actual.default) };
83
+ }
84
+ catch {
85
+ return {};
86
+ }
87
+ });
88
+ vi.mock('@inquirer/expand', async (importOriginal) => {
89
+ try {
90
+ const actual = await importOriginal();
91
+ return { ...actual, default: wrapPrompt(actual.default) };
92
+ }
93
+ catch {
94
+ return {};
95
+ }
96
+ });
97
+ vi.mock('@inquirer/rawlist', async (importOriginal) => {
98
+ try {
99
+ const actual = await importOriginal();
100
+ return { ...actual, default: wrapPrompt(actual.default) };
101
+ }
102
+ catch {
103
+ return {};
104
+ }
105
+ });
106
+ vi.mock('@inquirer/number', async (importOriginal) => {
107
+ try {
108
+ const actual = await importOriginal();
109
+ return { ...actual, default: wrapPrompt(actual.default) };
110
+ }
111
+ catch {
112
+ return {};
113
+ }
114
+ });
115
+ vi.mock('@inquirer/search', async (importOriginal) => {
116
+ try {
117
+ const actual = await importOriginal();
118
+ return { ...actual, default: wrapPrompt(actual.default) };
119
+ }
120
+ catch {
121
+ return {};
122
+ }
123
+ });
124
+ vi.mock('@inquirer/editor', async (importOriginal) => {
125
+ try {
126
+ const actual = await importOriginal();
127
+ return { ...actual, default: wrapPrompt(actual.default) };
128
+ }
129
+ catch {
130
+ return {};
131
+ }
132
+ });
133
+ // Mock @inquirer/prompts barrel re-exports (covers `import { input } from '@inquirer/prompts'` style).
134
+ // While Vitest's module interception often propagates through ESM re-exports, an explicit mock
135
+ // ensures consistent behavior across all environments and bundler configurations.
136
+ // Only prompt functions are wrapped; other exports (like Separator) are passed through.
137
+ vi.mock('@inquirer/prompts', async (importOriginal) => {
138
+ try {
139
+ const actual = await importOriginal();
140
+ return {
141
+ ...actual,
142
+ input: wrapPrompt(actual.input),
143
+ select: wrapPrompt(actual.select),
144
+ confirm: wrapPrompt(actual.confirm),
145
+ checkbox: wrapPrompt(actual.checkbox),
146
+ password: wrapPrompt(actual.password),
147
+ expand: wrapPrompt(actual.expand),
148
+ rawlist: wrapPrompt(actual.rawlist),
149
+ number: wrapPrompt(actual.number),
150
+ search: wrapPrompt(actual.search),
151
+ editor: wrapPrompt(actual.editor),
152
+ };
153
+ }
154
+ catch {
155
+ return {};
156
+ }
157
+ });
158
+ // Mock the external editor to capture typed input instead of spawning a real editor.
159
+ // Buffers all screen.type() calls and submits on screen.keypress('enter'), matching
160
+ // the interaction pattern of other prompts (type → enter).
161
+ vi.mock('@inquirer/external-editor', async (importOriginal) => {
162
+ try {
163
+ await importOriginal();
164
+ }
165
+ catch {
166
+ return {};
167
+ }
168
+ return {
169
+ editAsync: (_text) => {
170
+ let buffer = '';
171
+ return new Promise((resolve) => {
172
+ const typeSpy = vi
173
+ .spyOn(screenInstance, 'type')
174
+ .mockImplementation((text) => {
175
+ buffer += text;
176
+ });
177
+ const keypressSpy = vi
178
+ .spyOn(screenInstance, 'keypress')
179
+ .mockImplementation((key) => {
180
+ const name = typeof key === 'string' ? key : key.name;
181
+ if (name === 'enter' || name === 'return') {
182
+ typeSpy.mockRestore();
183
+ keypressSpy.mockRestore();
184
+ resolve(buffer);
185
+ }
186
+ });
187
+ });
188
+ },
189
+ };
190
+ });
191
+ // Re-export Screen class and KeypressEvent type for advanced use cases
192
+ export { Screen } from './screen.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inquirer/testing",
3
- "version": "3.3.1",
3
+ "version": "3.3.3",
4
4
  "description": "Inquirer testing utilities",
5
5
  "keywords": [
6
6
  "answer",
@@ -83,16 +83,16 @@
83
83
  "tsc": "tsc"
84
84
  },
85
85
  "dependencies": {
86
- "@inquirer/type": "^4.0.3",
86
+ "@inquirer/type": "^4.0.5",
87
87
  "@xterm/headless": "^6.0.0",
88
88
  "mute-stream": "^3.0.0"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/jest": "^30.0.0",
92
92
  "@types/mute-stream": "^0.0.4",
93
- "@types/node": "^25.0.2",
94
- "typescript": "^5.9.3",
95
- "vitest": "^4.0.18"
93
+ "@types/node": "^25.5.2",
94
+ "typescript": "^6.0.2",
95
+ "vitest": "^4.1.2"
96
96
  },
97
97
  "peerDependencies": {
98
98
  "@inquirer/checkbox": ">=1.0.0",
@@ -167,5 +167,5 @@
167
167
  },
168
168
  "main": "./dist/index.js",
169
169
  "types": "./dist/index.d.ts",
170
- "gitHead": "1ce03199b82b4a5fb6f7c97ce374c6da5087444f"
170
+ "gitHead": "e68fe01d65359e083581c48c4a18cd8f97d88842"
171
171
  }