[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

test(core): colocate tests and migrate to test-utils

Nate Moore (Apr 2, 2026, 7:17 PM EDT) fe20db05 90c70653

+782 -895
+1 -1
packages/core/package.json
··· 48 48 "license": "MIT", 49 49 "packageManager": "pnpm@9.14.2", 50 50 "scripts": { 51 - "build": "bsh build", 51 + "build": "bsh build \"src/**/*.ts\" \"!src/**/*.test.ts\"", 52 52 "prepack": "pnpm build", 53 53 "test": "bsh test", 54 54 "lint": "bsh lint"
+348
packages/core/src/prompts/multi-line.test.ts
··· 1 + import { styleText } from 'node:util'; 2 + import { cursor } from 'sisteransi'; 3 + import { beforeEach, describe, expect, test } from 'vitest'; 4 + import { default as MultiLinePrompt } from './multi-line.js'; 5 + import { createMocks, type Mocks } from '@bomb.sh/tools/test-utils'; 6 + 7 + describe('MultiLinePrompt', () => { 8 + let mocks: Mocks<{ input: true; output: true }>; 9 + 10 + beforeEach(() => { 11 + mocks = createMocks({ input: true, output: true }); 12 + }); 13 + 14 + test('renders render() result', () => { 15 + const instance = new MultiLinePrompt({ 16 + input: mocks.input, 17 + output: mocks.output, 18 + render: () => 'foo', 19 + }); 20 + instance.prompt(); 21 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo']); 22 + }); 23 + 24 + test('sets default value on finalize if no value', async () => { 25 + const instance = new MultiLinePrompt({ 26 + input: mocks.input, 27 + output: mocks.output, 28 + render: () => 'foo', 29 + defaultValue: 'bleep bloop', 30 + }); 31 + const resultPromise = instance.prompt(); 32 + mocks.input.emit('keypress', '', { name: 'return' }); 33 + mocks.input.emit('keypress', '', { name: 'return' }); 34 + const result = await resultPromise; 35 + expect(result).to.equal('bleep bloop'); 36 + }); 37 + 38 + test('keeps value on finalize', async () => { 39 + const instance = new MultiLinePrompt({ 40 + input: mocks.input, 41 + output: mocks.output, 42 + render: () => 'foo', 43 + defaultValue: 'bleep bloop', 44 + }); 45 + const resultPromise = instance.prompt(); 46 + mocks.input.emit('keypress', 'x', { name: 'x' }); 47 + mocks.input.emit('keypress', '', { name: 'return' }); 48 + mocks.input.emit('keypress', '', { name: 'return' }); 49 + const result = await resultPromise; 50 + expect(result).to.equal('x'); 51 + }); 52 + 53 + describe('cursor', () => { 54 + test('can get cursor', () => { 55 + const instance = new MultiLinePrompt({ 56 + input: mocks.input, 57 + output: mocks.output, 58 + render: () => 'foo', 59 + }); 60 + 61 + expect(instance.cursor).to.equal(0); 62 + }); 63 + }); 64 + 65 + describe('userInputWithCursor', () => { 66 + test('returns value on submit', () => { 67 + const instance = new MultiLinePrompt({ 68 + input: mocks.input, 69 + output: mocks.output, 70 + render: () => 'foo', 71 + }); 72 + instance.prompt(); 73 + mocks.input.emit('keypress', 'x', { name: 'x' }); 74 + mocks.input.emit('keypress', '', { name: 'return' }); 75 + mocks.input.emit('keypress', '', { name: 'return' }); 76 + expect(instance.userInputWithCursor).to.equal('x'); 77 + }); 78 + 79 + test('highlights cursor position', () => { 80 + const instance = new MultiLinePrompt({ 81 + input: mocks.input, 82 + output: mocks.output, 83 + render: () => 'foo', 84 + }); 85 + instance.prompt(); 86 + const keys = 'foo'; 87 + for (let i = 0; i < keys.length; i++) { 88 + mocks.input.emit('keypress', keys[i], { name: keys[i] }); 89 + } 90 + mocks.input.emit('keypress', undefined, { name: 'left' }); 91 + expect(instance.userInputWithCursor).to.equal(`fo${styleText('inverse', 'o')}`); 92 + }); 93 + 94 + test('shows cursor at end if beyond value', () => { 95 + const instance = new MultiLinePrompt({ 96 + input: mocks.input, 97 + output: mocks.output, 98 + render: () => 'foo', 99 + }); 100 + instance.prompt(); 101 + const keys = 'foo'; 102 + for (let i = 0; i < keys.length; i++) { 103 + mocks.input.emit('keypress', keys[i], { name: keys[i] }); 104 + } 105 + mocks.input.emit('keypress', undefined, { name: 'right' }); 106 + expect(instance.userInputWithCursor).to.equal('foo█'); 107 + }); 108 + }); 109 + 110 + describe('key', () => { 111 + test('return inserts newline', () => { 112 + const instance = new MultiLinePrompt({ 113 + input: mocks.input, 114 + output: mocks.output, 115 + render: () => 'foo', 116 + }); 117 + instance.prompt(); 118 + mocks.input.emit('keypress', 'x', { name: 'x' }); 119 + mocks.input.emit('keypress', '', { name: 'return' }); 120 + expect(instance.userInput).to.equal('x\n'); 121 + }); 122 + 123 + test('double return submits', async () => { 124 + const instance = new MultiLinePrompt({ 125 + input: mocks.input, 126 + output: mocks.output, 127 + render: () => 'foo', 128 + }); 129 + const resultPromise = instance.prompt(); 130 + mocks.input.emit('keypress', 'x', { name: 'x' }); 131 + mocks.input.emit('keypress', '', { name: 'return' }); 132 + mocks.input.emit('keypress', '', { name: 'return' }); 133 + const result = await resultPromise; 134 + expect(result).to.equal('x'); 135 + }); 136 + 137 + test('double return inserts when showSubmit is true', async () => { 138 + const instance = new MultiLinePrompt({ 139 + input: mocks.input, 140 + output: mocks.output, 141 + render: () => 'foo', 142 + showSubmit: true, 143 + }); 144 + const resultPromise = instance.prompt(); 145 + mocks.input.emit('keypress', 'x', { name: 'x' }); 146 + mocks.input.emit('keypress', '', { name: 'return' }); 147 + mocks.input.emit('keypress', '', { name: 'return' }); 148 + mocks.input.emit('keypress', '\t', { name: 'tab' }); 149 + mocks.input.emit('keypress', '', { name: 'return' }); 150 + const result = await resultPromise; 151 + expect(result).to.equal('x\n\n'); 152 + }); 153 + 154 + test('typing when submit selected jumps back to text', async () => { 155 + const instance = new MultiLinePrompt({ 156 + input: mocks.input, 157 + output: mocks.output, 158 + render: () => 'foo', 159 + showSubmit: true, 160 + }); 161 + const resultPromise = instance.prompt(); 162 + mocks.input.emit('keypress', 'x', { name: 'x' }); 163 + mocks.input.emit('keypress', '\t', { name: 'tab' }); 164 + mocks.input.emit('keypress', 'y', { name: 'y' }); 165 + mocks.input.emit('keypress', '\t', { name: 'tab' }); 166 + mocks.input.emit('keypress', '', { name: 'return' }); 167 + const result = await resultPromise; 168 + expect(result).to.equal('xy'); 169 + }); 170 + 171 + test('backspace deletes previous char', async () => { 172 + const instance = new MultiLinePrompt({ 173 + input: mocks.input, 174 + output: mocks.output, 175 + render: () => 'foo', 176 + }); 177 + const resultPromise = instance.prompt(); 178 + mocks.input.emit('keypress', 'x', { name: 'x' }); 179 + mocks.input.emit('keypress', 'y', { name: 'y' }); 180 + mocks.input.emit('keypress', '', { name: 'backspace' }); 181 + mocks.input.emit('keypress', '', { name: 'return' }); 182 + mocks.input.emit('keypress', '', { name: 'return' }); 183 + const result = await resultPromise; 184 + expect(result).to.equal('x'); 185 + }); 186 + 187 + test('delete deletes next char', async () => { 188 + const instance = new MultiLinePrompt({ 189 + input: mocks.input, 190 + output: mocks.output, 191 + render: () => 'foo', 192 + }); 193 + const resultPromise = instance.prompt(); 194 + mocks.input.emit('keypress', 'x', { name: 'x' }); 195 + mocks.input.emit('keypress', 'y', { name: 'y' }); 196 + mocks.input.emit('keypress', '', { name: 'left' }); 197 + mocks.input.emit('keypress', '', { name: 'delete' }); 198 + mocks.input.emit('keypress', '', { name: 'return' }); 199 + mocks.input.emit('keypress', '', { name: 'return' }); 200 + const result = await resultPromise; 201 + expect(result).to.equal('x'); 202 + }); 203 + 204 + test('delete does nothing at end', async () => { 205 + const instance = new MultiLinePrompt({ 206 + input: mocks.input, 207 + output: mocks.output, 208 + render: () => 'foo', 209 + }); 210 + const resultPromise = instance.prompt(); 211 + mocks.input.emit('keypress', 'x', { name: 'x' }); 212 + mocks.input.emit('keypress', '', { name: 'delete' }); 213 + mocks.input.emit('keypress', '', { name: 'return' }); 214 + mocks.input.emit('keypress', '', { name: 'return' }); 215 + const result = await resultPromise; 216 + expect(result).to.equal('x'); 217 + }); 218 + 219 + test('backspace does nothing at start', async () => { 220 + const instance = new MultiLinePrompt({ 221 + input: mocks.input, 222 + output: mocks.output, 223 + render: () => 'foo', 224 + }); 225 + const resultPromise = instance.prompt(); 226 + mocks.input.emit('keypress', 'x', { name: 'x' }); 227 + mocks.input.emit('keypress', '', { name: 'left' }); 228 + mocks.input.emit('keypress', '', { name: 'backspace' }); 229 + mocks.input.emit('keypress', '', { name: 'return' }); 230 + mocks.input.emit('keypress', '', { name: 'return' }); 231 + const result = await resultPromise; 232 + expect(result).to.equal('x'); 233 + }); 234 + 235 + test('left moves left until start', async () => { 236 + const instance = new MultiLinePrompt({ 237 + input: mocks.input, 238 + output: mocks.output, 239 + render: () => 'foo', 240 + }); 241 + const resultPromise = instance.prompt(); 242 + mocks.input.emit('keypress', 'x', { name: 'x' }); 243 + mocks.input.emit('keypress', '', { name: 'left' }); 244 + mocks.input.emit('keypress', '', { name: 'left' }); 245 + mocks.input.emit('keypress', 'y', { name: 'y' }); 246 + mocks.input.emit('keypress', '', { name: 'return' }); 247 + mocks.input.emit('keypress', '', { name: 'return' }); 248 + const result = await resultPromise; 249 + expect(result).to.equal('yx'); 250 + }); 251 + 252 + test('right moves right until end', async () => { 253 + const instance = new MultiLinePrompt({ 254 + input: mocks.input, 255 + output: mocks.output, 256 + render: () => 'foo', 257 + }); 258 + const resultPromise = instance.prompt(); 259 + mocks.input.emit('keypress', 'x', { name: 'x' }); 260 + mocks.input.emit('keypress', 'y', { name: 'y' }); 261 + mocks.input.emit('keypress', '', { name: 'left' }); 262 + mocks.input.emit('keypress', '', { name: 'right' }); 263 + mocks.input.emit('keypress', '', { name: 'right' }); 264 + mocks.input.emit('keypress', 'z', { name: 'z' }); 265 + mocks.input.emit('keypress', '', { name: 'return' }); 266 + mocks.input.emit('keypress', '', { name: 'return' }); 267 + const result = await resultPromise; 268 + expect(result).to.equal('xyz'); 269 + }); 270 + 271 + test('left moves across lines', async () => { 272 + const instance = new MultiLinePrompt({ 273 + input: mocks.input, 274 + output: mocks.output, 275 + render: () => 'foo', 276 + }); 277 + const resultPromise = instance.prompt(); 278 + mocks.input.emit('keypress', 'x', { name: 'x' }); 279 + mocks.input.emit('keypress', '', { name: 'return' }); 280 + mocks.input.emit('keypress', 'y', { name: 'y' }); 281 + mocks.input.emit('keypress', '', { name: 'left' }); 282 + mocks.input.emit('keypress', '', { name: 'left' }); 283 + mocks.input.emit('keypress', 'z', { name: 'z' }); 284 + mocks.input.emit('keypress', '', { name: 'return' }); 285 + mocks.input.emit('keypress', '', { name: 'return' }); 286 + const result = await resultPromise; 287 + expect(result).to.equal('xz\ny'); 288 + }); 289 + 290 + test('right moves across lines', async () => { 291 + const instance = new MultiLinePrompt({ 292 + input: mocks.input, 293 + output: mocks.output, 294 + render: () => 'foo', 295 + }); 296 + const resultPromise = instance.prompt(); 297 + mocks.input.emit('keypress', 'x', { name: 'x' }); 298 + mocks.input.emit('keypress', '', { name: 'return' }); 299 + mocks.input.emit('keypress', 'y', { name: 'y' }); 300 + mocks.input.emit('keypress', '', { name: 'left' }); 301 + mocks.input.emit('keypress', '', { name: 'left' }); 302 + mocks.input.emit('keypress', '', { name: 'right' }); 303 + mocks.input.emit('keypress', '', { name: 'right' }); 304 + mocks.input.emit('keypress', 'z', { name: 'z' }); 305 + mocks.input.emit('keypress', '', { name: 'return' }); 306 + mocks.input.emit('keypress', '', { name: 'return' }); 307 + const result = await resultPromise; 308 + expect(result).to.equal('x\nyz'); 309 + }); 310 + 311 + test('up moves up a line', async () => { 312 + const instance = new MultiLinePrompt({ 313 + input: mocks.input, 314 + output: mocks.output, 315 + render: () => 'foo', 316 + }); 317 + const resultPromise = instance.prompt(); 318 + mocks.input.emit('keypress', 'x', { name: 'x' }); 319 + mocks.input.emit('keypress', '', { name: 'return' }); 320 + mocks.input.emit('keypress', 'y', { name: 'y' }); 321 + mocks.input.emit('keypress', '', { name: 'up' }); 322 + mocks.input.emit('keypress', 'z', { name: 'z' }); 323 + mocks.input.emit('keypress', '', { name: 'return' }); 324 + mocks.input.emit('keypress', '', { name: 'return' }); 325 + const result = await resultPromise; 326 + expect(result).to.equal('xz\ny'); 327 + }); 328 + 329 + test('down moves down a line', async () => { 330 + const instance = new MultiLinePrompt({ 331 + input: mocks.input, 332 + output: mocks.output, 333 + render: () => 'foo', 334 + }); 335 + const resultPromise = instance.prompt(); 336 + mocks.input.emit('keypress', 'x', { name: 'x' }); 337 + mocks.input.emit('keypress', '', { name: 'return' }); 338 + mocks.input.emit('keypress', 'y', { name: 'y' }); 339 + mocks.input.emit('keypress', '', { name: 'up' }); 340 + mocks.input.emit('keypress', '', { name: 'down' }); 341 + mocks.input.emit('keypress', 'z', { name: 'z' }); 342 + mocks.input.emit('keypress', '', { name: 'return' }); 343 + mocks.input.emit('keypress', '', { name: 'return' }); 344 + const result = await resultPromise; 345 + expect(result).to.equal('x\nyz'); 346 + }); 347 + }); 348 + });
+91
packages/core/src/prompts/password.test.ts
··· 1 + import { styleText } from 'node:util'; 2 + import { cursor } from 'sisteransi'; 3 + import { beforeEach, describe, expect, test } from 'vitest'; 4 + import { default as PasswordPrompt } from './password.js'; 5 + import { createMocks, type Mocks } from '@bomb.sh/tools/test-utils'; 6 + 7 + describe('PasswordPrompt', () => { 8 + let mocks: Mocks<{ input: true; output: true }>; 9 + 10 + beforeEach(() => { 11 + mocks = createMocks({ input: true, output: true }); 12 + }); 13 + 14 + test('renders render() result', () => { 15 + const instance = new PasswordPrompt({ 16 + input: mocks.input, 17 + output: mocks.output, 18 + render: () => 'foo', 19 + }); 20 + // leave the promise hanging since we don't want to submit in this test 21 + instance.prompt(); 22 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo']); 23 + }); 24 + 25 + describe('cursor', () => { 26 + test('can get cursor', () => { 27 + const instance = new PasswordPrompt({ 28 + input: mocks.input, 29 + output: mocks.output, 30 + render: () => 'foo', 31 + }); 32 + 33 + expect(instance.cursor).to.equal(0); 34 + }); 35 + }); 36 + 37 + describe('userInputWithCursor', () => { 38 + test('returns masked value on submit', () => { 39 + const instance = new PasswordPrompt({ 40 + input: mocks.input, 41 + output: mocks.output, 42 + render: () => 'foo', 43 + }); 44 + instance.prompt(); 45 + const keys = 'foo'; 46 + for (let i = 0; i < keys.length; i++) { 47 + mocks.input.emit('keypress', keys[i], { name: keys[i] }); 48 + } 49 + mocks.input.emit('keypress', '', { name: 'return' }); 50 + expect(instance.userInputWithCursor).to.equal('•••'); 51 + }); 52 + 53 + test('renders marker at end', () => { 54 + const instance = new PasswordPrompt({ 55 + input: mocks.input, 56 + output: mocks.output, 57 + render: () => 'foo', 58 + }); 59 + instance.prompt(); 60 + mocks.input.emit('keypress', 'x', { name: 'x' }); 61 + expect(instance.userInputWithCursor).to.equal(`•${styleText(['inverse', 'hidden'], '_')}`); 62 + }); 63 + 64 + test('renders cursor inside value', () => { 65 + const instance = new PasswordPrompt({ 66 + input: mocks.input, 67 + output: mocks.output, 68 + render: () => 'foo', 69 + }); 70 + instance.prompt(); 71 + mocks.input.emit('keypress', 'x', { name: 'x' }); 72 + mocks.input.emit('keypress', 'y', { name: 'y' }); 73 + mocks.input.emit('keypress', 'z', { name: 'z' }); 74 + mocks.input.emit('keypress', undefined, { name: 'left' }); 75 + mocks.input.emit('keypress', undefined, { name: 'left' }); 76 + expect(instance.userInputWithCursor).to.equal(`•${styleText('inverse', '•')}•`); 77 + }); 78 + 79 + test('renders custom mask', () => { 80 + const instance = new PasswordPrompt({ 81 + input: mocks.input, 82 + output: mocks.output, 83 + render: () => 'foo', 84 + mask: 'X', 85 + }); 86 + instance.prompt(); 87 + mocks.input.emit('keypress', 'x', { name: 'x' }); 88 + expect(instance.userInputWithCursor).to.equal(`X${styleText(['inverse', 'hidden'], '_')}`); 89 + }); 90 + }); 91 + });
-26
packages/core/test/mock-readable.ts
··· 1 - import { Readable } from 'node:stream'; 2 - 3 - export class MockReadable extends Readable { 4 - protected _buffer: unknown[] | null = []; 5 - 6 - _read() { 7 - if (this._buffer === null) { 8 - this.push(null); 9 - return; 10 - } 11 - 12 - for (const val of this._buffer) { 13 - this.push(val); 14 - } 15 - 16 - this._buffer = []; 17 - } 18 - 19 - pushValue(val: unknown): void { 20 - this._buffer?.push(val); 21 - } 22 - 23 - close(): void { 24 - this._buffer = null; 25 - } 26 - }
-14
packages/core/test/mock-writable.ts
··· 1 - import { Writable } from 'node:stream'; 2 - 3 - export class MockWritable extends Writable { 4 - public buffer: string[] = []; 5 - 6 - _write( 7 - chunk: any, 8 - _encoding: BufferEncoding, 9 - callback: (error?: Error | null | undefined) => void 10 - ): void { 11 - this.buffer.push(chunk.toString()); 12 - callback(); 13 - } 14 - }
+45 -52
packages/core/test/prompts/autocomplete.test.ts packages/core/src/prompts/autocomplete.test.ts
··· 1 1 import { cursor } from 'sisteransi'; 2 - import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 3 - import { default as AutocompletePrompt } from '../../src/prompts/autocomplete.js'; 4 - import { MockReadable } from '../mock-readable.js'; 5 - import { MockWritable } from '../mock-writable.js'; 2 + import { beforeEach, describe, expect, test } from 'vitest'; 3 + import { default as AutocompletePrompt } from './autocomplete.js'; 4 + import { createMocks, type Mocks } from '@bomb.sh/tools/test-utils'; 6 5 7 6 describe('AutocompletePrompt', () => { 8 - let input: MockReadable; 9 - let output: MockWritable; 7 + let mocks: Mocks<{ input: true; output: true }>; 10 8 const testOptions = [ 11 9 { value: 'apple', label: 'Apple' }, 12 10 { value: 'banana', label: 'Banana' }, ··· 16 14 ]; 17 15 18 16 beforeEach(() => { 19 - input = new MockReadable(); 20 - output = new MockWritable(); 21 - }); 22 - 23 - afterEach(() => { 24 - vi.restoreAllMocks(); 17 + mocks = createMocks({ input: true, output: true }); 25 18 }); 26 19 27 20 test('renders render() result', () => { 28 21 const instance = new AutocompletePrompt({ 29 - input, 30 - output, 22 + input: mocks.input, 23 + output: mocks.output, 31 24 render: () => 'foo', 32 25 options: testOptions, 33 26 }); 34 27 instance.prompt(); 35 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); 28 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo']); 36 29 }); 37 30 38 31 test('initial options match provided options', () => { 39 32 const instance = new AutocompletePrompt({ 40 - input, 41 - output, 33 + input: mocks.input, 34 + output: mocks.output, 42 35 render: () => 'foo', 43 36 options: testOptions, 44 37 }); ··· 52 45 53 46 test('cursor navigation with event emitter', () => { 54 47 const instance = new AutocompletePrompt({ 55 - input, 56 - output, 48 + input: mocks.input, 49 + output: mocks.output, 57 50 render: () => 'foo', 58 51 options: testOptions, 59 52 }); ··· 78 71 79 72 test('initialValue selects correct option', () => { 80 73 const instance = new AutocompletePrompt({ 81 - input, 82 - output, 74 + input: mocks.input, 75 + output: mocks.output, 83 76 render: () => 'foo', 84 77 options: testOptions, 85 78 initialValue: ['cherry'], ··· 95 88 96 89 test('initialValue defaults to first option when non-multiple', () => { 97 90 const instance = new AutocompletePrompt({ 98 - input, 99 - output, 91 + input: mocks.input, 92 + output: mocks.output, 100 93 render: () => 'foo', 101 94 options: testOptions, 102 95 }); ··· 107 100 108 101 test('initialValue is empty when multiple', () => { 109 102 const instance = new AutocompletePrompt({ 110 - input, 111 - output, 103 + input: mocks.input, 104 + output: mocks.output, 112 105 render: () => 'foo', 113 106 options: testOptions, 114 107 multiple: true, ··· 120 113 121 114 test('filtering through user input', () => { 122 115 const instance = new AutocompletePrompt({ 123 - input, 124 - output, 116 + input: mocks.input, 117 + output: mocks.output, 125 118 render: () => 'foo', 126 119 options: testOptions, 127 120 }); ··· 132 125 expect(instance.filteredOptions.length).to.equal(testOptions.length); 133 126 134 127 // Simulate typing 'a' by emitting keypress event 135 - input.emit('keypress', 'a', { name: 'a' }); 128 + mocks.input.emit('keypress', 'a', { name: 'a' }); 136 129 137 130 // Check that filtered options are updated to include options with 'a' 138 131 expect(instance.filteredOptions.length).to.be.lessThan(testOptions.length); ··· 144 137 145 138 test('default filter function works correctly', () => { 146 139 const instance = new AutocompletePrompt({ 147 - input, 148 - output, 140 + input: mocks.input, 141 + output: mocks.output, 149 142 render: () => 'foo', 150 143 options: testOptions, 151 144 }); 152 145 153 146 instance.prompt(); 154 147 155 - input.emit('keypress', 'a', { name: 'a' }); 156 - input.emit('keypress', 'p', { name: 'p' }); 148 + mocks.input.emit('keypress', 'a', { name: 'a' }); 149 + mocks.input.emit('keypress', 'p', { name: 'p' }); 157 150 158 151 expect(instance.filteredOptions).toEqual([ 159 152 { value: 'apple', label: 'Apple' }, 160 153 { value: 'grape', label: 'Grape' }, 161 154 ]); 162 155 163 - input.emit('keypress', 'z', { name: 'z' }); 156 + mocks.input.emit('keypress', 'z', { name: 'z' }); 164 157 165 158 expect(instance.filteredOptions).toEqual([]); 166 159 }); 167 160 168 161 test('submit without nav resolves to first option in non-multiple', async () => { 169 162 const instance = new AutocompletePrompt({ 170 - input, 171 - output, 163 + input: mocks.input, 164 + output: mocks.output, 172 165 render: () => 'foo', 173 166 options: testOptions, 174 167 }); 175 168 176 169 const promise = instance.prompt(); 177 - input.emit('keypress', '', { name: 'return' }); 170 + mocks.input.emit('keypress', '', { name: 'return' }); 178 171 const result = await promise; 179 172 180 173 expect(instance.selectedValues).to.deep.equal(['apple']); ··· 183 176 184 177 test('submit without nav resolves to [] in multiple', async () => { 185 178 const instance = new AutocompletePrompt({ 186 - input, 187 - output, 179 + input: mocks.input, 180 + output: mocks.output, 188 181 render: () => 'foo', 189 182 options: testOptions, 190 183 multiple: true, 191 184 }); 192 185 193 186 const promise = instance.prompt(); 194 - input.emit('keypress', '', { name: 'return' }); 187 + mocks.input.emit('keypress', '', { name: 'return' }); 195 188 const result = await promise; 196 189 197 190 expect(instance.selectedValues).to.deep.equal([]); ··· 200 193 201 194 test('Tab with empty input and placeholder fills input and submit returns matching option', async () => { 202 195 const instance = new AutocompletePrompt({ 203 - input, 204 - output, 196 + input: mocks.input, 197 + output: mocks.output, 205 198 render: () => 'foo', 206 199 options: testOptions, 207 200 placeholder: 'apple', 208 201 }); 209 202 210 203 const promise = instance.prompt(); 211 - input.emit('keypress', '\t', { name: 'tab' }); 212 - input.emit('keypress', '', { name: 'return' }); 204 + mocks.input.emit('keypress', '\t', { name: 'tab' }); 205 + mocks.input.emit('keypress', '', { name: 'return' }); 213 206 const result = await promise; 214 207 215 208 expect(instance.userInput).to.equal('apple'); ··· 222 215 { value: 'banana', label: 'Banana' }, 223 216 ]; 224 217 const instance = new AutocompletePrompt({ 225 - input, 226 - output, 218 + input: mocks.input, 219 + output: mocks.output, 227 220 render: () => 'foo', 228 221 options: () => dynamicOptions, 229 222 }); 230 223 231 224 instance.prompt(); 232 225 233 - input.emit('keypress', 'z', { name: 'z' }); 226 + mocks.input.emit('keypress', 'z', { name: 'z' }); 234 227 235 228 expect(instance.filteredOptions).toEqual(dynamicOptions); 236 229 }); ··· 242 235 { value: 'cherry', label: 'Cherry' }, 243 236 ]; 244 237 const instance = new AutocompletePrompt({ 245 - input, 246 - output, 238 + input: mocks.input, 239 + output: mocks.output, 247 240 render: () => 'foo', 248 241 options: () => dynamicOptions, 249 242 filter: (search, opt) => (opt.label ?? '').toLowerCase().endsWith(search.toLowerCase()), ··· 251 244 252 245 instance.prompt(); 253 246 254 - input.emit('keypress', 'a', { name: 'a' }); 247 + mocks.input.emit('keypress', 'a', { name: 'a' }); 255 248 256 249 // 'endsWith' matches Banana but not Apple or Cherry 257 250 expect(instance.filteredOptions).toEqual([{ value: 'banana', label: 'Banana' }]); ··· 259 252 260 253 test('Tab with non-matching placeholder does not fill input', async () => { 261 254 const instance = new AutocompletePrompt({ 262 - input, 263 - output, 255 + input: mocks.input, 256 + output: mocks.output, 264 257 render: () => 'foo', 265 258 options: testOptions, 266 259 placeholder: 'Type to search...', 267 260 }); 268 261 269 262 instance.prompt(); 270 - input.emit('keypress', '\t', { name: 'tab' }); 263 + mocks.input.emit('keypress', '\t', { name: 'tab' }); 271 264 272 265 // Placeholder does not match any option, so input must not be filled with placeholder 273 266 expect(instance.userInput).not.to.equal('Type to search...');
+20 -27
packages/core/test/prompts/confirm.test.ts packages/core/src/prompts/confirm.test.ts
··· 1 1 import { cursor } from 'sisteransi'; 2 - import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 3 - import { default as ConfirmPrompt } from '../../src/prompts/confirm.js'; 4 - import { MockReadable } from '../mock-readable.js'; 5 - import { MockWritable } from '../mock-writable.js'; 2 + import { beforeEach, describe, expect, test } from 'vitest'; 3 + import { default as ConfirmPrompt } from './confirm.js'; 4 + import { createMocks, type Mocks } from '@bomb.sh/tools/test-utils'; 6 5 7 6 describe('ConfirmPrompt', () => { 8 - let input: MockReadable; 9 - let output: MockWritable; 7 + let mocks: Mocks<{ input: true; output: true }>; 10 8 11 9 beforeEach(() => { 12 - input = new MockReadable(); 13 - output = new MockWritable(); 14 - }); 15 - 16 - afterEach(() => { 17 - vi.restoreAllMocks(); 10 + mocks = createMocks({ input: true, output: true }); 18 11 }); 19 12 20 13 test('renders render() result', () => { 21 14 const instance = new ConfirmPrompt({ 22 - input, 23 - output, 15 + input: mocks.input, 16 + output: mocks.output, 24 17 render: () => 'foo', 25 18 active: 'yes', 26 19 inactive: 'no', 27 20 }); 28 21 instance.prompt(); 29 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); 22 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo']); 30 23 }); 31 24 32 25 test('sets value and submits on confirm (y)', () => { 33 26 const instance = new ConfirmPrompt({ 34 - input, 35 - output, 27 + input: mocks.input, 28 + output: mocks.output, 36 29 render: () => 'foo', 37 30 active: 'yes', 38 31 inactive: 'no', ··· 40 33 }); 41 34 42 35 instance.prompt(); 43 - input.emit('keypress', 'y', { name: 'y' }); 36 + mocks.input.emit('keypress', 'y', { name: 'y' }); 44 37 45 38 expect(instance.value).to.equal(true); 46 39 expect(instance.state).to.equal('submit'); ··· 48 41 49 42 test('sets value and submits on confirm (n)', () => { 50 43 const instance = new ConfirmPrompt({ 51 - input, 52 - output, 44 + input: mocks.input, 45 + output: mocks.output, 53 46 render: () => 'foo', 54 47 active: 'yes', 55 48 inactive: 'no', ··· 57 50 }); 58 51 59 52 instance.prompt(); 60 - input.emit('keypress', 'n', { name: 'n' }); 53 + mocks.input.emit('keypress', 'n', { name: 'n' }); 61 54 62 55 expect(instance.value).to.equal(false); 63 56 expect(instance.state).to.equal('submit'); ··· 66 59 describe('cursor', () => { 67 60 test('cursor is 1 when inactive', () => { 68 61 const instance = new ConfirmPrompt({ 69 - input, 70 - output, 62 + input: mocks.input, 63 + output: mocks.output, 71 64 render: () => 'foo', 72 65 active: 'yes', 73 66 inactive: 'no', ··· 75 68 }); 76 69 77 70 instance.prompt(); 78 - input.emit('keypress', '', { name: 'return' }); 71 + mocks.input.emit('keypress', '', { name: 'return' }); 79 72 expect(instance.cursor).to.equal(1); 80 73 }); 81 74 82 75 test('cursor is 0 when active', () => { 83 76 const instance = new ConfirmPrompt({ 84 - input, 85 - output, 77 + input: mocks.input, 78 + output: mocks.output, 86 79 render: () => 'foo', 87 80 active: 'yes', 88 81 inactive: 'no', ··· 90 83 }); 91 84 92 85 instance.prompt(); 93 - input.emit('keypress', '', { name: 'return' }); 86 + mocks.input.emit('keypress', '', { name: 'return' }); 94 87 expect(instance.cursor).to.equal(0); 95 88 }); 96 89 });
+98 -105
packages/core/test/prompts/date.test.ts packages/core/src/prompts/date.test.ts
··· 1 1 import { cursor } from 'sisteransi'; 2 - import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 3 - import { default as DatePrompt } from '../../src/prompts/date.js'; 4 - import { isCancel } from '../../src/utils/index.js'; 5 - import { MockReadable } from '../mock-readable.js'; 6 - import { MockWritable } from '../mock-writable.js'; 2 + import { beforeEach, describe, expect, test } from 'vitest'; 3 + import { default as DatePrompt } from './date.js'; 4 + import { isCancel } from '../utils/index.js'; 5 + import { createMocks, type Mocks } from '@bomb.sh/tools/test-utils'; 7 6 8 7 const d = (iso: string) => { 9 8 const [y, m, day] = iso.slice(0, 10).split('-').map(Number); ··· 11 10 }; 12 11 13 12 describe('DatePrompt', () => { 14 - let input: MockReadable; 15 - let output: MockWritable; 13 + let mocks: Mocks<{ input: true; output: true }>; 16 14 17 15 beforeEach(() => { 18 - input = new MockReadable(); 19 - output = new MockWritable(); 20 - }); 21 - 22 - afterEach(() => { 23 - vi.restoreAllMocks(); 16 + mocks = createMocks({ input: true, output: true }); 24 17 }); 25 18 26 19 test('renders render() result', () => { 27 20 const instance = new DatePrompt({ 28 - input, 29 - output, 21 + input: mocks.input, 22 + output: mocks.output, 30 23 render: () => 'foo', 31 24 format: 'YMD', 32 25 }); 33 26 instance.prompt(); 34 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); 27 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo']); 35 28 }); 36 29 37 30 test('initial value displays correctly', () => { 38 31 const instance = new DatePrompt({ 39 - input, 40 - output, 32 + input: mocks.input, 33 + output: mocks.output, 41 34 render: () => 'foo', 42 35 format: 'YMD', 43 36 initialValue: d('2025-01-15'), ··· 50 43 51 44 test('left/right navigates between segments', () => { 52 45 const instance = new DatePrompt({ 53 - input, 54 - output, 46 + input: mocks.input, 47 + output: mocks.output, 55 48 render: () => 'foo', 56 49 format: 'YMD', 57 50 initialValue: d('2025-01-15'), 58 51 }); 59 52 instance.prompt(); 60 53 expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 }); 61 - input.emit('keypress', undefined, { name: 'right' }); 54 + mocks.input.emit('keypress', undefined, { name: 'right' }); 62 55 expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 1, positionInSegment: 0 }); 63 - input.emit('keypress', undefined, { name: 'right' }); 56 + mocks.input.emit('keypress', undefined, { name: 'right' }); 64 57 expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 2, positionInSegment: 0 }); 65 - input.emit('keypress', undefined, { name: 'left' }); 58 + mocks.input.emit('keypress', undefined, { name: 'left' }); 66 59 expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 1, positionInSegment: 0 }); 67 60 }); 68 61 69 62 test('up/down increments and decrements segment', () => { 70 63 const instance = new DatePrompt({ 71 - input, 72 - output, 64 + input: mocks.input, 65 + output: mocks.output, 73 66 render: () => 'foo', 74 67 format: 'YMD', 75 68 initialValue: d('2025-01-15'), 76 69 }); 77 70 instance.prompt(); 78 - input.emit('keypress', undefined, { name: 'right' }); // move to month 79 - input.emit('keypress', undefined, { name: 'up' }); 71 + mocks.input.emit('keypress', undefined, { name: 'right' }); // move to month 72 + mocks.input.emit('keypress', undefined, { name: 'up' }); 80 73 expect(instance.userInput).to.equal('2025/02/15'); 81 - input.emit('keypress', undefined, { name: 'down' }); 74 + mocks.input.emit('keypress', undefined, { name: 'down' }); 82 75 expect(instance.userInput).to.equal('2025/01/15'); 83 76 }); 84 77 85 78 test('up/down on one segment leaves other segments blank', () => { 86 79 const instance = new DatePrompt({ 87 - input, 88 - output, 80 + input: mocks.input, 81 + output: mocks.output, 89 82 render: () => 'foo', 90 83 format: 'YMD', 91 84 }); 92 85 instance.prompt(); 93 86 expect(instance.userInput).to.equal('____/__/__'); 94 - input.emit('keypress', undefined, { name: 'up' }); // up on year (first segment) 87 + mocks.input.emit('keypress', undefined, { name: 'up' }); // up on year (first segment) 95 88 expect(instance.userInput).to.equal('0001/__/__'); 96 - input.emit('keypress', undefined, { name: 'right' }); // move to month 97 - input.emit('keypress', undefined, { name: 'up' }); 89 + mocks.input.emit('keypress', undefined, { name: 'right' }); // move to month 90 + mocks.input.emit('keypress', undefined, { name: 'up' }); 98 91 expect(instance.userInput).to.equal('0001/01/__'); 99 92 }); 100 93 101 94 test('with minDate/maxDate, up on blank segment starts at min', () => { 102 95 const instance = new DatePrompt({ 103 - input, 104 - output, 96 + input: mocks.input, 97 + output: mocks.output, 105 98 render: () => 'foo', 106 99 format: 'YMD', 107 100 minDate: d('2025-03-10'), ··· 109 102 }); 110 103 instance.prompt(); 111 104 expect(instance.userInput).to.equal('____/__/__'); 112 - input.emit('keypress', undefined, { name: 'up' }); 105 + mocks.input.emit('keypress', undefined, { name: 'up' }); 113 106 expect(instance.userInput).to.equal('2025/__/__'); 114 - input.emit('keypress', undefined, { name: 'right' }); 115 - input.emit('keypress', undefined, { name: 'up' }); 107 + mocks.input.emit('keypress', undefined, { name: 'right' }); 108 + mocks.input.emit('keypress', undefined, { name: 'up' }); 116 109 expect(instance.userInput).to.equal('2025/03/__'); 117 - input.emit('keypress', undefined, { name: 'right' }); 118 - input.emit('keypress', undefined, { name: 'up' }); 110 + mocks.input.emit('keypress', undefined, { name: 'right' }); 111 + mocks.input.emit('keypress', undefined, { name: 'up' }); 119 112 expect(instance.userInput).to.equal('2025/03/10'); 120 113 }); 121 114 122 115 test('with minDate/maxDate, down on blank segment starts at max', () => { 123 116 const instance = new DatePrompt({ 124 - input, 125 - output, 117 + input: mocks.input, 118 + output: mocks.output, 126 119 render: () => 'foo', 127 120 format: 'YMD', 128 121 minDate: d('2025-03-10'), 129 122 maxDate: d('2025-11-20'), 130 123 }); 131 124 instance.prompt(); 132 - input.emit('keypress', undefined, { name: 'down' }); 125 + mocks.input.emit('keypress', undefined, { name: 'down' }); 133 126 expect(instance.userInput).to.equal('2025/__/__'); 134 - input.emit('keypress', undefined, { name: 'right' }); 135 - input.emit('keypress', undefined, { name: 'down' }); 127 + mocks.input.emit('keypress', undefined, { name: 'right' }); 128 + mocks.input.emit('keypress', undefined, { name: 'down' }); 136 129 expect(instance.userInput).to.equal('2025/11/__'); 137 - input.emit('keypress', undefined, { name: 'right' }); 138 - input.emit('keypress', undefined, { name: 'down' }); 130 + mocks.input.emit('keypress', undefined, { name: 'right' }); 131 + mocks.input.emit('keypress', undefined, { name: 'down' }); 139 132 expect(instance.userInput).to.equal('2025/11/20'); 140 133 }); 141 134 142 135 test('digit-by-digit editing from left to right', () => { 143 136 const instance = new DatePrompt({ 144 - input, 145 - output, 137 + input: mocks.input, 138 + output: mocks.output, 146 139 render: () => 'foo', 147 140 format: 'YMD', 148 141 initialValue: d('2025-01-15'), ··· 150 143 instance.prompt(); 151 144 expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 }); 152 145 // Type 2,0,2,3 to change year to 2023 (right-to-left fill) 153 - input.emit('keypress', '2', { name: undefined, sequence: '2' }); 146 + mocks.input.emit('keypress', '2', { name: undefined, sequence: '2' }); 154 147 expect(instance.userInput).to.equal('___2/01/15'); 155 - input.emit('keypress', '0', { name: undefined, sequence: '0' }); 148 + mocks.input.emit('keypress', '0', { name: undefined, sequence: '0' }); 156 149 expect(instance.userInput).to.equal('__20/01/15'); 157 - input.emit('keypress', '2', { name: undefined, sequence: '2' }); 150 + mocks.input.emit('keypress', '2', { name: undefined, sequence: '2' }); 158 151 expect(instance.userInput).to.equal('_202/01/15'); 159 - input.emit('keypress', '3', { name: undefined, sequence: '3' }); 152 + mocks.input.emit('keypress', '3', { name: undefined, sequence: '3' }); 160 153 expect(instance.userInput).to.equal('2023/01/15'); 161 154 }); 162 155 163 156 test('backspace clears entire segment at any cursor position', () => { 164 157 const instance = new DatePrompt({ 165 - input, 166 - output, 158 + input: mocks.input, 159 + output: mocks.output, 167 160 render: () => 'foo', 168 161 format: 'YMD', 169 162 initialValue: d('2025-12-21'), 170 163 }); 171 164 instance.prompt(); 172 165 expect(instance.userInput).to.equal('2025/12/21'); 173 - input.emit('keypress', undefined, { name: 'backspace', sequence: '\x7f' }); 166 + mocks.input.emit('keypress', undefined, { name: 'backspace', sequence: '\x7f' }); 174 167 expect(instance.userInput).to.equal('____/12/21'); 175 168 expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 }); 176 169 }); 177 170 178 171 test('backspace clears segment when cursor at first char (2___)', () => { 179 172 const instance = new DatePrompt({ 180 - input, 181 - output, 173 + input: mocks.input, 174 + output: mocks.output, 182 175 render: () => 'foo', 183 176 format: 'YMD', 184 177 }); 185 178 instance.prompt(); 186 - input.emit('keypress', '2', { name: undefined, sequence: '2' }); 179 + mocks.input.emit('keypress', '2', { name: undefined, sequence: '2' }); 187 180 expect(instance.userInput).to.equal('___2/__/__'); 188 - input.emit('keypress', '\x7f', { name: undefined, sequence: '\x7f' }); 181 + mocks.input.emit('keypress', '\x7f', { name: undefined, sequence: '\x7f' }); 189 182 expect(instance.userInput).to.equal('____/__/__'); 190 183 expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 }); 191 184 }); 192 185 193 186 test('digit input updates segment and jumps to next when complete', () => { 194 187 const instance = new DatePrompt({ 195 - input, 196 - output, 188 + input: mocks.input, 189 + output: mocks.output, 197 190 render: () => 'foo', 198 191 format: 'YMD', 199 192 }); 200 193 instance.prompt(); 201 194 for (const c of '2025') { 202 - input.emit('keypress', c, { name: undefined, sequence: c }); 195 + mocks.input.emit('keypress', c, { name: undefined, sequence: c }); 203 196 } 204 197 expect(instance.userInput).to.equal('2025/__/__'); 205 198 expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 1, positionInSegment: 0 }); ··· 207 200 208 201 test('submit returns Date for valid date', async () => { 209 202 const instance = new DatePrompt({ 210 - input, 211 - output, 203 + input: mocks.input, 204 + output: mocks.output, 212 205 render: () => 'foo', 213 206 format: 'YMD', 214 207 initialValue: d('2025-01-31'), 215 208 }); 216 209 const resultPromise = instance.prompt(); 217 - input.emit('keypress', undefined, { name: 'return' }); 210 + mocks.input.emit('keypress', undefined, { name: 'return' }); 218 211 const result = await resultPromise; 219 212 expect(result).toBeInstanceOf(Date); 220 213 expect((result as Date).toISOString().slice(0, 10)).to.equal('2025-01-31'); ··· 222 215 223 216 test('can cancel', async () => { 224 217 const instance = new DatePrompt({ 225 - input, 226 - output, 218 + input: mocks.input, 219 + output: mocks.output, 227 220 render: () => 'foo', 228 221 format: 'YMD', 229 222 initialValue: d('2025-01-15'), 230 223 }); 231 224 const resultPromise = instance.prompt(); 232 - input.emit('keypress', 'escape', { name: 'escape' }); 225 + mocks.input.emit('keypress', 'escape', { name: 'escape' }); 233 226 const result = await resultPromise; 234 227 expect(isCancel(result)).toBe(true); 235 228 }); 236 229 237 230 test('defaultValue used when invalid date submitted', async () => { 238 231 const instance = new DatePrompt({ 239 - input, 240 - output, 232 + input: mocks.input, 233 + output: mocks.output, 241 234 render: () => 'foo', 242 235 format: 'YMD', 243 236 defaultValue: d('2025-06-15'), 244 237 }); 245 238 const resultPromise = instance.prompt(); 246 - input.emit('keypress', undefined, { name: 'return' }); 239 + mocks.input.emit('keypress', undefined, { name: 'return' }); 247 240 const result = await resultPromise; 248 241 expect(result).toBeInstanceOf(Date); 249 242 expect((result as Date).toISOString().slice(0, 10)).to.equal('2025-06-15'); ··· 251 244 252 245 test('supports MDY format', () => { 253 246 const instance = new DatePrompt({ 254 - input, 255 - output, 247 + input: mocks.input, 248 + output: mocks.output, 256 249 render: () => 'foo', 257 250 format: 'MDY', 258 251 initialValue: d('2025-01-15'), ··· 263 256 264 257 test('supports DMY format', () => { 265 258 const instance = new DatePrompt({ 266 - input, 267 - output, 259 + input: mocks.input, 260 + output: mocks.output, 268 261 render: () => 'foo', 269 262 format: 'DMY', 270 263 initialValue: d('2025-01-15'), ··· 275 268 276 269 test('rejects invalid month via pending tens digit', () => { 277 270 const instance = new DatePrompt({ 278 - input, 279 - output, 271 + input: mocks.input, 272 + output: mocks.output, 280 273 render: () => 'foo', 281 274 format: 'YMD', 282 275 }); 283 276 instance.prompt(); 284 277 // Navigate to month 285 - input.emit('keypress', undefined, { name: 'right' }); 278 + mocks.input.emit('keypress', undefined, { name: 'right' }); 286 279 // Type '1' → '01' with pending tens digit (since 1 <= 1) 287 - input.emit('keypress', '1', { name: undefined, sequence: '1' }); 280 + mocks.input.emit('keypress', '1', { name: undefined, sequence: '1' }); 288 281 expect(instance.segmentValues.month).to.equal('01'); 289 282 // Type '3' → tries '13' which is > 12 → inline error 290 - input.emit('keypress', '3', { name: undefined, sequence: '3' }); 283 + mocks.input.emit('keypress', '3', { name: undefined, sequence: '3' }); 291 284 expect(instance.inlineError).to.equal('There are only 12 months in a year'); 292 285 }); 293 286 294 287 test('rejects invalid day via pending tens digit', () => { 295 288 const instance = new DatePrompt({ 296 - input, 297 - output, 289 + input: mocks.input, 290 + output: mocks.output, 298 291 render: () => 'foo', 299 292 format: 'YMD', 300 293 }); 301 294 instance.prompt(); 302 295 // Navigate to day 303 - input.emit('keypress', undefined, { name: 'right' }); 304 - input.emit('keypress', undefined, { name: 'right' }); 296 + mocks.input.emit('keypress', undefined, { name: 'right' }); 297 + mocks.input.emit('keypress', undefined, { name: 'right' }); 305 298 // Type '2' → '02' with pending (2 <= 2) 306 - input.emit('keypress', '2', { name: undefined, sequence: '2' }); 307 - input.emit('keypress', '0', { name: undefined, sequence: '0' }); 299 + mocks.input.emit('keypress', '2', { name: undefined, sequence: '2' }); 300 + mocks.input.emit('keypress', '0', { name: undefined, sequence: '0' }); 308 301 expect(instance.inlineError).to.equal(''); 309 302 }); 310 303 311 304 describe('segmentValues and segmentCursor', () => { 312 305 test('segmentValues reflects current input', () => { 313 306 const instance = new DatePrompt({ 314 - input, 315 - output, 307 + input: mocks.input, 308 + output: mocks.output, 316 309 render: () => 'foo', 317 310 format: 'YMD', 318 311 initialValue: d('2025-01-15'), ··· 326 319 327 320 test('segmentCursor tracks cursor position', () => { 328 321 const instance = new DatePrompt({ 329 - input, 330 - output, 322 + input: mocks.input, 323 + output: mocks.output, 331 324 render: () => 'foo', 332 325 format: 'YMD', 333 326 initialValue: d('2025-01-15'), 334 327 }); 335 328 instance.prompt(); 336 - input.emit('keypress', undefined, { name: 'right' }); // move to month 329 + mocks.input.emit('keypress', undefined, { name: 'right' }); // move to month 337 330 const cursor = instance.segmentCursor; 338 331 expect(cursor.segmentIndex).to.equal(1); 339 332 expect(cursor.positionInSegment).to.equal(0); ··· 341 334 342 335 test('segmentValues updates on submit', () => { 343 336 const instance = new DatePrompt({ 344 - input, 345 - output, 337 + input: mocks.input, 338 + output: mocks.output, 346 339 render: () => 'foo', 347 340 format: 'YMD', 348 341 initialValue: d('2025-01-15'), 349 342 }); 350 343 instance.prompt(); 351 - input.emit('keypress', undefined, { name: 'return' }); 344 + mocks.input.emit('keypress', undefined, { name: 'return' }); 352 345 const segmentValues = instance.segmentValues; 353 346 expect(segmentValues.year).to.equal('2025'); 354 347 expect(segmentValues.month).to.equal('01'); ··· 359 352 describe('formattedValue and segments', () => { 360 353 test('formattedValue returns formatted string', () => { 361 354 const instance = new DatePrompt({ 362 - input, 363 - output, 355 + input: mocks.input, 356 + output: mocks.output, 364 357 render: () => 'foo', 365 358 format: 'MDY', 366 359 initialValue: d('2025-03-15'), ··· 371 364 372 365 test('segments exposes segment config', () => { 373 366 const instance = new DatePrompt({ 374 - input, 375 - output, 367 + input: mocks.input, 368 + output: mocks.output, 376 369 render: () => 'foo', 377 370 format: 'DMY', 378 371 }); ··· 386 379 387 380 test('separator defaults to / for explicit format', () => { 388 381 const instance = new DatePrompt({ 389 - input, 390 - output, 382 + input: mocks.input, 383 + output: mocks.output, 391 384 render: () => 'foo', 392 385 format: 'YMD', 393 386 }); ··· 399 392 describe('locale detection', () => { 400 393 test('locale auto-detects format from Intl', () => { 401 394 const instance = new DatePrompt({ 402 - input, 403 - output, 395 + input: mocks.input, 396 + output: mocks.output, 404 397 render: () => 'foo', 405 398 locale: 'en-US', 406 399 initialValue: d('2025-03-15'), ··· 414 407 415 408 test('explicit format overrides locale', () => { 416 409 const instance = new DatePrompt({ 417 - input, 418 - output, 410 + input: mocks.input, 411 + output: mocks.output, 419 412 render: () => 'foo', 420 413 format: 'YMD', 421 414 locale: 'en-US', // would be MDY, but format takes precedence
-355
packages/core/test/prompts/multi-line.test.ts
··· 1 - import { styleText } from 'node:util'; 2 - import { cursor } from 'sisteransi'; 3 - import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 4 - import { default as MultiLinePrompt } from '../../src/prompts/multi-line.js'; 5 - import { MockReadable } from '../mock-readable.js'; 6 - import { MockWritable } from '../mock-writable.js'; 7 - 8 - describe('MultiLinePrompt', () => { 9 - let input: MockReadable; 10 - let output: MockWritable; 11 - 12 - beforeEach(() => { 13 - input = new MockReadable(); 14 - output = new MockWritable(); 15 - }); 16 - 17 - afterEach(() => { 18 - vi.restoreAllMocks(); 19 - }); 20 - 21 - test('renders render() result', () => { 22 - const instance = new MultiLinePrompt({ 23 - input, 24 - output, 25 - render: () => 'foo', 26 - }); 27 - instance.prompt(); 28 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); 29 - }); 30 - 31 - test('sets default value on finalize if no value', async () => { 32 - const instance = new MultiLinePrompt({ 33 - input, 34 - output, 35 - render: () => 'foo', 36 - defaultValue: 'bleep bloop', 37 - }); 38 - const resultPromise = instance.prompt(); 39 - input.emit('keypress', '', { name: 'return' }); 40 - input.emit('keypress', '', { name: 'return' }); 41 - const result = await resultPromise; 42 - expect(result).to.equal('bleep bloop'); 43 - }); 44 - 45 - test('keeps value on finalize', async () => { 46 - const instance = new MultiLinePrompt({ 47 - input, 48 - output, 49 - render: () => 'foo', 50 - defaultValue: 'bleep bloop', 51 - }); 52 - const resultPromise = instance.prompt(); 53 - input.emit('keypress', 'x', { name: 'x' }); 54 - input.emit('keypress', '', { name: 'return' }); 55 - input.emit('keypress', '', { name: 'return' }); 56 - const result = await resultPromise; 57 - expect(result).to.equal('x'); 58 - }); 59 - 60 - describe('cursor', () => { 61 - test('can get cursor', () => { 62 - const instance = new MultiLinePrompt({ 63 - input, 64 - output, 65 - render: () => 'foo', 66 - }); 67 - 68 - expect(instance.cursor).to.equal(0); 69 - }); 70 - }); 71 - 72 - describe('userInputWithCursor', () => { 73 - test('returns value on submit', () => { 74 - const instance = new MultiLinePrompt({ 75 - input, 76 - output, 77 - render: () => 'foo', 78 - }); 79 - instance.prompt(); 80 - input.emit('keypress', 'x', { name: 'x' }); 81 - input.emit('keypress', '', { name: 'return' }); 82 - input.emit('keypress', '', { name: 'return' }); 83 - expect(instance.userInputWithCursor).to.equal('x'); 84 - }); 85 - 86 - test('highlights cursor position', () => { 87 - const instance = new MultiLinePrompt({ 88 - input, 89 - output, 90 - render: () => 'foo', 91 - }); 92 - instance.prompt(); 93 - const keys = 'foo'; 94 - for (let i = 0; i < keys.length; i++) { 95 - input.emit('keypress', keys[i], { name: keys[i] }); 96 - } 97 - input.emit('keypress', undefined, { name: 'left' }); 98 - expect(instance.userInputWithCursor).to.equal(`fo${styleText('inverse', 'o')}`); 99 - }); 100 - 101 - test('shows cursor at end if beyond value', () => { 102 - const instance = new MultiLinePrompt({ 103 - input, 104 - output, 105 - render: () => 'foo', 106 - }); 107 - instance.prompt(); 108 - const keys = 'foo'; 109 - for (let i = 0; i < keys.length; i++) { 110 - input.emit('keypress', keys[i], { name: keys[i] }); 111 - } 112 - input.emit('keypress', undefined, { name: 'right' }); 113 - expect(instance.userInputWithCursor).to.equal('foo█'); 114 - }); 115 - }); 116 - 117 - describe('key', () => { 118 - test('return inserts newline', () => { 119 - const instance = new MultiLinePrompt({ 120 - input, 121 - output, 122 - render: () => 'foo', 123 - }); 124 - instance.prompt(); 125 - input.emit('keypress', 'x', { name: 'x' }); 126 - input.emit('keypress', '', { name: 'return' }); 127 - expect(instance.userInput).to.equal('x\n'); 128 - }); 129 - 130 - test('double return submits', async () => { 131 - const instance = new MultiLinePrompt({ 132 - input, 133 - output, 134 - render: () => 'foo', 135 - }); 136 - const resultPromise = instance.prompt(); 137 - input.emit('keypress', 'x', { name: 'x' }); 138 - input.emit('keypress', '', { name: 'return' }); 139 - input.emit('keypress', '', { name: 'return' }); 140 - const result = await resultPromise; 141 - expect(result).to.equal('x'); 142 - }); 143 - 144 - test('double return inserts when showSubmit is true', async () => { 145 - const instance = new MultiLinePrompt({ 146 - input, 147 - output, 148 - render: () => 'foo', 149 - showSubmit: true, 150 - }); 151 - const resultPromise = instance.prompt(); 152 - input.emit('keypress', 'x', { name: 'x' }); 153 - input.emit('keypress', '', { name: 'return' }); 154 - input.emit('keypress', '', { name: 'return' }); 155 - input.emit('keypress', '\t', { name: 'tab' }); 156 - input.emit('keypress', '', { name: 'return' }); 157 - const result = await resultPromise; 158 - expect(result).to.equal('x\n\n'); 159 - }); 160 - 161 - test('typing when submit selected jumps back to text', async () => { 162 - const instance = new MultiLinePrompt({ 163 - input, 164 - output, 165 - render: () => 'foo', 166 - showSubmit: true, 167 - }); 168 - const resultPromise = instance.prompt(); 169 - input.emit('keypress', 'x', { name: 'x' }); 170 - input.emit('keypress', '\t', { name: 'tab' }); 171 - input.emit('keypress', 'y', { name: 'y' }); 172 - input.emit('keypress', '\t', { name: 'tab' }); 173 - input.emit('keypress', '', { name: 'return' }); 174 - const result = await resultPromise; 175 - expect(result).to.equal('xy'); 176 - }); 177 - 178 - test('backspace deletes previous char', async () => { 179 - const instance = new MultiLinePrompt({ 180 - input, 181 - output, 182 - render: () => 'foo', 183 - }); 184 - const resultPromise = instance.prompt(); 185 - input.emit('keypress', 'x', { name: 'x' }); 186 - input.emit('keypress', 'y', { name: 'y' }); 187 - input.emit('keypress', '', { name: 'backspace' }); 188 - input.emit('keypress', '', { name: 'return' }); 189 - input.emit('keypress', '', { name: 'return' }); 190 - const result = await resultPromise; 191 - expect(result).to.equal('x'); 192 - }); 193 - 194 - test('delete deletes next char', async () => { 195 - const instance = new MultiLinePrompt({ 196 - input, 197 - output, 198 - render: () => 'foo', 199 - }); 200 - const resultPromise = instance.prompt(); 201 - input.emit('keypress', 'x', { name: 'x' }); 202 - input.emit('keypress', 'y', { name: 'y' }); 203 - input.emit('keypress', '', { name: 'left' }); 204 - input.emit('keypress', '', { name: 'delete' }); 205 - input.emit('keypress', '', { name: 'return' }); 206 - input.emit('keypress', '', { name: 'return' }); 207 - const result = await resultPromise; 208 - expect(result).to.equal('x'); 209 - }); 210 - 211 - test('delete does nothing at end', async () => { 212 - const instance = new MultiLinePrompt({ 213 - input, 214 - output, 215 - render: () => 'foo', 216 - }); 217 - const resultPromise = instance.prompt(); 218 - input.emit('keypress', 'x', { name: 'x' }); 219 - input.emit('keypress', '', { name: 'delete' }); 220 - input.emit('keypress', '', { name: 'return' }); 221 - input.emit('keypress', '', { name: 'return' }); 222 - const result = await resultPromise; 223 - expect(result).to.equal('x'); 224 - }); 225 - 226 - test('backspace does nothing at start', async () => { 227 - const instance = new MultiLinePrompt({ 228 - input, 229 - output, 230 - render: () => 'foo', 231 - }); 232 - const resultPromise = instance.prompt(); 233 - input.emit('keypress', 'x', { name: 'x' }); 234 - input.emit('keypress', '', { name: 'left' }); 235 - input.emit('keypress', '', { name: 'backspace' }); 236 - input.emit('keypress', '', { name: 'return' }); 237 - input.emit('keypress', '', { name: 'return' }); 238 - const result = await resultPromise; 239 - expect(result).to.equal('x'); 240 - }); 241 - 242 - test('left moves left until start', async () => { 243 - const instance = new MultiLinePrompt({ 244 - input, 245 - output, 246 - render: () => 'foo', 247 - }); 248 - const resultPromise = instance.prompt(); 249 - input.emit('keypress', 'x', { name: 'x' }); 250 - input.emit('keypress', '', { name: 'left' }); 251 - input.emit('keypress', '', { name: 'left' }); 252 - input.emit('keypress', 'y', { name: 'y' }); 253 - input.emit('keypress', '', { name: 'return' }); 254 - input.emit('keypress', '', { name: 'return' }); 255 - const result = await resultPromise; 256 - expect(result).to.equal('yx'); 257 - }); 258 - 259 - test('right moves right until end', async () => { 260 - const instance = new MultiLinePrompt({ 261 - input, 262 - output, 263 - render: () => 'foo', 264 - }); 265 - const resultPromise = instance.prompt(); 266 - input.emit('keypress', 'x', { name: 'x' }); 267 - input.emit('keypress', 'y', { name: 'y' }); 268 - input.emit('keypress', '', { name: 'left' }); 269 - input.emit('keypress', '', { name: 'right' }); 270 - input.emit('keypress', '', { name: 'right' }); 271 - input.emit('keypress', 'z', { name: 'z' }); 272 - input.emit('keypress', '', { name: 'return' }); 273 - input.emit('keypress', '', { name: 'return' }); 274 - const result = await resultPromise; 275 - expect(result).to.equal('xyz'); 276 - }); 277 - 278 - test('left moves across lines', async () => { 279 - const instance = new MultiLinePrompt({ 280 - input, 281 - output, 282 - render: () => 'foo', 283 - }); 284 - const resultPromise = instance.prompt(); 285 - input.emit('keypress', 'x', { name: 'x' }); 286 - input.emit('keypress', '', { name: 'return' }); 287 - input.emit('keypress', 'y', { name: 'y' }); 288 - input.emit('keypress', '', { name: 'left' }); 289 - input.emit('keypress', '', { name: 'left' }); 290 - input.emit('keypress', 'z', { name: 'z' }); 291 - input.emit('keypress', '', { name: 'return' }); 292 - input.emit('keypress', '', { name: 'return' }); 293 - const result = await resultPromise; 294 - expect(result).to.equal('xz\ny'); 295 - }); 296 - 297 - test('right moves across lines', async () => { 298 - const instance = new MultiLinePrompt({ 299 - input, 300 - output, 301 - render: () => 'foo', 302 - }); 303 - const resultPromise = instance.prompt(); 304 - input.emit('keypress', 'x', { name: 'x' }); 305 - input.emit('keypress', '', { name: 'return' }); 306 - input.emit('keypress', 'y', { name: 'y' }); 307 - input.emit('keypress', '', { name: 'left' }); 308 - input.emit('keypress', '', { name: 'left' }); 309 - input.emit('keypress', '', { name: 'right' }); 310 - input.emit('keypress', '', { name: 'right' }); 311 - input.emit('keypress', 'z', { name: 'z' }); 312 - input.emit('keypress', '', { name: 'return' }); 313 - input.emit('keypress', '', { name: 'return' }); 314 - const result = await resultPromise; 315 - expect(result).to.equal('x\nyz'); 316 - }); 317 - 318 - test('up moves up a line', async () => { 319 - const instance = new MultiLinePrompt({ 320 - input, 321 - output, 322 - render: () => 'foo', 323 - }); 324 - const resultPromise = instance.prompt(); 325 - input.emit('keypress', 'x', { name: 'x' }); 326 - input.emit('keypress', '', { name: 'return' }); 327 - input.emit('keypress', 'y', { name: 'y' }); 328 - input.emit('keypress', '', { name: 'up' }); 329 - input.emit('keypress', 'z', { name: 'z' }); 330 - input.emit('keypress', '', { name: 'return' }); 331 - input.emit('keypress', '', { name: 'return' }); 332 - const result = await resultPromise; 333 - expect(result).to.equal('xz\ny'); 334 - }); 335 - 336 - test('down moves down a line', async () => { 337 - const instance = new MultiLinePrompt({ 338 - input, 339 - output, 340 - render: () => 'foo', 341 - }); 342 - const resultPromise = instance.prompt(); 343 - input.emit('keypress', 'x', { name: 'x' }); 344 - input.emit('keypress', '', { name: 'return' }); 345 - input.emit('keypress', 'y', { name: 'y' }); 346 - input.emit('keypress', '', { name: 'up' }); 347 - input.emit('keypress', '', { name: 'down' }); 348 - input.emit('keypress', 'z', { name: 'z' }); 349 - input.emit('keypress', '', { name: 'return' }); 350 - input.emit('keypress', '', { name: 'return' }); 351 - const result = await resultPromise; 352 - expect(result).to.equal('x\nyz'); 353 - }); 354 - }); 355 - });
+48 -55
packages/core/test/prompts/multi-select.test.ts packages/core/src/prompts/multi-select.test.ts
··· 1 1 import { cursor } from 'sisteransi'; 2 - import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 3 - import { default as MultiSelectPrompt } from '../../src/prompts/multi-select.js'; 4 - import { MockReadable } from '../mock-readable.js'; 5 - import { MockWritable } from '../mock-writable.js'; 2 + import { beforeEach, describe, expect, test } from 'vitest'; 3 + import { default as MultiSelectPrompt } from './multi-select.js'; 4 + import { createMocks, type Mocks } from '@bomb.sh/tools/test-utils'; 6 5 7 6 describe('MultiSelectPrompt', () => { 8 - let input: MockReadable; 9 - let output: MockWritable; 7 + let mocks: Mocks<{ input: true; output: true }>; 10 8 11 9 beforeEach(() => { 12 - input = new MockReadable(); 13 - output = new MockWritable(); 14 - }); 15 - 16 - afterEach(() => { 17 - vi.restoreAllMocks(); 10 + mocks = createMocks({ input: true, output: true }); 18 11 }); 19 12 20 13 test('renders render() result', () => { 21 14 const instance = new MultiSelectPrompt({ 22 - input, 23 - output, 15 + input: mocks.input, 16 + output: mocks.output, 24 17 render: () => 'foo', 25 18 options: [{ value: 'foo' }, { value: 'bar' }], 26 19 }); 27 20 instance.prompt(); 28 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); 21 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo']); 29 22 }); 30 23 31 24 describe('cursor', () => { 32 25 test('cursor is index of selected item', () => { 33 26 const instance = new MultiSelectPrompt({ 34 - input, 35 - output, 27 + input: mocks.input, 28 + output: mocks.output, 36 29 render: () => 'foo', 37 30 options: [{ value: 'foo' }, { value: 'bar' }], 38 31 }); ··· 40 33 instance.prompt(); 41 34 42 35 expect(instance.cursor).to.equal(0); 43 - input.emit('keypress', 'down', { name: 'down' }); 36 + mocks.input.emit('keypress', 'down', { name: 'down' }); 44 37 expect(instance.cursor).to.equal(1); 45 38 }); 46 39 47 40 test('cursor loops around', () => { 48 41 const instance = new MultiSelectPrompt({ 49 - input, 50 - output, 42 + input: mocks.input, 43 + output: mocks.output, 51 44 render: () => 'foo', 52 45 options: [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }], 53 46 }); ··· 55 48 instance.prompt(); 56 49 57 50 expect(instance.cursor).to.equal(0); 58 - input.emit('keypress', 'up', { name: 'up' }); 51 + mocks.input.emit('keypress', 'up', { name: 'up' }); 59 52 expect(instance.cursor).to.equal(2); 60 - input.emit('keypress', 'down', { name: 'down' }); 53 + mocks.input.emit('keypress', 'down', { name: 'down' }); 61 54 expect(instance.cursor).to.equal(0); 62 55 }); 63 56 64 57 test('left behaves as up', () => { 65 58 const instance = new MultiSelectPrompt({ 66 - input, 67 - output, 59 + input: mocks.input, 60 + output: mocks.output, 68 61 render: () => 'foo', 69 62 options: [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }], 70 63 }); 71 64 72 65 instance.prompt(); 73 66 74 - input.emit('keypress', 'left', { name: 'left' }); 67 + mocks.input.emit('keypress', 'left', { name: 'left' }); 75 68 expect(instance.cursor).to.equal(2); 76 69 }); 77 70 78 71 test('right behaves as down', () => { 79 72 const instance = new MultiSelectPrompt({ 80 - input, 81 - output, 73 + input: mocks.input, 74 + output: mocks.output, 82 75 render: () => 'foo', 83 76 options: [{ value: 'foo' }, { value: 'bar' }], 84 77 }); 85 78 86 79 instance.prompt(); 87 80 88 - input.emit('keypress', 'left', { name: 'left' }); 81 + mocks.input.emit('keypress', 'left', { name: 'left' }); 89 82 expect(instance.cursor).to.equal(1); 90 83 }); 91 84 92 85 test('initial values is selected', () => { 93 86 const instance = new MultiSelectPrompt({ 94 - input, 95 - output, 87 + input: mocks.input, 88 + output: mocks.output, 96 89 render: () => 'foo', 97 90 options: [{ value: 'foo' }, { value: 'bar' }], 98 91 initialValues: ['bar'], ··· 103 96 104 97 test('select all when press "a" key', () => { 105 98 const instance = new MultiSelectPrompt({ 106 - input, 107 - output, 99 + input: mocks.input, 100 + output: mocks.output, 108 101 render: () => 'foo', 109 102 options: [{ value: 'foo' }, { value: 'bar' }], 110 103 }); 111 104 instance.prompt(); 112 105 113 - input.emit('keypress', 'down', { name: 'down' }); 114 - input.emit('keypress', 'space', { name: 'space' }); 115 - input.emit('keypress', 'a', { name: 'a' }); 106 + mocks.input.emit('keypress', 'down', { name: 'down' }); 107 + mocks.input.emit('keypress', 'space', { name: 'space' }); 108 + mocks.input.emit('keypress', 'a', { name: 'a' }); 116 109 expect(instance.value).toEqual(['foo', 'bar']); 117 110 }); 118 111 119 112 test('select invert when press "i" key', () => { 120 113 const instance = new MultiSelectPrompt({ 121 - input, 122 - output, 114 + input: mocks.input, 115 + output: mocks.output, 123 116 render: () => 'foo', 124 117 options: [{ value: 'foo' }, { value: 'bar' }], 125 118 }); 126 119 instance.prompt(); 127 120 128 - input.emit('keypress', 'down', { name: 'down' }); 129 - input.emit('keypress', 'space', { name: 'space' }); 130 - input.emit('keypress', 'i', { name: 'i' }); 121 + mocks.input.emit('keypress', 'down', { name: 'down' }); 122 + mocks.input.emit('keypress', 'space', { name: 'space' }); 123 + mocks.input.emit('keypress', 'i', { name: 'i' }); 131 124 expect(instance.value).toEqual(['foo']); 132 125 }); 133 126 134 127 test('disabled options are skipped', () => { 135 128 const instance = new MultiSelectPrompt({ 136 - input, 137 - output, 129 + input: mocks.input, 130 + output: mocks.output, 138 131 render: () => 'foo', 139 132 options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }], 140 133 }); 141 134 instance.prompt(); 142 135 143 136 expect(instance.cursor).to.equal(0); 144 - input.emit('keypress', 'down', { name: 'down' }); 137 + mocks.input.emit('keypress', 'down', { name: 'down' }); 145 138 expect(instance.cursor).to.equal(2); 146 - input.emit('keypress', 'up', { name: 'up' }); 139 + mocks.input.emit('keypress', 'up', { name: 'up' }); 147 140 expect(instance.cursor).to.equal(0); 148 141 }); 149 142 150 143 test('initial cursorAt on disabled option', () => { 151 144 const instance = new MultiSelectPrompt({ 152 - input, 153 - output, 145 + input: mocks.input, 146 + output: mocks.output, 154 147 render: () => 'foo', 155 148 options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }], 156 149 cursorAt: 'bar', ··· 164 157 describe('toggleAll', () => { 165 158 test('selects all enabled options', () => { 166 159 const instance = new MultiSelectPrompt({ 167 - input, 168 - output, 160 + input: mocks.input, 161 + output: mocks.output, 169 162 render: () => 'foo', 170 163 options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }], 171 164 }); 172 165 instance.prompt(); 173 166 174 - input.emit('keypress', 'a', { name: 'a' }); 167 + mocks.input.emit('keypress', 'a', { name: 'a' }); 175 168 expect(instance.value).toEqual(['foo', 'baz']); 176 169 }); 177 170 178 171 test('unselects all enabled options if all selected', () => { 179 172 const instance = new MultiSelectPrompt({ 180 - input, 181 - output, 173 + input: mocks.input, 174 + output: mocks.output, 182 175 render: () => 'foo', 183 176 options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }], 184 177 initialValues: ['foo', 'baz'], 185 178 }); 186 179 instance.prompt(); 187 180 188 - input.emit('keypress', 'a', { name: 'a' }); 181 + mocks.input.emit('keypress', 'a', { name: 'a' }); 189 182 expect(instance.value).toEqual([]); 190 183 }); 191 184 }); ··· 193 186 describe('toggleInvert', () => { 194 187 test('inverts selection of enabled options', () => { 195 188 const instance = new MultiSelectPrompt({ 196 - input, 197 - output, 189 + input: mocks.input, 190 + output: mocks.output, 198 191 render: () => 'foo', 199 192 options: [ 200 193 { value: 'foo' }, ··· 206 199 }); 207 200 instance.prompt(); 208 201 209 - input.emit('keypress', 'i', { name: 'i' }); 202 + mocks.input.emit('keypress', 'i', { name: 'i' }); 210 203 expect(instance.value).toEqual(['qux']); 211 204 }); 212 205 });
-98
packages/core/test/prompts/password.test.ts
··· 1 - import { styleText } from 'node:util'; 2 - import { cursor } from 'sisteransi'; 3 - import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 4 - import { default as PasswordPrompt } from '../../src/prompts/password.js'; 5 - import { MockReadable } from '../mock-readable.js'; 6 - import { MockWritable } from '../mock-writable.js'; 7 - 8 - describe('PasswordPrompt', () => { 9 - let input: MockReadable; 10 - let output: MockWritable; 11 - 12 - beforeEach(() => { 13 - input = new MockReadable(); 14 - output = new MockWritable(); 15 - }); 16 - 17 - afterEach(() => { 18 - vi.restoreAllMocks(); 19 - }); 20 - 21 - test('renders render() result', () => { 22 - const instance = new PasswordPrompt({ 23 - input, 24 - output, 25 - render: () => 'foo', 26 - }); 27 - // leave the promise hanging since we don't want to submit in this test 28 - instance.prompt(); 29 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); 30 - }); 31 - 32 - describe('cursor', () => { 33 - test('can get cursor', () => { 34 - const instance = new PasswordPrompt({ 35 - input, 36 - output, 37 - render: () => 'foo', 38 - }); 39 - 40 - expect(instance.cursor).to.equal(0); 41 - }); 42 - }); 43 - 44 - describe('userInputWithCursor', () => { 45 - test('returns masked value on submit', () => { 46 - const instance = new PasswordPrompt({ 47 - input, 48 - output, 49 - render: () => 'foo', 50 - }); 51 - instance.prompt(); 52 - const keys = 'foo'; 53 - for (let i = 0; i < keys.length; i++) { 54 - input.emit('keypress', keys[i], { name: keys[i] }); 55 - } 56 - input.emit('keypress', '', { name: 'return' }); 57 - expect(instance.userInputWithCursor).to.equal('•••'); 58 - }); 59 - 60 - test('renders marker at end', () => { 61 - const instance = new PasswordPrompt({ 62 - input, 63 - output, 64 - render: () => 'foo', 65 - }); 66 - instance.prompt(); 67 - input.emit('keypress', 'x', { name: 'x' }); 68 - expect(instance.userInputWithCursor).to.equal(`•${styleText(['inverse', 'hidden'], '_')}`); 69 - }); 70 - 71 - test('renders cursor inside value', () => { 72 - const instance = new PasswordPrompt({ 73 - input, 74 - output, 75 - render: () => 'foo', 76 - }); 77 - instance.prompt(); 78 - input.emit('keypress', 'x', { name: 'x' }); 79 - input.emit('keypress', 'y', { name: 'y' }); 80 - input.emit('keypress', 'z', { name: 'z' }); 81 - input.emit('keypress', undefined, { name: 'left' }); 82 - input.emit('keypress', undefined, { name: 'left' }); 83 - expect(instance.userInputWithCursor).to.equal(`•${styleText('inverse', '•')}•`); 84 - }); 85 - 86 - test('renders custom mask', () => { 87 - const instance = new PasswordPrompt({ 88 - input, 89 - output, 90 - render: () => 'foo', 91 - mask: 'X', 92 - }); 93 - instance.prompt(); 94 - input.emit('keypress', 'x', { name: 'x' }); 95 - expect(instance.userInputWithCursor).to.equal(`X${styleText(['inverse', 'hidden'], '_')}`); 96 - }); 97 - }); 98 - });
+57 -64
packages/core/test/prompts/prompt.test.ts packages/core/src/prompts/prompt.test.ts
··· 1 1 import { cursor } from 'sisteransi'; 2 - import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 3 - import { default as Prompt } from '../../src/prompts/prompt.js'; 4 - import { isCancel } from '../../src/utils/index.js'; 5 - import { MockReadable } from '../mock-readable.js'; 6 - import { MockWritable } from '../mock-writable.js'; 2 + import { beforeEach, describe, expect, test, vi } from 'vitest'; 3 + import { default as Prompt } from './prompt.js'; 4 + import { isCancel } from '../utils/index.js'; 5 + import { createMocks, type Mocks } from '@bomb.sh/tools/test-utils'; 7 6 8 7 describe('Prompt', () => { 9 - let input: MockReadable; 10 - let output: MockWritable; 8 + let mocks: Mocks<{ input: true; output: true }>; 11 9 12 10 beforeEach(() => { 13 - input = new MockReadable(); 14 - output = new MockWritable(); 15 - }); 16 - 17 - afterEach(() => { 18 - vi.restoreAllMocks(); 11 + mocks = createMocks({ input: true, output: true }); 19 12 }); 20 13 21 14 test('renders render() result', () => { 22 15 const instance = new Prompt({ 23 - input, 24 - output, 16 + input: mocks.input, 17 + output: mocks.output, 25 18 render: () => 'foo', 26 19 }); 27 20 // leave the promise hanging since we don't want to submit in this test 28 21 instance.prompt(); 29 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); 22 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo']); 30 23 }); 31 24 32 25 test('submits on return key', async () => { 33 26 const instance = new Prompt({ 34 - input, 35 - output, 27 + input: mocks.input, 28 + output: mocks.output, 36 29 render: () => 'foo', 37 30 }); 38 31 const resultPromise = instance.prompt(); 39 - input.emit('keypress', '', { name: 'return' }); 32 + mocks.input.emit('keypress', '', { name: 'return' }); 40 33 const result = await resultPromise; 41 34 expect(result).to.equal(undefined); 42 35 expect(isCancel(result)).to.equal(false); 43 36 expect(instance.state).to.equal('submit'); 44 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo', '\n', cursor.show]); 37 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo', '\n', cursor.show]); 45 38 }); 46 39 47 40 test('cancels on ctrl-c', async () => { 48 41 const instance = new Prompt({ 49 - input, 50 - output, 42 + input: mocks.input, 43 + output: mocks.output, 51 44 render: () => 'foo', 52 45 }); 53 46 const resultPromise = instance.prompt(); 54 - input.emit('keypress', '\x03', { name: 'c' }); 47 + mocks.input.emit('keypress', '\x03', { name: 'c' }); 55 48 const result = await resultPromise; 56 49 expect(isCancel(result)).to.equal(true); 57 50 expect(instance.state).to.equal('cancel'); 58 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo', '\n', cursor.show]); 51 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo', '\n', cursor.show]); 59 52 }); 60 53 61 54 test('does not write initialValue to value', () => { 62 55 const eventSpy = vi.fn(); 63 56 const instance = new Prompt({ 64 - input, 65 - output, 57 + input: mocks.input, 58 + output: mocks.output, 66 59 render: () => 'foo', 67 60 initialValue: 'bananas', 68 61 }); ··· 75 68 test('re-renders on resize', () => { 76 69 const renderFn = vi.fn().mockImplementation(() => 'foo'); 77 70 const instance = new Prompt({ 78 - input, 79 - output, 71 + input: mocks.input, 72 + output: mocks.output, 80 73 render: renderFn, 81 74 }); 82 75 instance.prompt(); 83 76 84 77 expect(renderFn).toHaveBeenCalledTimes(1); 85 78 86 - output.emit('resize'); 79 + mocks.output.emit('resize'); 87 80 88 81 expect(renderFn).toHaveBeenCalledTimes(2); 89 82 }); 90 83 91 84 test('state is active after first render', async () => { 92 85 const instance = new Prompt({ 93 - input, 94 - output, 86 + input: mocks.input, 87 + output: mocks.output, 95 88 render: () => 'foo', 96 89 }); 97 90 ··· 105 98 test('emits truthy confirm on y press', () => { 106 99 const eventFn = vi.fn(); 107 100 const instance = new Prompt({ 108 - input, 109 - output, 101 + input: mocks.input, 102 + output: mocks.output, 110 103 render: () => 'foo', 111 104 }); 112 105 ··· 114 107 115 108 instance.prompt(); 116 109 117 - input.emit('keypress', 'y', { name: 'y' }); 110 + mocks.input.emit('keypress', 'y', { name: 'y' }); 118 111 119 112 expect(eventFn).toBeCalledWith(true); 120 113 }); ··· 122 115 test('emits falsey confirm on n press', () => { 123 116 const eventFn = vi.fn(); 124 117 const instance = new Prompt({ 125 - input, 126 - output, 118 + input: mocks.input, 119 + output: mocks.output, 127 120 render: () => 'foo', 128 121 }); 129 122 ··· 131 124 132 125 instance.prompt(); 133 126 134 - input.emit('keypress', 'n', { name: 'n' }); 127 + mocks.input.emit('keypress', 'n', { name: 'n' }); 135 128 136 129 expect(eventFn).toBeCalledWith(false); 137 130 }); ··· 139 132 test('emits key event for unknown chars', () => { 140 133 const eventSpy = vi.fn(); 141 134 const instance = new Prompt({ 142 - input, 143 - output, 135 + input: mocks.input, 136 + output: mocks.output, 144 137 render: () => 'foo', 145 138 }); 146 139 ··· 148 141 149 142 instance.prompt(); 150 143 151 - input.emit('keypress', 'z', { name: 'z' }); 144 + mocks.input.emit('keypress', 'z', { name: 'z' }); 152 145 153 146 expect(eventSpy).toBeCalledWith('z', { name: 'z' }); 154 147 }); ··· 157 150 const keys = ['up', 'down', 'left', 'right']; 158 151 const eventSpy = vi.fn(); 159 152 const instance = new Prompt({ 160 - input, 161 - output, 153 + input: mocks.input, 154 + output: mocks.output, 162 155 render: () => 'foo', 163 156 }); 164 157 ··· 167 160 instance.prompt(); 168 161 169 162 for (const key of keys) { 170 - input.emit('keypress', key, { name: key }); 163 + mocks.input.emit('keypress', key, { name: key }); 171 164 expect(eventSpy).toBeCalledWith(key); 172 165 } 173 166 }); ··· 182 175 const eventSpy = vi.fn(); 183 176 const instance = new Prompt( 184 177 { 185 - input, 186 - output, 178 + input: mocks.input, 179 + output: mocks.output, 187 180 render: () => 'foo', 188 181 }, 189 182 false ··· 194 187 instance.prompt(); 195 188 196 189 for (const [alias, key] of keys) { 197 - input.emit('keypress', alias, { name: alias }); 190 + mocks.input.emit('keypress', alias, { name: alias }); 198 191 expect(eventSpy).toBeCalledWith(key); 199 192 } 200 193 }); ··· 203 196 const abortController = new AbortController(); 204 197 205 198 const instance = new Prompt({ 206 - input, 207 - output, 199 + input: mocks.input, 200 + output: mocks.output, 208 201 render: () => 'foo', 209 202 signal: abortController.signal, 210 203 }); ··· 223 216 abortController.abort(); 224 217 225 218 const instance = new Prompt({ 226 - input, 227 - output, 219 + input: mocks.input, 220 + output: mocks.output, 228 221 render: () => 'foo', 229 222 signal: abortController.signal, 230 223 }); ··· 235 228 236 229 test('accepts invalid initial value', () => { 237 230 const instance = new Prompt({ 238 - input, 239 - output, 231 + input: mocks.input, 232 + output: mocks.output, 240 233 render: () => 'foo', 241 234 initialValue: 'invalid', 242 235 validate: (value) => (value === 'valid' ? undefined : 'must be valid'), ··· 249 242 250 243 test('validates value on return', () => { 251 244 const instance = new Prompt({ 252 - input, 253 - output, 245 + input: mocks.input, 246 + output: mocks.output, 254 247 render: () => 'foo', 255 248 validate: (value) => (value === 'valid' ? undefined : 'must be valid'), 256 249 }); ··· 258 251 259 252 instance.value = 'invalid'; 260 253 261 - input.emit('keypress', '', { name: 'return' }); 254 + mocks.input.emit('keypress', '', { name: 'return' }); 262 255 263 256 expect(instance.state).to.equal('error'); 264 257 expect(instance.error).to.equal('must be valid'); ··· 266 259 267 260 test('validates value with Error object', () => { 268 261 const instance = new Prompt({ 269 - input, 270 - output, 262 + input: mocks.input, 263 + output: mocks.output, 271 264 render: () => 'foo', 272 265 validate: (value) => (value === 'valid' ? undefined : new Error('must be valid')), 273 266 }); 274 267 instance.prompt(); 275 268 276 269 instance.value = 'invalid'; 277 - input.emit('keypress', '', { name: 'return' }); 270 + mocks.input.emit('keypress', '', { name: 'return' }); 278 271 279 272 expect(instance.state).to.equal('error'); 280 273 expect(instance.error).to.equal('must be valid'); ··· 282 275 283 276 test('validates value with regex validation', () => { 284 277 const instance = new Prompt<string>({ 285 - input, 286 - output, 278 + input: mocks.input, 279 + output: mocks.output, 287 280 render: () => 'foo', 288 281 validate: (value) => (/^[A-Z]+$/.test(value ?? '') ? undefined : 'Invalid value'), 289 282 }); 290 283 instance.prompt(); 291 284 292 285 instance.value = 'Invalid Value $$$'; 293 - input.emit('keypress', '', { name: 'return' }); 286 + mocks.input.emit('keypress', '', { name: 'return' }); 294 287 295 288 expect(instance.state).to.equal('error'); 296 289 expect(instance.error).to.equal('Invalid value'); ··· 298 291 299 292 test('accepts valid value with regex validation', () => { 300 293 const instance = new Prompt<string>({ 301 - input, 302 - output, 294 + input: mocks.input, 295 + output: mocks.output, 303 296 render: () => 'foo', 304 297 validate: (value) => (/^[A-Z]+$/.test(value ?? '') ? undefined : 'Invalid value'), 305 298 }); 306 299 instance.prompt(); 307 300 308 301 instance.value = 'VALID'; 309 - input.emit('keypress', '', { name: 'return' }); 302 + mocks.input.emit('keypress', '', { name: 'return' }); 310 303 311 304 expect(instance.state).to.equal('submit'); 312 305 expect(instance.error).to.equal('');
+31 -38
packages/core/test/prompts/select.test.ts packages/core/src/prompts/select.test.ts
··· 1 1 import { cursor } from 'sisteransi'; 2 - import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 3 - import { default as SelectPrompt } from '../../src/prompts/select.js'; 4 - import { MockReadable } from '../mock-readable.js'; 5 - import { MockWritable } from '../mock-writable.js'; 2 + import { beforeEach, describe, expect, test } from 'vitest'; 3 + import { default as SelectPrompt } from './select.js'; 4 + import { createMocks, type Mocks } from '@bomb.sh/tools/test-utils'; 6 5 7 6 describe('SelectPrompt', () => { 8 - let input: MockReadable; 9 - let output: MockWritable; 7 + let mocks: Mocks<{ input: true; output: true }>; 10 8 11 9 beforeEach(() => { 12 - input = new MockReadable(); 13 - output = new MockWritable(); 14 - }); 15 - 16 - afterEach(() => { 17 - vi.restoreAllMocks(); 10 + mocks = createMocks({ input: true, output: true }); 18 11 }); 19 12 20 13 test('renders render() result', () => { 21 14 const instance = new SelectPrompt({ 22 - input, 23 - output, 15 + input: mocks.input, 16 + output: mocks.output, 24 17 render: () => 'foo', 25 18 options: [{ value: 'foo' }, { value: 'bar' }], 26 19 }); 27 20 instance.prompt(); 28 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); 21 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo']); 29 22 }); 30 23 31 24 describe('cursor', () => { 32 25 test('cursor is index of selected item', () => { 33 26 const instance = new SelectPrompt({ 34 - input, 35 - output, 27 + input: mocks.input, 28 + output: mocks.output, 36 29 render: () => 'foo', 37 30 options: [{ value: 'foo' }, { value: 'bar' }], 38 31 }); ··· 40 33 instance.prompt(); 41 34 42 35 expect(instance.cursor).to.equal(0); 43 - input.emit('keypress', 'down', { name: 'down' }); 36 + mocks.input.emit('keypress', 'down', { name: 'down' }); 44 37 expect(instance.cursor).to.equal(1); 45 38 }); 46 39 47 40 test('cursor loops around', () => { 48 41 const instance = new SelectPrompt({ 49 - input, 50 - output, 42 + input: mocks.input, 43 + output: mocks.output, 51 44 render: () => 'foo', 52 45 options: [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }], 53 46 }); ··· 55 48 instance.prompt(); 56 49 57 50 expect(instance.cursor).to.equal(0); 58 - input.emit('keypress', 'up', { name: 'up' }); 51 + mocks.input.emit('keypress', 'up', { name: 'up' }); 59 52 expect(instance.cursor).to.equal(2); 60 - input.emit('keypress', 'down', { name: 'down' }); 53 + mocks.input.emit('keypress', 'down', { name: 'down' }); 61 54 expect(instance.cursor).to.equal(0); 62 55 }); 63 56 64 57 test('left behaves as up', () => { 65 58 const instance = new SelectPrompt({ 66 - input, 67 - output, 59 + input: mocks.input, 60 + output: mocks.output, 68 61 render: () => 'foo', 69 62 options: [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }], 70 63 }); 71 64 72 65 instance.prompt(); 73 66 74 - input.emit('keypress', 'left', { name: 'left' }); 67 + mocks.input.emit('keypress', 'left', { name: 'left' }); 75 68 expect(instance.cursor).to.equal(2); 76 69 }); 77 70 78 71 test('right behaves as down', () => { 79 72 const instance = new SelectPrompt({ 80 - input, 81 - output, 73 + input: mocks.input, 74 + output: mocks.output, 82 75 render: () => 'foo', 83 76 options: [{ value: 'foo' }, { value: 'bar' }], 84 77 }); 85 78 86 79 instance.prompt(); 87 80 88 - input.emit('keypress', 'left', { name: 'left' }); 81 + mocks.input.emit('keypress', 'left', { name: 'left' }); 89 82 expect(instance.cursor).to.equal(1); 90 83 }); 91 84 92 85 test('initial value is selected', () => { 93 86 const instance = new SelectPrompt({ 94 - input, 95 - output, 87 + input: mocks.input, 88 + output: mocks.output, 96 89 render: () => 'foo', 97 90 options: [{ value: 'foo' }, { value: 'bar' }], 98 91 initialValue: 'bar', ··· 103 96 104 97 test('cursor skips disabled options (down)', () => { 105 98 const instance = new SelectPrompt({ 106 - input, 107 - output, 99 + input: mocks.input, 100 + output: mocks.output, 108 101 render: () => 'foo', 109 102 options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }], 110 103 }); 111 104 instance.prompt(); 112 105 expect(instance.cursor).to.equal(0); 113 - input.emit('keypress', 'down', { name: 'down' }); 106 + mocks.input.emit('keypress', 'down', { name: 'down' }); 114 107 expect(instance.cursor).to.equal(2); 115 108 }); 116 109 117 110 test('cursor skips disabled options (up)', () => { 118 111 const instance = new SelectPrompt({ 119 - input, 120 - output, 112 + input: mocks.input, 113 + output: mocks.output, 121 114 render: () => 'foo', 122 115 initialValue: 'baz', 123 116 options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }], 124 117 }); 125 118 instance.prompt(); 126 119 expect(instance.cursor).to.equal(2); 127 - input.emit('keypress', 'up', { name: 'up' }); 120 + mocks.input.emit('keypress', 'up', { name: 'up' }); 128 121 expect(instance.cursor).to.equal(0); 129 122 }); 130 123 131 124 test('cursor skips initial disabled option', () => { 132 125 const instance = new SelectPrompt({ 133 - input, 134 - output, 126 + input: mocks.input, 127 + output: mocks.output, 135 128 render: () => 'foo', 136 129 options: [{ value: 'foo', disabled: true }, { value: 'bar' }, { value: 'baz' }], 137 130 });
+35 -42
packages/core/test/prompts/text.test.ts packages/core/src/prompts/text.test.ts
··· 1 1 import { styleText } from 'node:util'; 2 2 import { cursor } from 'sisteransi'; 3 - import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 4 - import { default as TextPrompt } from '../../src/prompts/text.js'; 5 - import { MockReadable } from '../mock-readable.js'; 6 - import { MockWritable } from '../mock-writable.js'; 3 + import { beforeEach, describe, expect, test } from 'vitest'; 4 + import { default as TextPrompt } from './text.js'; 5 + import { createMocks, type Mocks } from '@bomb.sh/tools/test-utils'; 7 6 8 7 describe('TextPrompt', () => { 9 - let input: MockReadable; 10 - let output: MockWritable; 8 + let mocks: Mocks<{ input: true; output: true }>; 11 9 12 10 beforeEach(() => { 13 - input = new MockReadable(); 14 - output = new MockWritable(); 15 - }); 16 - 17 - afterEach(() => { 18 - vi.restoreAllMocks(); 11 + mocks = createMocks({ input: true, output: true }); 19 12 }); 20 13 21 14 test('renders render() result', () => { 22 15 const instance = new TextPrompt({ 23 - input, 24 - output, 16 + input: mocks.input, 17 + output: mocks.output, 25 18 render: () => 'foo', 26 19 }); 27 20 // leave the promise hanging since we don't want to submit in this test 28 21 instance.prompt(); 29 - expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); 22 + expect(mocks.output.buffer).to.deep.equal([cursor.hide, 'foo']); 30 23 }); 31 24 32 25 test('sets default value on finalize if no value', async () => { 33 26 const instance = new TextPrompt({ 34 - input, 35 - output, 27 + input: mocks.input, 28 + output: mocks.output, 36 29 render: () => 'foo', 37 30 defaultValue: 'bleep bloop', 38 31 }); 39 32 const resultPromise = instance.prompt(); 40 - input.emit('keypress', '', { name: 'return' }); 33 + mocks.input.emit('keypress', '', { name: 'return' }); 41 34 const result = await resultPromise; 42 35 expect(result).to.equal('bleep bloop'); 43 36 }); 44 37 45 38 test('keeps value on finalize', async () => { 46 39 const instance = new TextPrompt({ 47 - input, 48 - output, 40 + input: mocks.input, 41 + output: mocks.output, 49 42 render: () => 'foo', 50 43 defaultValue: 'bleep bloop', 51 44 }); 52 45 const resultPromise = instance.prompt(); 53 - input.emit('keypress', 'x', { name: 'x' }); 54 - input.emit('keypress', '', { name: 'return' }); 46 + mocks.input.emit('keypress', 'x', { name: 'x' }); 47 + mocks.input.emit('keypress', '', { name: 'return' }); 55 48 const result = await resultPromise; 56 49 expect(result).to.equal('x'); 57 50 }); ··· 59 52 describe('cursor', () => { 60 53 test('can get cursor', () => { 61 54 const instance = new TextPrompt({ 62 - input, 63 - output, 55 + input: mocks.input, 56 + output: mocks.output, 64 57 render: () => 'foo', 65 58 }); 66 59 ··· 71 64 describe('userInputWithCursor', () => { 72 65 test('returns value on submit', () => { 73 66 const instance = new TextPrompt({ 74 - input, 75 - output, 67 + input: mocks.input, 68 + output: mocks.output, 76 69 render: () => 'foo', 77 70 }); 78 71 instance.prompt(); 79 - input.emit('keypress', 'x', { name: 'x' }); 80 - input.emit('keypress', '', { name: 'return' }); 72 + mocks.input.emit('keypress', 'x', { name: 'x' }); 73 + mocks.input.emit('keypress', '', { name: 'return' }); 81 74 expect(instance.userInputWithCursor).to.equal('x'); 82 75 }); 83 76 84 77 test('highlights cursor position', () => { 85 78 const instance = new TextPrompt({ 86 - input, 87 - output, 79 + input: mocks.input, 80 + output: mocks.output, 88 81 render: () => 'foo', 89 82 }); 90 83 instance.prompt(); 91 84 const keys = 'foo'; 92 85 for (let i = 0; i < keys.length; i++) { 93 - input.emit('keypress', keys[i], { name: keys[i] }); 86 + mocks.input.emit('keypress', keys[i], { name: keys[i] }); 94 87 } 95 - input.emit('keypress', undefined, { name: 'left' }); 88 + mocks.input.emit('keypress', undefined, { name: 'left' }); 96 89 expect(instance.userInputWithCursor).to.equal(`fo${styleText('inverse', 'o')}`); 97 90 }); 98 91 99 92 test('shows cursor at end if beyond value', () => { 100 93 const instance = new TextPrompt({ 101 - input, 102 - output, 94 + input: mocks.input, 95 + output: mocks.output, 103 96 render: () => 'foo', 104 97 }); 105 98 instance.prompt(); 106 99 const keys = 'foo'; 107 100 for (let i = 0; i < keys.length; i++) { 108 - input.emit('keypress', keys[i], { name: keys[i] }); 101 + mocks.input.emit('keypress', keys[i], { name: keys[i] }); 109 102 } 110 - input.emit('keypress', undefined, { name: 'right' }); 103 + mocks.input.emit('keypress', undefined, { name: 'right' }); 111 104 expect(instance.userInputWithCursor).to.equal('foo█'); 112 105 }); 113 106 114 107 test('does not use placeholder as value when pressing enter', async () => { 115 108 const instance = new TextPrompt({ 116 - input, 117 - output, 109 + input: mocks.input, 110 + output: mocks.output, 118 111 render: () => 'foo', 119 112 placeholder: ' (hit Enter to use default)', 120 113 defaultValue: 'default-value', 121 114 }); 122 115 const resultPromise = instance.prompt(); 123 - input.emit('keypress', '', { name: 'return' }); 116 + mocks.input.emit('keypress', '', { name: 'return' }); 124 117 const result = await resultPromise; 125 118 expect(result).to.equal('default-value'); 126 119 }); 127 120 128 121 test('returns empty string when no value and no default', async () => { 129 122 const instance = new TextPrompt({ 130 - input, 131 - output, 123 + input: mocks.input, 124 + output: mocks.output, 132 125 render: () => 'foo', 133 126 placeholder: ' (hit Enter to use default)', 134 127 }); 135 128 const resultPromise = instance.prompt(); 136 - input.emit('keypress', '', { name: 'return' }); 129 + mocks.input.emit('keypress', '', { name: 'return' }); 137 130 const result = await resultPromise; 138 131 expect(result).to.equal(''); 139 132 });
+8 -18
packages/core/test/utils.test.ts packages/core/src/utils/index.test.ts
··· 1 1 import type { Key } from 'node:readline'; 2 2 import { cursor } from 'sisteransi'; 3 - import { afterEach, describe, expect, test, vi } from 'vitest'; 4 - import { block } from '../src/utils/index.js'; 5 - import { MockReadable } from './mock-readable.js'; 6 - import { MockWritable } from './mock-writable.js'; 3 + import { describe, expect, test, vi } from 'vitest'; 4 + import { block } from './index.js'; 5 + import { createMocks } from '@bomb.sh/tools/test-utils'; 7 6 8 7 describe('utils', () => { 9 - afterEach(() => { 10 - vi.restoreAllMocks(); 11 - }); 12 - 13 8 describe('block', () => { 14 9 test('clears output on keypress', () => { 15 - const input = new MockReadable(); 16 - const output = new MockWritable(); 10 + const { input, output } = createMocks({ input: true, output: true }); 17 11 const callback = block({ input, output }); 18 12 19 13 const event: Key = { ··· 26 20 }); 27 21 28 22 test('clears output vertically when return pressed', () => { 29 - const input = new MockReadable(); 30 - const output = new MockWritable(); 23 + const { input, output } = createMocks({ input: true, output: true }); 31 24 const callback = block({ input, output }); 32 25 33 26 const event: Key = { ··· 40 33 }); 41 34 42 35 test('ignores additional keypresses after dispose', () => { 43 - const input = new MockReadable(); 44 - const output = new MockWritable(); 36 + const { input, output } = createMocks({ input: true, output: true }); 45 37 const callback = block({ input, output }); 46 38 47 39 const event: Key = { ··· 55 47 }); 56 48 57 49 test('exits on ctrl-c', () => { 58 - const input = new MockReadable(); 59 - const output = new MockWritable(); 50 + const { input, output } = createMocks({ input: true, output: true }); 60 51 // purposely don't keep the callback since we would exit the process 61 52 block({ input, output }); 62 53 const spy = vi.spyOn(process, 'exit').mockImplementation((() => { ··· 73 64 }); 74 65 75 66 test('does not clear if overwrite=false', () => { 76 - const input = new MockReadable(); 77 - const output = new MockWritable(); 67 + const { input, output } = createMocks({ input: true, output: true }); 78 68 const callback = block({ input, output, overwrite: false }); 79 69 80 70 const event: Key = {
packages/core/test/utils/cursor.test.ts packages/core/src/utils/cursor.test.ts