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 BACKSPACE = { name: 'backspace' }; 56const TABULATION = { name: 'tab' }; 57const WORD_LEFT = { name: 'left', ctrl: true }; 58const WORD_RIGHT = { name: 'right', ctrl: true }; 59const GO_TO_END = { name: 'end' }; 60const SIGINT = { name: 'c', ctrl: true }; 61const ESCAPE = { name: 'escape', meta: true }; 62 63const prompt = '> '; 64 65const tests = [ 66 { 67 env: { NODE_REPL_HISTORY: defaultHistoryPath }, 68 test: (function*() { 69 // Deleting Array iterator should not break history feature. 70 // 71 // Using a generator function instead of an object to allow the test to 72 // keep iterating even when Array.prototype[Symbol.iterator] has been 73 // deleted. 74 yield 'const ArrayIteratorPrototype ='; 75 yield ' Object.getPrototypeOf(Array.prototype[Symbol.iterator]());'; 76 yield ENTER; 77 yield 'const {next} = ArrayIteratorPrototype;'; 78 yield ENTER; 79 yield 'const realArrayIterator = Array.prototype[Symbol.iterator];'; 80 yield ENTER; 81 yield 'delete Array.prototype[Symbol.iterator];'; 82 yield ENTER; 83 yield 'delete ArrayIteratorPrototype.next;'; 84 yield ENTER; 85 yield UP; 86 yield UP; 87 yield DOWN; 88 yield DOWN; 89 yield 'fu'; 90 yield 'n'; 91 yield RIGHT; 92 yield BACKSPACE; 93 yield LEFT; 94 yield LEFT; 95 yield 'A'; 96 yield BACKSPACE; 97 yield GO_TO_END; 98 yield BACKSPACE; 99 yield WORD_LEFT; 100 yield WORD_RIGHT; 101 yield ESCAPE; 102 yield ENTER; 103 yield 'require("./'; 104 yield TABULATION; 105 yield SIGINT; 106 yield 'import("./'; 107 yield TABULATION; 108 yield SIGINT; 109 yield 'Array.proto'; 110 yield RIGHT; 111 yield '.pu'; 112 yield ENTER; 113 yield 'ArrayIteratorPrototype.next = next;'; 114 yield ENTER; 115 yield 'Array.prototype[Symbol.iterator] = realArrayIterator;'; 116 yield ENTER; 117 })(), 118 expected: [], 119 clean: false 120 }, 121]; 122const numtests = tests.length; 123 124const runTestWrap = common.mustCall(runTest, numtests); 125 126function cleanupTmpFile() { 127 try { 128 // Write over the file, clearing any history 129 fs.writeFileSync(defaultHistoryPath, ''); 130 } catch (err) { 131 if (err.code === 'ENOENT') return true; 132 throw err; 133 } 134 return true; 135} 136 137function runTest() { 138 const opts = tests.shift(); 139 if (!opts) return; // All done 140 141 const { expected, skip } = opts; 142 143 // Test unsupported on platform. 144 if (skip) { 145 setImmediate(runTestWrap, true); 146 return; 147 } 148 const lastChunks = []; 149 let i = 0; 150 151 REPL.createInternalRepl(opts.env, { 152 input: new ActionStream(), 153 output: new stream.Writable({ 154 write(chunk, _, next) { 155 const output = chunk.toString(); 156 157 if (!opts.showEscapeCodes && 158 (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) { 159 return next(); 160 } 161 162 lastChunks.push(output); 163 164 if (expected.length && !opts.checkTotal) { 165 try { 166 assert.strictEqual(output, expected[i]); 167 } catch (e) { 168 console.error(`Failed test # ${numtests - tests.length}`); 169 console.error('Last outputs: ' + inspect(lastChunks, { 170 breakLength: 5, colors: true 171 })); 172 throw e; 173 } 174 // TODO(BridgeAR): Auto close on last chunk! 175 i++; 176 } 177 178 next(); 179 } 180 }), 181 allowBlockingCompletions: true, 182 completer: opts.completer, 183 prompt, 184 useColors: false, 185 preview: opts.preview, 186 terminal: true 187 }, function(err, repl) { 188 if (err) { 189 console.error(`Failed test # ${numtests - tests.length}`); 190 throw err; 191 } 192 193 repl.once('close', () => { 194 if (opts.clean) 195 cleanupTmpFile(); 196 197 if (opts.checkTotal) { 198 assert.deepStrictEqual(lastChunks, expected); 199 } else if (expected.length !== i) { 200 console.error(tests[numtests - tests.length - 1]); 201 throw new Error(`Failed test # ${numtests - tests.length}`); 202 } 203 204 setImmediate(runTestWrap, true); 205 }); 206 207 if (opts.columns) { 208 Object.defineProperty(repl, 'columns', { 209 value: opts.columns, 210 enumerable: true 211 }); 212 } 213 repl.input.run(opts.test); 214 }); 215} 216 217// run the tests 218runTest(); 219