'use strict'; // Flags: --expose-internals const common = require('../common'); const stream = require('stream'); const REPL = require('internal/repl'); const assert = require('assert'); const fs = require('fs'); const path = require('path'); const { inspect } = require('util'); common.skipIfDumbTerminal(); common.allowGlobals('aaaa'); const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); const defaultHistoryPath = path.join(tmpdir.path, '.node_repl_history'); // Create an input stream specialized for testing an array of actions class ActionStream extends stream.Stream { run(data) { const _iter = data[Symbol.iterator](); const doAction = () => { const next = _iter.next(); if (next.done) { // Close the repl. Note that it must have a clean prompt to do so. this.emit('keypress', '', { ctrl: true, name: 'd' }); return; } const action = next.value; if (typeof action === 'object') { this.emit('keypress', '', action); } else { this.emit('data', `${action}`); } setImmediate(doAction); }; doAction(); } resume() {} pause() {} } ActionStream.prototype.readable = true; // Mock keys const ENTER = { name: 'enter' }; const UP = { name: 'up' }; const DOWN = { name: 'down' }; const BACKSPACE = { name: 'backspace' }; const SEARCH_BACKWARDS = { name: 'r', ctrl: true }; const SEARCH_FORWARDS = { name: 's', ctrl: true }; const ESCAPE = { name: 'escape' }; const CTRL_C = { name: 'c', ctrl: true }; const DELETE_WORD_LEFT = { name: 'w', ctrl: true }; const prompt = '> '; // TODO(BridgeAR): Add tests for lines that exceed the maximum columns. const tests = [ { // Creates few history to navigate for env: { NODE_REPL_HISTORY: defaultHistoryPath }, test: [ 'console.log("foo")', ENTER, 'ab = "aaaa"', ENTER, 'repl.repl.historyIndex', ENTER, 'console.log("foo")', ENTER, 'let ba = 9', ENTER, 'ab = "aaaa"', ENTER, '555 - 909', ENTER, '{key : {key2 :[] }}', ENTER, 'Array(100).fill(1)', ENTER ], expected: [], clean: false }, { env: { NODE_REPL_HISTORY: defaultHistoryPath }, showEscapeCodes: true, checkTotal: true, useColors: true, test: [ '7', // 1 SEARCH_FORWARDS, SEARCH_FORWARDS, // 3 'a', SEARCH_BACKWARDS, // 5 SEARCH_FORWARDS, SEARCH_BACKWARDS, // 7 'a', BACKSPACE, // 9 DELETE_WORD_LEFT, 'aa', // 11 SEARCH_BACKWARDS, SEARCH_BACKWARDS, // 13 SEARCH_BACKWARDS, SEARCH_BACKWARDS, // 15 SEARCH_FORWARDS, ESCAPE, // 17 ENTER ], // A = Cursor n up // B = Cursor n down // C = Cursor n forward // D = Cursor n back // G = Cursor to column n // J = Erase in screen; 0 = right; 1 = left; 2 = total // K = Erase in line; 0 = right; 1 = left; 2 = total expected: [ // 0. Start '\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', // 1. '7' '7', // 2. SEARCH FORWARDS '\nfwd-i-search: _', '\x1B[1A', '\x1B[4G', // 3. SEARCH FORWARDS '\x1B[3G', '\x1B[0J', '7\nfwd-i-search: _', '\x1B[1A', '\x1B[4G', // 4. 'a' '\x1B[3G', '\x1B[0J', '7\nfailed-fwd-i-search: a_', '\x1B[1A', '\x1B[4G', // 5. SEARCH BACKWARDS '\x1B[3G', '\x1B[0J', 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', '\x1B[1A', '\x1B[6G', // 6. SEARCH FORWARDS '\x1B[3G', '\x1B[0J', '7\nfailed-fwd-i-search: a_', '\x1B[1A', '\x1B[4G', // 7. SEARCH BACKWARDS '\x1B[3G', '\x1B[0J', 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', '\x1B[1A', '\x1B[6G', // 8. 'a' '\x1B[3G', '\x1B[0J', 'ab = "aa\x1B[4maa\x1B[24m"\nbck-i-search: aa_', '\x1B[1A', '\x1B[11G', // 9. BACKSPACE '\x1B[3G', '\x1B[0J', 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', '\x1B[1A', '\x1B[6G', // 10. DELETE WORD LEFT (works as backspace) '\x1B[3G', '\x1B[0J', '7\nbck-i-search: _', '\x1B[1A', '\x1B[4G', // 11. 'a' '\x1B[3G', '\x1B[0J', 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', '\x1B[1A', '\x1B[6G', // 11. 'aa' - continued '\x1B[3G', '\x1B[0J', 'ab = "aa\x1B[4maa\x1B[24m"\nbck-i-search: aa_', '\x1B[1A', '\x1B[11G', // 12. SEARCH BACKWARDS '\x1B[3G', '\x1B[0J', 'ab = "a\x1B[4maa\x1B[24ma"\nbck-i-search: aa_', '\x1B[1A', '\x1B[10G', // 13. SEARCH BACKWARDS '\x1B[3G', '\x1B[0J', 'ab = "\x1B[4maa\x1B[24maa"\nbck-i-search: aa_', '\x1B[1A', '\x1B[9G', // 14. SEARCH BACKWARDS '\x1B[3G', '\x1B[0J', '7\nfailed-bck-i-search: aa_', '\x1B[1A', '\x1B[4G', // 15. SEARCH BACKWARDS '\x1B[3G', '\x1B[0J', '7\nfailed-bck-i-search: aa_', '\x1B[1A', '\x1B[4G', // 16. SEARCH FORWARDS '\x1B[3G', '\x1B[0J', 'ab = "\x1B[4maa\x1B[24maa"\nfwd-i-search: aa_', '\x1B[1A', '\x1B[9G', // 17. ESCAPE '\x1B[3G', '\x1B[0J', '7', // 18. ENTER '\r\n', '\x1B[33m7\x1B[39m\n', '\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', '\r\n' ], clean: false }, { env: { NODE_REPL_HISTORY: defaultHistoryPath }, showEscapeCodes: true, skip: !process.features.inspector, checkTotal: true, useColors: false, test: [ 'fu', // 1 SEARCH_BACKWARDS, '}', // 3 SEARCH_BACKWARDS, CTRL_C, // 5 CTRL_C, '1+1', // 7 ENTER, SEARCH_BACKWARDS, // 9 '+', '\r', // 11 '2', SEARCH_BACKWARDS, // 13 're', UP, // 15 DOWN, SEARCH_FORWARDS, // 17 '\n' ], expected: [ '\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', 'f', 'u', ' // nction', '\x1B[5G', '\x1B[0K', '\nbck-i-search: _', '\x1B[1A', '\x1B[5G', '\x1B[3G', '\x1B[0J', '{key : {key2 :[] }}\nbck-i-search: }_', '\x1B[1A', '\x1B[21G', '\x1B[3G', '\x1B[0J', '{key : {key2 :[] }}\nbck-i-search: }_', '\x1B[1A', '\x1B[20G', '\x1B[3G', '\x1B[0J', 'fu', '\r\n', '\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', '1', '+', '1', '\n// 2', '\x1B[6G', '\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A', '\r\n', '2\n', '\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', '\nbck-i-search: _', '\x1B[1A', '\x1B[3G', '\x1B[0J', '1+1\nbck-i-search: +_', '\x1B[1A', '\x1B[4G', '\x1B[3G', '\x1B[0J', '1+1', '\x1B[4G', '\x1B[2C', '\r\n', '2\n', '\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', '2', '\n// 2', '\x1B[4G', '\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A', '\nbck-i-search: _', '\x1B[1A', '\x1B[4G', '\x1B[3G', '\x1B[0J', 'Array(100).fill(1)\nbck-i-search: r_', '\x1B[1A', '\x1B[5G', '\x1B[3G', '\x1B[0J', 'repl.repl.historyIndex\nbck-i-search: re_', '\x1B[1A', '\x1B[8G', '\x1B[3G', '\x1B[0J', 'repl.repl.historyIndex', '\x1B[8G', '\x1B[1G', '\x1B[0J', `${prompt}ab = "aaaa"`, '\x1B[14G', '\x1B[1G', '\x1B[0J', `${prompt}repl.repl.historyIndex`, '\x1B[25G', '\n// 8', '\x1B[25G', '\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A', '\nfwd-i-search: _', '\x1B[1A', '\x1B[25G', '\x1B[3G', '\x1B[0J', 'repl.repl.historyIndex', '\r\n', '-1\n', '\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', '\r\n' ], clean: false } ]; const numtests = tests.length; const runTestWrap = common.mustCall(runTest, numtests); function cleanupTmpFile() { try { // Write over the file, clearing any history fs.writeFileSync(defaultHistoryPath, ''); } catch (err) { if (err.code === 'ENOENT') return true; throw err; } return true; } function runTest() { const opts = tests.shift(); if (!opts) return; // All done const { expected, skip } = opts; // Test unsupported on platform. if (skip) { setImmediate(runTestWrap, true); return; } const lastChunks = []; let i = 0; REPL.createInternalRepl(opts.env, { input: new ActionStream(), output: new stream.Writable({ write(chunk, _, next) { const output = chunk.toString(); if (!opts.showEscapeCodes && (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) { return next(); } lastChunks.push(output); if (expected.length && !opts.checkTotal) { try { assert.strictEqual(output, expected[i]); } catch (e) { console.error(`Failed test # ${numtests - tests.length}`); console.error('Last outputs: ' + inspect(lastChunks, { breakLength: 5, colors: true })); throw e; } i++; } next(); } }), completer: opts.completer, prompt, useColors: opts.useColors || false, terminal: true }, function(err, repl) { if (err) { console.error(`Failed test # ${numtests - tests.length}`); throw err; } repl.once('close', () => { if (opts.clean) cleanupTmpFile(); if (opts.checkTotal) { assert.deepStrictEqual(lastChunks, expected); } else if (expected.length !== i) { console.error(tests[numtests - tests.length - 1]); throw new Error(`Failed test # ${numtests - tests.length}`); } setImmediate(runTestWrap, true); }); if (opts.columns) { Object.defineProperty(repl, 'columns', { value: opts.columns, enumerable: true }); } repl.inputStream.run(opts.test); }); } // run the tests runTest();