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