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