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', '\nbck-i-search: _', '\x1B[1A', '\x1B[5G', 216 '\x1B[3G', '\x1B[0J', 217 '{key : {key2 :[] }}\nbck-i-search: }_', '\x1B[1A', '\x1B[21G', 218 '\x1B[3G', '\x1B[0J', 219 '{key : {key2 :[] }}\nbck-i-search: }_', '\x1B[1A', '\x1B[20G', 220 '\x1B[3G', '\x1B[0J', 221 'fu', 222 '\r\n', 223 '\x1B[1G', '\x1B[0J', 224 prompt, '\x1B[3G', 225 '1', '+', '1', '\n// 2', '\x1B[6G', '\x1B[1A', 226 '\x1B[1B', '\x1B[2K', '\x1B[1A', 227 '\r\n', 228 '2\n', 229 '\x1B[1G', '\x1B[0J', 230 prompt, '\x1B[3G', 231 '\nbck-i-search: _', '\x1B[1A', 232 '\x1B[3G', '\x1B[0J', 233 '1+1\nbck-i-search: +_', '\x1B[1A', '\x1B[4G', 234 '\x1B[3G', '\x1B[0J', 235 '1+1', '\x1B[4G', 236 '\x1B[2C', 237 '\r\n', 238 '2\n', 239 '\x1B[1G', '\x1B[0J', 240 prompt, '\x1B[3G', 241 '2', 242 '\nbck-i-search: _', '\x1B[1A', '\x1B[4G', 243 '\x1B[3G', '\x1B[0J', 244 'Array(100).fill(1)\nbck-i-search: r_', '\x1B[1A', '\x1B[5G', 245 '\x1B[3G', '\x1B[0J', 246 'repl.repl.historyIndex\nbck-i-search: re_', '\x1B[1A', '\x1B[8G', 247 '\x1B[3G', '\x1B[0J', 248 'repl.repl.historyIndex', '\x1B[8G', 249 '\x1B[1G', '\x1B[0J', 250 `${prompt}ab = "aaaa"`, '\x1B[14G', 251 '\x1B[1G', '\x1B[0J', 252 `${prompt}repl.repl.historyIndex`, '\x1B[25G', '\n// 8', 253 '\x1B[25G', '\x1B[1A', 254 '\x1B[1B', '\x1B[2K', '\x1B[1A', 255 '\nfwd-i-search: _', '\x1B[1A', '\x1B[25G', 256 '\x1B[3G', '\x1B[0J', 257 'repl.repl.historyIndex', 258 '\r\n', 259 '-1\n', 260 '\x1B[1G', '\x1B[0J', 261 prompt, '\x1B[3G', 262 '\r\n', 263 ], 264 clean: false 265 }, 266]; 267const numtests = tests.length; 268 269const runTestWrap = common.mustCall(runTest, numtests); 270 271function cleanupTmpFile() { 272 try { 273 // Write over the file, clearing any history 274 fs.writeFileSync(defaultHistoryPath, ''); 275 } catch (err) { 276 if (err.code === 'ENOENT') return true; 277 throw err; 278 } 279 return true; 280} 281 282function runTest() { 283 const opts = tests.shift(); 284 if (!opts) return; // All done 285 286 const { expected, skip } = opts; 287 288 // Test unsupported on platform. 289 if (skip) { 290 setImmediate(runTestWrap, true); 291 return; 292 } 293 294 const lastChunks = []; 295 let i = 0; 296 297 REPL.createInternalRepl(opts.env, { 298 input: new ActionStream(), 299 output: new stream.Writable({ 300 write(chunk, _, next) { 301 const output = chunk.toString(); 302 303 if (!opts.showEscapeCodes && 304 (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) { 305 return next(); 306 } 307 308 lastChunks.push(output); 309 310 if (expected.length && !opts.checkTotal) { 311 try { 312 assert.strictEqual(output, expected[i]); 313 } catch (e) { 314 console.error(`Failed test # ${numtests - tests.length}`); 315 console.error('Last outputs: ' + inspect(lastChunks, { 316 breakLength: 5, colors: true 317 })); 318 throw e; 319 } 320 i++; 321 } 322 323 next(); 324 } 325 }), 326 completer: opts.completer, 327 prompt, 328 useColors: opts.useColors || false, 329 terminal: true 330 }, function(err, repl) { 331 if (err) { 332 console.error(`Failed test # ${numtests - tests.length}`); 333 throw err; 334 } 335 336 repl.once('close', () => { 337 if (opts.clean) 338 cleanupTmpFile(); 339 340 if (opts.checkTotal) { 341 assert.deepStrictEqual(lastChunks, expected); 342 } else if (expected.length !== i) { 343 console.error(tests[numtests - tests.length - 1]); 344 throw new Error(`Failed test # ${numtests - tests.length}`); 345 } 346 347 setImmediate(runTestWrap, true); 348 }); 349 350 if (opts.columns) { 351 Object.defineProperty(repl, 'columns', { 352 value: opts.columns, 353 enumerable: true 354 }); 355 } 356 repl.inputStream.run(opts.test); 357 }); 358} 359 360// run the tests 361runTest(); 362