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(); 14common.allowGlobals('aaaa'); 15 16const tmpdir = require('../common/tmpdir'); 17tmpdir.refresh(); 18 19const defaultHistoryPath = path.join(tmpdir.path, '.node_repl_history'); 20 21// Create an input stream specialized for testing an array of actions 22class ActionStream extends stream.Stream { 23 run(data) { 24 const _iter = data[Symbol.iterator](); 25 const doAction = () => { 26 const next = _iter.next(); 27 if (next.done) { 28 // Close the repl. Note that it must have a clean prompt to do so. 29 this.emit('keypress', '', { ctrl: true, name: 'd' }); 30 return; 31 } 32 const action = next.value; 33 34 if (typeof action === 'object') { 35 this.emit('keypress', '', action); 36 } else { 37 this.emit('data', `${action}`); 38 } 39 setImmediate(doAction); 40 }; 41 doAction(); 42 } 43 resume() {} 44 pause() {} 45} 46ActionStream.prototype.readable = true; 47 48// Mock keys 49const ENTER = { name: 'enter' }; 50const UP = { name: 'up' }; 51const DOWN = { name: 'down' }; 52const BACKSPACE = { name: 'backspace' }; 53const SEARCH_BACKWARDS = { name: 'r', ctrl: true }; 54const SEARCH_FORWARDS = { name: 's', ctrl: true }; 55const ESCAPE = { name: 'escape' }; 56const CTRL_C = { name: 'c', ctrl: true }; 57const DELETE_WORD_LEFT = { name: 'w', ctrl: true }; 58 59const prompt = '> '; 60 61// TODO(BridgeAR): Add tests for lines that exceed the maximum columns. 62const tests = [ 63 { // Creates few history to navigate for 64 env: { NODE_REPL_HISTORY: defaultHistoryPath }, 65 test: [ 66 'console.log("foo")', ENTER, 67 'ab = "aaaa"', ENTER, 68 'repl.repl.historyIndex', ENTER, 69 'console.log("foo")', ENTER, 70 'let ba = 9', ENTER, 71 'ab = "aaaa"', ENTER, 72 '555 - 909', ENTER, 73 '{key : {key2 :[] }}', ENTER, 74 'Array(100).fill(1)', ENTER, 75 ], 76 expected: [], 77 clean: false 78 }, 79 { 80 env: { NODE_REPL_HISTORY: defaultHistoryPath }, 81 showEscapeCodes: true, 82 checkTotal: true, 83 useColors: true, 84 test: [ 85 '7', // 1 86 SEARCH_FORWARDS, 87 SEARCH_FORWARDS, // 3 88 'a', 89 SEARCH_BACKWARDS, // 5 90 SEARCH_FORWARDS, 91 SEARCH_BACKWARDS, // 7 92 'a', 93 BACKSPACE, // 9 94 DELETE_WORD_LEFT, 95 'aa', // 11 96 SEARCH_BACKWARDS, 97 SEARCH_BACKWARDS, // 13 98 SEARCH_BACKWARDS, 99 SEARCH_BACKWARDS, // 15 100 SEARCH_FORWARDS, 101 ESCAPE, // 17 102 ENTER, 103 ], 104 // A = Cursor n up 105 // B = Cursor n down 106 // C = Cursor n forward 107 // D = Cursor n back 108 // G = Cursor to column n 109 // J = Erase in screen; 0 = right; 1 = left; 2 = total 110 // K = Erase in line; 0 = right; 1 = left; 2 = total 111 expected: [ 112 // 0. Start 113 '\x1B[1G', '\x1B[0J', 114 prompt, '\x1B[3G', 115 // 1. '7' 116 '7', 117 // 2. SEARCH FORWARDS 118 '\nfwd-i-search: _', '\x1B[1A', '\x1B[4G', 119 // 3. SEARCH FORWARDS 120 '\x1B[3G', '\x1B[0J', 121 '7\nfwd-i-search: _', '\x1B[1A', '\x1B[4G', 122 // 4. 'a' 123 '\x1B[3G', '\x1B[0J', 124 '7\nfailed-fwd-i-search: a_', '\x1B[1A', '\x1B[4G', 125 // 5. SEARCH BACKWARDS 126 '\x1B[3G', '\x1B[0J', 127 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', 128 '\x1B[1A', '\x1B[6G', 129 // 6. SEARCH FORWARDS 130 '\x1B[3G', '\x1B[0J', 131 '7\nfailed-fwd-i-search: a_', '\x1B[1A', '\x1B[4G', 132 // 7. SEARCH BACKWARDS 133 '\x1B[3G', '\x1B[0J', 134 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', 135 '\x1B[1A', '\x1B[6G', 136 // 8. 'a' 137 '\x1B[3G', '\x1B[0J', 138 'ab = "aa\x1B[4maa\x1B[24m"\nbck-i-search: aa_', 139 '\x1B[1A', '\x1B[11G', 140 // 9. BACKSPACE 141 '\x1B[3G', '\x1B[0J', 142 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', 143 '\x1B[1A', '\x1B[6G', 144 // 10. DELETE WORD LEFT (works as backspace) 145 '\x1B[3G', '\x1B[0J', 146 '7\nbck-i-search: _', '\x1B[1A', '\x1B[4G', 147 // 11. 'a' 148 '\x1B[3G', '\x1B[0J', 149 'Arr\x1B[4ma\x1B[24my(100).fill(1)\nbck-i-search: a_', 150 '\x1B[1A', '\x1B[6G', 151 // 11. 'aa' - continued 152 '\x1B[3G', '\x1B[0J', 153 'ab = "aa\x1B[4maa\x1B[24m"\nbck-i-search: aa_', 154 '\x1B[1A', '\x1B[11G', 155 // 12. SEARCH BACKWARDS 156 '\x1B[3G', '\x1B[0J', 157 'ab = "a\x1B[4maa\x1B[24ma"\nbck-i-search: aa_', 158 '\x1B[1A', '\x1B[10G', 159 // 13. SEARCH BACKWARDS 160 '\x1B[3G', '\x1B[0J', 161 'ab = "\x1B[4maa\x1B[24maa"\nbck-i-search: aa_', 162 '\x1B[1A', '\x1B[9G', 163 // 14. SEARCH BACKWARDS 164 '\x1B[3G', '\x1B[0J', 165 '7\nfailed-bck-i-search: aa_', '\x1B[1A', '\x1B[4G', 166 // 15. SEARCH BACKWARDS 167 '\x1B[3G', '\x1B[0J', 168 '7\nfailed-bck-i-search: aa_', '\x1B[1A', '\x1B[4G', 169 // 16. SEARCH FORWARDS 170 '\x1B[3G', '\x1B[0J', 171 'ab = "\x1B[4maa\x1B[24maa"\nfwd-i-search: aa_', 172 '\x1B[1A', '\x1B[9G', 173 // 17. ESCAPE 174 '\x1B[3G', '\x1B[0J', 175 '7', 176 // 18. ENTER 177 '\r\n', 178 '\x1B[33m7\x1B[39m\n', 179 '\x1B[1G', '\x1B[0J', 180 prompt, 181 '\x1B[3G', 182 '\r\n', 183 ], 184 clean: false 185 }, 186 { 187 env: { NODE_REPL_HISTORY: defaultHistoryPath }, 188 showEscapeCodes: true, 189 skip: !process.features.inspector, 190 checkTotal: true, 191 useColors: false, 192 test: [ 193 'fu', // 1 194 SEARCH_BACKWARDS, 195 '}', // 3 196 SEARCH_BACKWARDS, 197 CTRL_C, // 5 198 CTRL_C, 199 '1+1', // 7 200 ENTER, 201 SEARCH_BACKWARDS, // 9 202 '+', 203 '\r', // 11 204 '2', 205 SEARCH_BACKWARDS, // 13 206 're', 207 UP, // 15 208 DOWN, 209 SEARCH_FORWARDS, // 17 210 '\n', 211 ], 212 expected: [ 213 '\x1B[1G', '\x1B[0J', 214 prompt, '\x1B[3G', 215 'f', 'u', ' // nction', 216 '\x1B[5G', '\x1B[0K', 217 '\nbck-i-search: _', '\x1B[1A', '\x1B[5G', 218 '\x1B[3G', '\x1B[0J', 219 '{key : {key2 :[] }}\nbck-i-search: }_', '\x1B[1A', '\x1B[21G', 220 '\x1B[3G', '\x1B[0J', 221 '{key : {key2 :[] }}\nbck-i-search: }_', '\x1B[1A', '\x1B[20G', 222 '\x1B[3G', '\x1B[0J', 223 'fu', 224 '\r\n', 225 '\x1B[1G', '\x1B[0J', 226 prompt, '\x1B[3G', 227 '1', '+', '1', '\n// 2', '\x1B[6G', '\x1B[1A', 228 '\x1B[1B', '\x1B[2K', '\x1B[1A', 229 '\r\n', 230 '2\n', 231 '\x1B[1G', '\x1B[0J', 232 prompt, '\x1B[3G', 233 '\nbck-i-search: _', '\x1B[1A', 234 '\x1B[3G', '\x1B[0J', 235 '1+1\nbck-i-search: +_', '\x1B[1A', '\x1B[4G', 236 '\x1B[3G', '\x1B[0J', 237 '1+1', '\x1B[4G', 238 '\x1B[2C', 239 '\r\n', 240 '2\n', 241 '\x1B[1G', '\x1B[0J', 242 prompt, '\x1B[3G', 243 '2', '\n// 2', '\x1B[4G', '\x1B[1A', 244 '\x1B[1B', '\x1B[2K', '\x1B[1A', 245 '\nbck-i-search: _', '\x1B[1A', '\x1B[4G', 246 '\x1B[3G', '\x1B[0J', 247 'Array(100).fill(1)\nbck-i-search: r_', '\x1B[1A', '\x1B[5G', 248 '\x1B[3G', '\x1B[0J', 249 'repl.repl.historyIndex\nbck-i-search: re_', '\x1B[1A', '\x1B[8G', 250 '\x1B[3G', '\x1B[0J', 251 'repl.repl.historyIndex', '\x1B[8G', 252 '\x1B[1G', '\x1B[0J', 253 `${prompt}ab = "aaaa"`, '\x1B[14G', 254 '\x1B[1G', '\x1B[0J', 255 `${prompt}repl.repl.historyIndex`, '\x1B[25G', '\n// 8', 256 '\x1B[25G', '\x1B[1A', 257 '\x1B[1B', '\x1B[2K', '\x1B[1A', 258 '\nfwd-i-search: _', '\x1B[1A', '\x1B[25G', 259 '\x1B[3G', '\x1B[0J', 260 'repl.repl.historyIndex', 261 '\r\n', 262 '-1\n', 263 '\x1B[1G', '\x1B[0J', 264 prompt, '\x1B[3G', 265 '\r\n', 266 ], 267 clean: false 268 }, 269]; 270const numtests = tests.length; 271 272const runTestWrap = common.mustCall(runTest, numtests); 273 274function cleanupTmpFile() { 275 try { 276 // Write over the file, clearing any history 277 fs.writeFileSync(defaultHistoryPath, ''); 278 } catch (err) { 279 if (err.code === 'ENOENT') return true; 280 throw err; 281 } 282 return true; 283} 284 285function runTest() { 286 const opts = tests.shift(); 287 if (!opts) return; // All done 288 289 const { expected, skip } = opts; 290 291 // Test unsupported on platform. 292 if (skip) { 293 setImmediate(runTestWrap, true); 294 return; 295 } 296 297 const lastChunks = []; 298 let i = 0; 299 300 REPL.createInternalRepl(opts.env, { 301 input: new ActionStream(), 302 output: new stream.Writable({ 303 write(chunk, _, next) { 304 const output = chunk.toString(); 305 306 if (!opts.showEscapeCodes && 307 (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) { 308 return next(); 309 } 310 311 lastChunks.push(output); 312 313 if (expected.length && !opts.checkTotal) { 314 try { 315 assert.strictEqual(output, expected[i]); 316 } catch (e) { 317 console.error(`Failed test # ${numtests - tests.length}`); 318 console.error('Last outputs: ' + inspect(lastChunks, { 319 breakLength: 5, colors: true 320 })); 321 throw e; 322 } 323 i++; 324 } 325 326 next(); 327 } 328 }), 329 completer: opts.completer, 330 prompt, 331 useColors: opts.useColors || false, 332 terminal: true 333 }, function(err, repl) { 334 if (err) { 335 console.error(`Failed test # ${numtests - tests.length}`); 336 throw err; 337 } 338 339 repl.once('close', () => { 340 if (opts.clean) 341 cleanupTmpFile(); 342 343 if (opts.checkTotal) { 344 assert.deepStrictEqual(lastChunks, expected); 345 } else if (expected.length !== i) { 346 console.error(tests[numtests - tests.length - 1]); 347 throw new Error(`Failed test # ${numtests - tests.length}`); 348 } 349 350 setImmediate(runTestWrap, true); 351 }); 352 353 if (opts.columns) { 354 Object.defineProperty(repl, 'columns', { 355 value: opts.columns, 356 enumerable: true 357 }); 358 } 359 repl.inputStream.run(opts.test); 360 }); 361} 362 363// run the tests 364runTest(); 365