/test/parallel/test-readline-interface.js
JavaScript | 439 lines | 364 code | 43 blank | 32 comment | 9 complexity | 135675da526d7f68f514800bb238fbd0 MD5 | raw file
1// Flags: --expose_internals
2'use strict';
3const common = require('../common');
4const assert = require('assert');
5const readline = require('readline');
6const internalReadline = require('internal/readline');
7const EventEmitter = require('events').EventEmitter;
8const inherits = require('util').inherits;
9const Writable = require('stream').Writable;
10const Readable = require('stream').Readable;
11
12function FakeInput() {
13 EventEmitter.call(this);
14}
15inherits(FakeInput, EventEmitter);
16FakeInput.prototype.resume = function() {};
17FakeInput.prototype.pause = function() {};
18FakeInput.prototype.write = function() {};
19FakeInput.prototype.end = function() {};
20
21function isWarned(emitter) {
22 for (var name in emitter) {
23 var listeners = emitter[name];
24 if (listeners.warned) return true;
25 }
26 return false;
27}
28
29[ true, false ].forEach(function(terminal) {
30 var fi;
31 var rli;
32 var called;
33
34 // disable history
35 fi = new FakeInput();
36 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal,
37 historySize: 0 });
38 assert.strictEqual(rli.historySize, 0);
39
40 fi.emit('data', 'asdf\n');
41 assert.deepStrictEqual(rli.history, terminal ? [] : undefined);
42 rli.close();
43
44 // default history size 30
45 fi = new FakeInput();
46 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal});
47 assert.strictEqual(rli.historySize, 30);
48
49 fi.emit('data', 'asdf\n');
50 assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : undefined);
51 rli.close();
52
53 // sending a full line
54 fi = new FakeInput();
55 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
56 called = false;
57 rli.on('line', function(line) {
58 called = true;
59 assert.equal(line, 'asdf');
60 });
61 fi.emit('data', 'asdf\n');
62 assert.ok(called);
63
64 // sending a blank line
65 fi = new FakeInput();
66 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
67 called = false;
68 rli.on('line', function(line) {
69 called = true;
70 assert.equal(line, '');
71 });
72 fi.emit('data', '\n');
73 assert.ok(called);
74
75 // sending a single character with no newline
76 fi = new FakeInput();
77 rli = new readline.Interface(fi, {});
78 called = false;
79 rli.on('line', function(line) {
80 called = true;
81 });
82 fi.emit('data', 'a');
83 assert.ok(!called);
84 rli.close();
85
86 // sending a single character with no newline and then a newline
87 fi = new FakeInput();
88 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
89 called = false;
90 rli.on('line', function(line) {
91 called = true;
92 assert.equal(line, 'a');
93 });
94 fi.emit('data', 'a');
95 assert.ok(!called);
96 fi.emit('data', '\n');
97 assert.ok(called);
98 rli.close();
99
100 // sending multiple newlines at once
101 fi = new FakeInput();
102 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
103 var expectedLines = ['foo', 'bar', 'baz'];
104 var callCount = 0;
105 rli.on('line', function(line) {
106 assert.equal(line, expectedLines[callCount]);
107 callCount++;
108 });
109 fi.emit('data', expectedLines.join('\n') + '\n');
110 assert.equal(callCount, expectedLines.length);
111 rli.close();
112
113 // sending multiple newlines at once that does not end with a new line
114 fi = new FakeInput();
115 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
116 expectedLines = ['foo', 'bar', 'baz', 'bat'];
117 callCount = 0;
118 rli.on('line', function(line) {
119 assert.equal(line, expectedLines[callCount]);
120 callCount++;
121 });
122 fi.emit('data', expectedLines.join('\n'));
123 assert.equal(callCount, expectedLines.length - 1);
124 rli.close();
125
126 // sending multiple newlines at once that does not end with a new(empty)
127 // line and a `end` event
128 fi = new FakeInput();
129 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
130 expectedLines = ['foo', 'bar', 'baz', ''];
131 callCount = 0;
132 rli.on('line', function(line) {
133 assert.equal(line, expectedLines[callCount]);
134 callCount++;
135 });
136 rli.on('close', function() {
137 callCount++;
138 });
139 fi.emit('data', expectedLines.join('\n'));
140 fi.emit('end');
141 assert.equal(callCount, expectedLines.length);
142 rli.close();
143
144 // sending multiple newlines at once that does not end with a new line
145 // and a `end` event(last line is)
146
147 // \r\n should emit one line event, not two
148 fi = new FakeInput();
149 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
150 expectedLines = ['foo', 'bar', 'baz', 'bat'];
151 callCount = 0;
152 rli.on('line', function(line) {
153 assert.equal(line, expectedLines[callCount]);
154 callCount++;
155 });
156 fi.emit('data', expectedLines.join('\r\n'));
157 assert.equal(callCount, expectedLines.length - 1);
158 rli.close();
159
160 // \r\n should emit one line event when split across multiple writes.
161 fi = new FakeInput();
162 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
163 expectedLines = ['foo', 'bar', 'baz', 'bat'];
164 callCount = 0;
165 rli.on('line', function(line) {
166 assert.equal(line, expectedLines[callCount]);
167 callCount++;
168 });
169 expectedLines.forEach(function(line) {
170 fi.emit('data', line + '\r');
171 fi.emit('data', '\n');
172 });
173 assert.equal(callCount, expectedLines.length);
174 rli.close();
175
176 // \r should behave like \n when alone
177 fi = new FakeInput();
178 rli = new readline.Interface({ input: fi, output: fi, terminal: true });
179 expectedLines = ['foo', 'bar', 'baz', 'bat'];
180 callCount = 0;
181 rli.on('line', function(line) {
182 assert.equal(line, expectedLines[callCount]);
183 callCount++;
184 });
185 fi.emit('data', expectedLines.join('\r'));
186 assert.equal(callCount, expectedLines.length - 1);
187 rli.close();
188
189 // \r at start of input should output blank line
190 fi = new FakeInput();
191 rli = new readline.Interface({ input: fi, output: fi, terminal: true });
192 expectedLines = ['', 'foo' ];
193 callCount = 0;
194 rli.on('line', function(line) {
195 assert.equal(line, expectedLines[callCount]);
196 callCount++;
197 });
198 fi.emit('data', '\rfoo\r');
199 assert.equal(callCount, expectedLines.length);
200 rli.close();
201
202 // \t when there is no completer function should behave like an ordinary
203 // character
204 fi = new FakeInput();
205 rli = new readline.Interface({ input: fi, output: fi, terminal: true });
206 called = false;
207 rli.on('line', function(line) {
208 assert.equal(line, '\t');
209 assert.strictEqual(called, false);
210 called = true;
211 });
212 fi.emit('data', '\t');
213 fi.emit('data', '\n');
214 assert.ok(called);
215 rli.close();
216
217 // \t does not become part of the input when there is a completer function
218 fi = new FakeInput();
219 var completer = function(line) {
220 return [[], line];
221 };
222 rli = new readline.Interface({
223 input: fi,
224 output: fi,
225 terminal: true,
226 completer: completer
227 });
228 called = false;
229 rli.on('line', function(line) {
230 assert.equal(line, 'foo');
231 assert.strictEqual(called, false);
232 called = true;
233 });
234 for (var character of '\tfo\to\t') {
235 fi.emit('data', character);
236 }
237 fi.emit('data', '\n');
238 assert.ok(called);
239 rli.close();
240
241 // constructor throws if completer is not a function or undefined
242 fi = new FakeInput();
243 assert.throws(function() {
244 readline.createInterface({
245 input: fi,
246 completer: 'string is not valid'
247 });
248 }, function(err) {
249 if (err instanceof TypeError) {
250 if (/Argument "completer" must be a function/.test(err)) {
251 return true;
252 }
253 }
254 return false;
255 });
256
257 // sending a multi-byte utf8 char over multiple writes
258 var buf = Buffer.from('☮', 'utf8');
259 fi = new FakeInput();
260 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
261 callCount = 0;
262 rli.on('line', function(line) {
263 callCount++;
264 assert.equal(line, buf.toString('utf8'));
265 });
266 [].forEach.call(buf, function(i) {
267 fi.emit('data', Buffer.from([i]));
268 });
269 assert.equal(callCount, 0);
270 fi.emit('data', '\n');
271 assert.equal(callCount, 1);
272 rli.close();
273
274 // Regression test for repl freeze, #1968:
275 // check that nothing fails if 'keypress' event throws.
276 fi = new FakeInput();
277 rli = new readline.Interface({ input: fi, output: fi, terminal: true });
278 var keys = [];
279 fi.on('keypress', function(key) {
280 keys.push(key);
281 if (key === 'X') {
282 throw new Error('bad thing happened');
283 }
284 });
285 try {
286 fi.emit('data', 'fooX');
287 } catch (e) { }
288 fi.emit('data', 'bar');
289 assert.equal(keys.join(''), 'fooXbar');
290 rli.close();
291
292 // calling readline without `new`
293 fi = new FakeInput();
294 rli = readline.Interface({ input: fi, output: fi, terminal: terminal });
295 called = false;
296 rli.on('line', function(line) {
297 called = true;
298 assert.equal(line, 'asdf');
299 });
300 fi.emit('data', 'asdf\n');
301 assert.ok(called);
302 rli.close();
303
304 if (terminal) {
305 // question
306 fi = new FakeInput();
307 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
308 expectedLines = ['foo'];
309 rli.question(expectedLines[0], function() {
310 rli.close();
311 });
312 var cursorPos = rli._getCursorPos();
313 assert.equal(cursorPos.rows, 0);
314 assert.equal(cursorPos.cols, expectedLines[0].length);
315 rli.close();
316
317 // sending a multi-line question
318 fi = new FakeInput();
319 rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
320 expectedLines = ['foo', 'bar'];
321 rli.question(expectedLines.join('\n'), function() {
322 rli.close();
323 });
324 cursorPos = rli._getCursorPos();
325 assert.equal(cursorPos.rows, expectedLines.length - 1);
326 assert.equal(cursorPos.cols, expectedLines.slice(-1)[0].length);
327 rli.close();
328 }
329
330 // isFullWidthCodePoint() should return false for non-numeric values
331 [true, false, null, undefined, {}, [], 'あ'].forEach((v) => {
332 assert.strictEqual(internalReadline.isFullWidthCodePoint('あ'), false);
333 });
334
335 // wide characters should be treated as two columns.
336 assert.equal(internalReadline.isFullWidthCodePoint('a'.charCodeAt(0)), false);
337 assert.equal(internalReadline.isFullWidthCodePoint('あ'.charCodeAt(0)), true);
338 assert.equal(internalReadline.isFullWidthCodePoint('谢'.charCodeAt(0)), true);
339 assert.equal(internalReadline.isFullWidthCodePoint('고'.charCodeAt(0)), true);
340 assert.equal(internalReadline.isFullWidthCodePoint(0x1f251), true);
341 assert.equal(internalReadline.getStringWidth('abcde'), 5);
342 assert.equal(internalReadline.getStringWidth('古池や'), 6);
343 assert.equal(internalReadline.getStringWidth('ノード.js'), 9);
344 assert.equal(internalReadline.getStringWidth('你好'), 4);
345 assert.equal(internalReadline.getStringWidth('안녕하세요'), 10);
346 assert.equal(internalReadline.getStringWidth('A\ud83c\ude00BC'), 5);
347
348 // check if vt control chars are stripped
349 assert.strictEqual(
350 internalReadline.stripVTControlCharacters('\u001b[31m> \u001b[39m'),
351 '> '
352 );
353 assert.strictEqual(
354 internalReadline.stripVTControlCharacters('\u001b[31m> \u001b[39m> '),
355 '> > '
356 );
357 assert.strictEqual(
358 internalReadline.stripVTControlCharacters('\u001b[31m\u001b[39m'),
359 ''
360 );
361 assert.strictEqual(
362 internalReadline.stripVTControlCharacters('> '),
363 '> '
364 );
365 assert.equal(internalReadline.getStringWidth('\u001b[31m> \u001b[39m'), 2);
366 assert.equal(internalReadline.getStringWidth('\u001b[31m> \u001b[39m> '), 4);
367 assert.equal(internalReadline.getStringWidth('\u001b[31m\u001b[39m'), 0);
368 assert.equal(internalReadline.getStringWidth('> '), 2);
369
370 assert.deepStrictEqual(fi.listeners(terminal ? 'keypress' : 'data'), []);
371
372 // check EventEmitter memory leak
373 for (var i = 0; i < 12; i++) {
374 var rl = readline.createInterface({
375 input: process.stdin,
376 output: process.stdout
377 });
378 rl.close();
379 assert.equal(isWarned(process.stdin._events), false);
380 assert.equal(isWarned(process.stdout._events), false);
381 }
382
383 //can create a new readline Interface with a null output arugument
384 fi = new FakeInput();
385 rli = new readline.Interface({input: fi, output: null, terminal: terminal });
386
387 called = false;
388 rli.on('line', function(line) {
389 called = true;
390 assert.equal(line, 'asdf');
391 });
392 fi.emit('data', 'asdf\n');
393 assert.ok(called);
394
395 assert.doesNotThrow(function() {
396 rli.setPrompt('ddd> ');
397 });
398
399 assert.doesNotThrow(function() {
400 rli.prompt();
401 });
402
403 assert.doesNotThrow(function() {
404 rli.write('really shouldnt be seeing this');
405 });
406
407 assert.doesNotThrow(function() {
408 rli.question('What do you think of node.js? ', function(answer) {
409 console.log('Thank you for your valuable feedback:', answer);
410 rli.close();
411 });
412 });
413
414 {
415 const expected = terminal
416 ? ['\u001b[1G', '\u001b[0J', '$ ', '\u001b[3G']
417 : ['$ '];
418
419 let counter = 0;
420 const output = new Writable({
421 write: common.mustCall((chunk, enc, cb) => {
422 assert.strictEqual(chunk.toString(), expected[counter++]);
423 cb();
424 rl.close();
425 }, expected.length)
426 });
427
428 const rl = readline.createInterface({
429 input: new Readable({ read: () => {} }),
430 output: output,
431 prompt: '$ ',
432 terminal: terminal
433 });
434
435 rl.prompt();
436
437 assert.strictEqual(rl._prompt, '$ ');
438 }
439});