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