From 4ab19dad2d1a5382560974906d67856996642620 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Fri, 4 Apr 2025 10:28:20 +0100 Subject: [PATCH] test: add tests for `text` prompt Adds initial tests for the `text()` prompt. --- .../src/__snapshots__/index.test.ts.snap | 264 ++++++++++++++++-- packages/prompts/src/index.test.ts | 142 +++++++++- packages/prompts/src/index.ts | 6 +- 3 files changed, 389 insertions(+), 23 deletions(-) diff --git a/packages/prompts/src/__snapshots__/index.test.ts.snap b/packages/prompts/src/__snapshots__/index.test.ts.snap index c45f71ef..21a236f8 100644 --- a/packages/prompts/src/__snapshots__/index.test.ts.snap +++ b/packages/prompts/src/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`isCI = false > spinner > message > sets message for next frame 1`] = ` +exports[`prompts (isCI = false) > spinner > message > sets message for next frame 1`] = ` [ "[?25l", "│ @@ -12,7 +12,7 @@ exports[`isCI = false > spinner > message > sets message for next frame 1`] = ` ] `; -exports[`isCI = false > spinner > start > renders frames at interval 1`] = ` +exports[`prompts (isCI = false) > spinner > start > renders frames at interval 1`] = ` [ "[?25l", "│ @@ -30,7 +30,7 @@ exports[`isCI = false > spinner > start > renders frames at interval 1`] = ` ] `; -exports[`isCI = false > spinner > start > renders message 1`] = ` +exports[`prompts (isCI = false) > spinner > start > renders message 1`] = ` [ "[?25l", "│ @@ -39,7 +39,7 @@ exports[`isCI = false > spinner > start > renders message 1`] = ` ] `; -exports[`isCI = false > spinner > start > renders timer when indicator is "timer" 1`] = ` +exports[`prompts (isCI = false) > spinner > start > renders timer when indicator is "timer" 1`] = ` [ "[?25l", "│ @@ -48,7 +48,7 @@ exports[`isCI = false > spinner > start > renders timer when indicator is "timer ] `; -exports[`isCI = false > spinner > stop > renders cancel symbol if code = 1 1`] = ` +exports[`prompts (isCI = false) > spinner > stop > renders cancel symbol if code = 1 1`] = ` [ "[?25l", "│ @@ -62,7 +62,7 @@ exports[`isCI = false > spinner > stop > renders cancel symbol if code = 1 1`] = ] `; -exports[`isCI = false > spinner > stop > renders error symbol if code > 1 1`] = ` +exports[`prompts (isCI = false) > spinner > stop > renders error symbol if code > 1 1`] = ` [ "[?25l", "│ @@ -76,7 +76,7 @@ exports[`isCI = false > spinner > stop > renders error symbol if code > 1 1`] = ] `; -exports[`isCI = false > spinner > stop > renders message 1`] = ` +exports[`prompts (isCI = false) > spinner > stop > renders message 1`] = ` [ "[?25l", "│ @@ -90,7 +90,7 @@ exports[`isCI = false > spinner > stop > renders message 1`] = ` ] `; -exports[`isCI = false > spinner > stop > renders submit symbol and stops spinner 1`] = ` +exports[`prompts (isCI = false) > spinner > stop > renders submit symbol and stops spinner 1`] = ` [ "[?25l", "│ @@ -104,7 +104,123 @@ exports[`isCI = false > spinner > stop > renders submit symbol and stops spinner ] `; -exports[`isCI = true > spinner > message > sets message for next frame 1`] = ` +exports[`prompts (isCI = false) > text > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "■ foo +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > renders cancelled value if one set 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "│ xy█", + "", + "", + "", + "", + "■ foo +│ xy +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > renders message 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "◇ foo +│ undefined", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > renders placeholder if set 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ bar +└ +", + "", + "", + "", + "◇ foo +│ bar", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > text > renders submitted value 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "│ xy█", + "", + "", + "", + "", + "◇ foo +│ xy", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > spinner > message > sets message for next frame 1`] = ` [ "[?25l", "│ @@ -118,7 +234,7 @@ exports[`isCI = true > spinner > message > sets message for next frame 1`] = ` ] `; -exports[`isCI = true > spinner > start > renders frames at interval 1`] = ` +exports[`prompts (isCI = true) > spinner > start > renders frames at interval 1`] = ` [ "[?25l", "│ @@ -127,7 +243,7 @@ exports[`isCI = true > spinner > start > renders frames at interval 1`] = ` ] `; -exports[`isCI = true > spinner > start > renders message 1`] = ` +exports[`prompts (isCI = true) > spinner > start > renders message 1`] = ` [ "[?25l", "│ @@ -136,7 +252,7 @@ exports[`isCI = true > spinner > start > renders message 1`] = ` ] `; -exports[`isCI = true > spinner > start > renders timer when indicator is "timer" 1`] = ` +exports[`prompts (isCI = true) > spinner > start > renders timer when indicator is "timer" 1`] = ` [ "[?25l", "│ @@ -145,7 +261,7 @@ exports[`isCI = true > spinner > start > renders timer when indicator is "timer" ] `; -exports[`isCI = true > spinner > stop > renders cancel symbol if code = 1 1`] = ` +exports[`prompts (isCI = true) > spinner > stop > renders cancel symbol if code = 1 1`] = ` [ "[?25l", "│ @@ -161,7 +277,7 @@ exports[`isCI = true > spinner > stop > renders cancel symbol if code = 1 1`] = ] `; -exports[`isCI = true > spinner > stop > renders error symbol if code > 1 1`] = ` +exports[`prompts (isCI = true) > spinner > stop > renders error symbol if code > 1 1`] = ` [ "[?25l", "│ @@ -177,7 +293,7 @@ exports[`isCI = true > spinner > stop > renders error symbol if code > 1 1`] = ` ] `; -exports[`isCI = true > spinner > stop > renders message 1`] = ` +exports[`prompts (isCI = true) > spinner > stop > renders message 1`] = ` [ "[?25l", "│ @@ -193,7 +309,7 @@ exports[`isCI = true > spinner > stop > renders message 1`] = ` ] `; -exports[`isCI = true > spinner > stop > renders submit symbol and stops spinner 1`] = ` +exports[`prompts (isCI = true) > spinner > stop > renders submit symbol and stops spinner 1`] = ` [ "[?25l", "│ @@ -208,3 +324,119 @@ exports[`isCI = true > spinner > stop > renders submit symbol and stops spinner "[?25h", ] `; + +exports[`prompts (isCI = true) > text > can cancel 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "■ foo +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > renders cancelled value if one set 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "│ xy█", + "", + "", + "", + "", + "■ foo +│ xy +│", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > renders message 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "◇ foo +│ undefined", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > renders placeholder if set 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ bar +└ +", + "", + "", + "", + "◇ foo +│ bar", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > text > renders submitted value 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "│ xy█", + "", + "", + "", + "", + "◇ foo +│ xy", + " +", + "[?25h", +] +`; diff --git a/packages/prompts/src/index.test.ts b/packages/prompts/src/index.test.ts index 64f5c571..b2fbb5ba 100644 --- a/packages/prompts/src/index.test.ts +++ b/packages/prompts/src/index.test.ts @@ -1,4 +1,4 @@ -import { Writable } from 'node:stream'; +import { Readable, Writable } from 'node:stream'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; import * as prompts from './index.js'; @@ -16,8 +16,35 @@ class MockWritable extends Writable { } } -describe.each(['true', 'false'])('isCI = %s', (isCI) => { +class MockReadable extends Readable { + protected _buffer: unknown[] | null = []; + + _read() { + if (this._buffer === null) { + this.push(null); + return; + } + + for (const val of this._buffer) { + this.push(val); + } + + this._buffer = []; + } + + pushValue(val: unknown): void { + this._buffer?.push(val); + } + + close(): void { + this._buffer = null; + } +} + +describe.each(['true', 'false'])('prompts (isCI = %s)', (isCI) => { let originalCI: string | undefined; + let output: MockWritable; + let input: MockReadable; beforeAll(() => { originalCI = process.env.CI; @@ -28,16 +55,21 @@ describe.each(['true', 'false'])('isCI = %s', (isCI) => { process.env.CI = originalCI; }); - describe('spinner', () => { - let output: MockWritable; + beforeEach(() => { + output = new MockWritable(); + input = new MockReadable(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + describe('spinner', () => { beforeEach(() => { vi.useFakeTimers(); - output = new MockWritable(); }); afterEach(() => { - vi.restoreAllMocks(); vi.useRealTimers(); }); @@ -152,4 +184,102 @@ describe.each(['true', 'false'])('isCI = %s', (isCI) => { }); }); }); + + describe('text', () => { + test('renders message', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders placeholder if set', async () => { + const result = prompts.text({ + message: 'foo', + placeholder: 'bar', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + // TODO (43081j): uncomment this when #263 is fixed + // expect(value).toBe('bar'); + }); + + test(' applies placeholder', async () => { + const result = prompts.text({ + message: 'foo', + placeholder: 'bar', + input, + output, + }); + + input.emit('keypress', '\t', { name: 'tab' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('bar'); + }); + + test('can cancel', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'escape', { name: 'escape' }); + + const value = await result; + + expect(prompts.isCancel(value)).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders cancelled value if one set', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', 'y', { name: 'y' }); + input.emit('keypress', '', { name: 'escape' }); + + const value = await result; + + expect(prompts.isCancel(value)).toBe(true); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders submitted value', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', 'y', { name: 'y' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('xy'); + expect(output.buffer).toMatchSnapshot(); + }); + }); }); diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 8e465314..c2983d6d 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -1,4 +1,4 @@ -import type { Writable } from 'node:stream'; +import type { Readable, Writable } from 'node:stream'; import { stripVTControlCharacters as strip } from 'node:util'; import { ConfirmPrompt, @@ -104,6 +104,8 @@ export interface TextOptions { defaultValue?: string; initialValue?: string; validate?: (value: string) => string | Error | undefined; + input?: Readable; + output?: Writable; } export const text = (opts: TextOptions) => { return new TextPrompt({ @@ -111,6 +113,8 @@ export const text = (opts: TextOptions) => { placeholder: opts.placeholder, defaultValue: opts.defaultValue, initialValue: opts.initialValue, + output: opts.output, + input: opts.input, render() { const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; const placeholder = opts.placeholder