@gjsify/readline 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,931 @@
1
+ // Ported from refs/node-test/parallel/test-readline-{interface,csi,async-iterators,line-separators}.js
2
+ // Original: MIT license, Node.js contributors
3
+
4
+ import { describe, it, expect } from '@gjsify/unit';
5
+ import { createInterface, clearLine, clearScreenDown, cursorTo, moveCursor, Interface } from 'node:readline';
6
+ import { Readable, Writable, PassThrough } from 'node:stream';
7
+
8
+ export default async () => {
9
+ await describe('readline exports', async () => {
10
+ await it('should export createInterface as a function', async () => {
11
+ expect(typeof createInterface).toBe('function');
12
+ });
13
+
14
+ await it('should export Interface class', async () => {
15
+ expect(typeof Interface).toBe('function');
16
+ });
17
+
18
+ await it('should export utility functions', async () => {
19
+ expect(typeof clearLine).toBe('function');
20
+ expect(typeof clearScreenDown).toBe('function');
21
+ expect(typeof cursorTo).toBe('function');
22
+ expect(typeof moveCursor).toBe('function');
23
+ });
24
+ });
25
+
26
+ await describe('readline.Interface', async () => {
27
+ await it('should create an interface with input stream', async () => {
28
+ const input = new Readable({ read() {} });
29
+ const rl = createInterface({ input });
30
+ expect(rl).toBeDefined();
31
+ expect(typeof rl.close).toBe('function');
32
+ rl.close();
33
+ });
34
+
35
+ await it('should be instanceof Interface', async () => {
36
+ const input = new Readable({ read() {} });
37
+ const rl = createInterface({ input });
38
+ expect(rl instanceof Interface).toBe(true);
39
+ rl.close();
40
+ });
41
+
42
+ await it('should accept positional arguments (input, output)', async () => {
43
+ const input = new PassThrough();
44
+ const output = new PassThrough();
45
+ const rl = createInterface(input, output);
46
+ expect(rl).toBeDefined();
47
+ rl.close();
48
+ });
49
+
50
+ await it('should use default prompt "> "', async () => {
51
+ const input = new Readable({ read() {} });
52
+ const rl = createInterface({ input });
53
+ expect(rl.getPrompt()).toBe('> ');
54
+ rl.close();
55
+ });
56
+
57
+ await it('should support set/getPrompt', async () => {
58
+ const input = new Readable({ read() {} });
59
+ const rl = createInterface({ input, prompt: '>> ' });
60
+ expect(rl.getPrompt()).toBe('>> ');
61
+ rl.setPrompt('$ ');
62
+ expect(rl.getPrompt()).toBe('$ ');
63
+ rl.close();
64
+ });
65
+
66
+ await it('should have write method', async () => {
67
+ const input = new Readable({ read() {} });
68
+ const rl = createInterface({ input });
69
+ expect(typeof rl.write).toBe('function');
70
+ rl.close();
71
+ });
72
+
73
+ await it('close should be safe to call twice', async () => {
74
+ const input = new Readable({ read() {} });
75
+ const rl = createInterface({ input });
76
+ rl.close();
77
+ rl.close(); // should not throw
78
+ expect(true).toBeTruthy();
79
+ });
80
+
81
+ await it('should have getCursorPos method', async () => {
82
+ const input = new Readable({ read() {} });
83
+ const rl = createInterface({ input });
84
+ const pos = rl.getCursorPos();
85
+ expect(typeof pos.rows).toBe('number');
86
+ expect(typeof pos.cols).toBe('number');
87
+ rl.close();
88
+ });
89
+ });
90
+
91
+ await describe('readline line events', async () => {
92
+ await it('should emit line events from input stream', async () => {
93
+ const input = new PassThrough();
94
+ const rl = createInterface({ input });
95
+ const lines: string[] = [];
96
+ rl.on('line', (line: string) => lines.push(line));
97
+
98
+ input.write('hello\n');
99
+ input.write('world\n');
100
+ await new Promise<void>((r) => setTimeout(r, 10));
101
+
102
+ expect(lines.length).toBe(2);
103
+ expect(lines[0]).toBe('hello');
104
+ expect(lines[1]).toBe('world');
105
+ rl.close();
106
+ });
107
+
108
+ await it('should handle \\r\\n line endings', async () => {
109
+ const input = new PassThrough();
110
+ const rl = createInterface({ input });
111
+ const lines: string[] = [];
112
+ rl.on('line', (line: string) => lines.push(line));
113
+
114
+ input.write('line1\r\nline2\r\n');
115
+ await new Promise<void>((r) => setTimeout(r, 10));
116
+
117
+ expect(lines.length).toBe(2);
118
+ expect(lines[0]).toBe('line1');
119
+ expect(lines[1]).toBe('line2');
120
+ rl.close();
121
+ });
122
+
123
+ await it('should handle \\r line endings', async () => {
124
+ const input = new PassThrough();
125
+ const rl = createInterface({ input });
126
+ const lines: string[] = [];
127
+ rl.on('line', (line: string) => lines.push(line));
128
+
129
+ input.write('line1\rline2\r');
130
+ await new Promise<void>((r) => setTimeout(r, 10));
131
+
132
+ expect(lines.length).toBe(2);
133
+ expect(lines[0]).toBe('line1');
134
+ expect(lines[1]).toBe('line2');
135
+ rl.close();
136
+ });
137
+
138
+ await it('should handle mixed line endings', async () => {
139
+ const input = new PassThrough();
140
+ const rl = createInterface({ input });
141
+ const lines: string[] = [];
142
+ rl.on('line', (line: string) => lines.push(line));
143
+
144
+ input.write('a\nb\rc\r\nd\n');
145
+ await new Promise<void>((r) => setTimeout(r, 10));
146
+
147
+ expect(lines.length).toBe(4);
148
+ expect(lines[0]).toBe('a');
149
+ expect(lines[1]).toBe('b');
150
+ expect(lines[2]).toBe('c');
151
+ expect(lines[3]).toBe('d');
152
+ rl.close();
153
+ });
154
+
155
+ await it('should handle chunked input across line boundary', async () => {
156
+ const input = new PassThrough();
157
+ const rl = createInterface({ input });
158
+ const lines: string[] = [];
159
+ rl.on('line', (line: string) => lines.push(line));
160
+
161
+ input.write('hel');
162
+ input.write('lo\nwor');
163
+ input.write('ld\n');
164
+ await new Promise<void>((r) => setTimeout(r, 10));
165
+
166
+ expect(lines.length).toBe(2);
167
+ expect(lines[0]).toBe('hello');
168
+ expect(lines[1]).toBe('world');
169
+ rl.close();
170
+ });
171
+
172
+ await it('should handle empty lines', async () => {
173
+ const input = new PassThrough();
174
+ const rl = createInterface({ input });
175
+ const lines: string[] = [];
176
+ rl.on('line', (line: string) => lines.push(line));
177
+
178
+ input.write('\n\n\n');
179
+ await new Promise<void>((r) => setTimeout(r, 10));
180
+
181
+ expect(lines.length).toBe(3);
182
+ expect(lines[0]).toBe('');
183
+ expect(lines[1]).toBe('');
184
+ expect(lines[2]).toBe('');
185
+ rl.close();
186
+ });
187
+
188
+ await it('should handle multiple lines sent at once', async () => {
189
+ const input = new PassThrough();
190
+ const rl = createInterface({ input });
191
+ const lines: string[] = [];
192
+ rl.on('line', (line: string) => lines.push(line));
193
+
194
+ input.write('foo\nbar\nbaz\n');
195
+ await new Promise<void>((r) => setTimeout(r, 10));
196
+
197
+ expect(lines.length).toBe(3);
198
+ expect(lines[0]).toBe('foo');
199
+ expect(lines[1]).toBe('bar');
200
+ expect(lines[2]).toBe('baz');
201
+ rl.close();
202
+ });
203
+
204
+ await it('should emit line on input end with pending data', async () => {
205
+ const input = new PassThrough();
206
+ const rl = createInterface({ input });
207
+ const lines: string[] = [];
208
+ rl.on('line', (line: string) => lines.push(line));
209
+
210
+ input.write('no newline');
211
+ input.end();
212
+ await new Promise<void>((r) => setTimeout(r, 50));
213
+
214
+ expect(lines.length).toBe(1);
215
+ expect(lines[0]).toBe('no newline');
216
+ rl.close();
217
+ });
218
+
219
+ await it('should emit close after end with lines', async () => {
220
+ const input = new PassThrough();
221
+ const rl = createInterface({ input });
222
+ const lines: string[] = [];
223
+ let closed = false;
224
+ rl.on('line', (line: string) => lines.push(line));
225
+ rl.on('close', () => { closed = true; });
226
+
227
+ input.write('line1\nline2\n');
228
+ input.end();
229
+ await new Promise<void>((r) => setTimeout(r, 50));
230
+
231
+ expect(lines.length).toBe(2);
232
+ expect(closed).toBe(true);
233
+ rl.close();
234
+ });
235
+
236
+ await it('should handle long lines', async () => {
237
+ const input = new PassThrough();
238
+ const rl = createInterface({ input });
239
+ const lines: string[] = [];
240
+ rl.on('line', (line: string) => lines.push(line));
241
+
242
+ const longLine = 'x'.repeat(10000);
243
+ input.write(longLine + '\n');
244
+ await new Promise<void>((r) => setTimeout(r, 10));
245
+
246
+ expect(lines.length).toBe(1);
247
+ expect(lines[0]).toBe(longLine);
248
+ expect(lines[0].length).toBe(10000);
249
+ rl.close();
250
+ });
251
+
252
+ await it('should handle Unicode content in lines', async () => {
253
+ const input = new PassThrough();
254
+ const rl = createInterface({ input });
255
+ const lines: string[] = [];
256
+ rl.on('line', (line: string) => lines.push(line));
257
+
258
+ input.write('你好世界\n');
259
+ input.write('café\n');
260
+ input.write('🎉🎊\n');
261
+ await new Promise<void>((r) => setTimeout(r, 10));
262
+
263
+ expect(lines.length).toBe(3);
264
+ expect(lines[0]).toBe('你好世界');
265
+ expect(lines[1]).toBe('café');
266
+ expect(lines[2]).toBe('🎉🎊');
267
+ rl.close();
268
+ });
269
+
270
+ await it('should not emit line for data without newline before close', async () => {
271
+ const input = new Readable({ read() {} });
272
+ const rl = createInterface({ input });
273
+ const lines: string[] = [];
274
+ rl.on('line', (line: string) => lines.push(line));
275
+
276
+ // Manually close without ending input — no pending line is flushed
277
+ // (only input 'end' event flushes pending buffer)
278
+ rl.close();
279
+ expect(lines.length).toBe(0);
280
+ });
281
+
282
+ await it('should handle \\r\\n split across chunks', async () => {
283
+ const input = new PassThrough();
284
+ const rl = createInterface({ input });
285
+ const lines: string[] = [];
286
+ rl.on('line', (line: string) => lines.push(line));
287
+
288
+ input.write('hello\r');
289
+ await new Promise<void>((r) => setTimeout(r, 10));
290
+ input.write('\nworld\n');
291
+ await new Promise<void>((r) => setTimeout(r, 10));
292
+
293
+ // \r alone is treated as line ending, then \n starts a new (empty) line
294
+ // or \r\n is treated as single line ending depending on crlfDelay
295
+ expect(lines.length).toBeGreaterThan(0);
296
+ expect(lines[0]).toBe('hello');
297
+ rl.close();
298
+ });
299
+ });
300
+
301
+ await describe('readline close and lifecycle', async () => {
302
+ await it('should emit close event', async () => {
303
+ const input = new PassThrough();
304
+ const rl = createInterface({ input });
305
+ let closed = false;
306
+ rl.on('close', () => { closed = true; });
307
+
308
+ rl.close();
309
+ expect(closed).toBe(true);
310
+ });
311
+
312
+ await it('should emit close only once on double close', async () => {
313
+ const input = new Readable({ read() {} });
314
+ const rl = createInterface({ input });
315
+ let closeCount = 0;
316
+ rl.on('close', () => { closeCount++; });
317
+
318
+ rl.close();
319
+ rl.close();
320
+ expect(closeCount).toBe(1);
321
+ });
322
+
323
+ await it('should stop processing data after close', async () => {
324
+ const input = new PassThrough();
325
+ const rl = createInterface({ input });
326
+ const lines: string[] = [];
327
+ rl.on('line', (line: string) => lines.push(line));
328
+
329
+ input.write('before\n');
330
+ await new Promise<void>((r) => setTimeout(r, 10));
331
+ rl.close();
332
+
333
+ input.write('after\n');
334
+ await new Promise<void>((r) => setTimeout(r, 10));
335
+
336
+ expect(lines.length).toBe(1);
337
+ expect(lines[0]).toBe('before');
338
+ });
339
+ });
340
+
341
+ await describe('readline pause/resume', async () => {
342
+ await it('should emit pause event', async () => {
343
+ const input = new PassThrough();
344
+ const rl = createInterface({ input });
345
+ let paused = false;
346
+ rl.on('pause', () => { paused = true; });
347
+
348
+ rl.pause();
349
+ expect(paused).toBe(true);
350
+ rl.close();
351
+ });
352
+
353
+ await it('should emit resume event', async () => {
354
+ const input = new PassThrough();
355
+ const rl = createInterface({ input });
356
+ let resumed = false;
357
+ rl.on('resume', () => { resumed = true; });
358
+
359
+ rl.pause();
360
+ rl.resume();
361
+ expect(resumed).toBe(true);
362
+ rl.close();
363
+ });
364
+
365
+ await it('pause should not throw on repeated calls', async () => {
366
+ const input = new PassThrough();
367
+ const rl = createInterface({ input });
368
+ rl.pause();
369
+ rl.pause(); // should not throw
370
+ expect(true).toBeTruthy();
371
+ rl.close();
372
+ });
373
+
374
+ await it('resume after pause should work', async () => {
375
+ const input = new PassThrough();
376
+ const rl = createInterface({ input });
377
+ let pauseCount = 0;
378
+ let resumeCount = 0;
379
+ rl.on('pause', () => { pauseCount++; });
380
+ rl.on('resume', () => { resumeCount++; });
381
+
382
+ rl.pause();
383
+ rl.resume();
384
+ expect(pauseCount).toBe(1);
385
+ expect(resumeCount).toBe(1);
386
+ rl.close();
387
+ });
388
+ });
389
+
390
+ await describe('readline.question', async () => {
391
+ await it('should answer question from input', async () => {
392
+ const input = new PassThrough();
393
+ const output = new PassThrough();
394
+ const rl = createInterface({ input, output });
395
+
396
+ const answerPromise = new Promise<string>((resolve) => {
397
+ rl.question('Name? ', resolve);
398
+ });
399
+
400
+ input.write('Alice\n');
401
+ const answer = await answerPromise;
402
+ expect(answer).toBe('Alice');
403
+ rl.close();
404
+ });
405
+
406
+ await it('should write query to output', async () => {
407
+ const input = new PassThrough();
408
+ const chunks: string[] = [];
409
+ const output = new Writable({
410
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
411
+ });
412
+ const rl = createInterface({ input, output });
413
+
414
+ rl.question('Enter value: ', () => {});
415
+ expect(chunks.some(c => c.includes('Enter value: '))).toBe(true);
416
+
417
+ input.write('test\n');
418
+ await new Promise<void>((r) => setTimeout(r, 10));
419
+ rl.close();
420
+ });
421
+
422
+ await it('should handle multiple sequential questions', async () => {
423
+ const input = new PassThrough();
424
+ const output = new PassThrough();
425
+ const rl = createInterface({ input, output });
426
+
427
+ const answer1 = new Promise<string>((resolve) => {
428
+ rl.question('Q1? ', resolve);
429
+ });
430
+ input.write('A1\n');
431
+ expect(await answer1).toBe('A1');
432
+
433
+ const answer2 = new Promise<string>((resolve) => {
434
+ rl.question('Q2? ', resolve);
435
+ });
436
+ input.write('A2\n');
437
+ expect(await answer2).toBe('A2');
438
+
439
+ rl.close();
440
+ });
441
+ });
442
+
443
+ await describe('readline.prompt', async () => {
444
+ await it('should write prompt to output', async () => {
445
+ const input = new Readable({ read() {} });
446
+ const chunks: string[] = [];
447
+ const output = new Writable({
448
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
449
+ });
450
+ const rl = createInterface({ input, output, prompt: '>> ' });
451
+
452
+ rl.prompt();
453
+ expect(chunks.some(c => c.includes('>> '))).toBe(true);
454
+ rl.close();
455
+ });
456
+
457
+ await it('should use updated prompt after setPrompt', async () => {
458
+ const input = new Readable({ read() {} });
459
+ const chunks: string[] = [];
460
+ const output = new Writable({
461
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
462
+ });
463
+ const rl = createInterface({ input, output, prompt: '> ' });
464
+
465
+ rl.setPrompt('$ ');
466
+ rl.prompt();
467
+ expect(chunks.some(c => c.includes('$ '))).toBe(true);
468
+ rl.close();
469
+ });
470
+
471
+ await it('prompt after close should throw', async () => {
472
+ const input = new Readable({ read() {} });
473
+ const output = new Writable({
474
+ write(_chunk, _enc, cb) { cb(); }
475
+ });
476
+ const rl = createInterface({ input, output });
477
+
478
+ rl.close();
479
+ let threw = false;
480
+ try {
481
+ rl.prompt();
482
+ } catch {
483
+ threw = true;
484
+ }
485
+ expect(threw).toBe(true);
486
+ });
487
+ });
488
+
489
+ await describe('readline history', async () => {
490
+ await it('should have a history array', async () => {
491
+ const input = new PassThrough();
492
+ const rl = createInterface({ input, terminal: true });
493
+
494
+ expect(Array.isArray((rl as unknown as { history: string[] }).history)).toBe(true);
495
+ rl.close();
496
+ });
497
+
498
+ await it('should store lines in history (most recent first)', async () => {
499
+ const input = new PassThrough();
500
+ const output = new PassThrough();
501
+ const rl = createInterface({ input, output, terminal: true });
502
+
503
+ input.write('foo\n');
504
+ input.write('bar\n');
505
+ input.write('baz\n');
506
+ await new Promise<void>((r) => setTimeout(r, 20));
507
+
508
+ const history = (rl as unknown as { history: string[] }).history;
509
+ expect(history.length).toBe(3);
510
+ expect(history[0]).toBe('baz');
511
+ expect(history[1]).toBe('bar');
512
+ expect(history[2]).toBe('foo');
513
+ rl.close();
514
+ });
515
+
516
+ await it('should not store empty lines in history', async () => {
517
+ const input = new PassThrough();
518
+ const output = new PassThrough();
519
+ const rl = createInterface({ input, output, terminal: true });
520
+
521
+ input.write('\n\n\n');
522
+ await new Promise<void>((r) => setTimeout(r, 20));
523
+
524
+ const history = (rl as unknown as { history: string[] }).history;
525
+ expect(history.length).toBe(0);
526
+ rl.close();
527
+ });
528
+
529
+ await it('should not store duplicate consecutive lines', async () => {
530
+ const input = new PassThrough();
531
+ const output = new PassThrough();
532
+ const rl = createInterface({ input, output, terminal: true });
533
+
534
+ input.write('same\nsame\nsame\n');
535
+ await new Promise<void>((r) => setTimeout(r, 20));
536
+
537
+ const history = (rl as unknown as { history: string[] }).history;
538
+ expect(history.length).toBe(1);
539
+ expect(history[0]).toBe('same');
540
+ rl.close();
541
+ });
542
+ });
543
+
544
+ await describe('readline async iterator', async () => {
545
+ await it('should iterate over lines', async () => {
546
+ const input = new PassThrough();
547
+ const rl = createInterface({ input });
548
+ const collected: string[] = [];
549
+
550
+ const iterPromise = (async () => {
551
+ for await (const line of rl) {
552
+ collected.push(line);
553
+ }
554
+ })();
555
+
556
+ input.write('a\nb\nc\n');
557
+ input.end();
558
+ await iterPromise;
559
+
560
+ expect(collected.length).toBe(3);
561
+ expect(collected[0]).toBe('a');
562
+ expect(collected[1]).toBe('b');
563
+ expect(collected[2]).toBe('c');
564
+ });
565
+
566
+ await it('should handle empty input', async () => {
567
+ const input = new PassThrough();
568
+ const rl = createInterface({ input });
569
+ const collected: string[] = [];
570
+
571
+ const iterPromise = (async () => {
572
+ for await (const line of rl) {
573
+ collected.push(line);
574
+ }
575
+ })();
576
+
577
+ input.end();
578
+ await iterPromise;
579
+
580
+ expect(collected.length).toBe(0);
581
+ });
582
+
583
+ await it('should handle single line without trailing newline', async () => {
584
+ const input = new PassThrough();
585
+ const rl = createInterface({ input });
586
+ const collected: string[] = [];
587
+
588
+ const iterPromise = (async () => {
589
+ for await (const line of rl) {
590
+ collected.push(line);
591
+ }
592
+ })();
593
+
594
+ input.write('trailing');
595
+ input.end();
596
+ await iterPromise;
597
+
598
+ expect(collected.length).toBe(1);
599
+ expect(collected[0]).toBe('trailing');
600
+ });
601
+
602
+ await it('should handle multiline with Unicode content', async () => {
603
+ const input = new PassThrough();
604
+ const rl = createInterface({ input, crlfDelay: Infinity });
605
+ const collected: string[] = [];
606
+
607
+ const iterPromise = (async () => {
608
+ for await (const line of rl) {
609
+ collected.push(line);
610
+ }
611
+ })();
612
+
613
+ input.write('line 1\nline 2 南越国\nline 3\ntrailing');
614
+ input.end();
615
+ await iterPromise;
616
+
617
+ expect(collected.length).toBe(4);
618
+ expect(collected[0]).toBe('line 1');
619
+ expect(collected[1]).toBe('line 2 南越国');
620
+ expect(collected[2]).toBe('line 3');
621
+ expect(collected[3]).toBe('trailing');
622
+ });
623
+
624
+ await it('should handle lines ending with newline', async () => {
625
+ const input = new PassThrough();
626
+ const rl = createInterface({ input });
627
+ const collected: string[] = [];
628
+
629
+ const iterPromise = (async () => {
630
+ for await (const line of rl) {
631
+ collected.push(line);
632
+ }
633
+ })();
634
+
635
+ input.write('line 1\nline 2\nline 3 ends with newline\n');
636
+ input.end();
637
+ await iterPromise;
638
+
639
+ expect(collected.length).toBe(3);
640
+ expect(collected[0]).toBe('line 1');
641
+ expect(collected[1]).toBe('line 2');
642
+ expect(collected[2]).toBe('line 3 ends with newline');
643
+ });
644
+ });
645
+
646
+ await describe('readline.clearLine', async () => {
647
+ await it('direction 0 should clear entire line', async () => {
648
+ const chunks: string[] = [];
649
+ const stream = new Writable({
650
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
651
+ });
652
+ clearLine(stream, 0);
653
+ expect(chunks[0]).toBe('\x1b[2K');
654
+ });
655
+
656
+ await it('direction -1 should clear to line beginning', async () => {
657
+ const chunks: string[] = [];
658
+ const stream = new Writable({
659
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
660
+ });
661
+ clearLine(stream, -1);
662
+ expect(chunks[0]).toBe('\x1b[1K');
663
+ });
664
+
665
+ await it('direction 1 should clear to line end', async () => {
666
+ const chunks: string[] = [];
667
+ const stream = new Writable({
668
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
669
+ });
670
+ clearLine(stream, 1);
671
+ expect(chunks[0]).toBe('\x1b[0K');
672
+ });
673
+
674
+ await it('should return true', async () => {
675
+ const stream = new Writable({
676
+ write(_chunk, _enc, cb) { cb(); }
677
+ });
678
+ const result = clearLine(stream, 0);
679
+ expect(result).toBe(true);
680
+ });
681
+
682
+ await it('should handle null stream without throwing', async () => {
683
+ const result = clearLine(null as unknown as Writable, 0);
684
+ expect(result).toBe(true);
685
+ });
686
+
687
+ await it('should handle undefined stream without throwing', async () => {
688
+ const result = clearLine(undefined as unknown as Writable, 0);
689
+ expect(result).toBe(true);
690
+ });
691
+
692
+ await it('should invoke callback', async () => {
693
+ const stream = new Writable({
694
+ write(_chunk, _enc, cb) { cb(); }
695
+ });
696
+ let called = false;
697
+ clearLine(stream, 0, () => { called = true; });
698
+ await new Promise<void>((r) => setTimeout(r, 10));
699
+ expect(called).toBe(true);
700
+ });
701
+ });
702
+
703
+ await describe('readline.clearScreenDown', async () => {
704
+ await it('should write clear-screen-down escape', async () => {
705
+ const chunks: string[] = [];
706
+ const stream = new Writable({
707
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
708
+ });
709
+ clearScreenDown(stream);
710
+ expect(chunks[0]).toBe('\x1b[0J');
711
+ });
712
+
713
+ await it('should return true', async () => {
714
+ const stream = new Writable({
715
+ write(_chunk, _enc, cb) { cb(); }
716
+ });
717
+ expect(clearScreenDown(stream)).toBe(true);
718
+ });
719
+
720
+ await it('should handle null stream without throwing', async () => {
721
+ expect(clearScreenDown(null as unknown as Writable)).toBe(true);
722
+ });
723
+
724
+ await it('should invoke callback', async () => {
725
+ const stream = new Writable({
726
+ write(_chunk, _enc, cb) { cb(); }
727
+ });
728
+ let called = false;
729
+ clearScreenDown(stream, () => { called = true; });
730
+ await new Promise<void>((r) => setTimeout(r, 10));
731
+ expect(called).toBe(true);
732
+ });
733
+ });
734
+
735
+ await describe('readline.cursorTo', async () => {
736
+ await it('should move cursor to x position', async () => {
737
+ const chunks: string[] = [];
738
+ const stream = new Writable({
739
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
740
+ });
741
+ cursorTo(stream, 5);
742
+ expect(chunks[0]).toBe('\x1b[6G'); // x+1
743
+ });
744
+
745
+ await it('should move cursor to x,y position', async () => {
746
+ const chunks: string[] = [];
747
+ const stream = new Writable({
748
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
749
+ });
750
+ cursorTo(stream, 10, 5);
751
+ expect(chunks[0]).toBe('\x1b[6;11H'); // row+1;col+1
752
+ });
753
+
754
+ await it('should move cursor to 0,0', async () => {
755
+ const chunks: string[] = [];
756
+ const stream = new Writable({
757
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
758
+ });
759
+ cursorTo(stream, 0, 0);
760
+ expect(chunks[0]).toBe('\x1b[1;1H');
761
+ });
762
+
763
+ await it('should move cursor to column 1', async () => {
764
+ const chunks: string[] = [];
765
+ const stream = new Writable({
766
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
767
+ });
768
+ cursorTo(stream, 1);
769
+ expect(chunks[0]).toBe('\x1b[2G');
770
+ });
771
+
772
+ await it('should return true', async () => {
773
+ const stream = new Writable({
774
+ write(_chunk, _enc, cb) { cb(); }
775
+ });
776
+ expect(cursorTo(stream, 1)).toBe(true);
777
+ });
778
+
779
+ await it('should handle null stream without throwing', async () => {
780
+ expect(cursorTo(null as unknown as Writable, 1)).toBe(true);
781
+ });
782
+
783
+ await it('should accept callback as third argument', async () => {
784
+ const stream = new Writable({
785
+ write(_chunk, _enc, cb) { cb(); }
786
+ });
787
+ let called = false;
788
+ cursorTo(stream, 1, () => { called = true; });
789
+ await new Promise<void>((r) => setTimeout(r, 10));
790
+ expect(called).toBe(true);
791
+ });
792
+
793
+ await it('should accept callback as fourth argument', async () => {
794
+ const stream = new Writable({
795
+ write(_chunk, _enc, cb) { cb(); }
796
+ });
797
+ let called = false;
798
+ cursorTo(stream, 1, 2, () => { called = true; });
799
+ await new Promise<void>((r) => setTimeout(r, 10));
800
+ expect(called).toBe(true);
801
+ });
802
+ });
803
+
804
+ await describe('readline.moveCursor', async () => {
805
+ await it('should move right', async () => {
806
+ const chunks: string[] = [];
807
+ const stream = new Writable({
808
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
809
+ });
810
+ moveCursor(stream, 1, 0);
811
+ expect(chunks[0]).toBe('\x1b[1C');
812
+ });
813
+
814
+ await it('should move left', async () => {
815
+ const chunks: string[] = [];
816
+ const stream = new Writable({
817
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
818
+ });
819
+ moveCursor(stream, -1, 0);
820
+ expect(chunks[0]).toBe('\x1b[1D');
821
+ });
822
+
823
+ await it('should move down', async () => {
824
+ const chunks: string[] = [];
825
+ const stream = new Writable({
826
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
827
+ });
828
+ moveCursor(stream, 0, 1);
829
+ expect(chunks[0]).toBe('\x1b[1B');
830
+ });
831
+
832
+ await it('should move up', async () => {
833
+ const chunks: string[] = [];
834
+ const stream = new Writable({
835
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
836
+ });
837
+ moveCursor(stream, 0, -1);
838
+ expect(chunks[0]).toBe('\x1b[1A');
839
+ });
840
+
841
+ await it('should move right and down', async () => {
842
+ const chunks: string[] = [];
843
+ const stream = new Writable({
844
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
845
+ });
846
+ moveCursor(stream, 1, 1);
847
+ expect(chunks[0]).toBe('\x1b[1C\x1b[1B');
848
+ });
849
+
850
+ await it('should move left and up', async () => {
851
+ const chunks: string[] = [];
852
+ const stream = new Writable({
853
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
854
+ });
855
+ moveCursor(stream, -1, -1);
856
+ expect(chunks[0]).toBe('\x1b[1D\x1b[1A');
857
+ });
858
+
859
+ await it('should move right and up', async () => {
860
+ const chunks: string[] = [];
861
+ const stream = new Writable({
862
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
863
+ });
864
+ moveCursor(stream, 1, -1);
865
+ expect(chunks[0]).toBe('\x1b[1C\x1b[1A');
866
+ });
867
+
868
+ await it('should move left and down', async () => {
869
+ const chunks: string[] = [];
870
+ const stream = new Writable({
871
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
872
+ });
873
+ moveCursor(stream, -1, 1);
874
+ expect(chunks[0]).toBe('\x1b[1D\x1b[1B');
875
+ });
876
+
877
+ await it('should write nothing for 0,0', async () => {
878
+ const chunks: string[] = [];
879
+ const stream = new Writable({
880
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
881
+ });
882
+ moveCursor(stream, 0, 0);
883
+ expect(chunks.length).toBe(0);
884
+ });
885
+
886
+ await it('should handle larger dx/dy values', async () => {
887
+ const chunks: string[] = [];
888
+ const stream = new Writable({
889
+ write(chunk, _enc, cb) { chunks.push(chunk.toString()); cb(); }
890
+ });
891
+ moveCursor(stream, 3, -2);
892
+ expect(chunks[0]).toBe('\x1b[3C\x1b[2A');
893
+ });
894
+
895
+ await it('should return true', async () => {
896
+ const stream = new Writable({
897
+ write(_chunk, _enc, cb) { cb(); }
898
+ });
899
+ expect(moveCursor(stream, 1, 1)).toBe(true);
900
+ });
901
+
902
+ await it('should handle null stream without throwing', async () => {
903
+ expect(moveCursor(null as unknown as Writable, 1, 1)).toBe(true);
904
+ });
905
+
906
+ await it('should handle undefined stream without throwing', async () => {
907
+ expect(moveCursor(undefined as unknown as Writable, 1, 1)).toBe(true);
908
+ });
909
+
910
+ await it('should invoke callback', async () => {
911
+ const stream = new Writable({
912
+ write(_chunk, _enc, cb) { cb(); }
913
+ });
914
+ let called = false;
915
+ moveCursor(stream, 1, 1, () => { called = true; });
916
+ await new Promise<void>((r) => setTimeout(r, 10));
917
+ expect(called).toBe(true);
918
+ });
919
+
920
+ await it('should invoke callback for 0,0 move', async () => {
921
+ let called = false;
922
+ const stream = new Writable({
923
+ write(_chunk, _enc, cb) { cb(); }
924
+ });
925
+ moveCursor(stream, 0, 0, () => { called = true; });
926
+ // Callback may be called synchronously or asynchronously
927
+ await new Promise<void>((r) => setTimeout(r, 10));
928
+ expect(called).toBe(true);
929
+ });
930
+ });
931
+ };