• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3// Flags: --expose-internals
4
5const common = require('../common');
6const stream = require('stream');
7const REPL = require('internal/repl');
8const assert = require('assert');
9const fs = require('fs');
10const path = require('path');
11const { inspect } = require('util');
12
13common.skipIfDumbTerminal();
14
15const tmpdir = require('../common/tmpdir');
16tmpdir.refresh();
17
18const defaultHistoryPath = path.join(tmpdir.path, '.node_repl_history');
19
20// Create an input stream specialized for testing an array of actions
21class ActionStream extends stream.Stream {
22  run(data) {
23    const _iter = data[Symbol.iterator]();
24    const doAction = () => {
25      const next = _iter.next();
26      if (next.done) {
27        // Close the repl. Note that it must have a clean prompt to do so.
28        this.emit('keypress', '', { ctrl: true, name: 'd' });
29        return;
30      }
31      const action = next.value;
32
33      if (typeof action === 'object') {
34        this.emit('keypress', '', action);
35      } else {
36        this.emit('data', `${action}`);
37      }
38      setImmediate(doAction);
39    };
40    doAction();
41  }
42  resume() {}
43  pause() {}
44}
45ActionStream.prototype.readable = true;
46
47// Mock keys
48const ENTER = { name: 'enter' };
49const UP = { name: 'up' };
50const DOWN = { name: 'down' };
51const LEFT = { name: 'left' };
52const RIGHT = { name: 'right' };
53const DELETE = { name: 'delete' };
54const BACKSPACE = { name: 'backspace' };
55const WORD_LEFT = { name: 'left', ctrl: true };
56const WORD_RIGHT = { name: 'right', ctrl: true };
57const GO_TO_END = { name: 'end' };
58const DELETE_WORD_LEFT = { name: 'backspace', ctrl: true };
59const SIGINT = { name: 'c', ctrl: true };
60
61const prompt = '> ';
62const WAIT = '€';
63
64const prev = process.features.inspector;
65
66let completions = 0;
67
68const tests = [
69  { // Creates few history to navigate for
70    env: { NODE_REPL_HISTORY: defaultHistoryPath },
71    test: [ 'let ab = 45', ENTER,
72            '555 + 909', ENTER,
73            '{key : {key2 :[] }}', ENTER,
74            'Array(100).fill(1).map((e, i) => i ** i)', LEFT, LEFT, DELETE,
75            '2', ENTER],
76    expected: [],
77    clean: false
78  },
79  {
80    env: { NODE_REPL_HISTORY: defaultHistoryPath },
81    test: [UP, UP, UP, UP, UP, DOWN, DOWN, DOWN, DOWN, DOWN],
82    expected: [prompt,
83               `${prompt}Array(100).fill(1).map((e, i) => i ** 2)`,
84               prev && '\n// [ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, ' +
85                 '144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529,' +
86                 ' 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, ' +
87                 '1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936,' +
88                 ' 2025, 2116, 2209,...',
89               `${prompt}{key : {key2 :[] }}`,
90               prev && '\n// { key: { key2: [] } }',
91               `${prompt}555 + 909`,
92               prev && '\n// 1464',
93               `${prompt}let ab = 45`,
94               prompt,
95               `${prompt}let ab = 45`,
96               `${prompt}555 + 909`,
97               prev && '\n// 1464',
98               `${prompt}{key : {key2 :[] }}`,
99               prev && '\n// { key: { key2: [] } }',
100               `${prompt}Array(100).fill(1).map((e, i) => i ** 2)`,
101               prev && '\n// [ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, ' +
102                 '144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529,' +
103                 ' 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, ' +
104                 '1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936,' +
105                 ' 2025, 2116, 2209,...',
106               prompt].filter((e) => typeof e === 'string'),
107    clean: false
108  },
109  { // Creates more history entries to navigate through.
110    env: { NODE_REPL_HISTORY: defaultHistoryPath },
111    test: [
112      '555 + 909', ENTER, // Add a duplicate to the history set.
113      'const foo = true', ENTER,
114      '555n + 111n', ENTER,
115      '5 + 5', ENTER,
116      '55 - 13 === 42', ENTER
117    ],
118    expected: [],
119    clean: false
120  },
121  {
122    env: { NODE_REPL_HISTORY: defaultHistoryPath },
123    checkTotal: true,
124    preview: false,
125    showEscapeCodes: true,
126    test: [
127      '55', UP, UP, UP, UP, UP, UP, ENTER
128    ],
129    expected: [
130      '\x1B[1G', '\x1B[0J', prompt, '\x1B[3G',
131      // '55'
132      '5', '5',
133      // UP
134      '\x1B[1G', '\x1B[0J',
135      '> 55 - 13 === 42', '\x1B[17G',
136      // UP - skipping 5 + 5
137      '\x1B[1G', '\x1B[0J',
138      '> 555n + 111n', '\x1B[14G',
139      // UP - skipping const foo = true
140      '\x1B[1G', '\x1B[0J',
141      '> 555 + 909', '\x1B[12G',
142      // UP, UP
143      // UPs at the end of the history reset the line to the original input.
144      '\x1B[1G', '\x1B[0J',
145      '> 55', '\x1B[5G',
146      // ENTER
147      '\r\n', '55\n',
148      '\x1B[1G', '\x1B[0J',
149      '> ', '\x1B[3G',
150      '\r\n'
151    ],
152    clean: true
153  },
154  {
155    env: { NODE_REPL_HISTORY: defaultHistoryPath },
156    skip: !process.features.inspector,
157    test: [
158      // あ is a full width character with a length of one.
159      // �� is a full width character with a length of two.
160      // �� is a half width character with the length of two.
161      // '\u0301', '0x200D', '\u200E' are zero width characters.
162      `const x1 = '${'あ'.repeat(124)}'`, ENTER, // Fully visible
163      ENTER,
164      `const y1 = '${'あ'.repeat(125)}'`, ENTER, // Cut off
165      ENTER,
166      `const x2 = '${'��'.repeat(124)}'`, ENTER, // Fully visible
167      ENTER,
168      `const y2 = '${'��'.repeat(125)}'`, ENTER, // Cut off
169      ENTER,
170      `const x3 = '${'��'.repeat(248)}'`, ENTER, // Fully visible
171      ENTER,
172      `const y3 = '${'��'.repeat(249)}'`, ENTER, // Cut off
173      ENTER,
174      `const x4 = 'a${'\u0301'.repeat(1000)}'`, ENTER, // á
175      ENTER,
176      `const ${'veryLongName'.repeat(30)} = 'I should be previewed'`,
177      ENTER,
178      'const e = new RangeError("visible\\ninvisible")',
179      ENTER,
180      'e',
181      ENTER,
182      'veryLongName'.repeat(30),
183      ENTER,
184      `${'\x1B[90m \x1B[39m'.repeat(235)} fun`,
185      ENTER,
186      `${' '.repeat(236)} fun`,
187      ENTER
188    ],
189    expected: [],
190    clean: false
191  },
192  {
193    env: { NODE_REPL_HISTORY: defaultHistoryPath },
194    columns: 250,
195    checkTotal: true,
196    showEscapeCodes: true,
197    skip: !process.features.inspector,
198    test: [
199      UP,
200      UP,
201      UP,
202      WORD_LEFT,
203      UP,
204      BACKSPACE,
205      'x1',
206      BACKSPACE,
207      '2',
208      BACKSPACE,
209      '3',
210      BACKSPACE,
211      '4',
212      DELETE_WORD_LEFT,
213      'y1',
214      BACKSPACE,
215      '2',
216      BACKSPACE,
217      '3',
218      SIGINT
219    ],
220    // A = Cursor n up
221    // B = Cursor n down
222    // C = Cursor n forward
223    // D = Cursor n back
224    // G = Cursor to column n
225    // J = Erase in screen; 0 = right; 1 = left; 2 = total
226    // K = Erase in line; 0 = right; 1 = left; 2 = total
227    expected: [
228      // 0. Start
229      '\x1B[1G', '\x1B[0J',
230      prompt, '\x1B[3G',
231      // 1. UP
232      // This exceeds the maximum columns (250):
233      // Whitespace + prompt + ' // '.length + 'function'.length
234      // 236 + 2 + 4 + 8
235      '\x1B[1G', '\x1B[0J',
236      `${prompt}${' '.repeat(236)} fun`, '\x1B[243G',
237      ' // ction', '\x1B[243G',
238      ' // ction', '\x1B[243G',
239      '\x1B[0K',
240      // 2. UP
241      '\x1B[1G', '\x1B[0J',
242      `${prompt}${' '.repeat(235)} fun`, '\x1B[242G',
243      // TODO(BridgeAR): Investigate why the preview is generated twice.
244      ' // ction', '\x1B[242G',
245      ' // ction', '\x1B[242G',
246      // Preview cleanup
247      '\x1B[0K',
248      // 3. UP
249      '\x1B[1G', '\x1B[0J',
250      // 'veryLongName'.repeat(30).length === 360
251      // prompt.length === 2
252      // 360 % 250 + 2 === 112 (+1)
253      `${prompt}${'veryLongName'.repeat(30)}`, '\x1B[113G',
254      // "// 'I should be previewed'".length + 86 === 112 (+1)
255      "\n// 'I should be previewed'", '\x1B[113G', '\x1B[1A',
256      // Preview cleanup
257      '\x1B[1B', '\x1B[2K', '\x1B[1A',
258      // 4. WORD LEFT
259      // Almost identical as above. Just one extra line.
260      // Math.floor(360 / 250) === 1
261      '\x1B[1A',
262      '\x1B[1G', '\x1B[0J',
263      `${prompt}${'veryLongName'.repeat(30)}`, '\x1B[3G', '\x1B[1A',
264      '\x1B[1B', "\n// 'I should be previewed'", '\x1B[3G', '\x1B[2A',
265      // Preview cleanup
266      '\x1B[2B', '\x1B[2K', '\x1B[2A',
267      // 5. UP
268      '\x1B[1G', '\x1B[0J',
269      `${prompt}e`, '\x1B[4G',
270      // '// RangeError: visible'.length - 19 === 3 (+1)
271      '\n// RangeError: visible', '\x1B[4G', '\x1B[1A',
272      // Preview cleanup
273      '\x1B[1B', '\x1B[2K', '\x1B[1A',
274      // 6. Backspace
275      '\x1B[1G', '\x1B[0J',
276      '> ', '\x1B[3G', 'x', '1',
277      `\n// '${'あ'.repeat(124)}'`,
278      '\x1B[5G', '\x1B[1A',
279      '\x1B[1B', '\x1B[2K', '\x1B[1A',
280      '\x1B[1G', '\x1B[0J',
281      '> x', '\x1B[4G', '2',
282      `\n// '${'��'.repeat(124)}'`,
283      '\x1B[5G', '\x1B[1A',
284      '\x1B[1B', '\x1B[2K', '\x1B[1A',
285      '\x1B[1G', '\x1B[0J',
286      '> x', '\x1B[4G', '3',
287      `\n// '${'��'.repeat(248)}'`,
288      '\x1B[5G', '\x1B[1A',
289      '\x1B[1B', '\x1B[2K', '\x1B[1A',
290      '\x1B[1G', '\x1B[0J',
291      '> x', '\x1B[4G', '4',
292      `\n// 'a${'\u0301'.repeat(1000)}'`,
293      '\x1B[5G', '\x1B[1A',
294      '\x1B[1B', '\x1B[2K', '\x1B[1A',
295      '\x1B[1G', '\x1B[0J',
296      '> ', '\x1B[3G', 'y', '1',
297      `\n// '${'あ'.repeat(121)}...`,
298      '\x1B[5G', '\x1B[1A',
299      '\x1B[1B', '\x1B[2K', '\x1B[1A',
300      '\x1B[1G', '\x1B[0J',
301      '> y', '\x1B[4G', '2',
302      `\n// '${'��'.repeat(121)}...`,
303      '\x1B[5G', '\x1B[1A',
304      '\x1B[1B', '\x1B[2K', '\x1B[1A',
305      '\x1B[1G', '\x1B[0J',
306      '> y', '\x1B[4G', '3',
307      `\n// '${'��'.repeat(242)}...`,
308      '\x1B[5G', '\x1B[1A',
309      '\x1B[1B', '\x1B[2K', '\x1B[1A',
310      '\r\n',
311      '\x1B[1G', '\x1B[0J',
312      '> ', '\x1B[3G',
313      '\r\n'
314    ],
315    clean: true
316  },
317  {
318    env: { NODE_REPL_HISTORY: defaultHistoryPath },
319    showEscapeCodes: true,
320    skip: !process.features.inspector,
321    test: [
322      'fu',
323      'n',
324      RIGHT,
325      BACKSPACE,
326      LEFT,
327      LEFT,
328      'A',
329      BACKSPACE,
330      GO_TO_END,
331      BACKSPACE,
332      WORD_LEFT,
333      WORD_RIGHT,
334      ENTER
335    ],
336    // C = Cursor n forward
337    // D = Cursor n back
338    // G = Cursor to column n
339    // J = Erase in screen; 0 = right; 1 = left; 2 = total
340    // K = Erase in line; 0 = right; 1 = left; 2 = total
341    expected: [
342      // 0.
343      // 'f'
344      '\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', 'f',
345      // 'u'
346      'u', ' // nction', '\x1B[5G',
347      // 'n' - Cleanup
348      '\x1B[0K',
349      'n', ' // ction', '\x1B[6G',
350      // 1. Right. Cleanup
351      '\x1B[0K',
352      'ction',
353      // 2. Backspace. Refresh
354      '\x1B[1G', '\x1B[0J', `${prompt}functio`, '\x1B[10G',
355      // Autocomplete and refresh?
356      ' // n', '\x1B[10G', ' // n', '\x1B[10G',
357      // 3. Left. Cleanup
358      '\x1B[0K',
359      '\x1B[1D', '\x1B[10G', ' // n', '\x1B[9G',
360      // 4. Left. Cleanup
361      '\x1B[10G', '\x1B[0K', '\x1B[9G',
362      '\x1B[1D', '\x1B[10G', ' // n', '\x1B[8G',
363      // 5. 'A' - Cleanup
364      '\x1B[10G', '\x1B[0K', '\x1B[8G',
365      // Refresh
366      '\x1B[1G', '\x1B[0J', `${prompt}functAio`, '\x1B[9G',
367      // 6. Backspace. Refresh
368      '\x1B[1G', '\x1B[0J', `${prompt}functio`, '\x1B[8G', '\x1B[10G', ' // n',
369      '\x1B[8G', '\x1B[10G', ' // n',
370      '\x1B[8G', '\x1B[10G',
371      // 7. Go to end. Cleanup
372      '\x1B[0K', '\x1B[8G', '\x1B[2C',
373      'n',
374      // 8. Backspace. Refresh
375      '\x1B[1G', '\x1B[0J', `${prompt}functio`, '\x1B[10G',
376      // Autocomplete
377      ' // n', '\x1B[10G', ' // n', '\x1B[10G',
378      // 9. Word left. Cleanup
379      '\x1B[0K', '\x1B[7D', '\x1B[10G', ' // n', '\x1B[3G', '\x1B[10G',
380      // 10. Word right. Cleanup
381      '\x1B[0K', '\x1B[3G', '\x1B[7C', ' // n', '\x1B[10G',
382      '\x1B[0K',
383      // 11. ENTER
384      '\r\n',
385      'Uncaught ReferenceError: functio is not defined\n',
386      '\x1B[1G', '\x1B[0J',
387      prompt, '\x1B[3G', '\r\n'
388    ],
389    clean: true
390  },
391  {
392    // Check changed inspection defaults.
393    env: { NODE_REPL_HISTORY: defaultHistoryPath },
394    skip: !process.features.inspector,
395    test: [
396      'util.inspect.replDefaults.showHidden',
397      ENTER
398    ],
399    expected: [],
400    clean: false
401  },
402  {
403    env: { NODE_REPL_HISTORY: defaultHistoryPath },
404    skip: !process.features.inspector,
405    checkTotal: true,
406    test: [
407      '[ ]',
408      WORD_LEFT,
409      WORD_LEFT,
410      UP,
411      ' = true',
412      ENTER,
413      '[ ]',
414      ENTER
415    ],
416    expected: [
417      prompt,
418      '[', ' ', ']',
419      '\n// []', '\n// []', '\n// []',
420      '> util.inspect.replDefaults.showHidden',
421      '\n// false',
422      ' ', '=', ' ', 't', 'r', 'u', 'e',
423      'true\n',
424      '> ', '[', ' ', ']',
425      '\n// [ [length]: 0 ]',
426      '[ [length]: 0 ]\n',
427      '> ',
428    ],
429    clean: true
430  },
431  {
432    // Check that the completer ignores completions that are outdated.
433    env: { NODE_REPL_HISTORY: defaultHistoryPath },
434    completer(line, callback) {
435      if (line.endsWith(WAIT)) {
436        if (completions++ === 0) {
437          callback(null, [[`${WAIT}WOW`], line]);
438        } else {
439          setTimeout(callback, 1000, null, [[`${WAIT}WOW`], line]).unref();
440        }
441      } else {
442        callback(null, [[' Always visible'], line]);
443      }
444    },
445    skip: !process.features.inspector,
446    test: [
447      WAIT, // The first call is awaited before new input is triggered!
448      BACKSPACE,
449      's',
450      BACKSPACE,
451      WAIT, // The second call is not awaited. It won't trigger the preview.
452      BACKSPACE,
453      's',
454      BACKSPACE
455    ],
456    expected: [
457      prompt,
458      WAIT,
459      ' // WOW',
460      prompt,
461      's',
462      ' // Always visible',
463      prompt,
464      WAIT,
465      prompt,
466      's',
467      ' // Always visible',
468      prompt,
469    ],
470    clean: true
471  }
472];
473const numtests = tests.length;
474
475const runTestWrap = common.mustCall(runTest, numtests);
476
477function cleanupTmpFile() {
478  try {
479    // Write over the file, clearing any history
480    fs.writeFileSync(defaultHistoryPath, '');
481  } catch (err) {
482    if (err.code === 'ENOENT') return true;
483    throw err;
484  }
485  return true;
486}
487
488function runTest() {
489  const opts = tests.shift();
490  if (!opts) return; // All done
491
492  const { expected, skip } = opts;
493
494  // Test unsupported on platform.
495  if (skip) {
496    setImmediate(runTestWrap, true);
497    return;
498  }
499  const lastChunks = [];
500  let i = 0;
501
502  REPL.createInternalRepl(opts.env, {
503    input: new ActionStream(),
504    output: new stream.Writable({
505      write(chunk, _, next) {
506        const output = chunk.toString();
507
508        if (!opts.showEscapeCodes &&
509            (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) {
510          return next();
511        }
512
513        lastChunks.push(output);
514
515        if (expected.length && !opts.checkTotal) {
516          try {
517            assert.strictEqual(output, expected[i]);
518          } catch (e) {
519            console.error(`Failed test # ${numtests - tests.length}`);
520            console.error('Last outputs: ' + inspect(lastChunks, {
521              breakLength: 5, colors: true
522            }));
523            throw e;
524          }
525          // TODO(BridgeAR): Auto close on last chunk!
526          i++;
527        }
528
529        next();
530      }
531    }),
532    completer: opts.completer,
533    prompt,
534    useColors: false,
535    preview: opts.preview,
536    terminal: true
537  }, function(err, repl) {
538    if (err) {
539      console.error(`Failed test # ${numtests - tests.length}`);
540      throw err;
541    }
542
543    repl.once('close', () => {
544      if (opts.clean)
545        cleanupTmpFile();
546
547      if (opts.checkTotal) {
548        assert.deepStrictEqual(lastChunks, expected);
549      } else if (expected.length !== i) {
550        console.error(tests[numtests - tests.length - 1]);
551        throw new Error(`Failed test # ${numtests - tests.length}`);
552      }
553
554      setImmediate(runTestWrap, true);
555    });
556
557    if (opts.columns) {
558      Object.defineProperty(repl, 'columns', {
559        value: opts.columns,
560        enumerable: true
561      });
562    }
563    repl.input.run(opts.test);
564  });
565}
566
567// run the tests
568runTest();
569